From 61052a723a1ac8ddb243126065e4cf566301c8f5 Mon Sep 17 00:00:00 2001 From: Codex Date: Fri, 30 Oct 2026 11:12:20 -0500 Subject: [PATCH] Add reviewed bounded canvas helper and resilient Win3 example --- .github/workflows/canvas.yml | 24 + README.md | 3 + docs/BUILD.TXT | 5 + examples/CANVAS30/BUILD.BAT | 12 + examples/CANVAS30/CANVAS.C | 198 ++++++++ examples/CANVAS30/CANVAS.DEF | 9 + examples/CANVAS30/CANVAS.RSP | 5 + examples/CANVAS30/README.TXT | 58 +++ libtandywin/CANVAS/FAILURES.JSN | 164 +++++++ libtandywin/CANVAS/NATIVE.JSN | 264 +++++++++++ libtandywin/CANVAS/README.TXT | 110 +++++ libtandywin/CANVAS/TESTS/CANVTEST.C | 540 ++++++++++++++++++++++ libtandywin/CANVAS/TESTS/EXAMPLE.C | 318 +++++++++++++ libtandywin/CANVAS/TESTS/EXSTUB/windows.h | 70 +++ libtandywin/CANVAS/TESTS/RUN.PY | 64 +++ libtandywin/CANVAS/TESTS/windows.h | 30 ++ libtandywin/CANVAS/WTCANVAS.C | 183 ++++++++ libtandywin/CANVAS/WTCANVAS.H | 48 ++ 18 files changed, 2105 insertions(+) create mode 100644 .github/workflows/canvas.yml create mode 100644 examples/CANVAS30/BUILD.BAT create mode 100644 examples/CANVAS30/CANVAS.C create mode 100644 examples/CANVAS30/CANVAS.DEF create mode 100644 examples/CANVAS30/CANVAS.RSP create mode 100644 examples/CANVAS30/README.TXT create mode 100644 libtandywin/CANVAS/FAILURES.JSN create mode 100644 libtandywin/CANVAS/NATIVE.JSN create mode 100644 libtandywin/CANVAS/README.TXT create mode 100644 libtandywin/CANVAS/TESTS/CANVTEST.C create mode 100644 libtandywin/CANVAS/TESTS/EXAMPLE.C create mode 100644 libtandywin/CANVAS/TESTS/EXSTUB/windows.h create mode 100644 libtandywin/CANVAS/TESTS/RUN.PY create mode 100644 libtandywin/CANVAS/TESTS/windows.h create mode 100644 libtandywin/CANVAS/WTCANVAS.C create mode 100644 libtandywin/CANVAS/WTCANVAS.H diff --git a/.github/workflows/canvas.yml b/.github/workflows/canvas.yml new file mode 100644 index 0000000..fa314c1 --- /dev/null +++ b/.github/workflows/canvas.yml @@ -0,0 +1,24 @@ +name: Bounded canvas helper +on: + pull_request: + paths: + - 'libtandywin/CANVAS/**' + - 'examples/CANVAS30/**' + - '.github/workflows/canvas.yml' + push: + branches: [main] + paths: + - 'libtandywin/CANVAS/**' + - 'examples/CANVAS30/**' + - '.github/workflows/canvas.yml' +permissions: + contents: read +jobs: + host-contract: + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + with: + ref: ${{ github.event.pull_request.head.sha || github.sha }} + - name: Strict C89 actual-source tests (mock GDI, not native qualification) + run: python3 libtandywin/CANVAS/TESTS/RUN.PY --out "$RUNNER_TEMP/canvas" diff --git a/README.md b/README.md index 9d3db1c..c9e2417 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,9 @@ This library contains its consumer interfaces. * [Placement service](libtandywin/PLACE/README.TXT) supplies caller-owned normal window rectangles and recovery for the Windows XT window-moving desktop. It preserves minimized/maximized state without changing USER.EXE. +* [Bounded canvas/damage helper](libtandywin/CANVAS/README.TXT) is an opt-in + caller-owned GDI module with optional monochrome surfaces; + [CANVAS30](examples/CANVAS30/README.TXT) is a separate small consumer. * `LWTANDY.H` exposes the existing USER repaint helpers. * `LWAUDIO.H` forwards the app-local MCI/raw-player interfaces. * [TPLAY](docs/TPLAYXT.TXT) supplies the guarded Windows XT HPXY/XRSYNC launcher diff --git a/docs/BUILD.TXT b/docs/BUILD.TXT index f0e7b86..ea98eae 100644 --- a/docs/BUILD.TXT +++ b/docs/BUILD.TXT @@ -5,6 +5,11 @@ Host placement checks (strict C89, actual source with USER mocks): Native consumers link WTPLACE.C once using their existing Win16 toolchain. See libtandywin/PLACE/README.TXT for the Windows 3.0 ABI and measured evidence. +Host canvas/damage checks (strict C89, actual source with GDI mocks): + python libtandywin/CANVAS/TESTS/RUN.PY --out +Native consumers link CANVAS/WTCANVAS.C once with their existing Win16 +toolchain; see examples/CANVAS30 for a small MSC6 /G0 /Gw /AS consumer. + Host UI checks: python libtandywin/TESTS/RUN.PY Host production format, MOD and RAWCLK checks: diff --git a/examples/CANVAS30/BUILD.BAT b/examples/CANVAS30/BUILD.BAT new file mode 100644 index 0000000..f38c7d4 --- /dev/null +++ b/examples/CANVAS30/BUILD.BAT @@ -0,0 +1,12 @@ +@ECHO OFF +REM Run in a private staging directory with the five example/build files and +REM WTCANVAS.C/H. Caller supplies MSC6 + Win3 SDK in PATH, INCLUDE and LIB. +CL /nologo /c /G0 /Gw /AS /W3 /Od CANVAS.C WTCANVAS.C > BUILD.LOG +IF ERRORLEVEL 1 GOTO FAIL +LINK4 /MAP /NOE /NOD @CANVAS.RSP >> BUILD.LOG +IF ERRORLEVEL 1 GOTO FAIL +ECHO PASS >> BUILD.LOG +GOTO END +:FAIL +ECHO FAIL >> BUILD.LOG +:END diff --git a/examples/CANVAS30/CANVAS.C b/examples/CANVAS30/CANVAS.C new file mode 100644 index 0000000..76e9c1b --- /dev/null +++ b/examples/CANVAS30/CANVAS.C @@ -0,0 +1,198 @@ +/* Moving-marker consumer of WTCANVAS; no Cube geometry or driver access. */ +#define WINVER 0x0300 +#include +#include "WTCANVAS.H" + +static LTWCANVAS canvas; +static LTWDAMAGE damage; +static RECT marker; +static int position, direction = 1; +static BOOL windowDestroyed, retryPaint, paintFailed; + +static void model(void) +{ + int size; + size = canvas.width < 12 ? canvas.width : 12; + if (size > canvas.height) size = canvas.height; + if (position > canvas.width - size) position = canvas.width - size; + if (position < 0) position = 0; + marker.left = position; + marker.right = position + size; + marker.top = (canvas.height - size) / 2; + marker.bottom = marker.top + size; + LtwDamageBegin(&damage); + LtwDamageAdd(&damage, &marker); +} +static BOOL resize_canvas(HWND window) +{ + RECT client; + HDC dc; + int width, height; + BOOL ok; + if (!LtwCanvasDestroy(&canvas)) return FALSE; + LtwDamageInit(&damage, 0, 0); + GetClientRect(window, &client); + width = client.right < 160 ? client.right : 160; + height = client.bottom < 96 ? client.bottom : 96; + if (width <= 0 || height <= 0) return TRUE; + dc = GetDC(window); + if (!dc) return FALSE; + ok = LtwCanvasCreate(&canvas, dc, width, height, TRUE); + ReleaseDC(window, dc); + if (!ok) return FALSE; + LtwDamageInit(&damage, width, height); /* recreation forces full damage */ + model(); + return TRUE; +} +static BOOL draw_model(void) +{ + RECT dirty; + if (!LtwDamageGet(&damage, &dirty)) return TRUE; + if (!PatBlt(canvas.dc, dirty.left, dirty.top, + dirty.right - dirty.left, dirty.bottom - dirty.top, + BLACKNESS)) return FALSE; + return PatBlt(canvas.dc, marker.left, marker.top, + marker.right - marker.left, marker.bottom - marker.top, + WHITENESS); +} +static BOOL background(HWND window, HDC dc) +{ + RECT client, r; + BOOL ok = TRUE; + GetClientRect(window, &client); + /* Fill only outside the bounded canvas. BeginPaint owns expose clipping. */ + r = client; + r.left = canvas.width; + if (r.left < r.right && + !FillRect(dc, &r, GetStockObject(BLACK_BRUSH))) ok = FALSE; + r = client; + r.right = canvas.width; + r.top = canvas.height; + if (r.top < r.bottom && r.right > 0) + if (!FillRect(dc, &r, GetStockObject(BLACK_BRUSH))) ok = FALSE; + return ok; +} +long FAR PASCAL CanvasProc(HWND window, UINT message, WPARAM wp, LPARAM lp) +{ + RECT dirty; + PAINTSTRUCT paint; + HDC dc; + BOOL ok; + switch (message) { + case WM_CREATE: + windowDestroyed = FALSE; + return 0L; + case WM_SIZE: + if (wp != SIZEICONIC) { + ok = resize_canvas(window); + retryPaint = TRUE; + SetWindowText(window, ok ? "Bounded mono canvas" : + "Canvas allocation failed - resize to retry"); + InvalidateRect(window, NULL, FALSE); + } + return 0L; + case WM_TIMER: + if (wp != 1 || windowDestroyed || IsIconic(window)) return 0L; + if (retryPaint) InvalidateRect(window, NULL, FALSE); + if (!canvas.width) return 0L; + position += direction; + if (position <= 0 || position >= canvas.width - + (marker.right - marker.left)) direction = -direction; + model(); + /* Always invalidate the whole uncommitted old/current union. Thus + * coalesced timers/paints cannot lose damage before Commit below. */ + if (LtwDamageGet(&damage, &dirty)) + InvalidateRect(window, &dirty, FALSE); + return 0L; + case WM_ERASEBKGND: + return 1L; + case WM_PAINT: + /* A prior failed draw may have forced full damage without a timer + * yet. Include it BEFORE BeginPaint, so Commit can never discard + * damage excluded by an unrelated expose's update region. */ + if (retryPaint) InvalidateRect(window, NULL, FALSE); + else if (LtwDamageGet(&damage, &dirty)) + InvalidateRect(window, &dirty, FALSE); + dc = BeginPaint(window, &paint); + ok = background(window, dc); + if (canvas.width) { + if (ok) ok = draw_model(); + /* Expose can exceed animation damage: copy the actual paint + * bounds from the initialized backing surface, not merely dirty. + * GDI retains the possibly nonrectangular BeginPaint clip. */ + if (ok) ok = LtwCanvasPresent(&canvas, dc, 0, 0, &paint.rcPaint, + RGB(0, 0, 0), RGB(255, 255, 255)); + if (ok) LtwDamageCommit(&damage); + else LtwDamageReset(&damage); + } + EndPaint(window, &paint); + retryPaint = !ok; + if (ok && paintFailed && canvas.width) + SetWindowText(window, "Bounded mono canvas"); + paintFailed = !ok; + if (!ok) { + /* Do not spin a failed WM_PAINT. The next timer invalidates the + * full canvas and retries; retained old bounds are not committed. */ + SetWindowText(window, "Canvas drawing failed - retrying"); + } + return 0L; + case WM_DESTROY: + windowDestroyed = TRUE; + /* The task timer outlives this HWND. Retain ownership on failure; + * WinMain retries on later ticks and only quits after full cleanup. */ + if (LtwCanvasDestroy(&canvas)) PostQuitMessage(0); + return 0L; + } + return DefWindowProc(window, message, wp, lp); +} +int PASCAL WinMain(HINSTANCE instance, HINSTANCE previous, LPSTR command, int show) +{ + WNDCLASS wc; + HWND window; + MSG message; + UINT timer; + int x = CW_USEDEFAULT, y = CW_USEDEFAULT; + int width = 220, height = 150, screen; + (void)command; + if (!previous) { + wc.style = CS_HREDRAW | CS_VREDRAW; + wc.lpfnWndProc = CanvasProc; + wc.cbClsExtra = wc.cbWndExtra = 0; + wc.hInstance = instance; + wc.hIcon = LoadIcon(NULL, IDI_APPLICATION); + wc.hCursor = LoadCursor(NULL, IDC_ARROW); + wc.hbrBackground = NULL; + wc.lpszMenuName = NULL; + wc.lpszClassName = "LtwCanvas30"; + if (!RegisterClass(&wc)) return 1; + } + /* Default USER placement can leave the outer frame off a compact display. + * Clamp the initial outer size and use an explicit origin when needed. */ + screen = GetSystemMetrics(SM_CXSCREEN); + if (screen > 0 && width > screen) { width = screen; x = y = 0; } + screen = GetSystemMetrics(SM_CYSCREEN); + if (screen > 0 && height > screen) { height = screen; x = y = 0; } + /* One task timer drives animation and survives HWND destruction for + * cleanup retries. Reserve it before allocating any canvas resources. */ + timer = SetTimer(NULL, 0, 100, NULL); + if (!timer) return 1; + window = CreateWindow("LtwCanvas30", "Bounded mono canvas", + WS_OVERLAPPEDWINDOW, x, y, width, height, + NULL, NULL, instance, NULL); + if (!window) { KillTimer(NULL, timer); return 1; } + ShowWindow(window, show); + UpdateWindow(window); + while (GetMessage(&message, NULL, 0, 0)) { + if (!message.hwnd && message.message == WM_TIMER && + message.wParam == timer) { + if (windowDestroyed) { + if (LtwCanvasDestroy(&canvas)) PostQuitMessage(0); + } else SendMessage(window, WM_TIMER, 1, 0L); + } else { + TranslateMessage(&message); + DispatchMessage(&message); + } + } + KillTimer(NULL, timer); + return (int)message.wParam; +} diff --git a/examples/CANVAS30/CANVAS.DEF b/examples/CANVAS30/CANVAS.DEF new file mode 100644 index 0000000..490fbd3 --- /dev/null +++ b/examples/CANVAS30/CANVAS.DEF @@ -0,0 +1,9 @@ +NAME CANVAS +DESCRIPTION 'libwintandy bounded mono canvas example' +EXETYPE WINDOWS +CODE PRELOAD MOVEABLE DISCARDABLE +DATA PRELOAD MOVEABLE MULTIPLE +HEAPSIZE 1024 +STACKSIZE 4096 +EXPORTS + CanvasProc @1 diff --git a/examples/CANVAS30/CANVAS.RSP b/examples/CANVAS30/CANVAS.RSP new file mode 100644 index 0000000..356c7c6 --- /dev/null +++ b/examples/CANVAS30/CANVAS.RSP @@ -0,0 +1,5 @@ +CANVAS.OBJ WTCANVAS.OBJ +CANVAS.EXE +CANVAS.MAP +SLIBCEW LIBW +CANVAS.DEF diff --git a/examples/CANVAS30/README.TXT b/examples/CANVAS30/README.TXT new file mode 100644 index 0000000..89dcbd7 --- /dev/null +++ b/examples/CANVAS30/README.TXT @@ -0,0 +1,58 @@ +CANVAS30 - moving-marker example (Windows 3.0 real mode / 8088) + +Link the optional libtandywin/CANVAS/WTCANVAS.C, not LTWUI.C. The source uses a +maximum 160x96 monochrome surface anchored at client (0,0). Resize recreates a +smaller/larger bounded surface; unused client space stays black. Minimize +pauses movement. Exposes copy initialized retained pixels using BeginPaint's +clip; pending animation damage is included before paint and only committed +after successful drawing/copy. Failed draws force full retry on the next timer. + +Copy CANVAS.C, CANVAS.DEF, CANVAS.RSP, BUILD.BAT and WTCANVAS.C/H into a fresh +private staging directory. With caller-owned MSC6, LINK4 and Windows SDK +PATH/INCLUDE/LIB configured, run BUILD.BAT. SLIBCEW/LIBW must be provided by +the caller's SDK; LINK supplies its default DOS stub. /G0 /Gw /AS targets 8086 Windows small model. +The helper and consumer must use the same memory model. No SDK is bundled. + +Run only in an authorized, isolated Windows 3.0 test guest. Check motion, +cover/uncover, repeated minimize/restore, narrow/short windows and repeated +resize/open/close. A host or compile-only check is not a native pixel or timing +qualification. This example is deliberately not a saver, Cube port, widget +framework or display-driver change. + +Initial placement caps the outer frame to valid screen dimensions and uses +origin (0,0) if either dimension needs capping. Larger screens retain the +220x150 default USER placement. The canvas limit stays 160x96 client pixels. + +Partial native validation (2026-10-30) runs the actual overlapped WinMain and +message loop as a separate task on Windows 3.0 real mode, Tandy HX 8088/640 KB, +with exact current TR51616 and TR53216 drivers. Native timer motion, maximize, +minimize, restore to remembered maximized state, a second restore to normal, +resize, overlap/expose, system Close, relaunch and headless keyboard Alt-F4 +passed. Two launches/closes per run return to identical free memory and unload +the task. The original compact-screen frame clipped at 168 pixels; the fixed +160-column normal frame is exactly 160 pixels and fully visible. + +See ../../libtandywin/CANVAS/NATIVE.JSN for exact runtime, driver, footprint +and test limits. These are bounded emulator tests; physical mouse dragging, +long-duration exhaustion, other native modes and a runtime distribution are +not qualified. Source is not a published binary package. + +FAILURE OWNERSHIP / FULL-CLIENT RETRY +WinMain reserves one application-owned 100 ms task timer before creating the +window. It drives animation through the message loop; the helper owns no +timer. After WM_DESTROY, a failed canvas deletion leaves the task and timer +alive. Later ticks retry without a busy loop. Quit is posted and the timer is +cancelled only after all owned canvas handles are released. A persistent +deletion failure deliberately retains the task and ownership for later retry. + +Background FillRect results are part of paint success. A background, draw or +copy failure requests a full-client retry before BeginPaint; marker-only +damage or unrelated exposure cannot discard a failed outside-canvas region. +A successful paint clears the retry state and error caption. + +1485 host example assertions and normal/injected actual-WinMain runs in both +matched TR5 modes qualify this sample change. Native API wrappers partially +paint a background strip and fail, then fail DC and bitmap deletion on close. +They verify full-client black restoration and quit only after both handles +release. See ../../libtandywin/CANVAS/FAILURES.JSN for runtime identities, +footprint, exact scope and limits. Helper C/H remain unchanged. diff --git a/libtandywin/CANVAS/FAILURES.JSN b/libtandywin/CANVAS/FAILURES.JSN new file mode 100644 index 0000000..8e9bfb0 --- /dev/null +++ b/libtandywin/CANVAS/FAILURES.JSN @@ -0,0 +1,164 @@ +{ + "schema": 1, + "status": "PARTIAL_NATIVE_VALIDATION", + "source_code_commit": "58361f68cb164b39619648d5cb0f1f6b724962bc", + "helper_unchanged": true, + "production_runtime": { + "bytes": 8207, + "sha256": "869e7d28d7a267f367a43307aae84c19f07bdaa810dfe87e92eec1759155bbfd" + }, + "instrumented_runtime": { + "bytes": 10143, + "sha256": "d6e859b66eb8c794a7c5f2496384d9df0aafb684c80091291292a96116c62ad7" + }, + "host_example_assertions": 1485, + "reproduced_original_states": { + "destroy": { + "retained_dcs": 1, + "retained_bitmaps": 1, + "quit_posted": 1 + }, + "background": { + "damage_full": 0, + "update_pending": 0 + } + }, + "fixes": [ + "One application task timer is reserved before window/canvas creation, drives animation, survives HWND destruction and retries retained cleanup. Quit and timer cancellation follow complete release.", + "Background FillRect failure prevents damage commit and requests a full-client retry before BeginPaint, including when only a small unrelated expose arrives." + ], + "native_runs": { + "fault320": { + "driver": { + "name": "TR53216.DRV", + "bytes": 54896, + "sha256": "33f681b0238310ae2614b3c1b923d260c816261442b1fa5cd51867c25a6cef61" + }, + "example": { + "bytes": 10143, + "sha256": "d6e859b66eb8c794a7c5f2496384d9df0aafb684c80091291292a96116c62ad7" + }, + "controller_checks": 1268, + "layout_clipped": 0, + "free_after_close": [ + 287552, + 287552 + ], + "faults": { + "launches": 2, + "background_failures": 2, + "dc_delete_failures": 2, + "bitmap_delete_failures": 2, + "clean_quits": 2 + }, + "owned_stopped": true, + "lease_released": true + }, + "fault160": { + "driver": { + "name": "TR51616.DRV", + "bytes": 54880, + "sha256": "c03925071ea1dd5943654a7c180c9b7efc59340c4bee5ae086773d4228b9ce3d" + }, + "example": { + "bytes": 10143, + "sha256": "d6e859b66eb8c794a7c5f2496384d9df0aafb684c80091291292a96116c62ad7" + }, + "controller_checks": 1236, + "layout_clipped": 0, + "free_after_close": [ + 304800, + 304800 + ], + "faults": { + "launches": 2, + "background_failures": 2, + "dc_delete_failures": 2, + "bitmap_delete_failures": 2, + "clean_quits": 2 + }, + "owned_stopped": true, + "lease_released": true + }, + "clean160": { + "driver": { + "name": "TR51616.DRV", + "bytes": 54880, + "sha256": "c03925071ea1dd5943654a7c180c9b7efc59340c4bee5ae086773d4228b9ce3d" + }, + "example": { + "bytes": 8207, + "sha256": "869e7d28d7a267f367a43307aae84c19f07bdaa810dfe87e92eec1759155bbfd" + }, + "controller_checks": 1236, + "layout_clipped": 0, + "free_after_close": [ + 304800, + 304800 + ], + "faults": null, + "owned_stopped": true, + "lease_released": true + }, + "clean320": { + "driver": { + "name": "TR53216.DRV", + "bytes": 54896, + "sha256": "33f681b0238310ae2614b3c1b923d260c816261442b1fa5cd51867c25a6cef61" + }, + "example": { + "bytes": 8207, + "sha256": "869e7d28d7a267f367a43307aae84c19f07bdaa810dfe87e92eec1759155bbfd" + }, + "controller_checks": 1268, + "layout_clipped": 0, + "free_after_close": [ + 287552, + 287552 + ], + "faults": null, + "owned_stopped": true, + "lease_released": true + } + }, + "footprint": { + "previous": { + "bytes": 7663, + "sha256": "b444ec3724022dd7fed5b89f9d947431b68ab52d974fceca3ead815d69c42dea", + "code_bytes": 6208, + "data_bytes": 495, + "data_minimum": 544, + "heap": 1024, + "stack": 4096, + "ne_target": "0x300" + }, + "fixed": { + "bytes": 8207, + "sha256": "869e7d28d7a267f367a43307aae84c19f07bdaa810dfe87e92eec1759155bbfd", + "code_bytes": 6512, + "data_bytes": 527, + "data_minimum": 592, + "heap": 1024, + "stack": 4096, + "ne_target": "0x300" + }, + "fault_instrumented": { + "bytes": 10143, + "sha256": "d6e859b66eb8c794a7c5f2496384d9df0aafb684c80091291292a96116c62ad7", + "code_bytes": 7840, + "data_bytes": 927, + "data_minimum": 1200, + "heap": 1024, + "stack": 4096, + "ne_target": "0x300" + } + }, + "limits": [ + "Synthetic native GDI wrappers inject one FillRect failure with partial red painting, one DeleteDC failure and one bitmap deletion failure per launch. This is API-return failure qualification, not actual GDI resource exhaustion.", + "A persistent real deletion failure deliberately retains the task, canvas state and existing task timer and retries on later100ms ticks. It does not spin or abandon ownership; indefinite/permanent-failure UI behavior is not otherwise qualified.", + "Four native runs use matched current TR51616/TR53216 on private TandyHX 8088/640KB Windows3.0 real-mode fixtures. Other native modes, physical mouse actions, long-duration exhaustion and physical hardware remain unqualified.", + "The library helper C/H and its existing ABI/color/damage/copy tests are unchanged and reused. The sample message-loop changed, so normal and injected actual-WinMain tests were repeated; earlier sample binaries are not presented as the fixed runtime.", + "No push, PR, release, approval, merge or BOR/Mesen interaction." + ], + "evidence": "Full raw logs/configs/captures, failed controls, build inputs and patch verification retained in private CUBEVAL validation archive. No foreign tools or OS media bundled." +} diff --git a/libtandywin/CANVAS/NATIVE.JSN b/libtandywin/CANVAS/NATIVE.JSN new file mode 100644 index 0000000..e2d2f2b --- /dev/null +++ b/libtandywin/CANVAS/NATIVE.JSN @@ -0,0 +1,264 @@ +{ + "schema": 1, + "status": "PARTIAL_NATIVE_VALIDATION", + "source_code_commit": "31f076391afa540a191bdc12dd02f8341c6c267d", + "helper_source": { + "libtandywin/CANVAS/WTCANVAS.C": { + "bytes": 5879, + "sha256": "67271757b00c5fbc761c77f7a06a4ee08b20cc334f342b26be345e911a5e94f4" + }, + "libtandywin/CANVAS/WTCANVAS.H": { + "bytes": 2292, + "sha256": "06e8a9beb7ee7c6e6b7ed0d272452c26867178097a7e622b9de36c071d28d17b" + } + }, + "example_runtime": { + "original": { + "runtime": { + "bytes": 7663, + "sha256": "cd654026d65f7f4c91c13022dfe22653e2c66cb6d8f5852e3fde6a142ec13bee" + }, + "code_bytes": 6096, + "data_bytes": 495, + "data_minimum": 544, + "heap": 1024, + "stack": 4096, + "ne_target": "0x300" + }, + "fit": { + "runtime": { + "bytes": 7663, + "sha256": "b444ec3724022dd7fed5b89f9d947431b68ab52d974fceca3ead815d69c42dea" + }, + "code_bytes": 6208, + "data_bytes": 495, + "data_minimum": 544, + "heap": 1024, + "stack": 4096, + "ne_target": "0x300" + } + }, + "window_runs": { + "original160": { + "example": { + "bytes": 7663, + "sha256": "cd654026d65f7f4c91c13022dfe22653e2c66cb6d8f5852e3fde6a142ec13bee" + }, + "driver": { + "name": "TR51616.DRV", + "bytes": 54880, + "sha256": "c03925071ea1dd5943654a7c180c9b7efc59340c4bee5ae086773d4228b9ce3d" + }, + "cpu": "Tandy 1000 HX 8088 7159092 Hz; RESERVE 32", + "memory_kb": 640, + "checks": 1252, + "normal_frames": [ + [ + 0, + 0, + 168, + 150 + ], + [ + 0, + 18, + 168, + 168 + ] + ], + "layout_clipped": 4, + "launches": 2, + "closes": 2, + "free_after_close": [ + 304800, + 304800 + ], + "owned_stopped": true, + "lease_released": true + }, + "fit160": { + "example": { + "bytes": 7663, + "sha256": "b444ec3724022dd7fed5b89f9d947431b68ab52d974fceca3ead815d69c42dea" + }, + "driver": { + "name": "TR51616.DRV", + "bytes": 54880, + "sha256": "c03925071ea1dd5943654a7c180c9b7efc59340c4bee5ae086773d4228b9ce3d" + }, + "cpu": "Tandy 1000 HX 8088 7159092 Hz; RESERVE 32", + "memory_kb": 640, + "checks": 1236, + "normal_frames": [ + [ + 0, + 0, + 160, + 150 + ], + [ + 0, + 0, + 160, + 150 + ] + ], + "layout_clipped": 0, + "launches": 2, + "closes": 2, + "free_after_close": [ + 304800, + 304800 + ], + "owned_stopped": true, + "lease_released": true + }, + "fit320": { + "example": { + "bytes": 7663, + "sha256": "b444ec3724022dd7fed5b89f9d947431b68ab52d974fceca3ead815d69c42dea" + }, + "driver": { + "name": "TR53216.DRV", + "bytes": 54896, + "sha256": "33f681b0238310ae2614b3c1b923d260c816261442b1fa5cd51867c25a6cef61" + }, + "cpu": "Tandy 1000 HX 8088 7159092 Hz; RESERVE 32", + "memory_kb": 640, + "checks": 1268, + "normal_frames": [ + [ + 0, + 0, + 220, + 150 + ], + [ + 31, + 18, + 251, + 168 + ] + ], + "layout_clipped": 0, + "launches": 2, + "closes": 2, + "free_after_close": [ + 287552, + 287552 + ], + "owned_stopped": true, + "lease_released": true + } + }, + "prior_helper_modes": { + "tr51616": { + "driver": { + "name": "TR51616.DRV", + "bytes": 54880, + "sha256": "c03925071ea1dd5943654a7c180c9b7efc59340c4bee5ae086773d4228b9ce3d" + }, + "pixel_checks": 6834, + "warm_free_bytes": [ + 300992, + 300992, + 300992, + 300992, + 300992, + 300992, + 300992, + 300992, + 300992, + 300992, + 300992, + 300992 + ], + "native_abi": "ABI int=2 long=4 rect=8 canvas=12 damage=26", + "far_segments": "FAR state_segment=6451 stack_segment=19109", + "timing_mean_ms_per_frame": { + "color": { + "dirty": { + "draw_ms": 10.3125, + "copy_ms": 14.59375 + }, + "full": { + "draw_ms": 10.296875, + "copy_ms": 109.859375 + }, + "copy_reduction_percent": 86.71597212345328 + }, + "mono": { + "dirty": { + "draw_ms": 6.875, + "copy_ms": 17.15625 + }, + "full": { + "draw_ms": 6.0, + "copy_ms": 112.421875 + }, + "copy_reduction_percent": 84.73940236275192 + } + } + }, + "tr53216": { + "driver": { + "name": "TR53216.DRV", + "bytes": 54896, + "sha256": "33f681b0238310ae2614b3c1b923d260c816261442b1fa5cd51867c25a6cef61" + }, + "pixel_checks": 6834, + "warm_free_bytes": [ + 283744, + 283744, + 283744, + 283744, + 283744, + 283744, + 283744, + 283744, + 283744, + 283744, + 283744, + 283744 + ], + "native_abi": "ABI int=2 long=4 rect=8 canvas=12 damage=26", + "far_segments": "FAR state_segment=6503 stack_segment=20185", + "timing_mean_ms_per_frame": { + "color": { + "dirty": { + "draw_ms": 10.296875, + "copy_ms": 15.453125 + }, + "full": { + "draw_ms": 8.546875, + "copy_ms": 110.734375 + }, + "copy_reduction_percent": 86.04487089036263 + }, + "mono": { + "dirty": { + "draw_ms": 6.875, + "copy_ms": 17.15625 + }, + "full": { + "draw_ms": 7.734375, + "copy_ms": 111.5625 + }, + "copy_reduction_percent": 84.6218487394958 + } + } + } + }, + "host_example_assertions": 720, + "host_helper_assertions_reused": 44048, + "limits": [ + "Real original/modified overlapped WinMain and separate task are executed through WinExec, without instrumenting the application or helper. Controller reads visible client scanlines, not every pixel of every frame.", + "System commands exercise actual Windows maximize/minimize/restore and close paths. Second close uses native headless XT Alt-F4; physical mouse dragging/caption clicks are not qualified.", + "Two real launches/closes per run show identical quiescent free memory and task unload; long-duration exhaustion testing remains unqualified.", + "Native compact160x200 and320x200 with matched current TR5 drivers only;160x100 placement and invalid metrics are host tested, not new native fixtures.", + "Existing helper ABI/lifecycle/color/failure/timing results are reused because helper bytes are unchanged. The earlier copy timing is not an overlapped-window FPS measurement.", + "No push, PR, release, merge, approval or BOR/Mesen interaction." + ], + "evidence": "Full native logs, configs, source/build pins, original failure and captures retained in private CUBEVAL validation archive; no SDK, OS media, emulator image or ROM in source.", + "latest_sample_failure_validation": "FAILURES.JSN" +} diff --git a/libtandywin/CANVAS/README.TXT b/libtandywin/CANVAS/README.TXT new file mode 100644 index 0000000..e3ba7e0 --- /dev/null +++ b/libtandywin/CANVAS/README.TXT @@ -0,0 +1,110 @@ +libwintandy - opt-in bounded GDI canvas and damage helper + +Include Windows 3.0 WINDOWS.H then WTCANVAS.H; link WTCANVAS.C once with the +consumer using its existing Win16 memory model. No LTWUI/LWTANDY interface, +DLL export, display driver, timer owner or other consumer is changed. +Production is C89, integer-only, no allocated application buffers or global +state. FAR state/RECT arguments also accept caller stack storage under Win16. + +DAMAGE (usable without a bitmap) + Init(width,height) establishes local coordinates and forces full damage. + Begin discards only the current-frame union, not the last committed bounds. + Add each current object's conservative rectangle (including pen/effects). + Get returns the clipped union of current and previous committed objects. + Clear that damage to the application's background, redraw every current + object, and present/invalidate all damage. Commit ONLY after all required + drawing/presentation succeeds. On partial drawing or an uncertain backing + surface, Reset forces a full clear/redraw/retry. Do not commit failures. + +All rectangles use exclusive right/bottom. Empty, inverted or wholly outside +objects contribute nothing. No padding, geometry, background or occlusion is +inferred. Add every current object, not only objects that moved. No objects +this frame erases the previous union once; after Commit it becomes empty. +Each state is independent. Get is a non-consuming query; Commit is explicit. +Dimensions must be 1..32767. No rectangle arithmetic can wrap a 16-bit int. +Do not mutate fields directly or alias Get's output with the state's fields. + +CANVAS (optional) +Zero-initialize LTWCANVAS. Create uses an existing reference DC and explicit +small dimensions, refusing more than 32760 pixels or 32760 word-aligned mono +bytes. These are conservative allocation bounds, not a claim of guaranteed +free GDI memory. A color DDB's exact allocation is driver-dependent. FALSE +means no usable canvas; retry Destroy if resource handles remain after failed +cleanup. Create refuses any live handles, so it cannot silently leak/rebind. + +mono=TRUE uses CreateBitmap(...,1,1,...); FALSE uses CreateCompatibleBitmap on +the original reference DC, never the initially monochrome memory DC. All new +pixels are cleared black. Monochrome is ONLY for content known to need two +colors; it is not a substitute for arbitrary color/gray/palette rendering. +Cube's Flat path stays color; its Dither/Wire paths motivated this option. + +Borrow state.dc to draw using GDI. Restore caller-selected pens/brushes. Keep +the owned bitmap selected; retain MM_TEXT, zero origins, default black text / +white background and no clip region on the canvas DC. Do not select its bitmap +in another DC. Keep coordinates/dither origin fixed; only the transferred +rectangle shrinks. The helper never intercepts drawing to discover damage. + +Present accepts a local rectangle (NULL = full canvas), clips it to the +surface and copies to destination origin + local coordinates using SRCCOPY. +The caller owns the destination DC and must use MM_TEXT with coordinates +already appropriate to that DC. GDI retains its actual destination clip. +Empty/clipped-away input is a successful no-op. Overflowing signed 16-bit +destination bounds fail before GDI. Mono copies explicitly map zero bits to +zeroColor and one bits to oneColor; both destination colors are restored on +success and attempted on failure. Color canvases leave destination colors +alone. Restoration failure returns FALSE, so callers must not commit damage. + +Destroy restores the original bitmap, deletes the DC, then the owned bitmap. +It never deletes stock objects. It is safe to repeat, including after a partial +Create. Failed deletion retains the handle for retry and makes the state +unpresentable immediately. After Destroy begins, only retry Destroy until it +succeeds; never copy a live state or abandon retained handles. + +WINDOW LIFETIME / EXPOSE +A BitBlt success is not proof that every requested destination pixel was +visible: GDI may clip it. The window must still handle WM_PAINT. Re-present +ps.rcPaint from a valid initialized surface, even when outside animation +damage. Only Commit after all animation damage is covered by the paint update +region; do not replace BeginPaint clipping. The example unions pending damage +into the update region BEFORE BeginPaint, then copies the paint bounds. + +On resize, recreate the surface and Init damage to its new dimensions. On +origin/viewport changes, invalidate/erase the OLD destination area explicitly, +then Reset and invalidate the NEW area; this module does not own windows or +track screen positions. Ordinary exposes do not destroy a retained surface. +If its contents are lost/unknown, Reset and repaint the complete surface. +Minimize/pause animation at the application level; destroy on window teardown. +No direct screen painting, full-screen shadow, VRAM writes or driver escape is +needed. Never defer a display-driver operation through this application API. + +EXAMPLE / CHECKS +examples/CANVAS30 is a separate moving-marker consumer: small mono surface, +coalesced timers, expose recovery, tiny-client resize/recreate and teardown. +It is source/build input, not a qualified native runtime distribution. + python libtandywin/CANVAS/TESTS/RUN.PY --out +Host mocks exercise actual source; they do NOT qualify Windows behavior, +native ABI, pixel equivalence, 8088 performance or GDI memory availability. + +Native adoption gate: build with owned MSC6 /G0 /Gw /AS and Win3 SDK; compare +full-screen deterministic captures and GDI failures across expose, overlap, +move, resize, minimize/restore and repeated create/destroy; record CODE/DGROUP, +warm free memory, copied area and separated draw/copy times. Use the same +actual driver bytes in A/B. Cube's 160 fixture used old TNDY160, not current +TR51616, so its timing must not be generalized to the current driver/library. + +PARTIAL NATIVE RESULTS (2026-10-30) +NATIVE.JSN records matched current TR51616/TR53216 tests: the unchanged helper +passed native ABI/FAR state, pixel/lifecycle, mono/color mapping, failure retry +and bounded-copy timing checks. The example additionally executes its actual +overlapped WinMain as a separate task with real timers and system window +controls, then unloads after both system Close and headless keyboard Alt-F4. +Its compact initial frame now fits the physical screen; helper C/H are +unchanged. The old TNDY160-only observation above remains historical and is +not used as current-driver evidence. These results do not qualify every +client, arbitrary driver, full-period pixel equality or physical hardware. + +The sample's current failure paths are qualified separately in FAILURES.JSN. +It propagates background FillRect failure into full-client paint retry and +keeps its existing task timer/cleanup state alive after HWND destruction until +Destroy succeeds. The sample, not this helper, owns that timer and message +loop. Library C/H and the retry/ownership contract are unchanged. diff --git a/libtandywin/CANVAS/TESTS/CANVTEST.C b/libtandywin/CANVAS/TESTS/CANVTEST.C new file mode 100644 index 0000000..082c82b --- /dev/null +++ b/libtandywin/CANVAS/TESTS/CANVTEST.C @@ -0,0 +1,540 @@ +/* Actual-source host contract tests. GDI is mocked, never native evidence. */ +#include +#include +#include +#include +#include "windows.h" +#include "WTCANVAS.H" + +static unsigned long checks; +static const char *section; +#define CHECK(x) do { checks++; if (!(x)) { \ + fprintf(stderr, "%s:%d [%s] %s\n", __FILE__, __LINE__, section, #x); \ + exit(1); } } while (0) +#define MAX_HANDLES 64 +#define REF_DC 1 +#define DEST_DC 2 +#define STOCK_BITMAP 1 +#define ERROR_COLOR ((DWORD)0xffffffffUL) +enum { NEW_DC, NEW_MONO, NEW_COLOR, SELECT, DELETE_DC, DELETE_BITMAP, + TEXT_COLOR, BK_COLOR, PATBLT, BITBLT, OP_COUNT }; +typedef struct { + int alive; + HBITMAP selected; + DWORD text, background; +} MOCKDC; +typedef struct { int alive, width, height, mono; } MOCKBITMAP; +static MOCKDC dcs[MAX_HANDLES]; +static MOCKBITMAP bitmaps[MAX_HANDLES]; +static int calls[OP_COUNT], fail_at[OP_COUNT], sequence; +static int last_select_sequence, last_delete_dc_sequence; +static int last_delete_bitmap_sequence, memory_dcs, owned_bitmaps; +static HDC compatible_dc_reference, compatible_bitmap_reference; +static int blt_x, blt_y, blt_w, blt_h, blt_sx, blt_sy; +static HDC blt_source, blt_destination; +static DWORD blt_text, blt_background; + +static int fail(int operation) +{ + calls[operation]++; + sequence++; + return calls[operation] == fail_at[operation]; +} +static void reset_mock(void) +{ + CHECK(memory_dcs == 0 && owned_bitmaps == 0); + memset(dcs, 0, sizeof(dcs)); + memset(bitmaps, 0, sizeof(bitmaps)); + memset(calls, 0, sizeof(calls)); + memset(fail_at, 0, sizeof(fail_at)); + sequence = last_select_sequence = last_delete_dc_sequence = 0; + last_delete_bitmap_sequence = 0; + compatible_dc_reference = compatible_bitmap_reference = 0; + dcs[REF_DC].alive = dcs[DEST_DC].alive = 1; + dcs[REF_DC].text = RGB(1, 2, 3); + dcs[REF_DC].background = RGB(4, 5, 6); + dcs[DEST_DC].text = RGB(13, 14, 15); + dcs[DEST_DC].background = RGB(16, 17, 18); + bitmaps[STOCK_BITMAP].alive = 1; +} +static void dc_valid(HDC dc) +{ + CHECK(dc < MAX_HANDLES && dcs[dc].alive); +} +HDC CreateCompatibleDC(HDC reference) +{ + unsigned n; + dc_valid(reference); + compatible_dc_reference = reference; + if (fail(NEW_DC)) return 0; + for (n = 3; n < MAX_HANDLES; n++) if (!dcs[n].alive) { + dcs[n].alive = 1; + dcs[n].selected = STOCK_BITMAP; + dcs[n].text = RGB(0, 0, 0); + dcs[n].background = RGB(255, 255, 255); + memory_dcs++; + return (HDC)n; + } + CHECK(0); + return 0; +} +static HBITMAP new_bitmap(int width, int height, int mono) +{ + unsigned n; + for (n = 2; n < MAX_HANDLES; n++) if (!bitmaps[n].alive) { + bitmaps[n].alive = 1; + bitmaps[n].width = width; + bitmaps[n].height = height; + bitmaps[n].mono = mono; + owned_bitmaps++; + return (HBITMAP)n; + } + CHECK(0); + return 0; +} +HBITMAP CreateCompatibleBitmap(HDC reference, int width, int height) +{ + dc_valid(reference); + compatible_bitmap_reference = reference; + if (fail(NEW_COLOR)) return 0; + return new_bitmap(width, height, 0); +} +HBITMAP CreateBitmap(int width, int height, unsigned planes, + unsigned bits, const void *data) +{ + CHECK(planes == 1 && bits == 1 && data == NULL); + if (fail(NEW_MONO)) return 0; + return new_bitmap(width, height, 1); +} +HGDIOBJ SelectObject(HDC dc, HGDIOBJ object) +{ + HBITMAP old; + unsigned n; + dc_valid(dc); + CHECK(object < MAX_HANDLES && bitmaps[object].alive); + if (fail(SELECT)) return 0; + if (object != STOCK_BITMAP) + for (n = 3; n < MAX_HANDLES; n++) + CHECK(n == dc || !dcs[n].alive || dcs[n].selected != object); + old = dcs[dc].selected; + dcs[dc].selected = object; + last_select_sequence = sequence; + return old; +} +BOOL DeleteDC(HDC dc) +{ + dc_valid(dc); + CHECK(dc >= 3); + if (fail(DELETE_DC)) return FALSE; + last_delete_dc_sequence = sequence; + dcs[dc].alive = 0; + dcs[dc].selected = 0; + memory_dcs--; + return TRUE; +} +BOOL DeleteObject(HGDIOBJ object) +{ + unsigned n; + CHECK(object > STOCK_BITMAP && object < MAX_HANDLES); + CHECK(bitmaps[object].alive); + for (n = 3; n < MAX_HANDLES; n++) + CHECK(!dcs[n].alive || dcs[n].selected != object); + if (fail(DELETE_BITMAP)) return FALSE; + last_delete_bitmap_sequence = sequence; + bitmaps[object].alive = 0; + owned_bitmaps--; + return TRUE; +} +DWORD SetTextColor(HDC dc, DWORD color) +{ + DWORD old; + dc_valid(dc); + if (fail(TEXT_COLOR)) return ERROR_COLOR; + old = dcs[dc].text; + dcs[dc].text = color; + return old; +} +DWORD SetBkColor(HDC dc, DWORD color) +{ + DWORD old; + dc_valid(dc); + if (fail(BK_COLOR)) return ERROR_COLOR; + old = dcs[dc].background; + dcs[dc].background = color; + return old; +} +BOOL PatBlt(HDC dc, int x, int y, int width, int height, DWORD rop) +{ + dc_valid(dc); + CHECK(rop == BLACKNESS && x == 0 && y == 0); + CHECK(dcs[dc].selected > STOCK_BITMAP); + CHECK(width == bitmaps[dcs[dc].selected].width); + CHECK(height == bitmaps[dcs[dc].selected].height); + return !fail(PATBLT); +} +BOOL BitBlt(HDC dest, int x, int y, int width, int height, + HDC source, int sx, int sy, DWORD rop) +{ + dc_valid(dest); + dc_valid(source); + CHECK(rop == SRCCOPY && width > 0 && height > 0); + CHECK(sx >= 0 && sy >= 0 && dcs[source].selected > STOCK_BITMAP); + CHECK(sx + width <= bitmaps[dcs[source].selected].width); + CHECK(sy + height <= bitmaps[dcs[source].selected].height); + CHECK(x >= -32768 && y >= -32768); + CHECK((long)x + width <= 32767L && (long)y + height <= 32767L); + blt_x = x; blt_y = y; blt_w = width; blt_h = height; + blt_sx = sx; blt_sy = sy; blt_source = source; blt_destination = dest; + blt_text = dcs[dest].text; blt_background = dcs[dest].background; + return !fail(BITBLT); +} + +static RECT rect(int left, int top, int right, int bottom) +{ + RECT r; + r.left = (short)left; r.top = (short)top; + r.right = (short)right; r.bottom = (short)bottom; + return r; +} +static void same_rect(const RECT *r, int left, int top, int right, int bottom) +{ + CHECK(r->left == left && r->top == top); + CHECK(r->right == right && r->bottom == bottom); +} +static void damage_cases(void) +{ + LTWDAMAGE s, other; + RECT a, b, d; + section = "damage edge cases"; + CHECK(!LtwDamageInit(NULL, 20, 10)); + LtwDamageReset(NULL); LtwDamageBegin(NULL); LtwDamageCommit(NULL); + CHECK(!LtwDamageAdd(NULL, NULL)); + CHECK(!LtwDamageGet(NULL, NULL)); + CHECK(!LtwDamageInit(&s, 0, 10)); + CHECK(!LtwDamageGet(&s, &d)); same_rect(&d, 0, 0, 0, 0); + CHECK(!LtwDamageInit(&s, 10, -1)); + CHECK(!LtwDamageInit(&s, 32768, 10)); + CHECK(!LtwDamageInit(&s, 10, 32768)); + CHECK(LtwDamageInit(&s, 20, 10)); + CHECK(LtwDamageInit(&other, 7, 9)); + CHECK(LtwDamageGet(&s, &d)); same_rect(&d, 0, 0, 20, 10); + CHECK(!LtwDamageGet(&s, NULL)); + LtwDamageBegin(&s); + a = rect(1, 2, 5, 6); b = rect(15, 0, 25, 4); + CHECK(LtwDamageAdd(&s, &a)); + CHECK(LtwDamageAdd(&s, &b)); + CHECK(!LtwDamageAdd(&s, NULL)); + CHECK(LtwDamageGet(&s, &d)); same_rect(&d, 0, 0, 20, 10); + LtwDamageCommit(&s); + LtwDamageBegin(&s); + a = rect(-10, 8, 2, 20); + CHECK(LtwDamageAdd(&s, &a)); + CHECK(LtwDamageGet(&s, &d)); same_rect(&d, 0, 0, 20, 10); + /* No commit: a failed transfer cannot forget the previously shown frame. */ + LtwDamageBegin(&s); + a = rect(8, 6, 12, 9); CHECK(LtwDamageAdd(&s, &a)); + CHECK(LtwDamageGet(&s, &d)); same_rect(&d, 1, 0, 20, 9); + LtwDamageCommit(&s); + LtwDamageBegin(&s); /* Object disappears: erase last bounds once. */ + CHECK(LtwDamageGet(&s, &d)); same_rect(&d, 8, 6, 12, 9); + LtwDamageCommit(&s); + CHECK(!LtwDamageGet(&s, &d)); same_rect(&d, 0, 0, 0, 0); + a = rect(20, 0, 30, 4); CHECK(!LtwDamageAdd(&s, &a)); + a = rect(-5, 0, 0, 4); CHECK(!LtwDamageAdd(&s, &a)); + a = rect(0, 10, 4, 20); CHECK(!LtwDamageAdd(&s, &a)); + a = rect(0, -5, 4, 0); CHECK(!LtwDamageAdd(&s, &a)); + a = rect(6, 2, 4, 8); CHECK(!LtwDamageAdd(&s, &a)); + a = rect(2, 8, 4, 6); CHECK(!LtwDamageAdd(&s, &a)); + a = rect(3, 2, 3, 8); CHECK(!LtwDamageAdd(&s, &a)); + a = rect(-32768, -32768, 32767, 32767); + CHECK(LtwDamageAdd(&s, &a)); + CHECK(LtwDamageGet(&s, &d)); same_rect(&d, 0, 0, 20, 10); + LtwDamageCommit(&s); + LtwDamageBegin(&s); LtwDamageReset(&s); + CHECK(LtwDamageGet(&s, &d)); same_rect(&d, 0, 0, 20, 10); + CHECK(LtwDamageGet(&other, &d)); same_rect(&d, 0, 0, 7, 9); + CHECK(LtwDamageInit(&s, 3, 2)); /* Resize resets history/full. */ + CHECK(LtwDamageGet(&s, &d)); same_rect(&d, 0, 0, 3, 2); + CHECK(LtwDamageInit(&s, 32767, 32767)); + CHECK(LtwDamageGet(&s, &d)); same_rect(&d, 0, 0, 32767, 32767); +} + +#define PW 17 +#define PH 13 +static unsigned char old_pixels[PH][PW], new_pixels[PH][PW]; +static unsigned long rng = 0x13579UL; +static unsigned random_value(void) +{ + /* Fixed 32-bit generator even on LP64 hosts. */ + rng = (rng * 1664525UL + 1013904223UL) & 0xffffffffUL; + return (unsigned)((rng >> 8) & 0xffffUL); +} +static void damage_property(void) +{ + LTWDAMAGE s; + RECT r, d, again; + int frame, n, x, y, count, full = 1, any, left, top, right, bottom; + section = "2000-frame independent raster-union oracle"; + CHECK(LtwDamageInit(&s, PW, PH)); + memset(old_pixels, 0, sizeof(old_pixels)); + for (frame = 0; frame < 2000; frame++) { + memset(new_pixels, 0, sizeof(new_pixels)); + LtwDamageBegin(&s); + count = (int)(random_value() % 6U); + for (n = 0; n < count; n++) { + r = rect((int)(random_value() % 57U) - 20, + (int)(random_value() % 47U) - 17, + (int)(random_value() % 57U) - 20, + (int)(random_value() % 47U) - 17); + LtwDamageAdd(&s, &r); + for (y = 0; y < PH; y++) for (x = 0; x < PW; x++) + if (x >= r.left && x < r.right && y >= r.top && y < r.bottom) + new_pixels[y][x] = 1; + } + if (frame % 37 == 0) { LtwDamageReset(&s); full = 1; } + any = 0; left = PW; top = PH; right = bottom = 0; + for (y = 0; y < PH; y++) for (x = 0; x < PW; x++) { + if (full || old_pixels[y][x] || new_pixels[y][x]) { + any = 1; + if (x < left) left = x; + if (y < top) top = y; + if (x + 1 > right) right = x + 1; + if (y + 1 > bottom) bottom = y + 1; + } + } + CHECK(LtwDamageGet(&s, &d) == any); + if (any) same_rect(&d, left, top, right, bottom); + else same_rect(&d, 0, 0, 0, 0); + CHECK(LtwDamageGet(&s, &again) == any); + same_rect(&again, d.left, d.top, d.right, d.bottom); + if (frame % 11 != 0) { + LtwDamageCommit(&s); + memcpy(old_pixels, new_pixels, sizeof(old_pixels)); + full = 0; + } /* Otherwise simulate no presentation/commit and preserve history. */ + } +} + +static void blank_canvas(LTWCANVAS *s) +{ + memset(s, 0, sizeof(*s)); +} +static void clean_canvas(const LTWCANVAS *s) +{ + CHECK(!s->dc && !s->bitmap && !s->oldBitmap); + CHECK(!s->width && !s->height && !s->mono); + CHECK(memory_dcs == 0 && owned_bitmaps == 0); +} +static void surface_lifetime(void) +{ + LTWCANVAS s, second; + HDC dc; + HBITMAP bitmap; + int i; + section = "surface lifetime and bounds"; + reset_mock(); blank_canvas(&s); blank_canvas(&second); + CHECK(!LtwCanvasDestroy(NULL)); + CHECK(LtwCanvasDestroy(&s)); clean_canvas(&s); + CHECK(!LtwCanvasCreate(NULL, REF_DC, 20, 10, TRUE)); + CHECK(!LtwCanvasCreate(&s, 0, 20, 10, TRUE)); + CHECK(!LtwCanvasCreate(&s, REF_DC, 0, 10, TRUE)); + CHECK(!LtwCanvasCreate(&s, REF_DC, 10, -1, TRUE)); + CHECK(!LtwCanvasCreate(&s, REF_DC, 32768, 1, TRUE)); + CHECK(!LtwCanvasCreate(&s, REF_DC, 1, 32768, TRUE)); + CHECK(!LtwCanvasCreate(&s, REF_DC, 200, 200, TRUE)); + CHECK(!LtwCanvasCreate(&s, REF_DC, 1, 20000, TRUE)); + CHECK(calls[NEW_DC] == 0); clean_canvas(&s); + CHECK(LtwCanvasCreate(&s, REF_DC, 224, 128, TRUE)); + CHECK(s.width == 224 && s.height == 128 && s.mono); + CHECK(s.oldBitmap == STOCK_BITMAP && dcs[s.dc].selected == s.bitmap); + CHECK(compatible_dc_reference == REF_DC); + CHECK(calls[NEW_MONO] == 1 && calls[NEW_COLOR] == 0 && calls[PATBLT] == 1); + CHECK(dcs[s.dc].text == RGB(0, 0, 0)); + CHECK(dcs[s.dc].background == RGB(255, 255, 255)); + CHECK(!LtwCanvasCreate(&s, REF_DC, 20, 10, FALSE)); + CHECK(calls[NEW_DC] == 1); + CHECK(LtwCanvasCreate(&second, REF_DC, 30, 25, FALSE)); + CHECK(!second.mono && compatible_bitmap_reference == REF_DC); + CHECK(calls[NEW_COLOR] == 1); + dc = s.dc; bitmap = s.bitmap; + CHECK(LtwCanvasDestroy(&s)); + CHECK(!dcs[dc].alive && !bitmaps[bitmap].alive); + CHECK(last_select_sequence < last_delete_dc_sequence); + CHECK(last_delete_dc_sequence < last_delete_bitmap_sequence); + CHECK(memory_dcs == 1 && owned_bitmaps == 1); + CHECK(LtwCanvasDestroy(&second)); clean_canvas(&second); + CHECK(LtwCanvasDestroy(&s)); clean_canvas(&s); + for (i = 0; i < 200; i++) { + CHECK(LtwCanvasCreate(&s, REF_DC, 31, 17, i % 2)); + CHECK(LtwCanvasDestroy(&s)); clean_canvas(&s); + } + CHECK(LtwCanvasCreate(&s, REF_DC, 32760, 1, TRUE)); + CHECK(LtwCanvasDestroy(&s)); clean_canvas(&s); +} +static void allocation_failures(void) +{ + LTWCANVAS s; + int operation; + section = "allocation and initialization failures"; + for (operation = NEW_DC; operation <= PATBLT; operation++) { + if (operation == DELETE_DC || operation == DELETE_BITMAP) continue; + reset_mock(); blank_canvas(&s); + fail_at[operation] = 1; + CHECK(!LtwCanvasCreate(&s, REF_DC, 40, 30, operation != NEW_COLOR)); + clean_canvas(&s); + CHECK(LtwCanvasDestroy(&s)); + } + section = "cleanup failures retain ownership for retry"; + reset_mock(); blank_canvas(&s); + fail_at[NEW_MONO] = 1; fail_at[DELETE_DC] = 1; + CHECK(!LtwCanvasCreate(&s, REF_DC, 40, 30, TRUE)); + CHECK(s.dc && !s.bitmap && memory_dcs == 1); + CHECK(!LtwCanvasCreate(&s, REF_DC, 40, 30, TRUE)); + CHECK(LtwCanvasDestroy(&s)); clean_canvas(&s); + reset_mock(); blank_canvas(&s); + fail_at[SELECT] = 1; fail_at[DELETE_BITMAP] = 1; + CHECK(!LtwCanvasCreate(&s, REF_DC, 40, 30, TRUE)); + CHECK(!s.dc && s.bitmap && owned_bitmaps == 1); + CHECK(LtwCanvasDestroy(&s)); clean_canvas(&s); + reset_mock(); blank_canvas(&s); + CHECK(LtwCanvasCreate(&s, REF_DC, 40, 30, TRUE)); + fail_at[SELECT] = calls[SELECT] + 1; + CHECK(LtwCanvasDestroy(&s)); clean_canvas(&s); + reset_mock(); blank_canvas(&s); + CHECK(LtwCanvasCreate(&s, REF_DC, 40, 30, TRUE)); + fail_at[DELETE_DC] = calls[DELETE_DC] + 1; + CHECK(!LtwCanvasDestroy(&s)); + CHECK(s.dc && s.bitmap && s.oldBitmap && calls[DELETE_BITMAP] == 0); + CHECK(!s.width && !s.height && !s.mono); + CHECK(!LtwCanvasPresent(&s, DEST_DC, 0, 0, NULL, 0, 0)); + CHECK(calls[BITBLT] == 0); + CHECK(LtwCanvasDestroy(&s)); clean_canvas(&s); + reset_mock(); blank_canvas(&s); + CHECK(LtwCanvasCreate(&s, REF_DC, 40, 30, TRUE)); + fail_at[DELETE_BITMAP] = calls[DELETE_BITMAP] + 1; + CHECK(!LtwCanvasDestroy(&s)); + CHECK(!s.dc && s.bitmap && !s.oldBitmap); + CHECK(!s.width && !s.height && !s.mono); + CHECK(!LtwCanvasPresent(&s, DEST_DC, 0, 0, NULL, 0, 0)); + CHECK(LtwCanvasDestroy(&s)); clean_canvas(&s); + reset_mock(); blank_canvas(&s); + CHECK(LtwCanvasCreate(&s, REF_DC, 40, 30, TRUE)); + fail_at[SELECT] = calls[SELECT] + 1; fail_at[DELETE_DC] = 1; + CHECK(!LtwCanvasDestroy(&s)); + CHECK(dcs[s.dc].selected == s.bitmap && calls[DELETE_BITMAP] == 0); + CHECK(!LtwCanvasPresent(&s, DEST_DC, 0, 0, NULL, 0, 0)); + CHECK(LtwCanvasDestroy(&s)); clean_canvas(&s); +} + +static void presentation(void) +{ + LTWCANVAS s; + RECT r; + DWORD old_text, old_bk; + int text_calls, bk_calls, blt_calls; + section = "presentation geometry, clipping and explicit mono colors"; + reset_mock(); blank_canvas(&s); + CHECK(!LtwCanvasPresent(&s, DEST_DC, 0, 0, NULL, 0, 0)); + CHECK(!LtwCanvasPresent(NULL, DEST_DC, 0, 0, NULL, 0, 0)); + CHECK(LtwCanvasCreate(&s, REF_DC, 40, 30, TRUE)); + old_text = dcs[DEST_DC].text; old_bk = dcs[DEST_DC].background; + CHECK(!LtwCanvasPresent(&s, 0, 0, 0, NULL, 0, 0)); + r = rect(-8, 4, 60, 12); + CHECK(LtwCanvasPresent(&s, DEST_DC, 100, 200, &r, RGB(9, 8, 7), RGB(6, 5, 4))); + CHECK(blt_x == 100 && blt_y == 204 && blt_w == 40 && blt_h == 8); + CHECK(blt_sx == 0 && blt_sy == 4 && blt_source == s.dc && blt_destination == DEST_DC); + CHECK(blt_text == RGB(9, 8, 7) && blt_background == RGB(6, 5, 4)); + CHECK(dcs[DEST_DC].text == old_text && dcs[DEST_DC].background == old_bk); + r = rect(3, 7, 11, 19); + CHECK(LtwCanvasPresent(&s, DEST_DC, -20, -20, &r, 0, 0xffffffU)); + CHECK(blt_x == -17 && blt_y == -13 && blt_w == 8 && blt_h == 12); + CHECK(blt_sx == 3 && blt_sy == 7); + CHECK(LtwCanvasPresent(&s, DEST_DC, -32768, -32768, NULL, 0, 0xffffffU)); + CHECK(blt_x == -32768 && blt_y == -32768 && blt_w == 40 && blt_h == 30); + CHECK(LtwCanvasPresent(&s, DEST_DC, 32727, 32737, NULL, 0, 0xffffffU)); + blt_calls = calls[BITBLT]; text_calls = calls[TEXT_COLOR]; bk_calls = calls[BK_COLOR]; + CHECK(!LtwCanvasPresent(&s, DEST_DC, 32728, 0, NULL, 0, 0)); + CHECK(!LtwCanvasPresent(&s, DEST_DC, 0, 32738, NULL, 0, 0)); + r = rect(40, 0, 50, 10); + CHECK(LtwCanvasPresent(&s, DEST_DC, 0, 0, &r, 0, 0)); + r = rect(0, 3, 0, 8); CHECK(LtwCanvasPresent(&s, DEST_DC, 0, 0, &r, 0, 0)); + r = rect(9, 9, 2, 2); CHECK(LtwCanvasPresent(&s, DEST_DC, 0, 0, &r, 0, 0)); + CHECK(calls[BITBLT] == blt_calls && calls[TEXT_COLOR] == text_calls && calls[BK_COLOR] == bk_calls); + CHECK(LtwCanvasDestroy(&s)); clean_canvas(&s); + CHECK(LtwCanvasCreate(&s, REF_DC, 40, 30, FALSE)); + text_calls = calls[TEXT_COLOR]; bk_calls = calls[BK_COLOR]; + CHECK(LtwCanvasPresent(&s, DEST_DC, 10, 20, NULL, 0, 0)); + CHECK(calls[TEXT_COLOR] == text_calls && calls[BK_COLOR] == bk_calls); + CHECK(blt_text == old_text && blt_background == old_bk); + CHECK(blt_x == 10 && blt_y == 20 && blt_w == 40 && blt_h == 30); + CHECK(LtwCanvasDestroy(&s)); clean_canvas(&s); +} +static void presentation_failures(void) +{ + LTWCANVAS s; + LTWDAMAGE damage; + RECT r, d; + DWORD old_text, old_bk; + int operation, text_calls, bk_calls; + section = "presentation failure restoration and uncommitted damage"; + for (operation = TEXT_COLOR; operation <= BITBLT; operation++) { + if (operation == PATBLT) continue; + reset_mock(); blank_canvas(&s); + CHECK(LtwCanvasCreate(&s, REF_DC, 40, 30, TRUE)); + CHECK(LtwDamageInit(&damage, 40, 30)); + r = rect(2, 3, 8, 9); CHECK(LtwDamageAdd(&damage, &r)); + LtwDamageCommit(&damage); LtwDamageBegin(&damage); + r = rect(20, 21, 28, 29); CHECK(LtwDamageAdd(&damage, &r)); + CHECK(LtwDamageGet(&damage, &d)); + old_text = dcs[DEST_DC].text; old_bk = dcs[DEST_DC].background; + fail_at[operation] = calls[operation] + 1; + CHECK(!LtwCanvasPresent(&s, DEST_DC, 0, 0, &d, 0, 0xffffffU)); + CHECK(dcs[DEST_DC].text == old_text && dcs[DEST_DC].background == old_bk); + CHECK(damage.havePrevious && damage.previous.left == 2); + CHECK(LtwDamageGet(&damage, &d)); same_rect(&d, 2, 3, 28, 29); + if (operation != BITBLT) CHECK(calls[BITBLT] == 0); + LtwDamageReset(&damage); + CHECK(LtwDamageGet(&damage, &d)); same_rect(&d, 0, 0, 40, 30); + CHECK(LtwCanvasPresent(&s, DEST_DC, 0, 0, &d, 0, 0xffffffU)); + LtwDamageCommit(&damage); + CHECK(damage.previous.left == 20 && !damage.full); + CHECK(LtwCanvasDestroy(&s)); clean_canvas(&s); + } + section = "failed setup attempts rollback even when rollback fails"; + reset_mock(); blank_canvas(&s); + CHECK(LtwCanvasCreate(&s, REF_DC, 40, 30, TRUE)); + text_calls = calls[TEXT_COLOR]; bk_calls = calls[BK_COLOR]; + old_bk = dcs[DEST_DC].background; + fail_at[BK_COLOR] = calls[BK_COLOR] + 1; + fail_at[TEXT_COLOR] = calls[TEXT_COLOR] + 2; + CHECK(!LtwCanvasPresent(&s, DEST_DC, 0, 0, NULL, 0, 0xffffffU)); + CHECK(calls[TEXT_COLOR] == text_calls + 2 && calls[BK_COLOR] == bk_calls + 1); + CHECK(calls[BITBLT] == 0 && dcs[DEST_DC].background == old_bk); + CHECK(LtwCanvasDestroy(&s)); clean_canvas(&s); + section = "restoration failure is reported and other color still restored"; + for (operation = TEXT_COLOR; operation <= BK_COLOR; operation++) { + reset_mock(); blank_canvas(&s); + CHECK(LtwCanvasCreate(&s, REF_DC, 40, 30, TRUE)); + old_text = dcs[DEST_DC].text; old_bk = dcs[DEST_DC].background; + text_calls = calls[TEXT_COLOR]; bk_calls = calls[BK_COLOR]; + fail_at[operation] = calls[operation] + 2; + CHECK(!LtwCanvasPresent(&s, DEST_DC, 0, 0, NULL, 0, 0xffffffU)); + CHECK(calls[TEXT_COLOR] == text_calls + 2 && calls[BK_COLOR] == bk_calls + 2); + CHECK(calls[BITBLT] == 1); + if (operation == TEXT_COLOR) CHECK(dcs[DEST_DC].background == old_bk); + else CHECK(dcs[DEST_DC].text == old_text); + CHECK(LtwCanvasDestroy(&s)); clean_canvas(&s); + } +} +int main(void) +{ + section = "host type preflight"; + CHECK(sizeof(short) == 2 && SHRT_MIN == -32768 && SHRT_MAX == 32767); + CHECK(sizeof(DWORD) == 4 && UINT_MAX == 0xffffffffU); + CHECK(sizeof(RECT) == 8 && sizeof(HDC) == 2); + damage_cases(); damage_property(); surface_lifetime(); + allocation_failures(); presentation(); presentation_failures(); + printf("PASS: %lu assertions; 2000 damage frames; 200 lifecycle cycles.\n", checks); + puts("Actual WTCANVAS.C with host GDI mocks only; no native ABI/visual/timing claim."); + return 0; +} diff --git a/libtandywin/CANVAS/TESTS/EXAMPLE.C b/libtandywin/CANVAS/TESTS/EXAMPLE.C new file mode 100644 index 0000000..c94cab7 --- /dev/null +++ b/libtandywin/CANVAS/TESTS/EXAMPLE.C @@ -0,0 +1,318 @@ +/* Execute the unchanged example window procedure with host USER/GDI mocks. + * This proves message/call/state behavior, not actual Windows clipping. */ +#include "EXSTUB/windows.h" +#define main LibraryContractMain +#define PatBlt InitOnlyPatBlt +#include "CANVTEST.C" +#undef PatBlt +#undef main +#undef NULL +#define NULL 0 +#include "../../../examples/CANVAS30/CANVAS.C" + +#define WINDOW 1 +static RECT client_rect, update_rect; +static BOOL update_pending, iconic; +static int invalidations, begins, ends, timers, kills, quit; +static int before_begin_invalidations; +static const char *title; +static int screen_width, screen_height, created_x, created_y; +static int created_width, created_height; +static int fill_calls, fail_fill_at; +static int script_mode, script_ticks, retry_failures, create_calls; +#define TASK_TIMER 77 + +int GetSystemMetrics(int index) +{ + return index == SM_CXSCREEN ? screen_width : screen_height; +} + +void GetClientRect(HWND window, RECT *client) +{ + CHECK(window == WINDOW); + *client = client_rect; +} +HDC GetDC(HWND window) +{ + CHECK(window == WINDOW); + return REF_DC; +} +void ReleaseDC(HWND window, HDC dc) +{ + CHECK(window == WINDOW && dc == REF_DC); +} +UINT SetTimer(HWND window, UINT id, UINT milliseconds, void *callback) +{ + CHECK(!window && id == 0 && milliseconds == 100 && !callback); + timers++; + return script_mode == 4 ? 0 : TASK_TIMER; +} +void KillTimer(HWND window, UINT id) +{ + CHECK(!window && id == TASK_TIMER); + CHECK(!memory_dcs && !owned_bitmaps); + kills++; +} +void SetWindowText(HWND window, const char *text) +{ + CHECK(window == WINDOW && text); + title = text; +} +BOOL InvalidateRect(HWND window, const RECT *r, BOOL erase) +{ + RECT clipped; + CHECK(window == WINDOW && !erase); + invalidations++; + clipped = r ? *r : client_rect; + if (clipped.left < 0) clipped.left = 0; + if (clipped.top < 0) clipped.top = 0; + if (clipped.right > client_rect.right) clipped.right = client_rect.right; + if (clipped.bottom > client_rect.bottom) clipped.bottom = client_rect.bottom; + if (clipped.left >= clipped.right || clipped.top >= clipped.bottom) return TRUE; + if (!update_pending) update_rect = clipped; + else { + if (clipped.left < update_rect.left) update_rect.left = clipped.left; + if (clipped.top < update_rect.top) update_rect.top = clipped.top; + if (clipped.right > update_rect.right) update_rect.right = clipped.right; + if (clipped.bottom > update_rect.bottom) update_rect.bottom = clipped.bottom; + } + update_pending = TRUE; + return TRUE; +} +BOOL IsIconic(HWND window) +{ + CHECK(window == WINDOW); + return iconic; +} +HDC BeginPaint(HWND window, PAINTSTRUCT *paint) +{ + CHECK(window == WINDOW && update_pending); + paint->rcPaint = update_rect; + update_pending = FALSE; + before_begin_invalidations = invalidations; + begins++; + return DEST_DC; +} +void EndPaint(HWND window, const PAINTSTRUCT *paint) +{ + CHECK(window == WINDOW && paint); + ends++; +} +HGDIOBJ GetStockObject(int object) +{ + CHECK(object == BLACK_BRUSH); + return 42; +} +int FillRect(HDC dc, const RECT *r, HGDIOBJ brush) +{ + CHECK(dc == DEST_DC && brush == 42); + CHECK(r->left < r->right && r->top < r->bottom); + fill_calls++; + if (fill_calls == fail_fill_at) return FALSE; + return TRUE; +} +BOOL PatBlt(HDC dc, int x, int y, int width, int height, DWORD rop) +{ + dc_valid(dc); + CHECK(rop == BLACKNESS || rop == WHITENESS); + CHECK(dcs[dc].selected > STOCK_BITMAP); + CHECK(x >= 0 && y >= 0 && width > 0 && height > 0); + CHECK(x + width <= bitmaps[dcs[dc].selected].width); + CHECK(y + height <= bitmaps[dcs[dc].selected].height); + return !fail(PATBLT); +} +void PostQuitMessage(int code) +{ + CHECK(code == 0); + CHECK(!memory_dcs && !owned_bitmaps); + quit++; +} +long DefWindowProc(HWND window, UINT message, WPARAM wp, LPARAM lp) +{ + (void)window; (void)message; (void)wp; (void)lp; + return 19L; +} +HICON LoadIcon(HINSTANCE instance, const char *name) +{ + (void)instance; (void)name; + return 1; +} +HCURSOR LoadCursor(HINSTANCE instance, const char *name) +{ + (void)instance; (void)name; + return 1; +} +BOOL RegisterClass(const WNDCLASS *wc) +{ + (void)wc; + return TRUE; +} +HWND CreateWindow(const char *class_name, const char *window_title, DWORD style, + int x, int y, int width, int height, HWND parent, + unsigned short menu, HINSTANCE instance, void *parameter) +{ + (void)class_name; (void)window_title; (void)style; + created_x = x; created_y = y; + created_width = width; created_height = height; + (void)parent; (void)menu; (void)instance; + (void)parameter; + create_calls++; + if (script_mode == 5) return 0; + if (script_mode >= 1 && script_mode <= 3) { + CHECK(CanvasProc(WINDOW, WM_CREATE, 0, 0) == 0L); + CHECK(CanvasProc(WINDOW, WM_SIZE, 0, 0) == 0L); + CHECK(CanvasProc(WINDOW, WM_PAINT, 0, 0) == 0L); + fail_at[script_mode == 2 ? DELETE_BITMAP : DELETE_DC] = + calls[script_mode == 2 ? DELETE_BITMAP : DELETE_DC] + 1; + CHECK(CanvasProc(WINDOW, WM_DESTROY, 0, 0) == 0L); + CHECK(!quit && windowDestroyed && !canvas.width); + } + return WINDOW; +} +void ShowWindow(HWND window, int show) { (void)window; (void)show; } +void UpdateWindow(HWND window) { (void)window; } +BOOL GetMessage(MSG *message, HWND window, UINT first, UINT last) +{ + (void)window; (void)first; (void)last; + message->wParam = 0; + if (script_mode >= 1 && script_mode <= 3 && !quit) { + CHECK(timers == 1 && kills == 0); + CHECK(windowDestroyed && owned_bitmaps == 1); + CHECK(memory_dcs == (script_mode == 2 ? 0 : 1)); + if (script_mode == 3 && retry_failures > 0) { + fail_at[DELETE_DC] = calls[DELETE_DC] + 1; + retry_failures--; + } + script_ticks++; + CHECK(script_ticks <= 4); + message->hwnd = 0; message->message = WM_TIMER; + message->wParam = TASK_TIMER; + return TRUE; + } + return FALSE; +} +void TranslateMessage(const MSG *message) { (void)message; } +void DispatchMessage(const MSG *message) { (void)message; } +long SendMessage(HWND window, UINT message, WPARAM wp, LPARAM lp) +{ CHECK(window == WINDOW); return CanvasProc(window, message, wp, lp); } + +int main(void) +{ + RECT expose; + int initial_invalidations, old_calls, i; + section = "actual example failed-paint partial-expose recovery"; + reset_mock(); + client_rect = rect(0, 0, 40, 30); + CHECK(CanvasProc(WINDOW, WM_CREATE, 0, 0) == 0L && timers == 0); + CHECK(CanvasProc(WINDOW, WM_SIZE, 0, 0) == 0L); + CHECK(update_pending && canvas.width == 40 && canvas.height == 30); + CHECK(CanvasProc(WINDOW, WM_PAINT, 0, 0) == 0L); + CHECK(!damage.full && damage.havePrevious && damage.previous.left == 0); + CHECK(blt_w == 40 && blt_h == 30 && begins == ends); + CHECK(CanvasProc(WINDOW, WM_TIMER, 1, 0) == 0L); + CHECK(position == 1 && update_pending); + fail_at[BITBLT] = calls[BITBLT] + 1; + CHECK(CanvasProc(WINDOW, WM_PAINT, 0, 0) == 0L); + CHECK(damage.full && damage.previous.left == 0 && !update_pending); + CHECK(title && strstr(title, "failed")); + /* External exposure happens BEFORE the next timer can invalidate full. */ + expose = rect(35, 25, 40, 30); + CHECK(InvalidateRect(WINDOW, &expose, FALSE)); + initial_invalidations = invalidations; + CHECK(CanvasProc(WINDOW, WM_PAINT, 0, 0) == 0L); + CHECK(before_begin_invalidations == initial_invalidations + 1); + CHECK(blt_x == 0 && blt_y == 0 && blt_w == 40 && blt_h == 30); + CHECK(!damage.full && damage.previous.left == 1 && !update_pending); + CHECK(begins == ends); + + section = "actual example coalesced timers and outside exposure"; + for (i = 0; i < 5; i++) CHECK(CanvasProc(WINDOW, WM_TIMER, 1, 0) == 0L); + CHECK(position == 6 && damage.previous.left == 1 && update_pending); + CHECK(CanvasProc(WINDOW, WM_PAINT, 0, 0) == 0L); + CHECK(damage.previous.left == 6); + CHECK(blt_x == 1 && blt_w == 17); + expose = rect(35, 25, 40, 30); + CHECK(InvalidateRect(WINDOW, &expose, FALSE)); + CHECK(CanvasProc(WINDOW, WM_PAINT, 0, 0) == 0L); + CHECK(blt_x <= 6 && blt_x + blt_w == 40); + + section = "actual example resize, minimize, zero size, destroy"; + iconic = TRUE; + old_calls = calls[BITBLT]; + CHECK(CanvasProc(WINDOW, WM_TIMER, 1, 0) == 0L); + CHECK(position == 6 && calls[BITBLT] == old_calls); + CHECK(CanvasProc(WINDOW, WM_SIZE, SIZEICONIC, 0) == 0L); + CHECK(canvas.width == 40); + iconic = FALSE; + client_rect = rect(0, 0, 8, 6); + CHECK(CanvasProc(WINDOW, WM_SIZE, 0, 0) == 0L); + CHECK(canvas.width == 8 && canvas.height == 6 && damage.full); + CHECK(CanvasProc(WINDOW, WM_PAINT, 0, 0) == 0L); + CHECK(blt_x == 0 && blt_y == 0 && blt_w == 8 && blt_h == 6); + client_rect = rect(0, 0, 0, 0); + CHECK(CanvasProc(WINDOW, WM_SIZE, 0, 0) == 0L); + CHECK(!canvas.width && !canvas.height && !memory_dcs && !owned_bitmaps); + client_rect = rect(0, 0, 190, 120); + CHECK(CanvasProc(WINDOW, WM_SIZE, 0, 0) == 0L); + CHECK(canvas.width == 160 && canvas.height == 96); + fail_fill_at = fill_calls + 1; + CHECK(CanvasProc(WINDOW, WM_PAINT, 0, 0) == 0L); + CHECK(retryPaint && paintFailed && damage.full && !update_pending); + CHECK(CanvasProc(WINDOW, WM_TIMER, 1, 0) == 0L); + CHECK(update_pending && update_rect.right == 190 && update_rect.bottom == 120); + CHECK(CanvasProc(WINDOW, WM_PAINT, 0, 0) == 0L); + CHECK(!retryPaint && !paintFailed && !damage.full); + CHECK(title && !strstr(title, "failed")); + /* Bottom-strip failure with a marker-only expose before the next tick. */ + fail_fill_at = fill_calls + 2; + CHECK(InvalidateRect(WINDOW, NULL, FALSE)); + CHECK(CanvasProc(WINDOW, WM_PAINT, 0, 0) == 0L); + CHECK(retryPaint && damage.full); + expose = rect(5, 5, 7, 7); + CHECK(InvalidateRect(WINDOW, &expose, FALSE)); + CHECK(CanvasProc(WINDOW, WM_PAINT, 0, 0) == 0L); + CHECK(update_rect.right == 190 && update_rect.bottom == 120); + CHECK(!retryPaint && !damage.full); + CHECK(blt_w == 160 && blt_h == 96); + CHECK(CanvasProc(WINDOW, WM_DESTROY, 0, 0) == 0L); + CHECK(kills == 0 && quit == 1); + clean_canvas(&canvas); + section = "actual WinMain initial outer-frame fit"; + screen_width = 160; screen_height = 200; + CHECK(WinMain(1, 0, "", 5) == 0); + CHECK(created_width == 160 && created_height == 150); + CHECK(created_x == 0 && created_y == 0); + screen_width = 320; screen_height = 200; + CHECK(WinMain(1, 0, "", 5) == 0); + CHECK(created_width == 220 && created_height == 150); + CHECK(created_x == CW_USEDEFAULT && created_y == CW_USEDEFAULT); + section = "actual WinMain retries retained cleanup before quit"; + for (i = 1; i <= 3; i++) { + reset_mock(); quit = kills = timers = script_ticks = 0; + script_mode = i; retry_failures = i == 3 ? 2 : 0; + client_rect = rect(0, 0, 40, 30); + CHECK(WinMain(1, 0, "", 5) == 0); + CHECK(script_ticks == (i == 3 ? 3 : 1)); + CHECK(quit == 1 && kills == 1 && timers == 1); + clean_canvas(&canvas); + } + section = "timer reservation and window creation failure"; + script_mode = 4; create_calls = timers = kills = quit = 0; + CHECK(WinMain(1, 0, "", 5) == 1); + CHECK(!create_calls && timers == 1 && !kills && !quit); + script_mode = 5; create_calls = timers = kills = quit = 0; + CHECK(WinMain(1, 0, "", 5) == 1); + CHECK(create_calls == 1 && timers == 1 && kills == 1 && !quit); + script_mode = 0; + screen_width = 160; screen_height = 100; + CHECK(WinMain(1, 0, "", 5) == 0); + CHECK(created_width == 160 && created_height == 100); + CHECK(created_x == 0 && created_y == 0); + screen_width = screen_height = 0; + CHECK(WinMain(1, 0, "", 5) == 0); + CHECK(created_width == 220 && created_height == 150); + CHECK(created_x == CW_USEDEFAULT && created_y == CW_USEDEFAULT); + printf("PASS: %lu example assertions; failed-paint/expose, coalesced timers, resize/minimize/zero-size/destroy.\n", checks); + puts("Actual CANVAS.C window procedure with host USER/GDI mocks; not native visual QA."); + return 0; +} diff --git a/libtandywin/CANVAS/TESTS/EXSTUB/windows.h b/libtandywin/CANVAS/TESTS/EXSTUB/windows.h new file mode 100644 index 0000000..6fef0e7 --- /dev/null +++ b/libtandywin/CANVAS/TESTS/EXSTUB/windows.h @@ -0,0 +1,70 @@ +#ifndef CANVAS_EXAMPLE_TEST_WINDOWS_H +#define CANVAS_EXAMPLE_TEST_WINDOWS_H +#include "../windows.h" +#undef NULL +#define NULL 0 +#define PASCAL +typedef unsigned short HWND; +typedef unsigned short HINSTANCE; +typedef unsigned short HICON; +typedef unsigned short HCURSOR; +typedef unsigned int UINT; +typedef unsigned short WPARAM; +typedef long LPARAM; +typedef struct { RECT rcPaint; } PAINTSTRUCT; +typedef struct { HWND hwnd; UINT message; WPARAM wParam; } MSG; +typedef struct { + UINT style; + long (*lpfnWndProc)(HWND, UINT, WPARAM, LPARAM); + int cbClsExtra, cbWndExtra; + HINSTANCE hInstance; + HICON hIcon; + HCURSOR hCursor; + HGDIOBJ hbrBackground; + const char *lpszMenuName, *lpszClassName; +} WNDCLASS; +#define WM_CREATE 1 +#define WM_SIZE 5 +#define WM_TIMER 275 +#define WM_ERASEBKGND 20 +#define WM_PAINT 15 +#define WM_DESTROY 2 +#define SIZEICONIC 1 +#define WHITENESS ((DWORD)0x00ff0062UL) +#define BLACK_BRUSH 4 +#define CS_HREDRAW 2 +#define CS_VREDRAW 1 +#define IDI_APPLICATION ((const char *)32512) +#define IDC_ARROW ((const char *)32512) +#define WS_OVERLAPPEDWINDOW 0x00cf0000UL +#define CW_USEDEFAULT (-32768) +#define SM_CXSCREEN 0 +#define SM_CYSCREEN 1 +int GetSystemMetrics(int index); +void GetClientRect(HWND window, RECT *client); +HDC GetDC(HWND window); +void ReleaseDC(HWND window, HDC dc); +UINT SetTimer(HWND window, UINT id, UINT milliseconds, void *callback); +void KillTimer(HWND window, UINT id); +void SetWindowText(HWND window, const char *text); +BOOL InvalidateRect(HWND window, const RECT *r, BOOL erase); +BOOL IsIconic(HWND window); +HDC BeginPaint(HWND window, PAINTSTRUCT *paint); +void EndPaint(HWND window, const PAINTSTRUCT *paint); +HGDIOBJ GetStockObject(int object); +int FillRect(HDC dc, const RECT *r, HGDIOBJ brush); +void PostQuitMessage(int code); +long DefWindowProc(HWND window, UINT message, WPARAM wp, LPARAM lp); +HICON LoadIcon(HINSTANCE instance, const char *name); +HCURSOR LoadCursor(HINSTANCE instance, const char *name); +BOOL RegisterClass(const WNDCLASS *wc); +HWND CreateWindow(const char *class_name, const char *title, DWORD style, + int x, int y, int width, int height, HWND parent, + unsigned short menu, HINSTANCE instance, void *parameter); +void ShowWindow(HWND window, int show); +void UpdateWindow(HWND window); +BOOL GetMessage(MSG *message, HWND window, UINT first, UINT last); +void TranslateMessage(const MSG *message); +void DispatchMessage(const MSG *message); +long SendMessage(HWND window, UINT message, WPARAM wp, LPARAM lp); +#endif diff --git a/libtandywin/CANVAS/TESTS/RUN.PY b/libtandywin/CANVAS/TESTS/RUN.PY new file mode 100644 index 0000000..14879a6 --- /dev/null +++ b/libtandywin/CANVAS/TESTS/RUN.PY @@ -0,0 +1,64 @@ +"""Strict C89 actual-source canvas/example checks with host mocks, not native QA.""" +from pathlib import Path +import argparse +import hashlib +import json +import os +import shlex +import subprocess +import sys + +root = Path(__file__).resolve().parents[1] +repository = root.parents[1] +parser = argparse.ArgumentParser(description=__doc__) +parser.add_argument('--out', type=Path, required=True) +args = parser.parse_args() +args.out.mkdir(parents=True, exist_ok=False) +out = args.out.resolve() +launcher = Path(r'C:\Users\chad\Documents\Codex\toolchains\gcc-ucrt64.py') +if os.name == 'nt': + if not launcher.is_file(): + parser.error('Shared Windows GCC launcher missing: ' + str(launcher)) + compiler = [sys.executable, str(launcher), '--'] +else: + compiler = shlex.split(os.environ.get('CC', 'cc')) +flags = ['-x', 'c', '-std=c89', '-pedantic', '-Wall', '-Wextra', '-Wshadow', + '-Wstrict-prototypes', '-Werror'] +env = os.environ.copy() +if os.name == 'nt': + env['PATH'] = r'C:\msys64\ucrt64\bin' + os.pathsep + env.get('PATH', '') +record = {'evidence': 'actual source with host USER/GDI mocks only; not native qualification', + 'checks': {}} +inputs = [root / 'WTCANVAS.C', root / 'WTCANVAS.H', root / 'TESTS/windows.h', + root / 'TESTS/CANVTEST.C', root / 'TESTS/EXAMPLE.C', + root / 'TESTS/EXSTUB/windows.h', repository / 'examples/CANVAS30/CANVAS.C'] +record['source'] = {p.relative_to(repository).as_posix(): { + 'bytes': len(p.read_bytes()), 'sha256': hashlib.sha256(p.read_bytes()).hexdigest()} + for p in inputs} +for name, source, stub in [('CANVTEST', 'CANVTEST.C', root / 'TESTS'), + ('EXAMPLE', 'EXAMPLE.C', root / 'TESTS/EXSTUB')]: + exe = out / (name + '.EXE') + # The actual example uses native int-to-RECT assignments; host RECT is + # deliberately short while host int is wider. Library/contracts also pass + # Wconversion; do not suppress all warnings merely to model that ABI. + check_flags = flags + (['-Wconversion'] if name == 'CANVTEST' else []) + command = compiler + check_flags + ['-I' + str(stub), '-I' + str(root), + str(root / 'WTCANVAS.C'), str(root / 'TESTS' / source), '-o', str(exe)] + compiled = subprocess.run(command, capture_output=True, timeout=60) + (out / (name + '-COMPILE.LOG')).write_bytes(compiled.stdout + compiled.stderr) + item = {'command': command, 'compile_returncode': compiled.returncode} + record['checks'][name] = item + if not compiled.returncode: + run = subprocess.run([str(exe)], capture_output=True, timeout=30, env=env) + (out / (name + '-RUN.LOG')).write_bytes(run.stdout + run.stderr) + item['returncode'] = run.returncode + item['output'] = (run.stdout + run.stderr).decode('ascii', errors='replace') + sys.stdout.buffer.write(run.stdout) + sys.stderr.buffer.write(run.stderr) + code = run.returncode + else: + sys.stderr.buffer.write(compiled.stdout + compiled.stderr) + code = compiled.returncode + (out / 'RESULT.JSON').write_text(json.dumps(record, indent=2) + '\n', encoding='ascii') + if code: + raise SystemExit(code) diff --git a/libtandywin/CANVAS/TESTS/windows.h b/libtandywin/CANVAS/TESTS/windows.h new file mode 100644 index 0000000..d729efe --- /dev/null +++ b/libtandywin/CANVAS/TESTS/windows.h @@ -0,0 +1,30 @@ +#ifndef WTCANVAS_TEST_WINDOWS_H +#define WTCANVAS_TEST_WINDOWS_H +/* Host-only GDI model: Win16-sized RECT/handles, 32-bit DWORD. Not an SDK. */ +#include +typedef unsigned short HDC; +typedef unsigned short HBITMAP; +typedef unsigned short HGDIOBJ; +typedef unsigned int DWORD; +typedef int BOOL; +typedef char *LPSTR; +typedef struct { short left, top, right, bottom; } RECT; +#define FAR +#define TRUE 1 +#define FALSE 0 +#define BLACKNESS ((DWORD)0x00000042UL) +#define SRCCOPY ((DWORD)0x00cc0020UL) +#define RGB(r,g,b) ((DWORD)(r) | ((DWORD)(g) << 8) | ((DWORD)(b) << 16)) +HDC CreateCompatibleDC(HDC reference); +HBITMAP CreateCompatibleBitmap(HDC reference, int width, int height); +HBITMAP CreateBitmap(int width, int height, unsigned planes, + unsigned bits, const void *data); +HGDIOBJ SelectObject(HDC dc, HGDIOBJ object); +BOOL DeleteDC(HDC dc); +BOOL DeleteObject(HGDIOBJ object); +DWORD SetTextColor(HDC dc, DWORD color); +DWORD SetBkColor(HDC dc, DWORD color); +BOOL PatBlt(HDC dc, int x, int y, int width, int height, DWORD rop); +BOOL BitBlt(HDC dest, int x, int y, int width, int height, + HDC source, int sx, int sy, DWORD rop); +#endif diff --git a/libtandywin/CANVAS/WTCANVAS.C b/libtandywin/CANVAS/WTCANVAS.C new file mode 100644 index 0000000..ed9145d --- /dev/null +++ b/libtandywin/CANVAS/WTCANVAS.C @@ -0,0 +1,183 @@ +#define WINVER 0x0300 +#include +#include "WTCANVAS.H" + +static void empty_rect(RECT FAR *r) +{ + r->left = r->top = r->right = r->bottom = 0; +} +static BOOL dimensions(int width, int height) +{ + return width > 0 && height > 0 && width <= 32767 && height <= 32767; +} +static BOOL clip_rect(const RECT FAR *in, int width, int height, + RECT FAR *out) +{ + RECT r; + empty_rect(&r); + if (in && dimensions(width, height)) { + r = *in; + if (r.left < 0) r.left = 0; + if (r.top < 0) r.top = 0; + if (r.right > width) r.right = (short)width; + if (r.bottom > height) r.bottom = (short)height; + if (r.left >= r.right || r.top >= r.bottom) empty_rect(&r); + } + *out = r; + return r.right > r.left && r.bottom > r.top; +} +static void union_rect(RECT FAR *a, const RECT FAR *b) +{ + if (b->left < a->left) a->left = b->left; + if (b->top < a->top) a->top = b->top; + if (b->right > a->right) a->right = b->right; + if (b->bottom > a->bottom) a->bottom = b->bottom; +} +BOOL LtwDamageInit(LTWDAMAGE FAR *s, int width, int height) +{ + if (!s) return FALSE; + s->width = s->height = 0; + empty_rect(&s->previous); + empty_rect(&s->current); + s->havePrevious = s->haveCurrent = s->full = FALSE; + if (!dimensions(width, height)) return FALSE; + s->width = width; + s->height = height; + s->full = TRUE; + return TRUE; +} +void LtwDamageReset(LTWDAMAGE FAR *s) +{ + if (s) s->full = TRUE; +} +void LtwDamageBegin(LTWDAMAGE FAR *s) +{ + if (!s) return; + s->haveCurrent = FALSE; + empty_rect(&s->current); +} +BOOL LtwDamageAdd(LTWDAMAGE FAR *s, const RECT FAR *object) +{ + RECT r; + if (!s || !clip_rect(object, s->width, s->height, &r)) return FALSE; + if (s->haveCurrent) union_rect(&s->current, &r); + else s->current = r; + s->haveCurrent = TRUE; + return TRUE; +} +BOOL LtwDamageGet(const LTWDAMAGE FAR *s, RECT FAR *dirty) +{ + if (!dirty) return FALSE; + empty_rect(dirty); + if (!s || !dimensions(s->width, s->height)) return FALSE; + if (s->full) { + dirty->right = (short)s->width; + dirty->bottom = (short)s->height; + return TRUE; + } + if (s->havePrevious) *dirty = s->previous; + if (s->haveCurrent) { + if (s->havePrevious) union_rect(dirty, &s->current); + else *dirty = s->current; + } + return s->havePrevious || s->haveCurrent; +} +void LtwDamageCommit(LTWDAMAGE FAR *s) +{ + if (!s) return; + s->previous = s->current; + s->havePrevious = s->haveCurrent; + s->full = FALSE; +} + +BOOL LtwCanvasDestroy(LTWCANVAS FAR *s) +{ + if (!s) return FALSE; + /* A partly destroyed canvas must never be presented. */ + s->width = s->height = 0; + s->mono = FALSE; + if (s->dc) { + if (s->oldBitmap) SelectObject(s->dc, s->oldBitmap); + /* DeleteDC detaches even if restoring the selection failed. Never + * delete a bitmap while a live DC may still have it selected. */ + if (!DeleteDC(s->dc)) return FALSE; + s->dc = 0; + s->oldBitmap = 0; + } + if (s->bitmap) { + if (!DeleteObject(s->bitmap)) return FALSE; + s->bitmap = 0; + } + s->oldBitmap = 0; + s->width = s->height = 0; + s->mono = FALSE; + return TRUE; +} +BOOL LtwCanvasCreate(LTWCANVAS FAR *s, HDC reference, + int width, int height, BOOL mono) +{ + if (!s || s->dc || s->bitmap || s->oldBitmap || !reference || + !dimensions(width, height) || + (long)width * height > LTW_CANVAS_MAX_PIXELS || + ((long)width + 15L) / 16L * 2L * height > + LTW_CANVAS_MAX_MONO_BYTES) return FALSE; + s->width = s->height = 0; + s->mono = FALSE; + s->dc = CreateCompatibleDC(reference); + if (!s->dc) return FALSE; + s->bitmap = mono ? CreateBitmap(width, height, 1, 1, (LPSTR)0) : + CreateCompatibleBitmap(reference, width, height); + if (!s->bitmap) goto fail; + s->oldBitmap = SelectObject(s->dc, s->bitmap); + if (!s->oldBitmap) goto fail; + if (SetTextColor(s->dc, RGB(0, 0, 0)) == 0xffffffffUL || + SetBkColor(s->dc, RGB(255, 255, 255)) == 0xffffffffUL || + !PatBlt(s->dc, 0, 0, width, height, BLACKNESS)) goto fail; + s->width = width; + s->height = height; + s->mono = mono != FALSE; + return TRUE; +fail: + /* Failed cleanup leaves handles owned by s, so Destroy can be retried. */ + LtwCanvasDestroy(s); + return FALSE; +} +BOOL LtwCanvasPresent(const LTWCANVAS FAR *s, HDC destination, + int x, int y, const RECT FAR *rect, + DWORD zeroColor, DWORD oneColor) +{ + RECT r; + DWORD oldText, oldBk; + BOOL ok; + if (!s || !s->dc || !s->bitmap || !s->oldBitmap || !destination || + !dimensions(s->width, s->height)) return FALSE; + if (rect) { + if (!clip_rect(rect, s->width, s->height, &r)) return TRUE; + } else { + r.left = r.top = 0; + r.right = (short)s->width; + r.bottom = (short)s->height; + } + /* Check before 16-bit addition. Exclusive right/bottom must fit too. */ + if ((long)x + r.left < -32768L || (long)y + r.top < -32768L || + (long)x + r.right > 32767L || (long)y + r.bottom > 32767L) + return FALSE; + oldText = oldBk = 0; + if (s->mono) { + oldText = SetTextColor(destination, zeroColor); + if (oldText == 0xffffffffUL) return FALSE; + oldBk = SetBkColor(destination, oneColor); + if (oldBk == 0xffffffffUL) { + SetTextColor(destination, oldText); + return FALSE; + } + } + ok = BitBlt(destination, x + r.left, y + r.top, + r.right - r.left, r.bottom - r.top, + s->dc, r.left, r.top, SRCCOPY); + if (s->mono) { + if (SetBkColor(destination, oldBk) == 0xffffffffUL) ok = FALSE; + if (SetTextColor(destination, oldText) == 0xffffffffUL) ok = FALSE; + } + return ok; +} diff --git a/libtandywin/CANVAS/WTCANVAS.H b/libtandywin/CANVAS/WTCANVAS.H new file mode 100644 index 0000000..fb3aa32 --- /dev/null +++ b/libtandywin/CANVAS/WTCANVAS.H @@ -0,0 +1,48 @@ +/* Optional Windows 3.0 / 8088 canvas helpers. Include WINDOWS.H first. */ +#ifndef WTCANVAS_H +#define WTCANVAS_H + +/* Right/bottom are exclusive. Coordinates are local to an unchanged canvas. + * Add ALL current objects each frame, including pen/effect padding. */ +typedef struct { + int width, height; + RECT previous, current; + BOOL havePrevious, haveCurrent, full; +} LTWDAMAGE; + +BOOL LtwDamageInit(LTWDAMAGE FAR *state, int width, int height); +void LtwDamageReset(LTWDAMAGE FAR *state); /* next damage is full */ +void LtwDamageBegin(LTWDAMAGE FAR *state); /* begin current object union */ +BOOL LtwDamageAdd(LTWDAMAGE FAR *state, const RECT FAR *object); +BOOL LtwDamageGet(const LTWDAMAGE FAR *state, RECT FAR *dirty); +/* Commit only after successful clear + draw + presentation of ALL damage. + * After any partial/failed drawing, Reset before trying the next frame. */ +void LtwDamageCommit(LTWDAMAGE FAR *state); + +/* Zero initialize. Owns one DC + bitmap; never copy a live state. The caller + * borrows dc to draw, restoring pens/brushes and keeping its bitmap selected, + * MM_TEXT mapping, zero origin, no clip region and default mono colors. + * No timers, windows, VRAM, global state or implicit screen ownership. */ +#define LTW_CANVAS_MAX_PIXELS 32760L +#define LTW_CANVAS_MAX_MONO_BYTES 32760L +typedef struct { + HDC dc; + HBITMAP bitmap, oldBitmap; + int width, height; + BOOL mono; +} LTWCANVAS; + +/* Explicit small dimensions; initially black. Color uses reference, NOT the + * freshly-created (monochrome) memory DC. Refuse a nonempty state. */ +BOOL LtwCanvasCreate(LTWCANVAS FAR *state, HDC reference, + int width, int height, BOOL mono); +/* Restores original bitmap, deletes DC before bitmap; retry if FALSE. */ +BOOL LtwCanvasDestroy(LTWCANVAS FAR *state); +/* NULL rect means full canvas. Empty/clipped-away rect succeeds as a no-op. + * Caller owns MM_TEXT destination DC and its lifetime/clip. Mono expansion + * maps 0 to zeroColor and 1 to oneColor, then restores both destination colors. + * Color canvases ignore these colors. Failure never commits damage. */ +BOOL LtwCanvasPresent(const LTWCANVAS FAR *state, HDC destination, + int x, int y, const RECT FAR *rect, + DWORD zeroColor, DWORD oneColor); +#endif