diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/datastream/DataStreamProcessor.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/datastream/DataStreamProcessor.java index 7e82c96..dcf28db 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/datastream/DataStreamProcessor.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/datastream/DataStreamProcessor.java @@ -997,7 +997,13 @@ public class DataStreamProcessor { break; case haus.nightmare.lib3270j.graphics.GocaConstants.SF_OBJDATA: // 0x85: Graphics Object Data / GOCA if (fieldLen > 3) { + graphicsPlane.setCharDimensions(qrBuilder.getCharWidth(), qrBuilder.getCharHeight()); graphicsPlane.setScreenDimensions(screen.getCols(), screen.getRows()); + int targetW = screen.getCols() * qrBuilder.getCharWidth(); + int targetH = screen.getRows() * qrBuilder.getCharHeight(); + if (graphicsPlane.getCanvasWidth() != targetW || graphicsPlane.getCanvasHeight() != targetH) { + graphicsPlane.resize(targetW, targetH); + } gocaDecoder.decodeStream(data, pos + 3, fieldLen - 3); notifyScreenUpdated(); } @@ -1311,9 +1317,10 @@ public class DataStreamProcessor { int orderOffset = (fieldLen >= 7) ? (offset + 7) : (offset + 4); int orderLen = Math.max(0, fieldLen - (orderOffset - offset)); + graphicsPlane.setCharDimensions(qrBuilder.getCharWidth(), qrBuilder.getCharHeight()); graphicsPlane.setScreenDimensions(screen.getCols(), screen.getRows()); - int targetW = screen.getCols() * 9; - int targetH = screen.getRows() * 16; + int targetW = screen.getCols() * qrBuilder.getCharWidth(); + int targetH = screen.getRows() * qrBuilder.getCharHeight(); if (graphicsPlane.getCanvasWidth() != targetW || graphicsPlane.getCanvasHeight() != targetH) { graphicsPlane.resize(targetW, targetH); } diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/datastream/QueryReplyBuilder.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/datastream/QueryReplyBuilder.java index 385c902..bc66b49 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/datastream/QueryReplyBuilder.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/datastream/QueryReplyBuilder.java @@ -424,8 +424,8 @@ public class QueryReplyBuilder { out.write((Yr_HOD >> 16) & 0xFF); out.write((Yr_HOD >> 8) & 0xFF); out.write(Yr_HOD & 0xFF); - int charW = getCharWidth(); - int charH = getCharHeight(); + int charW = getCharWidth(maxRows); + int charH = getCharHeight(maxRows); out.write(charW); // AW out.write(charH); // AH int buf = maxCols * maxRows; @@ -435,6 +435,11 @@ public class QueryReplyBuilder { } public int getCharWidth() { + int rows = (screen != null) ? screen.getMaxRows() : MODEL_2_ROWS; + return getCharWidth(rows); + } + + public int getCharWidth(int rows) { if (screen != null && screen.getTranslator() != null && screen.getTranslator().isDBCS()) { return 12; } @@ -442,18 +447,11 @@ public class QueryReplyBuilder { } 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). + int rows = (screen != null) ? screen.getMaxRows() : MODEL_2_ROWS; + return getCharHeight(rows); + } + + public int getCharHeight(int rows) { return graphicsMode.isVectorGraphicsEnabled() ? 0x10 : SH_3279_2; } diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/graphics/GocaDecoder.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/graphics/GocaDecoder.java index 8d53cfd..a5140d6 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/graphics/GocaDecoder.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/graphics/GocaDecoder.java @@ -395,13 +395,16 @@ public class GocaDecoder { if (order == GocaConstants.G_NOP1 || order == 0xFF || order == 0x00 || order == GocaConstants.G_COMT) { return 1; } - // Orders with optional 0x00 trailing length/qualifier byte (e.g. 3E 00, 71 00, 60 00, 7E 00, 3F 00, 93 00, 91 00) + // Orders with optional 0x00 trailing length/qualifier byte (e.g. 3E 00, 71 00, 60 00, 7E 00, 3F 00) if (order == GocaConstants.G_ENDPROLOGUE || order == GocaConstants.G_ENDSEGM || order == GocaConstants.G_GEAR || order == GocaConstants.G_GERASE || - order == GocaConstants.G_GPOP || order == GocaConstants.G_GEIMG || - (inImage && order == GocaConstants.G_GEIMG_ALT)) { + order == GocaConstants.G_GPOP) { return (idx + 1 < end && data[idx + 1] == 0x00) ? 2 : 1; } + // G_GEIMG (0x93 End Image): self-defining draw order (e.g. 93 02 00 00 or 93 00 or 93) + if (order == GocaConstants.G_GEIMG) { + return (idx + 1 < end) ? ((data[idx + 1] & 0xFF) + 2) : 1; + } if (idx + 1 >= end) { return -1; } @@ -552,31 +555,45 @@ public class GocaDecoder { idx += orderLen; break; } - case GocaConstants.G_GEIMG: // 0x93 - case GocaConstants.G_GEIMG_ALT: { // 0x91 - if (inImage || order == GocaConstants.G_GEIMG) { + case GocaConstants.G_GEIMG: { // 0x93 End Image + endImage(); + idx += orderLen; + break; + } + case GocaConstants.G_GBIMGC: { // 0x91: Begin Image at Current Position (G_GBIMGC) + if (inImage) { endImage(); - idx += orderLen; - } else { - // 0x91: Begin Image at Current Position (G_GBIMGC) - if (payloadLen >= 4 && idx + 2 + payloadLen <= end) { - int w = readCoord(inputData, idx + 2); - int h = readCoord(inputData, idx + 4); - int bitDepth = GocaConstants.BPP_1; - int compression = GocaConstants.IMG_UNCOMPRESSED; - if (payloadLen >= 5) { - int fmt = inputData[idx + 6] & 0xFF; - if (fmt == 2) bitDepth = GocaConstants.BPP_2; - else if (fmt == 4) bitDepth = GocaConstants.BPP_4; - else if (fmt == 8) bitDepth = GocaConstants.BPP_8; - } - if (payloadLen >= 6) { - compression = inputData[idx + 7] & 0xFF; - } - beginImage(curX, curY, w, h, bitDepth, compression); - } - idx += orderLen; } + int w = 0, h = 0; + int bitDepth = GocaConstants.BPP_1; + int compression = GocaConstants.IMG_UNCOMPRESSED; + if (payloadLen >= 6 && idx + 2 + payloadLen <= end) { + w = readCoord(inputData, idx + 4); + h = readCoord(inputData, idx + 6); + if (payloadLen >= 7) { + int fmt = inputData[idx + 8] & 0xFF; + if (fmt == 2) bitDepth = GocaConstants.BPP_2; + else if (fmt == 4) bitDepth = GocaConstants.BPP_4; + else if (fmt == 8) bitDepth = GocaConstants.BPP_8; + } + if (payloadLen >= 8) { + compression = inputData[idx + 9] & 0xFF; + } + } else if (payloadLen >= 4 && idx + 2 + payloadLen <= end) { + w = readCoord(inputData, idx + 2); + h = readCoord(inputData, idx + 4); + if (payloadLen >= 5) { + int fmt = inputData[idx + 6] & 0xFF; + if (fmt == 2) bitDepth = GocaConstants.BPP_2; + else if (fmt == 4) bitDepth = GocaConstants.BPP_4; + else if (fmt == 8) bitDepth = GocaConstants.BPP_8; + } + if (payloadLen >= 6) { + compression = inputData[idx + 7] & 0xFF; + } + } + beginImage(curX, curY, w, h, bitDepth, compression); + idx += orderLen; break; } case GocaConstants.G_BEGSEGM: { // Begin Segment (0x70) @@ -986,21 +1003,39 @@ public class GocaDecoder { break; } case GocaConstants.G_GBIMG: { // Begin Image (0xD1) + if (inImage) { + endImage(); + } if (payloadLen >= 8 && idx + 2 + payloadLen <= end) { int x = readCoord(inputData, idx + 2); int y = readCoord(inputData, idx + 4); - int w = readCoord(inputData, idx + 6); - int h = readCoord(inputData, idx + 8); + int w, h; int bitDepth = GocaConstants.BPP_1; int compression = GocaConstants.IMG_UNCOMPRESSED; - if (payloadLen >= 9) { - int fmt = inputData[idx + 10] & 0xFF; - if (fmt == 2) bitDepth = GocaConstants.BPP_2; - else if (fmt == 4) bitDepth = GocaConstants.BPP_4; - else if (fmt == 8) bitDepth = GocaConstants.BPP_8; - } if (payloadLen >= 10) { - compression = inputData[idx + 11] & 0xFF; + w = readCoord(inputData, idx + 8); + h = readCoord(inputData, idx + 10); + if (payloadLen >= 11) { + int fmt = inputData[idx + 12] & 0xFF; + if (fmt == 2) bitDepth = GocaConstants.BPP_2; + else if (fmt == 4) bitDepth = GocaConstants.BPP_4; + else if (fmt == 8) bitDepth = GocaConstants.BPP_8; + } + if (payloadLen >= 12) { + compression = inputData[idx + 13] & 0xFF; + } + } else { + w = readCoord(inputData, idx + 6); + h = readCoord(inputData, idx + 8); + if (payloadLen >= 9) { + int fmt = inputData[idx + 10] & 0xFF; + if (fmt == 2) bitDepth = GocaConstants.BPP_2; + else if (fmt == 4) bitDepth = GocaConstants.BPP_4; + else if (fmt == 8) bitDepth = GocaConstants.BPP_8; + } + if (payloadLen >= 10) { + compression = inputData[idx + 11] & 0xFF; + } } beginImage(x, y, w, h, bitDepth, compression); } diff --git a/lib3270j/src/main/java/haus/nightmare/lib3270j/graphics/GraphicsPlane.java b/lib3270j/src/main/java/haus/nightmare/lib3270j/graphics/GraphicsPlane.java index 1357bda..bbad5f0 100644 --- a/lib3270j/src/main/java/haus/nightmare/lib3270j/graphics/GraphicsPlane.java +++ b/lib3270j/src/main/java/haus/nightmare/lib3270j/graphics/GraphicsPlane.java @@ -418,9 +418,27 @@ public class GraphicsPlane implements PixelBuffer { return transform; } + // Character cell dimensions matching Query Reply presentation space + private int charWidth = 9; + private int charHeight = 16; + + public synchronized void setCharDimensions(int width, int height) { + if (width > 0) this.charWidth = width; + if (height > 0) this.charHeight = height; + this.transform.setDefaultCharMetrics(this.charWidth, this.charHeight); + } + + public int getCharWidth() { + return charWidth; + } + + public int getCharHeight() { + return charHeight; + } + public int getTotalWidth() { int cols = screenCols > 0 ? screenCols : 80; - return cols * 9; + return cols * charWidth; } /** @@ -457,7 +475,7 @@ public class GraphicsPlane implements PixelBuffer { */ public int getTotalHeight() { int rows = screenRows > 0 ? screenRows : 24; - return rows * 16; + return rows * charHeight; } public int getXMax() { diff --git a/lib3270j/src/test/java/haus/nightmare/lib3270j/graphics/GocaDecoderTest.java b/lib3270j/src/test/java/haus/nightmare/lib3270j/graphics/GocaDecoderTest.java index d4e8d36..328d516 100644 --- a/lib3270j/src/test/java/haus/nightmare/lib3270j/graphics/GocaDecoderTest.java +++ b/lib3270j/src/test/java/haus/nightmare/lib3270j/graphics/GocaDecoderTest.java @@ -285,6 +285,79 @@ public class GocaDecoderTest { assertTrue(plane.hasContent(), "Expected plane to have content after image decoding"); } + @Test + public void testImonGddmImageRendering() throws Exception { + GraphicsPlane plane = new GraphicsPlane(1040, 1118); + plane.setCharDimensions(13, 26); + plane.setScreenDimensions(80, 43); + GocaDecoder decoder = new GocaDecoder(plane); + + assertEquals(1040, plane.getTotalWidth()); + assertEquals(1118, plane.getTotalHeight()); + assertEquals(520, plane.getXMax()); + assertEquals(558, plane.getYMax()); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + // GSCP: Set Current Position (377, 494) + out.write(GocaConstants.G_GSCP); + out.write(0x04); + out.write(0x01); out.write(0x79); // X = 377 + out.write(0x01); out.write(0xEE); // Y = 494 + + // GSCOL: Color 1 = Blue + out.write(GocaConstants.G_GSCOL); + out.write(0x01); + + // G_GBIMGC (0x91): Begin Image Current Position: len=6, flags=0, w=80, h=80 + out.write(GocaConstants.G_GBIMGC); + out.write(0x06); + out.write(0x00); out.write(0x00); // flags/format + out.write(0x00); out.write(0x50); // w = 80 + out.write(0x00); out.write(0x50); // h = 80 + + // G_GIMD (0x92): 10 bytes scanline + out.write(GocaConstants.G_GIMD); + out.write(0x0A); + out.write(new byte[] { (byte)0xFF, (byte)0xFF, (byte)0xFF, (byte)0xFF, (byte)0xFF, (byte)0xFF, (byte)0xFF, (byte)0xFF, (byte)0xFF, (byte)0xFF }); + + // G_GEIMG (0x93): End Image with 2-byte operand (02 00 00 as sent by IMON) + out.write(GocaConstants.G_GEIMG); + out.write(0x02); + out.write(0x00); out.write(0x00); + + // Subsequent order to verify stream synchronization: GSCOL Color 2 = Red + out.write(GocaConstants.G_GSCOL); + out.write(0x02); + + // G_GBIMGC (0x91): Second image tile + out.write(GocaConstants.G_GBIMGC); + out.write(0x06); + out.write(0x00); out.write(0x00); + out.write(0x00); out.write(0x50); + out.write(0x00); out.write(0x50); + + out.write(GocaConstants.G_GIMD); + out.write(0x0A); + out.write(new byte[] { (byte)0xAA, (byte)0xAA, (byte)0xAA, (byte)0xAA, (byte)0xAA, (byte)0xAA, (byte)0xAA, (byte)0xAA, (byte)0xAA, (byte)0xAA }); + + out.write(GocaConstants.G_GEIMG); + out.write(0x02); + out.write(0x00); out.write(0x00); + + byte[] stream = out.toByteArray(); + decoder.decodeStream(stream, 0, stream.length); + + assertTrue(plane.hasContent(), "Expected plane to have content after IMON GOCA decoding"); + int mappedX = plane.mapX(377); + int mappedY = plane.mapY(494); + assertEquals(897, mappedX); + assertEquals(64, mappedY); + + int[] rgb = plane.getRgbBuffer(); + int pixelAtImg = rgb[mappedY * 1040 + mappedX]; + assertNotEquals(0, pixelAtImg, "Pixel at image location should be rendered"); + } + @Test public void testDirectObjectControlSf24() { haus.nightmare.lib3270j.screen.ScreenBuffer sb = new haus.nightmare.lib3270j.screen.ScreenBuffer(