GDDM Tweaking

This commit is contained in:
2026-08-27 17:05:23 +00:00
parent ebc0b5ae6f
commit 540a8dfd1d
13 changed files with 886 additions and 138 deletions
@@ -223,6 +223,10 @@ public class Telnet3270Client {
public void sysReq() { inputProcessor.sysReq(); }
/** Reset (unlock keyboard). */
public void reset() { inputProcessor.reset(); }
/** Trigger 3270 CURSR SEL (Cursor Select) key at current cursor position. */
public boolean cursorSelect() { return inputProcessor.cursorSelect(); }
/** Trigger Light Pen selection at the specified screen address. */
public boolean lightPenSelect(int address) { return inputProcessor.lightPenSelect(address); }
public haus.nightmare.lib3270j.graphics.ProgramSymbolManager getProgramSymbolManager() {
return dsProcessor.getProgramSymbolManager();
@@ -832,9 +832,15 @@ public class DataStreamProcessor {
programSymbolManager.loadps(psData);
}
break;
case haus.nightmare.lib3270j.graphics.GocaConstants.SF_OBJDATA_SUB: { // 0x0F: Object Control / Data Unit
// Per IBM HOD processDataunit(), 0x0F activates graphic cursor and initializes data unit
log.info(String.format("SF 0x0F sub=0x0F (Data Unit Object Control len=%d): activating graphics cursor", fieldLen));
gocaDecoder.setGraphicsCursorActive(true);
notifyScreenUpdated();
break;
}
case haus.nightmare.lib3270j.graphics.GocaConstants.SF_OBJCNTL_SUB: // 0x11: Object Control (Procedure orders)
case haus.nightmare.lib3270j.graphics.GocaConstants.SF_OBJPICT_SUB: // 0x10: Object Picture (Picture segments)
case haus.nightmare.lib3270j.graphics.GocaConstants.SF_OBJDATA_SUB: { // 0x0F: Object Data (GOCA draw orders)
case haus.nightmare.lib3270j.graphics.GocaConstants.SF_OBJPICT_SUB: { // 0x10: Object Picture (Picture segments)
int flags = (fieldLen >= 6) ? (data[pos + 5] & 0xC0) : 0xC0;
int orderOffset = (fieldLen >= 7) ? (pos + 7) : (pos + 4);
int orderLen = Math.max(0, fieldLen - (orderOffset - pos));
@@ -0,0 +1,206 @@
package haus.nightmare.lib3270j.graphics;
import java.awt.Point;
/**
* Dedicated scaling layer for IBM 3179G / GDDM GOCA graphics presentation space.
* Matches IBM Host On-Demand (HODTransform.java, PS3179G.java, HODInput.java).
*
* <p>Coordinate System Architecture:
* <ul>
* <li>GOCA Presentation Space: Centered at (0, 0).
* X ranges [-xMax .. +xMax] where width = cols * defaultCharWidth (defsx = 9).
* Y ranges [-yMax .. +yMax] where height = rows * defaultCharHeight (defsy = 16 or 12).
* (+Y is UP, -Y is DOWN, +X is RIGHT, -X is LEFT).</li>
* <li>Base Presentation Space (px, py): Unsigned top-left origin (0, 0).
* px = gx + xMax (ranges 0 .. totalWidth).
* py = yMax - gy (ranges 0 .. totalHeight).</li>
* <li>Display Screen / Window Space (sx, sy): Physical Swing pixel coordinates relative
* to terminal character grid rendering offset (ox, oy).
* sx = ox + (int) Math.round(px * transformX).
* sy = oy + (int) Math.round(py * transformY).
* where transformX = cellWidth / defaultCharWidth, transformY = cellHeight / defaultCharHeight.</li>
* </ul>
*
* <p>Why simple fixed pixel scaling or hardcoded multipliers failed in past iterations:
* <ul>
* <li><b>Failure Mode 1</b>: Hardcoding totalHeight = rows * 12 caused coordinate truncation in 43-row mode (Model 4),
* capping yMax at 257 instead of 343 and making the top menu / EXIT button unreachable.</li>
* <li><b>Failure Mode 2</b>: Hardcoding totalHeight = rows * 16 without viewport alignment resulted in yMax = 343.
* In ADMDRAW, GDDM defines the entire active drawing area between y = -169 and y = +200 (~370 units total height).
* Mapping this into a 688-high canvas placed the top menu at Row 9.2 (middle of the screen) with a large blank void above,
* and clicking the visual menu emitted gy = 224 (missing the [187..200] menu bounding box).</li>
* <li><b>Failure Mode 3</b>: Intermediate raster buffering (e.g. fixed 800x600 canvas stretched to gridW x gridH)
* introduced rounding artifacts in bidirectional coordinate conversion (unmapX/unmapY).</li>
* <li><b>Failure Mode 4</b>: Overlapping dropdown menus (FILE, DRAW, TRANSFORM) lingering simultaneously when
* segment retention/clearing was decoupled from GDDM's segment lifecycle.</li>
* </ul>
*/
public class GddmCoordinateTransform {
public static final int DEFAULT_CHAR_WIDTH = 9; // defsx: 3179G standard character cell width
public static final int DEFAULT_CHAR_HEIGHT_16 = 16; // defsy: 3179G standard character cell height
public static final int DEFAULT_CHAR_HEIGHT_12 = 12; // defsy: 3279-2 alternate character cell height
private int defaultCharWidth = DEFAULT_CHAR_WIDTH;
private int defaultCharHeight = DEFAULT_CHAR_HEIGHT_16;
private int screenCols = 80;
private int screenRows = 24;
private double transformX = 1.0;
private double transformY = 1.0;
public GddmCoordinateTransform() {
this(80, 24, DEFAULT_CHAR_WIDTH, DEFAULT_CHAR_HEIGHT_16);
}
public GddmCoordinateTransform(int screenCols, int screenRows) {
this(screenCols, screenRows, DEFAULT_CHAR_WIDTH, DEFAULT_CHAR_HEIGHT_16);
}
public GddmCoordinateTransform(int screenCols, int screenRows, int defCharWidth, int defCharHeight) {
this.screenCols = screenCols > 0 ? screenCols : 80;
this.screenRows = screenRows > 0 ? screenRows : 24;
this.defaultCharWidth = defCharWidth > 0 ? defCharWidth : DEFAULT_CHAR_WIDTH;
this.defaultCharHeight = defCharHeight > 0 ? defCharHeight : DEFAULT_CHAR_HEIGHT_16;
}
/**
* Updates screen grid and cell dimensions from UI.
*
* @param cellWidth Width of a character cell in Swing pixels
* @param cellHeight Height of a character cell in Swing pixels
*/
public void updateDisplayMetrics(int cellWidth, int cellHeight) {
if (cellWidth > 0 && defaultCharWidth > 0) {
this.transformX = (double) cellWidth / (double) defaultCharWidth;
}
if (cellHeight > 0 && defaultCharHeight > 0) {
this.transformY = (double) cellHeight / (double) defaultCharHeight;
}
}
public void setScreenDimensions(int cols, int rows) {
if (cols > 0) this.screenCols = cols;
if (rows > 0) this.screenRows = rows;
}
public void setDefaultCharMetrics(int defWidth, int defHeight) {
if (defWidth > 0) this.defaultCharWidth = defWidth;
if (defHeight > 0) this.defaultCharHeight = defHeight;
}
public int getTotalWidth() {
return screenCols * defaultCharWidth;
}
public int getTotalHeight() {
return screenRows * defaultCharHeight;
}
public int getXMax() {
int totalW = getTotalWidth();
return (totalW - 1) / 2 + (totalW - 1) % 2;
}
public int getYMax() {
int totalH = getTotalHeight();
return (totalH - 1) / 2;
}
public double getTransformX() {
return transformX;
}
public double getTransformY() {
return transformY;
}
public int getDefaultCharWidth() {
return defaultCharWidth;
}
public int getDefaultCharHeight() {
return defaultCharHeight;
}
/**
* Converts a GOCA signed coordinate (gx, gy) to base presentation space coordinate (px, py).
*/
public Point gocaToBase(int gx, int gy) {
int px = gx + getXMax();
int py = getYMax() - gy;
return new Point(px, py);
}
/**
* Converts a base presentation space coordinate (px, py) to GOCA signed coordinate (gx, gy).
*/
public Point baseToGoca(int px, int py) {
int gx = px - getXMax();
int gy = getYMax() - py;
return new Point(gx, gy);
}
/**
* Converts a GOCA signed coordinate (gx, gy) directly to Swing screen pixel coordinate (sx, sy).
*/
public Point gocaToScreenPixel(int gx, int gy, int ox, int oy, int cellWidth, int cellHeight) {
updateDisplayMetrics(cellWidth, cellHeight);
int px = gx + getXMax();
int py = getYMax() - gy;
int sx = ox + (int) Math.round(px * transformX);
int sy = oy + (int) Math.round(py * transformY);
return new Point(sx, sy);
}
/**
* Converts a mouse click in Swing screen coordinates (mouseX, mouseY) directly to GOCA signed coordinate (gx, gy).
* Matches HODInput.getHODInput:
* x = mouseX / transformX
* y = mouseY / transformY
* gx = x - xMax
* gy = yMax - y
*/
public Point screenPixelToGoca(int mouseX, int mouseY, int ox, int oy, int cellWidth, int cellHeight) {
updateDisplayMetrics(cellWidth, cellHeight);
double relX = mouseX - ox;
double relY = mouseY - oy;
int px = (transformX > 0) ? (int) Math.round(relX / transformX) : (int) relX;
int py = (transformY > 0) ? (int) Math.round(relY / transformY) : (int) relY;
int gx = px - getXMax();
int gy = getYMax() - py;
return new Point(gx, gy);
}
/**
* Maps GOCA (gx, gy) to fixed canvas buffer dimensions (canvasWidth, canvasHeight).
*/
public Point gocaToCanvasPixel(int gx, int gy, int canvasWidth, int canvasHeight) {
int totalW = getTotalWidth();
int totalH = getTotalHeight();
int xMax = getXMax();
int yMax = getYMax();
int px = (int) Math.round((double) (gx + xMax) * canvasWidth / (totalW > 0 ? totalW : 1));
int py = (int) Math.round((double) (yMax - gy) * canvasHeight / (totalH > 0 ? totalH : 1));
return new Point(px, py);
}
/**
* Maps fixed canvas buffer pixel (canvasX, canvasY) to GOCA (gx, gy).
*/
public Point canvasPixelToGoca(int canvasX, int canvasY, int canvasWidth, int canvasHeight) {
int totalW = getTotalWidth();
int totalH = getTotalHeight();
int xMax = getXMax();
int yMax = getYMax();
int nx = (int) Math.round((double) canvasX * totalW / (canvasWidth > 0 ? canvasWidth : 1));
int ny = (int) Math.round((double) canvasY * totalH / (canvasHeight > 0 ? canvasHeight : 1));
return new Point(nx - xMax, yMax - ny);
}
}
@@ -164,7 +164,7 @@ public class GocaDecoder {
for (int i = activeSegmentsInOrder.size() - 1; i >= 0; i--) {
SegmentBounds sb = activeSegmentsInOrder.get(i);
if (sb.contains(gx, gy, 25)) {
System.err.println(String.format(
logger.info(String.format(
"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
));
@@ -173,14 +173,14 @@ public class GocaDecoder {
}
for (SegmentBounds sb : segmentBoundsMap.values()) {
if (sb.contains(gx, gy, 25)) {
System.err.println(String.format(
logger.info(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));
logger.info(String.format("findPickedSegment: goca=(%d, %d) NO HIT (defaulting to 0/canvas)", gx, gy));
return 0;
}
@@ -436,7 +436,7 @@ public class GocaDecoder {
if (currentSegId != 0) {
SegmentBounds sb = segmentBoundsMap.get(currentSegId);
if (sb != null) {
System.err.println(String.format(
logger.info(String.format(
"GocaDecoder: ENDSEGM segId=%d bounds=[%d..%d, %d..%d] tag=%d",
sb.segId, sb.minX, sb.maxX, sb.minY, sb.maxY, sb.tag
));
@@ -805,12 +805,17 @@ public class GocaDecoder {
idx += 2;
break;
}
case GocaConstants.P_ERASE: { // 0x0A: Erase Graphics Presentation Space
case GocaConstants.P_ERASE: { // 0x0A: Erase Graphics Presentation Space (HODEraseGraphicsPlane)
plane.clear();
resetAttributes();
curX = 0;
curY = 0;
activeSegmentsInOrder.clear();
segmentBoundsMap.clear();
segmentStore.clear();
segmentOrderList.clear();
chainedTargets.clear();
logger.info("GOCA P_ERASE: erased graphics presentation space and cleared segment stores");
idx += 2;
break;
}
@@ -818,55 +823,15 @@ public class GocaDecoder {
idx += 12;
break;
}
case GocaConstants.P_SCUDEF: { // 0x21: Drawing Process Control / Segment Execute
if (idx + 5 < end) {
int pLen = data[idx + 1] & 0xFF;
if (pLen >= 6) {
int flags0 = data[idx + 2] & 0xFF;
int flags1 = data[idx + 3] & 0xFF;
int startSeg = ((data[idx + 4] & 0xFF) << 8) | (data[idx + 5] & 0xFF);
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",
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++) {
byte[] segBytes = segmentStore.get(s);
if (segBytes != null) {
int savedSegId = currentSegId;
currentSegId = s;
SegmentBounds sb = segmentBoundsMap.computeIfAbsent(s, SegmentBounds::new);
activeSegmentsInOrder.remove(sb);
activeSegmentsInOrder.add(sb);
callDepth++;
decodeStreamDirect(segBytes, 0, segBytes.length);
callDepth--;
currentSegId = savedSegId;
}
}
}
}
case GocaConstants.P_SCUDEF: { // 0x21: Set Current Defaults (HODCurrentDefaults)
// Note: Per IBM 3179G / HOD architecture, 0x21 sets default drawing attributes (color, line, pattern).
// It is NOT an executive segment redraw order. A prior attempt treated 0x21 as an invented
// P_SCUDEF segment redraw loop, which caused old dropdown menus and segments to be repeatedly
// repainted on top of the screen, creating ghost artifacts and stale bounding boxes.
if (idx + 1 < end) {
int len = (data[idx + 1] & 0xFF) + 2;
idx += len;
int pLen = data[idx + 1] & 0xFF;
logger.fine(String.format("GOCA Set Current Defaults (0x21): len=%d", pLen));
idx += pLen + 2;
} else {
idx++;
}
@@ -1302,9 +1267,9 @@ public class GocaDecoder {
int textLen = end - pos;
if (textLen <= 0) return;
// IBM 3279 vector graphics base cell is 9x12
// IBM 3179G vector graphics base cell is 9x16
double cw = charWidth > 0 ? ((double) charWidth * plane.getCanvasWidth() / (plane.getScreenCols() * 9.0)) : 10.0;
double ch = charHeight > 0 ? ((double) charHeight * plane.getCanvasHeight() / (plane.getScreenRows() * 12.0)) : 14.0;
double ch = charHeight > 0 ? ((double) charHeight * plane.getCanvasHeight() / (plane.getScreenRows() * 16.0)) : 14.0;
int totalW = textLen * (charWidth > 0 ? charWidth : 9);
int totalH = (charHeight > 0 ? charHeight : 14);
@@ -11,8 +11,8 @@ public class GraphicInputBuilder {
0x00, 0x38, 0x0F, 0x0F, 0x00, (byte) 0xC0, 0x00, 0x40,
0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x23, 0x00, 0x23, 0x00, 0x00, 0x00, 0x1F, 0x01,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04,
0x00, 0x04, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, (byte) 0x80, 0x00
};
@@ -30,16 +30,17 @@ public class GraphicInputBuilder {
*/
public static byte[] buildGraphicInput(int gocaX, int gocaY, int buttonOrAidCode,
boolean isMouseAction, boolean isShift, boolean isCtrl) {
return buildGraphicInput(gocaX, gocaY, buttonOrAidCode, isMouseAction, isShift, isCtrl, 0, 0);
return buildGraphicInput(gocaX, gocaY, 0, 0, buttonOrAidCode, isMouseAction, isShift, isCtrl);
}
/**
* 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 (legacy overload).
* Note: Per IBM HOD / 3179G architecture, pick correlation is evaluated host-side by GDDM using (gx, gy).
*/
public static byte[] buildGraphicInput(int gocaX, int gocaY, int buttonOrAidCode,
boolean isMouseAction, boolean isShift, boolean isCtrl,
int pickedSegId, int pickTag) {
return buildGraphicInput(gocaX, gocaY, 0, 0, buttonOrAidCode, isMouseAction, isShift, isCtrl, pickedSegId, pickTag);
return buildGraphicInput(gocaX, gocaY, 0, 0, buttonOrAidCode, isMouseAction, isShift, isCtrl);
}
/**
@@ -48,6 +49,14 @@ public class GraphicInputBuilder {
public static byte[] buildGraphicInput(int gocaX, int gocaY, int row, int col, int buttonOrAidCode,
boolean isMouseAction, boolean isShift, boolean isCtrl,
int pickedSegId, int pickTag) {
return buildGraphicInput(gocaX, gocaY, row, col, buttonOrAidCode, isMouseAction, isShift, isCtrl);
}
/**
* Builds the 56-byte Graphic Input Structured Field matching IBM Host On-Demand (HODInput.java).
*/
public static byte[] buildGraphicInput(int gocaX, int gocaY, int row, int col, int buttonOrAidCode,
boolean isMouseAction, boolean isShift, boolean isCtrl) {
byte[] sf = new byte[MASK.length];
System.arraycopy(MASK, 0, sf, 0, MASK.length);
@@ -55,13 +64,8 @@ public class GraphicInputBuilder {
sf[0] = (byte) ((MASK.length >> 8) & 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);
}
// Bytes 16-19: Device correlation class descriptor (0x23, 0x00, 0x23, 0x00)
// Note: Preserved from MASK per IBM Host On-Demand (HODInput.java); do not overwrite with row/col.
// Byte 24-25: GOCA X coordinate (signed 16-bit big-endian)
sf[24] = (byte) ((gocaX >> 8) & 0xFF);
@@ -72,15 +76,15 @@ public class GraphicInputBuilder {
sf[27] = (byte) (gocaY & 0xFF);
if (isMouseAction) {
// Byte 28-31: Picked Segment ID (4 bytes big-endian)
sf[28] = (byte) ((pickedSegId >> 24) & 0xFF);
sf[29] = (byte) ((pickedSegId >> 16) & 0xFF);
sf[30] = (byte) ((pickedSegId >> 8) & 0xFF);
sf[31] = (byte) (pickedSegId & 0xFF);
// Byte 28-31: Fixed mouse trigger class constant (0x00000004 per IBM HODInput.java)
sf[28] = 0x00;
sf[29] = 0x00;
sf[30] = 0x00;
sf[31] = 0x04;
// Byte 32-33: Pick Tag / Correlation (2 bytes big-endian)
sf[32] = (byte) ((pickTag >> 8) & 0xFF);
sf[33] = (byte) (pickTag & 0xFF);
// Byte 32-33: Fixed mouse correlation class constant (0x0004 per IBM HODInput.java)
sf[32] = 0x00;
sf[33] = 0x04;
sf[34] = isShift ? (byte) 0x80 : (isCtrl ? (byte) 0x40 : 0x00);
sf[35] = (byte) (buttonOrAidCode == 2 ? 0x02 : 0x01); // Button 1 = Pick, Button 2 = Action
@@ -115,6 +115,7 @@ public class GraphicsPlane {
public synchronized void setScreenDimensions(int cols, int rows) {
this.screenCols = cols > 0 ? cols : 80;
this.screenRows = rows > 0 ? rows : 24;
this.transform.setScreenDimensions(this.screenCols, this.screenRows);
}
public int getScreenCols() {
@@ -125,14 +126,53 @@ public class GraphicsPlane {
return screenRows;
}
// Dedicated GDDM coordinate transformation scaling layer (matching IBM HODTransform)
private final GddmCoordinateTransform transform = new GddmCoordinateTransform();
public GddmCoordinateTransform getTransform() {
return transform;
}
public int getTotalWidth() {
int cols = screenCols > 0 ? screenCols : 80;
return cols * 9;
}
/**
* Total vertical presentation space units.
*
* <p>Historical / Architecture Note for Future Iterations:
* <ul>
* <li><b>Failed Approach 1 (Hardcoded 12-pitch / rows * 12)</b>:
* Used 516 units (yMax = 257) on 43-row screen. Caused upper coordinate truncation;
* menus at gy > 257 were clipped and mathematically impossible to pick.</li>
* <li><b>Failed Approach 2 (Hardcoded 16-pitch / rows * 16 without viewport alignment)</b>:
* Used 688 units (yMax = 343). Visual rendering was shifted downward by ~50 pixels relative
* to GDDM's internal picture space viewport, causing clicks at Row 7 to emit gy = 226
* which missed the GDDM menu hit box and caused terminal alarm beeps.</li>
* <li><b>Failed Approach 3 (Overwriting Graphic Input SF bytes 16-19 with row/col)</b>:
* Overwrote device correlation descriptor 0x00230023, causing GDDM Structured Field
* parser to reject inbound correlation packets as malformed.</li>
* <li><b>Failed Approach 4 (Misinterpreting Procedure Order 0x21 as Segment Redraw loop)</b>:
* Order 0x21 is HODCurrentDefaults (Set Current Defaults), not an executive redraw loop.
* Treating 0x21 as segment execution caused previous menu frames and stored segments
* to be repeatedly repainted over active screens, leaving behind visual ghost artifacts.</li>
* <li><b>Failed Approach 5 (Static yMax=343 scaling vs GDDM ADMDRAW Viewport bounds)</b>:
* In ADMDRAW, GDDM defines the graphics presentation space between y = -169 (canvas bottom)
* and y = +200 (top menu bar), with total height ~370 units. Mapping this into a static
* yMax = 343 (688 total height) renders the top menu at Row 9.2 (middle of the screen) with
* a large vertical void above it, and clicking the visual menu sends gy = 224 which misses
* the [187..200] menu bounding box. Furthermore, multiple dropdown menus (FILE, DRAW, TRANSFORM)
* overlapped simultaneously because segment retention and clearing was decoupled from GDDM's
* actual viewport state.</li>
* <li><b>Solution Path</b>: Use {@link GddmCoordinateTransform} for dynamic display and aspect
* scaling, ensuring host Query Reply metrics (SDH/AH) and GDDM Picture Space Window
* orders are synchronized with client-side mouse transformation.</li>
* </ul>
*/
public int getTotalHeight() {
int rows = screenRows > 0 ? screenRows : 24;
return rows * 12;
return rows * 16;
}
public int getXMax() {
@@ -158,7 +198,7 @@ public class GraphicsPlane {
/**
* 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).
* IBM 3179G / HOD presentation space coordinate range is [-yMax .. +yMax] (height = rows * 16).
*/
public double mapYDouble(double gocaY) {
int totalH = getTotalHeight();
@@ -555,8 +595,20 @@ public class GraphicsPlane {
int fill = (fillColorArgb != 0) ? fillColorArgb : GocaConstants.GOCA_COLORS[0];
int bg = bgColorArgb;
boolean isTransparentBlack = (fill == GocaConstants.GOCA_COLORS[8] || (fill & 0x00FFFFFF) == 0) && (bgMix != GocaConstants.MIX_OVER);
if (pattern != GocaConstants.PT_EMPTY && (pattern != 0 || !drawBoundary) && !isTransparentBlack) {
// Note / Logic for Future Iterations:
// In IBM 3179G / GDDM architecture, solid black fills (fillColor = GOCA_COLORS[8] / 0xFF000000,
// pattern = PT_SOLID / 16) are the standard mechanism used by host applications to ERASE
// dropdown menus, dialog boxes, and dynamic regions from the screen.
//
// A prior failed attempt added an 'isTransparentBlack' guard:
// boolean isTransparentBlack = (fill == GOCA_COLORS[8] || (fill & 0x00FFFFFF) == 0) && (bgMix != MIX_OVER);
// if (... && !isTransparentBlack)
// This caused GDDM's menu erasure rectangles (which use fillColor = Black and bgMix = 5) to be
// silently skipped and discarded. As a result, closed dropdown menus were never erased, causing
// multiple menus (FILE, DRAW, TRANSFORM) to linger and stack on top of each other permanently.
//
// Solid black fills (0xFF000000) must always be rasterized to overwrite and erase previously drawn pixels.
if (pattern != GocaConstants.PT_EMPTY && (pattern != 0 || !drawBoundary)) {
// Find polygon vertical bounds across all points
int minY = py[0];
int maxY = py[0];
@@ -298,7 +298,7 @@ public class InputProcessor {
sendAidResponse(out.toByteArray());
}
private void sendAidResponse(byte[] data) {
protected void sendAidResponse(byte[] data) {
if (fsm != null && fsm.getConnectionState() != null && fsm.getConnectionState().isSscp()) {
fsm.sendSscpLuData(data);
} else if (fsm != null) {
@@ -307,10 +307,10 @@ public class InputProcessor {
}
/**
* Transmit a mouse / light-pen graphic input AID matching IBM Host On-Demand PS3179G.
* Transmit a mouse / light-pen graphic input AID matching IBM Host On-Demand PS3179G and DS3270.
*/
public void sendGraphicMouseAid(int aidCode, int button, boolean isShift, boolean isCtrl) {
if (fsm == null || !fsm.getConnectionState().isFullSession()) {
if (fsm != null && fsm.getConnectionState() != null && !fsm.getConnectionState().isFullSession()) {
return;
}
if (isKeyboardLocked()) {
@@ -322,8 +322,6 @@ public class InputProcessor {
if (gocaDecoder != null && gocaDecoder.isGraphicsCursorActive()) {
int gx = gocaDecoder.getGraphicCursorX();
int gy = gocaDecoder.getGraphicCursorY();
int pickedSeg = gocaDecoder.findPickedSegment(gx, gy);
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;
@@ -340,16 +338,53 @@ public class InputProcessor {
}
byte[] sf = haus.nightmare.lib3270j.graphics.GraphicInputBuilder.buildGraphicInput(
gx, gy, row, col, button, true, isShift, isCtrl, pickedSeg, pickTag
gx, gy, row, col, button, true, isShift, isCtrl
);
System.err.println(String.format(
"sendGraphicMouseAid: goca=(%d, %d) row=%d col=%d pickedSeg=%d pickTag=%d btn=%d shift=%b ctrl=%b",
gx, gy, row, col, pickedSeg, pickTag, button, isShift, isCtrl
StringBuilder sfHex = new StringBuilder();
for (byte b : sf) {
sfHex.append(String.format("%02X ", b & 0xFF));
}
log.info(String.format(
"sendGraphicMouseAid: goca=(%d, %d) row=%d col=%d btn=%d shift=%b ctrl=%b SF_HEX=[%s]",
gx, gy, row, col, button, isShift, isCtrl, sfHex.toString().trim()
));
// Structured Field AID (0x88) + 56-byte Graphic Input SF
out.write(AID_SF);
try {
out.write(sf);
} catch (java.io.IOException ignored) {}
// Trailing AID + cursor address + modified fields matching HOD DS3270.sendMouseAid
out.write(aidCode);
byte[] caddr = encodeAddress(screen.getCursorAddress(), screen.getRows(), screen.getCols());
out.write(caddr[0] & 0xFF);
out.write(caddr[1] & 0xFF);
if (screen.isFormatted()) {
int size = screen.getRows() * screen.getCols();
for (int i = 0; i < size; i++) {
ExtendedAttribute ea = screen.getCell(i);
if (ea.isFieldAttribute() && faIsModified(ea.fa & 0xFF)) {
int fieldStart = (i + 1) % size;
out.write(ORDER_SBA);
byte[] addr = encodeAddress(fieldStart, screen.getRows(), screen.getCols());
out.write(addr[0] & 0xFF);
out.write(addr[1] & 0xFF);
int pos = fieldStart;
while (!screen.getCell(pos).isFieldAttribute()) {
int b = screen.getCell(pos).ec & 0xFF;
if (b != 0x00) {
out.write(b);
}
pos = (pos + 1) % size;
if (pos == fieldStart) break;
}
}
}
}
} else {
out.write(aidCode);
byte[] caddr = encodeAddress(screen.getCursorAddress(), screen.getRows(), screen.getCols());
@@ -363,11 +398,11 @@ public class InputProcessor {
// ========== Cursor movement ==========
/**
* Simulate a Text Light Pen selection.
* Simulate a Text Light Pen selection at the specified screen address.
* Conforms to IBM 3270 / Host On-Demand PS3270 processCurSelKey specifications.
*/
public boolean lightPenSelect(int address) {
if (screen == null || !screen.isFormatted()) {
System.out.println("LP: screen null or unformatted");
return false;
}
int size = screen.getRows() * screen.getCols();
@@ -375,15 +410,13 @@ public class InputProcessor {
address = ((address % size) + size) % size;
int faPos = screen.findFieldAttribute(address);
if (faPos < 0) {
System.out.println("LP: no FA found for addr=" + address);
return false;
}
ExtendedAttribute faCell = screen.getCell(faPos);
int fa = faCell.fa & 0xFF;
System.out.println("LP: addr=" + address + " faPos=" + faPos + " fa=0x" + String.format("%02X", fa)
+ " selectable=" + haus.nightmare.lib3270j.protocol.DS3270Constants.faIsSelectable(fa));
if (!haus.nightmare.lib3270j.protocol.DS3270Constants.faIsSelectable(fa)) {
if (!haus.nightmare.lib3270j.protocol.DS3270Constants.faIsSelectable(fa) ||
haus.nightmare.lib3270j.protocol.DS3270Constants.faIsZero(fa)) {
return false;
}
@@ -392,33 +425,52 @@ public class InputProcessor {
int ebcdic = desCell.ec & 0xFF;
char ascii = (char) desCell.ucs4;
screen.setCursorAddress(designatorPos);
// Set cursor to the clicked/selected position matching HOD / 3270 standards
screen.setCursorAddress(address);
if (ebcdic == 0x00 || ebcdic == 0x40 || ascii == ' ' || ascii == 0 || (ebcdic != 0x50 && ascii != '&' && ebcdic != 0x6F && ascii != '?' && ebcdic != 0x6E && ascii != '>')) {
faCell.fa |= haus.nightmare.lib3270j.protocol.DS3270Constants.FA_MODIFY;
sendAid(haus.nightmare.lib3270j.protocol.DS3270Constants.AID_SELECT);
return true;
} else if (ebcdic == 0x50 || ascii == '&') {
faCell.fa |= haus.nightmare.lib3270j.protocol.DS3270Constants.FA_MODIFY;
sendAid(haus.nightmare.lib3270j.protocol.DS3270Constants.AID_ENTER);
return true;
} else if (ebcdic == 0x6F || ascii == '?') {
desCell.ec = (byte) 0x6E;
desCell.ucs4 = '>';
faCell.fa |= haus.nightmare.lib3270j.protocol.DS3270Constants.FA_MODIFY;
screen.updateDisplaySnapshot();
return true;
} else if (ebcdic == 0x6E || ascii == '>') {
desCell.ec = (byte) 0x6F;
desCell.ucs4 = '?';
faCell.fa &= ~haus.nightmare.lib3270j.protocol.DS3270Constants.FA_MODIFY;
screen.updateDisplaySnapshot();
return true;
} else {
// Immediate Selection (Space or Null) -> AID_SELECT (0x7E)
if (ebcdic == 0x00 || ebcdic == 0x40 || ascii == ' ' || ascii == 0 || ascii == '\u3000') {
faCell.fa |= haus.nightmare.lib3270j.protocol.DS3270Constants.FA_MODIFY;
sendAid(haus.nightmare.lib3270j.protocol.DS3270Constants.AID_SELECT);
return true;
}
// Enter Immediate (&) -> AID_ENTER (0x7D)
else if (ebcdic == 0x50 || ascii == '&' || ascii == '\uFF06') {
faCell.fa |= haus.nightmare.lib3270j.protocol.DS3270Constants.FA_MODIFY;
sendAid(haus.nightmare.lib3270j.protocol.DS3270Constants.AID_ENTER);
return true;
}
// Deferred Selection (? -> >)
else if (ebcdic == 0x6F || ascii == '?' || ascii == '\uFF1F') {
desCell.ec = (byte) 0x6E;
desCell.ucs4 = '>';
faCell.fa |= haus.nightmare.lib3270j.protocol.DS3270Constants.FA_MODIFY;
screen.markAllChanged();
screen.updateDisplaySnapshot();
return true;
}
// Deferred Deselection (> -> ?)
else if (ebcdic == 0x6E || ascii == '>' || ascii == '\uFF1E') {
desCell.ec = (byte) 0x6F;
desCell.ucs4 = '?';
faCell.fa &= ~haus.nightmare.lib3270j.protocol.DS3270Constants.FA_MODIFY;
screen.markAllChanged();
screen.updateDisplaySnapshot();
return true;
}
// Any other character is an invalid designator and cannot be selected
else {
return false;
}
}
/**
* Simulate the 3270 CURSR SEL (Cursor Select) key at the current cursor position.
* Equivalent to processCurSelKey in IBM Host On-Demand PS3270.
*/
public boolean cursorSelect() {
if (screen == null) return false;
return lightPenSelect(screen.getCursorAddress());
}
public void cursorUp() {