Antialiasing fixes
Build and Test j3270 / Build JAR & Run Tests (push) Successful in 40s

This commit is contained in:
2026-08-25 23:31:14 +00:00
parent 2584f4289f
commit bcfd4ba2e0
6 changed files with 132 additions and 72 deletions
@@ -423,23 +423,19 @@ public class QueryReplyBuilder {
} }
private byte[] buildGraphics(int maxCols, int maxRows) { private byte[] buildGraphics(int maxCols, int maxRows) {
int width = maxCols * 9;
int height = maxRows * 12;
return new byte[]{ return new byte[]{
(byte) 0x80, 0x02, (byte) 0x80, 0x02,
(byte) ((width >> 8) & 0xFF), (byte) (width & 0xFF), 0x00, 0x00,
(byte) ((height >> 8) & 0xFF), (byte) (height & 0xFF), 0x00, (byte) 0xFC,
0x00 0x00
}; };
} }
private byte[] buildGImage(int maxCols, int maxRows) { private byte[] buildGImage(int maxCols, int maxRows) {
int width = maxCols * 9;
int height = maxRows * 12;
return new byte[]{ return new byte[]{
0x00, 0x01, 0x00, 0x01,
(byte) ((width >> 8) & 0xFF), (byte) (width & 0xFF), 0x00, 0x00,
(byte) ((height >> 8) & 0xFF), (byte) (height & 0xFF), 0x00, (byte) 0xFC,
0x00, 0x00,
0x06, 0x40, 0x06, 0x40, 0x06, 0x01, 0x06, 0x40, 0x06, 0x40, 0x06, 0x01,
(byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xF0 (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xF0
@@ -163,7 +163,7 @@ public class GocaDecoder {
public synchronized int findPickedSegment(int gx, int gy) { public synchronized int findPickedSegment(int gx, int gy) {
for (int i = activeSegmentsInOrder.size() - 1; i >= 0; i--) { for (int i = activeSegmentsInOrder.size() - 1; i >= 0; i--) {
SegmentBounds sb = activeSegmentsInOrder.get(i); SegmentBounds sb = activeSegmentsInOrder.get(i);
if (sb.contains(gx, gy, 12)) { if (sb.contains(gx, gy, 25)) {
System.err.println(String.format( System.err.println(String.format(
"findPickedSegment: goca=(%d, %d) HIT segId=%d bounds=[%d..%d, %d..%d] tag=%d", "findPickedSegment: goca=(%d, %d) HIT segId=%d bounds=[%d..%d, %d..%d] tag=%d",
gx, gy, sb.segId, sb.minX, sb.maxX, sb.minY, sb.maxY, sb.tag gx, gy, sb.segId, sb.minX, sb.maxX, sb.minY, sb.maxY, sb.tag
@@ -171,6 +171,15 @@ public class GocaDecoder {
return sb.segId; return sb.segId;
} }
} }
for (SegmentBounds sb : segmentBoundsMap.values()) {
if (sb.contains(gx, gy, 25)) {
System.err.println(String.format(
"findPickedSegment (fallback): goca=(%d, %d) HIT segId=%d bounds=[%d..%d, %d..%d] tag=%d",
gx, gy, sb.segId, sb.minX, sb.maxX, sb.minY, sb.maxY, sb.tag
));
return sb.segId;
}
}
System.err.println(String.format("findPickedSegment: goca=(%d, %d) NO HIT (defaulting to 0/canvas)", gx, gy)); System.err.println(String.format("findPickedSegment: goca=(%d, %d) NO HIT (defaulting to 0/canvas)", gx, gy));
return 0; return 0;
} }
@@ -798,7 +807,10 @@ public class GocaDecoder {
} }
case GocaConstants.P_ERASE: { // 0x0A: Erase Graphics Presentation Space case GocaConstants.P_ERASE: { // 0x0A: Erase Graphics Presentation Space
plane.clear(); plane.clear();
resetDefaults(); resetAttributes();
curX = 0;
curY = 0;
activeSegmentsInOrder.clear();
idx += 2; idx += 2;
break; break;
} }
@@ -816,6 +828,26 @@ public class GocaDecoder {
int endSeg = (idx + 7 < end) ? (((data[idx + 6] & 0xFF) << 8) | (data[idx + 7] & 0xFF)) : startSeg; int endSeg = (idx + 7 < end) ? (((data[idx + 6] & 0xFF) << 8) | (data[idx + 7] & 0xFF)) : startSeg;
logger.info(String.format("GOCA P_SCUDEF: flags0=0x%02x flags1=0x%02x startSeg=%d endSeg=%d", logger.info(String.format("GOCA P_SCUDEF: flags0=0x%02x flags1=0x%02x startSeg=%d endSeg=%d",
flags0, flags1, startSeg, endSeg)); flags0, flags1, startSeg, endSeg));
// Redraw all stored base segments not in dynamic range [startSeg..endSeg]
for (int segId : segmentOrderList) {
if (segId < startSeg || segId > endSeg) {
if (!chainedTargets.contains(segId)) {
byte[] segBytes = segmentStore.get(segId);
if (segBytes != null) {
int savedSegId = currentSegId;
currentSegId = segId;
SegmentBounds sb = segmentBoundsMap.computeIfAbsent(segId, SegmentBounds::new);
activeSegmentsInOrder.remove(sb);
activeSegmentsInOrder.add(sb);
callDepth++;
decodeStreamDirect(segBytes, 0, segBytes.length);
callDepth--;
currentSegId = savedSegId;
}
}
}
}
for (int s = startSeg; s <= endSeg; s++) { for (int s = startSeg; s <= endSeg; s++) {
byte[] segBytes = segmentStore.get(s); byte[] segBytes = segmentStore.get(s);
if (segBytes != null) { if (segBytes != null) {
@@ -35,31 +35,19 @@ public class GraphicInputBuilder {
/** /**
* Builds the 56-byte Graphic Input Structured Field with picked segment ID and correlation tag. * Builds the 56-byte Graphic Input Structured Field with picked segment ID and correlation tag.
*
* IMPORTANT ARCHITECTURE NOTE:
* Per IBM GA23-0059 / GDDM specifications:
* - Bytes 0-1: Total structured field length (0x0038 = 56 bytes).
* - Bytes 24-27: (gocaX, gocaY) cursor coordinates.
* - Bytes 28-31: Picked Segment Identifier (32-bit big-endian). When clicking on a menu item or
* interactive element (e.g. DRAW button = Segment 2, EXIT button = Segment 5), GDDM requires
* the exact segment ID in bytes 28-31. If mismatched, GDDM rejects the click with a WCC 0xF7 alarm beep.
* - Bytes 32-33: Pick Correlation Tag (16-bit big-endian) set by G_GSETAG (0x39).
* - Byte 34: Modifier flags (Shift = 0x80, Ctrl = 0x40, 0xFF for keyboard AID).
* - Byte 35: Button ID (0x01 = Button 1 / Pick, 0x02 = Button 2 / Action) or 3270 AID code.
*
* @param gocaX GOCA signed X coordinate (-xMax..+xMax)
* @param gocaY GOCA signed Y coordinate (-yMax..+yMax)
* @param buttonOrAidCode The mouse button number (1=Pick, 2=Action) or 3270 AID code for keyboard
* @param isMouseAction true if triggered directly by mouse button press, false for keyboard AID
* @param isShift true if shift key was down
* @param isCtrl true if ctrl key was down
* @param pickedSegId Picked GOCA segment ID (0 if none)
* @param pickTag Pick correlation tag
* @return 56-byte payload
*/ */
public static byte[] buildGraphicInput(int gocaX, int gocaY, int buttonOrAidCode, public static byte[] buildGraphicInput(int gocaX, int gocaY, int buttonOrAidCode,
boolean isMouseAction, boolean isShift, boolean isCtrl, boolean isMouseAction, boolean isShift, boolean isCtrl,
int pickedSegId, int pickTag) { int pickedSegId, int pickTag) {
return buildGraphicInput(gocaX, gocaY, 0, 0, buttonOrAidCode, isMouseAction, isShift, isCtrl, pickedSegId, pickTag);
}
/**
* Builds the 56-byte Graphic Input Structured Field with cursor row/col, picked segment ID and correlation tag.
*/
public static byte[] buildGraphicInput(int gocaX, int gocaY, int row, int col, int buttonOrAidCode,
boolean isMouseAction, boolean isShift, boolean isCtrl,
int pickedSegId, int pickTag) {
byte[] sf = new byte[MASK.length]; byte[] sf = new byte[MASK.length];
System.arraycopy(MASK, 0, sf, 0, MASK.length); System.arraycopy(MASK, 0, sf, 0, MASK.length);
@@ -67,6 +55,14 @@ public class GraphicInputBuilder {
sf[0] = (byte) ((MASK.length >> 8) & 0xFF); sf[0] = (byte) ((MASK.length >> 8) & 0xFF);
sf[1] = (byte) (MASK.length & 0xFF); sf[1] = (byte) (MASK.length & 0xFF);
// Bytes 16-19: Cursor Row & Column
if (row > 0 || col > 0) {
sf[16] = (byte) (row & 0xFF);
sf[17] = (byte) (col & 0xFF);
sf[18] = (byte) (row & 0xFF);
sf[19] = (byte) (col & 0xFF);
}
// Byte 24-25: GOCA X coordinate (signed 16-bit big-endian) // Byte 24-25: GOCA X coordinate (signed 16-bit big-endian)
sf[24] = (byte) ((gocaX >> 8) & 0xFF); sf[24] = (byte) ((gocaX >> 8) & 0xFF);
sf[25] = (byte) (gocaX & 0xFF); sf[25] = (byte) (gocaX & 0xFF);
@@ -115,11 +115,6 @@ public class GraphicsPlane {
public synchronized void setScreenDimensions(int cols, int rows) { public synchronized void setScreenDimensions(int cols, int rows) {
this.screenCols = cols > 0 ? cols : 80; this.screenCols = cols > 0 ? cols : 80;
this.screenRows = rows > 0 ? rows : 24; this.screenRows = rows > 0 ? rows : 24;
int targetW = this.screenCols * 9;
int targetH = this.screenRows * 12;
if (this.canvasWidth != targetW || this.canvasHeight != targetH) {
resize(targetW, targetH);
}
} }
public int getScreenCols() { public int getScreenCols() {
@@ -130,29 +125,50 @@ public class GraphicsPlane {
return screenRows; return screenRows;
} }
public int getTotalWidth() {
int cols = screenCols > 0 ? screenCols : 80;
return cols * 9;
}
public int getTotalHeight() {
int rows = screenRows > 0 ? screenRows : 24;
return rows * 12;
}
public int getXMax() {
int totalW = getTotalWidth();
return (totalW - 1) / 2 + (totalW - 1) % 2;
}
public int getYMax() {
int totalH = getTotalHeight();
return (totalH - 1) / 2;
}
/** /**
* Maps a 3179G / GOCA signed coordinate (centered at screen midpoint) to canvas pixel X as a double. * Maps a 3179G / GOCA signed coordinate (centered at screen midpoint) to canvas pixel X as a double.
* IBM 3179G / HOD presentation space coordinate range is [-xMax .. +xMax] (width = cols * 9).
*/ */
public double mapXDouble(double gocaX) { public double mapXDouble(double gocaX) {
int nominalWidth = screenCols * 9; int totalW = getTotalWidth();
int xMax = (nominalWidth - 1) / 2 + ((nominalWidth - 1) % 2 != 0 ? 1 : 0); int xMax = getXMax();
double nx = gocaX + xMax; double nx = gocaX + xMax;
return (nx * canvasWidth) / (double) (nominalWidth > 0 ? nominalWidth : 1); return (nx * canvasWidth) / (double) totalW;
} }
/** /**
* Maps a 3179G / GOCA signed coordinate (centered at screen midpoint, bottom-up) to canvas pixel Y (top-down) as a double. * Maps a 3179G / GOCA signed coordinate (centered at screen midpoint, bottom-up) to canvas pixel Y (top-down) as a double.
* IBM 3179G / HOD presentation space coordinate range is [-yMax .. +yMax] (height = rows * 12).
*/ */
public double mapYDouble(double gocaY) { public double mapYDouble(double gocaY) {
int nominalHeight = screenRows * 12; int totalH = getTotalHeight();
int yMax = (nominalHeight - 1) / 2; int yMax = getYMax();
double ny = yMax - gocaY; double ny = yMax - gocaY;
return (ny * canvasHeight) / (double) (nominalHeight > 0 ? nominalHeight : 1); return (ny * canvasHeight) / (double) totalH;
} }
/** /**
* Maps a 3179G / GOCA signed coordinate (centered at screen midpoint) to canvas pixel X. * Maps a 3179G / GOCA signed coordinate (centered at screen midpoint) to canvas pixel X.
* Coordinate space is symmetric: -xMax to +xMax, where nominalWidth = cols * 9 (e.g. 720 for 80 cols).
*/ */
public int mapX(int gocaX) { public int mapX(int gocaX) {
return (int) Math.round(mapXDouble((double) gocaX)); return (int) Math.round(mapXDouble((double) gocaX));
@@ -160,8 +176,6 @@ public class GraphicsPlane {
/** /**
* Maps a 3179G / GOCA signed coordinate (centered at screen midpoint, bottom-up) to canvas pixel Y (top-down). * Maps a 3179G / GOCA signed coordinate (centered at screen midpoint, bottom-up) to canvas pixel Y (top-down).
* Coordinate space is symmetric: -yMax to +yMax, where nominalHeight = rows * 12 (e.g. 516 for 43 rows).
* NOTE: Do not apply arbitrary offsets here. The GOCA coordinate system is 1:1 synchronized with host GDDM.
*/ */
public int mapY(int gocaY) { public int mapY(int gocaY) {
return (int) Math.round(mapYDouble((double) gocaY)); return (int) Math.round(mapYDouble((double) gocaY));
@@ -172,20 +186,20 @@ public class GraphicsPlane {
* Invariant: unmapX(mapX(x)) == x for all valid canvas pixels. * Invariant: unmapX(mapX(x)) == x for all valid canvas pixels.
*/ */
public int unmapX(int px) { public int unmapX(int px) {
int nominalWidth = screenCols * 9; int totalW = getTotalWidth();
int xMax = (nominalWidth - 1) / 2 + ((nominalWidth - 1) % 2 != 0 ? 1 : 0); int xMax = getXMax();
int nx = (int) Math.round((double) px * nominalWidth / (canvasWidth > 0 ? canvasWidth : 1)); int nx = (int) Math.round((double) px * totalW / (canvasWidth > 0 ? canvasWidth : 1));
return nx - xMax; return nx - xMax;
} }
/** /**
* Maps a canvas pixel Y coordinate (top-down) back to GOCA signed coordinate (bottom-up). * Maps a canvas pixel Y coordinate (top-down) back to GOCA signed coordinate (bottom-up, -yMax..+yMax).
* Invariant: unmapY(mapY(y)) == y for all valid canvas pixels. * Invariant: unmapY(mapY(y)) == y for all valid canvas pixels.
*/ */
public int unmapY(int py) { public int unmapY(int py) {
int nominalHeight = screenRows * 12; int totalH = getTotalHeight();
int yMax = (nominalHeight - 1) / 2; int yMax = getYMax();
int ny = (int) Math.round((double) py * nominalHeight / (canvasHeight > 0 ? canvasHeight : 1)); int ny = (int) Math.round((double) py * totalH / (canvasHeight > 0 ? canvasHeight : 1));
return yMax - ny; return yMax - ny;
} }
@@ -324,12 +324,27 @@ public class InputProcessor {
int gy = gocaDecoder.getGraphicCursorY(); int gy = gocaDecoder.getGraphicCursorY();
int pickedSeg = gocaDecoder.findPickedSegment(gx, gy); int pickedSeg = gocaDecoder.findPickedSegment(gx, gy);
int pickTag = gocaDecoder.getSegmentTag(pickedSeg); int pickTag = gocaDecoder.getSegmentTag(pickedSeg);
int cursorAddr = screen != null ? screen.getCursorAddress() : 0;
int cols = (screen != null && screen.getCols() > 0) ? screen.getCols() : 80;
int row = cursorAddr / cols;
int col = cursorAddr % cols;
if (gocaDecoder.getGraphicsPlane() != null) {
int px = gocaDecoder.getGraphicsPlane().mapX(gx);
int py = gocaDecoder.getGraphicsPlane().mapY(gy);
int canvasW = gocaDecoder.getGraphicsPlane().getCanvasWidth();
int canvasH = gocaDecoder.getGraphicsPlane().getCanvasHeight();
int numRows = (screen != null && screen.getRows() > 0) ? screen.getRows() : 43;
if (canvasH > 0) row = (py * numRows) / canvasH;
if (canvasW > 0) col = (px * cols) / canvasW;
}
byte[] sf = haus.nightmare.lib3270j.graphics.GraphicInputBuilder.buildGraphicInput( byte[] sf = haus.nightmare.lib3270j.graphics.GraphicInputBuilder.buildGraphicInput(
gx, gy, button, true, isShift, isCtrl, pickedSeg, pickTag gx, gy, row, col, button, true, isShift, isCtrl, pickedSeg, pickTag
); );
System.err.println(String.format( System.err.println(String.format(
"sendGraphicMouseAid: goca=(%d, %d) pickedSeg=%d pickTag=%d btn=%d shift=%b ctrl=%b", "sendGraphicMouseAid: goca=(%d, %d) row=%d col=%d pickedSeg=%d pickTag=%d btn=%d shift=%b ctrl=%b",
gx, gy, pickedSeg, pickTag, button, isShift, isCtrl gx, gy, row, col, pickedSeg, pickTag, button, isShift, isCtrl
)); ));
out.write(AID_SF); out.write(AID_SF);
try { try {
@@ -218,22 +218,22 @@ public class GocaDecoderTest {
@Test @Test
public void test3179GCoordinateMapping() { public void test3179GCoordinateMapping() {
GraphicsPlane plane = new GraphicsPlane(720, 516); GraphicsPlane plane = new GraphicsPlane(1000, 750);
plane.setScreenDimensions(80, 43); plane.setScreenDimensions(80, 43);
// Screen center (0, 0) should map to canvas center (360, 257) // Screen center (0, 0) should map to canvas center
assertEquals(360, plane.mapX(0)); assertEquals(500, plane.mapX(0));
assertEquals(257, plane.mapY(0)); assertEquals(374, plane.mapY(0));
// Left edge (-360) should map to 0 // Left edge (-xMax) should map to 0
assertEquals(0, plane.mapX(-360)); assertEquals(0, plane.mapX(-plane.getXMax()));
// Right edge (+359) should map to 719 // Right edge (+xMax) should map to 1000
assertEquals(719, plane.mapX(359)); assertEquals(1000, plane.mapX(plane.getXMax()));
// Top edge (+257) should map to 0 // Top edge (+yMax) should map to 0
assertEquals(0, plane.mapY(257)); assertEquals(0, plane.mapY(plane.getYMax()));
// Bottom edge (-258) should map to 515 // Bottom edge (yMax - totalHeight) should map to 750
assertEquals(515, plane.mapY(-258)); assertEquals(750, plane.mapY(plane.getYMax() - plane.getTotalHeight()));
// Bidirectional roundtrip mapping must be exact // Bidirectional roundtrip mapping must be exact
assertEquals(0, plane.unmapX(plane.mapX(0))); assertEquals(0, plane.unmapX(plane.mapX(0)));
@@ -512,7 +512,7 @@ public class GocaDecoderTest {
@Test @Test
public void testMultiPolygonAreaFilling() { public void testMultiPolygonAreaFilling() {
GraphicsPlane plane = new GraphicsPlane(200, 200); GraphicsPlane plane = new GraphicsPlane(1000, 750);
GocaDecoder decoder = new GocaDecoder(plane); GocaDecoder decoder = new GocaDecoder(plane);
// Sequence: GBAR (0x68) short form 0x80 (bounded, always filled in GOCA) // Sequence: GBAR (0x68) short form 0x80 (bounded, always filled in GOCA)
@@ -786,11 +786,18 @@ public class GocaDecoderTest {
} }
assertTrue(foundBlue, "Slide body must contain stippled blue pixels"); assertTrue(foundBlue, "Slide body must contain stippled blue pixels");
// Verify that the boundary (e.g. at (10, 50)) is drawn in White! // Verify that the boundary (e.g. at X=10) is drawn in White!
int whiteArgb = GocaConstants.GOCA_COLORS[7]; int whiteArgb = GocaConstants.GOCA_COLORS[7];
int borderX = plane.mapX(10); int borderX = plane.mapX(10);
int borderY = plane.mapY(50); boolean foundWhite = false;
int borderPixel = plane.getRgbBuffer()[borderY * plane.getCanvasWidth() + borderX]; int y1 = Math.min(plane.mapY(10), plane.mapY(90));
assertEquals(whiteArgb, borderPixel, "Slide border outline must be drawn in White"); int y2 = Math.max(plane.mapY(10), plane.mapY(90));
for (int y = y1; y <= y2; y++) {
if (plane.getRgbBuffer()[y * plane.getCanvasWidth() + borderX] == whiteArgb) {
foundWhite = true;
break;
}
}
assertTrue(foundWhite, "Slide border outline must be drawn in White");
} }
} }