Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions .github/workflows/canvas.yml
Original file line number Diff line number Diff line change
@@ -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"
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions docs/BUILD.TXT
Original file line number Diff line number Diff line change
Expand Up @@ -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 <new-output>
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:
Expand Down
12 changes: 12 additions & 0 deletions examples/CANVAS30/BUILD.BAT
Original file line number Diff line number Diff line change
@@ -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
198 changes: 198 additions & 0 deletions examples/CANVAS30/CANVAS.C
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
/* Moving-marker consumer of WTCANVAS; no Cube geometry or driver access. */
#define WINVER 0x0300
#include <windows.h>
#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;
}
9 changes: 9 additions & 0 deletions examples/CANVAS30/CANVAS.DEF
Original file line number Diff line number Diff line change
@@ -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
5 changes: 5 additions & 0 deletions examples/CANVAS30/CANVAS.RSP
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
CANVAS.OBJ WTCANVAS.OBJ
CANVAS.EXE
CANVAS.MAP
SLIBCEW LIBW
CANVAS.DEF
58 changes: 58 additions & 0 deletions examples/CANVAS30/README.TXT
Original file line number Diff line number Diff line change
@@ -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.
Loading
Loading