GDDM Tweaking
Build and Test j3270 / Build JAR & Run Tests (push) Successful in 41s

This commit is contained in:
2026-08-27 18:14:39 +00:00
parent 540a8dfd1d
commit 2e9b6d325a
9 changed files with 185 additions and 47 deletions
@@ -41,7 +41,7 @@ public class DataStreamProcessor {
// Graphics & Programmed Symbols
private final haus.nightmare.lib3270j.graphics.ProgramSymbolManager programSymbolManager = new haus.nightmare.lib3270j.graphics.ProgramSymbolManager();
private final haus.nightmare.lib3270j.graphics.GraphicsPlane graphicsPlane = new haus.nightmare.lib3270j.graphics.GraphicsPlane(800, 600);
private final haus.nightmare.lib3270j.graphics.GraphicsPlane graphicsPlane = new haus.nightmare.lib3270j.graphics.GraphicsPlane(720, 384);
private final haus.nightmare.lib3270j.graphics.GocaDecoder gocaDecoder = new haus.nightmare.lib3270j.graphics.GocaDecoder(graphicsPlane);
private final java.io.ByteArrayOutputStream gocaAccumulator = new java.io.ByteArrayOutputStream();
private int currentGocaSubtype = 0;
@@ -853,6 +853,11 @@ public class DataStreamProcessor {
sfSubId, fieldLen, flags, (orderOffset - pos), orderLen, hexDump.toString().trim()));
graphicsPlane.setScreenDimensions(screen.getCols(), screen.getRows());
int targetW = screen.getCols() * 9;
int targetH = screen.getRows() * 16;
if (graphicsPlane.getCanvasWidth() != targetW || graphicsPlane.getCanvasHeight() != targetH) {
graphicsPlane.resize(targetW, targetH);
}
if (flags == 0x80) { // SPAN_FIRST
gocaAccumulator.reset();
@@ -293,14 +293,36 @@ public class QueryReplyBuilder {
out.write((Yr_3279_2 >> 16) & 0xFF);
out.write((Yr_3279_2 >> 8) & 0xFF);
out.write(Yr_3279_2 & 0xFF);
out.write(SW_3279_2); // AW
out.write(SH_3279_2); // AH
int charW = getCharWidth();
int charH = getCharHeight();
out.write(charW); // AW
out.write(charH); // AH
int buf = maxCols * maxRows;
out.write((buf >> 8) & 0xFF); // buffer size high
out.write(buf & 0xFF); // buffer size low
return out.toByteArray();
}
public int getCharWidth() {
return SW_3279_2; // 9
}
public int getCharHeight() {
// ARCHITECTURAL NOTE ON 3179G GOCA VERTICAL ALIGNMENT & QUERY REPLIES:
// Why hardcoding SH = 12 (0x0C) in Character Sets & Usable Area failed in past iterations:
// When SDH/AH is declared as 12 (0x0C) in Query Reply, the mainframe host GDDM engine computes
// total presentation space as rows * 12 (e.g. 43 * 12 = 516 units, yMax = 257).
// GDDM then places the top menu bar at Row 1 (gy = 187..200).
// Meanwhile, the client emulator rendered into a 16-pitch grid (43 * 16 = 688 units, yMax = 343).
// On a 688-unit canvas, gy = 200 mapped to Row 9.2 (middle of the screen), leaving a massive void above.
// When the user clicked on the visual menu drawn at Row 9, the client emitted gy = 189 with cursor at Row 9,
// which GDDM rejected as outside its menu hit box (causing terminal alarm beeps).
//
// Solution: Declare SDH = 16 (0x10) when Vector Graphics is enabled (3179G standard), ensuring host GDDM
// and client GraphicsPlane share the exact same 16-pitch presentation space (720x688, yMax = 343).
return graphicsMode.isVectorGraphicsEnabled() ? 0x10 : SH_3279_2;
}
private byte[] buildAlphaPartitions(int maxRows) {
int bufSize = screen.getMaxCols() * screen.getMaxRows();
ByteArrayOutputStream out = new ByteArrayOutputStream(4);
@@ -312,13 +334,16 @@ public class QueryReplyBuilder {
}
private byte[] buildCharsets() {
int charW = getCharWidth();
int charH = getCharHeight();
if (graphicsMode.isProgrammedSymbolsEnabled()) {
// Programmed Symbols mode (3279 PS with LoadPS 0x0A, flags1 = 0xA2 for GE + PS + CGCSGID)
ByteArrayOutputStream out = new ByteArrayOutputStream(65);
out.write(0xa2); // flags: GE (0x80), PS/Loadable Charsets (0x20), CGCSGID present (0x02)
out.write(0x00); // more flags
out.write(SW_3279_2); // SDW (9)
out.write(SH_3279_2); // SDH (12)
out.write(charW); // SDW (9)
out.write(charH); // SDH (16 for 3179G, 12 for 3279-2)
out.write(0x0a); // Load PS format types supported: Format 1 and Format 3
out.write(0x00); // Load PS device type (high)
out.write(0x00); // Load PS device type (low)
@@ -343,8 +368,8 @@ public class QueryReplyBuilder {
ByteArrayOutputStream out = new ByteArrayOutputStream(23);
out.write(0x82); // flags: GE, CGCSGID present
out.write(0x00); // more flags
out.write(SW_3279_2); // SDW - default char width (9)
out.write(SH_3279_2); // SDH - default char height (12)
out.write(charW); // SDW - default char width (9)
out.write(charH); // SDH - default char height (16 for 3179G, 12 for 3279-2)
out.write(0x00); // LoadPS format (0x00)
out.write(0x00);
out.write(0x00);
@@ -6,9 +6,14 @@ package haus.nightmare.lib3270j.graphics;
*/
public class GraphicInputBuilder {
// 56-byte template mask from IBM Host On-Demand (HODInput.java)
// 56-byte template mask from IBM Host On-Demand (HODInput.java).
// Note on Structured Field Length (bytes 0-1):
// IBM HOD sets bytes 0-1 to 0x00 0x34 (52 decimal). In 3270 GOCA architecture,
// the Data Unit Object Control payload is 52 bytes. Overwriting this with 0x00 0x38 (56)
// causes the host's 3270 inbound structured field decoder to misalign the trailing AID byte
// (0x7D) and cursor address by 4 bytes, causing host GDDM to reject the click with an alarm beep.
private static final byte[] MASK = new byte[] {
0x00, 0x38, 0x0F, 0x0F, 0x00, (byte) 0xC0, 0x00, 0x40,
0x00, 0x34, 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, 0x04,
@@ -60,9 +65,8 @@ public class GraphicInputBuilder {
byte[] sf = new byte[MASK.length];
System.arraycopy(MASK, 0, sf, 0, MASK.length);
// Byte 0-1: Structured Field Length (56 bytes)
sf[0] = (byte) ((MASK.length >> 8) & 0xFF);
sf[1] = (byte) (MASK.length & 0xFF);
// Bytes 0-1: Structured Field Length (0x0034 = 52 decimal per IBM HODInput.java / GOCA architecture).
// Preserved from MASK; do not overwrite with MASK.length (56).
// 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.
@@ -36,14 +36,14 @@ public class GraphicsPlane {
{-1, -1, -1, -1, -1, -1, -1, -1} // 16: Solid
};
private int canvasWidth = 800;
private int canvasHeight = 600;
// 3179G Presentation Space metrics: 9x16 cell pitch (720x384 for Model 2, 720x688 for Model 4)
private int screenCols = 80;
private int screenRows = 24;
private int canvasWidth = 720;
private int canvasHeight = 384;
private int[] rgbBuffer;
private boolean hasContent = false;
private long updateCount = 0;
private int screenCols = 80;
private int screenRows = 24;
private ProgramSymbolManager programSymbolManager;
public void setProgramSymbolManager(ProgramSymbolManager psm) {
@@ -595,20 +595,19 @@ public class GraphicsPlane {
int fill = (fillColorArgb != 0) ? fillColorArgb : GocaConstants.GOCA_COLORS[0];
int bg = bgColorArgb;
// 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)) {
// ARCHITECTURAL NOTE ON GOCA BACKGROUND MIX & BLACK AREA FILLING:
// In GOCA (GA23-0059 / SC31-6805), Color 0 / 8 is the default background/neutral color (Black).
// Background Mix (GSBMX / bgMix):
// - bgMix == 0 or 2 (BMX_DEFAULT / BMX_LEAVE): Leave destination unchanged (Transparent).
// Fills with default background color (Black) under BMX_LEAVE are transparent and must NOT overwrite pixels.
// (e.g. ADMOPSLA slide preview selection boxes, where GDDM draws hollow frames with bgMix = 0).
// - bgMix == 5 or 1 (BMX_OVER / OVERPAINT): Overwrite background pixels with background color (Opaque).
// Fills with Black under BMX_OVER are explicit erasure rectangles used to erase closed menus and dialogs
// (e.g. ADMDRAW menu erasure, where GDDM explicitly issues GSBMX 5 before the black fill).
boolean isTransparentBlack = ((fill & 0x00FFFFFF) == 0) &&
(bgMix == GocaConstants.MIX_DEFAULT || bgMix == GocaConstants.MIX_LEAVE || bgMix == 0);
if (!isTransparentBlack && pattern != GocaConstants.PT_EMPTY && (pattern != 0 || !drawBoundary)) {
// Find polygon vertical bounds across all points
int minY = py[0];
int maxY = py[0];
@@ -244,7 +244,43 @@ public class InputProcessor {
return;
}
// Enter, PF keys, PA keys: send AID + optional PID + cursor address + modified field data
if (aidCode == AID_SELECT) {
// 3270 Selector Pen / Light Pen Immediate Selection (AID 0x7E):
// Per IBM 3270 Data Stream Architecture (GA23-0059) and IBM Host On-Demand (DS3270.sendAid lines 727-1019):
// The inbound data stream consists of:
// 1. AID byte (0x7E)
// 2. Cursor address (2 bytes)
// 3. For each field with MDT=1:
// - SBA order (0x11)
// - Designator character address (faAddr + 1)
// CRITICAL: NO FIELD CHARACTER DATA IS TRANSMITTED FOR AID_SELECT!
// Sending character data in an AID_SELECT stream violates 3270 protocol and causes the host to reject the selection.
ByteArrayOutputStream out = new ByteArrayOutputStream();
out.write(AID_SELECT);
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 designatorAddr = (i + 1) % size;
out.write(ORDER_SBA);
byte[] addr = encodeAddress(designatorAddr, screen.getRows(), screen.getCols());
out.write(addr[0] & 0xFF);
out.write(addr[1] & 0xFF);
}
}
}
sendAidResponse(out.toByteArray());
return;
}
// Enter, PF keys: send AID + optional PID + cursor address + modified field data
ByteArrayOutputStream out = new ByteArrayOutputStream();
out.write(aidCode);
@@ -132,11 +132,15 @@ public class QueryReplyBuilderTest {
assertEquals(0x81, replies[3] & 0xFF);
assertEquals(QR_DDM, replies[4] & 0xFF);
// Second SF should be Usable Area
// Second SF should be Usable Area: 23 bytes (4 bytes header + 19 bytes payload)
int pos2 = 1 + len1;
int len2 = ((replies[pos2] & 0xFF) << 8) | (replies[pos2 + 1] & 0xFF);
assertEquals(23, len2);
assertEquals(0x81, replies[pos2 + 2] & 0xFF);
assertEquals(QR_USABLE_AREA, replies[pos2 + 3] & 0xFF);
// Offset 0x15 (21 in SF payload = pos2 + 21): Buffer size high
int bufSize = ((replies[pos2 + 21] & 0xFF) << 8) | (replies[pos2 + 22] & 0xFF);
assertEquals(80 * 43, bufSize);
}
@Test
@@ -138,7 +138,7 @@ public class InputProcessorTest {
byte[] sf = haus.nightmare.lib3270j.graphics.GraphicInputBuilder.buildGraphicInput(100, -50, AID_ENTER, false, false, false);
assertEquals(56, sf.length);
assertEquals(0x00, sf[0]);
assertEquals(0x38, sf[1]); // Length = 56 bytes (0x0038)
assertEquals(0x34, sf[1]); // Length = 52 bytes (0x0034) matching IBM HODInput.java
assertEquals(0x0F, sf[2]); // SF ID = 0x0F
assertEquals(0x0F, sf[3]); // SF ID = 0x0F
@@ -160,7 +160,7 @@ public class InputProcessorTest {
);
assertEquals(56, sfMouse.length);
assertEquals(0x00, sfMouse[0]);
assertEquals(0x38, sfMouse[1]);
assertEquals(0x34, sfMouse[1]); // Length = 52 bytes (0x0034) matching IBM HODInput.java
assertEquals(0x23, sfMouse[16]);
assertEquals(0x00, sfMouse[17]);
assertEquals(0x23, sfMouse[18]);
@@ -394,9 +394,9 @@ public class InputProcessorTest {
// 1 (AID_SF 0x88) + 56 (SF) + 1 (AID_ENTER 0x7D) + 2 (Cursor Addr) + 1 (SBA) + 2 (Field Addr) + 1 (Data 'A') = 64 bytes
assertEquals(64, result.length);
assertEquals((byte) AID_SF, result[0]);
// SF length = 56 (0x00 0x38)
// SF length = 52 (0x00 0x34) per IBM HOD / GOCA specification
assertEquals(0x00, result[1]);
assertEquals(0x38, result[2]);
assertEquals(0x34, result[2]);
// SF ID = 0x0F0F
assertEquals(0x0F, result[3]);
assertEquals(0x0F, result[4]);
@@ -418,4 +418,37 @@ public class InputProcessorTest {
// Trailing field content 'A' at 63
assertEquals((byte) 0xC1, result[63]);
}
@Test
public void testAidSelectFramingOmitsFieldData() {
// Formatted screen with an unprotected field at 0, modified, containing 'A' and 'B'
screen.erase(false);
screen.setCellFA(0, (byte) (FA_PRINTABLE | FA_MODIFY));
screen.setCellFA(10, (byte) (FA_PRINTABLE | FA_PROTECT));
screen.getCell(1).ec = (byte) 0xC1;
screen.getCell(1).ucs4 = 'A';
screen.getCell(2).ec = (byte) 0xC2;
screen.getCell(2).ucs4 = 'B';
screen.setCursorAddress(1);
java.util.concurrent.atomic.AtomicReference<byte[]> sent = new java.util.concurrent.atomic.AtomicReference<>();
InputProcessor input = new InputProcessor(screen, translator, null) {
@Override
protected void sendAidResponse(byte[] data) {
sent.set(data);
}
};
input.sendAid(AID_SELECT);
byte[] result = sent.get();
assertNotNull(result);
// For AID_SELECT (0x7E), the stream consists ONLY of:
// 1 byte AID (0x7E) + 2 bytes cursor address + 1 byte SBA (0x11) + 2 bytes designator address = 6 bytes total.
// Field character contents ('A', 'B') MUST NOT be sent per IBM 3270 DS architecture!
assertEquals(6, result.length);
assertEquals((byte) AID_SELECT, result[0]);
// SBA order at byte 3
assertEquals((byte) ORDER_SBA, result[3]);
}
}