Fullscreen exclusive mode.

This commit is contained in:
Scott Duensing 2026-09-29 20:23:31 -05:00
parent 0dcd6bf212
commit 0e943b843f
93 changed files with 2366 additions and 139 deletions

1
.gitignore vendored
View file

@ -14,3 +14,4 @@ dvx.log
claudeDocs/
toolchains/
dist/
NOTES.md

View file

@ -41,7 +41,10 @@ CORE_LIBS = libdvx libtasks
.PHONY: all clean $(LIBS) loader widgets apps tools installer dist deploy-helpsrc compile-help deploy-sdk test test-quick test-asan test-host test-samples coverage
# Every build ends by making the release media in dist/ (a second or
# two), so dist/ always holds the latest DVX.
all: $(LIBS) loader tools widgets apps installer deploy-helpsrc compile-help deploy-sdk
src/installer/mkDist.sh $(or $(FLOPPY),1.44M)
$(LIBS):
$(MAKE) -C src/libs/kpunch/$@
@ -67,9 +70,9 @@ installer:
# Release media in dist/: the CD image and floppy images (FLOPPY=360K,
# 720K, 1.2M, 1.44M (default), 2.88M or a number of bytes per disk).
# See src/installer/mkDist.sh.
# See src/installer/mkDist.sh. `all` makes them; `make dist` is the
# same build under the name people look for.
dist: all
src/installer/mkDist.sh $(or $(FLOPPY),1.44M)
apps: libdvx libtasks dvxshell tools
$(MAKE) -C src/apps/kpunch
@ -241,6 +244,6 @@ clean:
$(MAKE) -C src/tools clean
$(MAKE) -C src/tools/proxy clean
$(MAKE) -C src/installer clean
rm -rf obj bin/host bin/test bin/system dist
rm -rf obj bin/host bin/test bin/system
rm -rf bin/config bin/widgets bin/libs bin/sdk
rm -f docs/*.html

View file

@ -190,6 +190,7 @@ img { max-width: 100%; }
<li><a href="#games.input">Keyboard, Mouse and Joysticks</a></li>
<li><a href="#games.timing">Timing and Frame Pacing</a></li>
<li><a href="#games.screen">The Screen: Framebuffers and Palettes</a></li>
<li><a href="#games.fullscreen">The Whole Screen</a></li>
<li><a href="#games.sound">Sound, Music and MIDI</a></li>
<li><a href="#games.files">Data Files and Saved Games</a></li>
<li><a href="#games.packaging">Packaging and Ending</a></li>
@ -274,6 +275,7 @@ img { max-width: 100%; }
<li><a href="#api.draw">accelParse</a></li>
<li><a href="#api.types">AccelTableT</a></li>
<li><a href="#lib.dvxnet.api">accept</a></li>
<li><a href="#games.fullscreen">Alt+Enter</a></li>
<li><a href="#user.windows">Alt+Tab</a></li>
<li><a href="#widget.ansiterm">AnsiTerm</a></li>
<li><a href="#widget.ansiterm">AnsiTermExApiT</a></li>
@ -530,6 +532,16 @@ img { max-width: 100%; }
<li><a href="#api.driver">dvxDrv.h</a></li>
<li><a href="#api.app">DvxEntropyHookT</a></li>
<li><a href="#api.dialog">dvxErrorBox</a></li>
<li><a href="#api.app">dvxExclusiveBegin</a></li>
<li><a href="#games.fullscreen">dvxExclusiveBegin</a></li>
<li><a href="#api.app">dvxExclusiveBlitOps</a></li>
<li><a href="#api.app">dvxExclusiveDisplay</a></li>
<li><a href="#api.app">dvxExclusiveEnd</a></li>
<li><a href="#api.app">dvxExclusiveFlush</a></li>
<li><a href="#api.app">dvxExclusivePresent8</a></li>
<li><a href="#games.fullscreen">dvxExclusivePresent8</a></li>
<li><a href="#api.app">dvxExclusiveSetPalette</a></li>
<li><a href="#api.app">dvxExclusiveWaitRetrace</a></li>
<li><a href="#api.dialog">dvxFileDialog</a></li>
<li><a href="#api.app">dvxFitWindow</a></li>
<li><a href="#api.app">dvxFitWindowH</a></li>
@ -660,6 +672,7 @@ img { max-width: 100%; }
<li><a href="#arch.events">Event Dispatch</a></li>
<li><a href="#arch.events">Events</a></li>
<li><a href="#lib.dvxnet.api">EWOULDBLOCK</a></li>
<li><a href="#games.fullscreen">Exclusive mode</a></li>
<li><a href="#games.packaging">exit</a></li>
<li><a href="#user.progman">Exit DVX</a></li>
<li><a href="#shell.lifecycle">Exit DVX</a></li>
@ -687,6 +700,9 @@ img { max-width: 100%; }
<li><a href="#games.screen">Framebuffer</a></li>
<li><a href="#lib.game">FRAMEWAIT</a></li>
<li><a href="#games.basic">FRAMEWAIT</a></li>
<li><a href="#user.programs">Full screen</a></li>
<li><a href="#games.fullscreen">Full screen</a></li>
<li><a href="#api.app">Full-screen exclusive mode</a></li>
<li><a href="#lib.game">game</a></li>
<li><a href="#games.input">Game Input</a></li>
<li><a href="#games.porting">Game library</a></li>
@ -1438,6 +1454,7 @@ img { max-width: 100%; }
<li><a href="#widget.box">wgtVBox</a></li>
<li><a href="#widget.separator">wgtVSeparator</a></li>
<li><a href="#widget.wrapbox">wgtWrapBox</a></li>
<li><a href="#user.programs">Whole screen</a></li>
<li><a href="#arch.widgets">Widget API</a></li>
<li><a href="#widget.base">Widget Base</a></li>
<li><a href="#api.widgetplugin">widgetAddChild</a></li>
@ -1820,6 +1837,8 @@ img { max-width: 100%; }
<li>Press Alt+Tab until its window comes to the front, or</li>
<li>Press Ctrl+Esc to open the Task Manager, pick the program and click Switch To.</li>
</ul>
<h2>Programs That Use the Whole Screen</h2>
<p>Some programs, games especially, can fill the whole screen instead of a window (many use Alt+Enter to switch). Your other programs keep running while they do. Alt+Tab and Ctrl+Esc still work and bring the desktop back; when you return to the program, it fills the screen again.</p>
<h2>Closing a Program</h2>
<p>Close the program's main window, or choose Exit from its File menu. A program that has unsaved work usually asks whether you want to save it first. See Working with Windows for all the ways to close a window.</p>
<h2>Help for Each Program</h2>
@ -4163,6 +4182,38 @@ void dvxBeep(void);</code></pre>
void dvxSetSoundHook(DvxSoundHookT fn);</code></pre>
<p>The function that plays the sounds, set by the sound library when it loads (NULL: silent). Programs do not call it.</p>
<h2>Full-Screen Exclusive Mode</h2>
<p>A window's program takes the whole screen in a video mode of its own (a game at 320x200, say) while every other program keeps running: windows keep painting into their own buffers, and the desktop is drawn again when the program lets go. The owner's window must have the focus: when it loses it (Alt+Tab, Ctrl+Esc, a dialog, another window coming to the front) the desktop comes back and onChange hears ExclusiveSuspendedE; when the window has the focus again the mode is set again and onChange hears ExclusiveActiveE (draw the whole frame: the screen and the backbuffer start black). While active every key but Alt+Tab, Ctrl+Esc and Alt+F4 goes to the window as in game input mode, the mouse cursor is not drawn, onMouse gets full-screen coordinates, dvxScreenshot saves the full screen, and dvxChangeVideoMode fails. Closing the window, the program ending or being ended, and dvxExclusiveEnd give the screen back. See the Writing Games guide.</p>
<pre><code>typedef enum {
ExclusiveActiveE, // the screen is the program's: draw the whole frame
ExclusiveSuspendedE, // its window lost the focus: the desktop is back until it has the focus again
ExclusiveEndedE // the mode could not be set again: full screen is over
} ExclusiveStateE;
typedef void (*ExclusiveFnT)(WindowT *win, ExclusiveStateE state, void *userData);</code></pre>
<h3>dvxExclusiveBegin</h3>
<pre><code>int32_t dvxExclusiveBegin(AppContextT *ctx, WindowT *win, int32_t width, int32_t height, int32_t bpp, ExclusiveFnT onChange, void *userData);</code></pre>
<p>Take the whole screen for win (raised and focused): the smallest mode of at least width x height, at bpp when there is one (0: any depth). At 8 bpp (or 0) a request that fits 320x200 gets VGA mode 13h, and one that fits 320x240 VGA Mode X (square pixels, two pages flipped at the retrace), unless the card has a VBE mode no larger. onChange (may be NULL) hears ExclusiveActiveE before this returns. A window that already has full screen gets the new size.</p>
<p>Returns: DVX_EXCL_OK; DVX_EXCL_ERR_ARGS (no such window, or a bad size); DVX_EXCL_ERR_BUSY (another window has full screen); DVX_EXCL_ERR_MODE (no mode could be set; the desktop is back).</p>
<h3>dvxExclusiveEnd</h3>
<pre><code>void dvxExclusiveEnd(AppContextT *ctx, WindowT *win);</code></pre>
<p>Give the screen back: the desktop's mode, palette and mouse, and a full repaint. onChange is not called. Nothing when win does not have full screen.</p>
<h3>dvxExclusiveDisplay / dvxExclusiveBlitOps</h3>
<pre><code>DisplayT *dvxExclusiveDisplay(AppContextT *ctx);
const BlitOpsT *dvxExclusiveBlitOps(AppContextT *ctx);</code></pre>
<p>The full-screen display (its size may be larger than asked for) and the span operations for its pixel format while active; NULL while suspended or without full screen. Draw into the display's backBuf, with DVX's draw functions if you like, then dvxExclusiveFlush.</p>
<h3>dvxExclusiveFlush</h3>
<pre><code>void dvxExclusiveFlush(AppContextT *ctx, const RectT *r);</code></pre>
<p>Copy a rectangle of the backbuffer to the screen (NULL: all of it). Nothing when not active. In Mode X a whole-screen flush goes to the hidden page, which is shown from the next vertical retrace (the call waits for it): no tearing; a smaller rectangle goes to the page on show.</p>
<h3>dvxExclusivePresent8</h3>
<pre><code>bool dvxExclusivePresent8(AppContextT *ctx, const uint8_t *src, int32_t srcW, int32_t srcH, int32_t srcPitch);</code></pre>
<p>Show an 8-bit picture of palette indexes on the whole screen: scaled by the largest whole factor that fits, centred on black, through the palette in any pixel format, and flushed (the whole screen, so Mode X flips pages). Returns false (and does nothing) when not active, so a caller falls back to drawing in its window.</p>
<h3>dvxExclusiveSetPalette</h3>
<pre><code>void dvxExclusiveSetPalette(AppContextT *ctx, const uint8_t *pal, int32_t first, int32_t count);</code></pre>
<p>Set palette entries first..first+count-1 (pal is the whole 256-entry table, R, G, B bytes per entry). At 8 bpp they go to the DAC at once, so a picture on screen changes colour; at other depths the next dvxExclusivePresent8 uses them. The palette is kept while suspended and set again on resuming; the desktop's palette comes back whenever the desktop does.</p>
<h3>dvxExclusiveWaitRetrace</h3>
<pre><code>void dvxExclusiveWaitRetrace(void);</code></pre>
<p>Wait for the start of the next vertical retrace (at most two frames' time), before a flush, so the frame is not torn.</p>
<h2>Accelerators</h2>
<h3>dvxCreateAccelTable</h3>
<pre><code>AccelTableT *dvxCreateAccelTable(void);</code></pre>
@ -7298,6 +7349,31 @@ for (int32_t i = 0; i &lt; 256; i++) {
</ul>
<p>Size the window so its content area is exactly the framebuffer times a whole scale that fits the screen (the template computes the scale from the display size and the chrome constants, then corrects with dvxResizeWindow after adding the menu bar). Copying only what changed matters on slower machines: a full 320x200 frame at 2x on a 16-bit desktop is 256 KB of pixels to convert and composite.</p>
</div>
<div class="topic" id="games.fullscreen">
<h1>The Whole Screen</h1>
<p>A game may also take the whole screen in a video mode of its own, as the template does with Alt+Enter. Every other program keeps running while it does: their windows go on painting into their own buffers, their timers fire, their sound plays, and the desktop is drawn again, as it is now, when the game lets go.</p>
<pre><code>static void fullChanged(WindowT *win, ExclusiveStateE state, void *userData) {
// Active: draw the whole frame. Suspended: draw in the window again.
// Ended: the mode could not be set again; full screen is over.
}
if (dvxExclusiveBegin(ctx, win, 320, 200, 8, fullChanged, NULL) == DVX_EXCL_OK) {
dvxExclusiveSetPalette(ctx, gamePal, 0, 256);
}
// every frame
if (!dvxExclusivePresent8(ctx, fb, 320, 200, 320)) {
// not on the whole screen: present into the window as usual
}</code></pre>
<ul>
<li>dvxExclusiveBegin sets the smallest mode of at least the size asked for, at the depth asked for when there is one (0: any). At 8 bits per pixel a request that fits 320x200 gets VGA mode 13h, and one that fits 320x240 gets VGA Mode X (square pixels, and two pages: a whole-screen flush is shown from the next retrace, so frames never tear), unless the card has a VBE mode no larger. Either way the game draws into an ordinary linear backbuffer. The display may be larger than asked for; dvxExclusiveDisplay says what it is.</li>
<li>The game's window must have the focus. When it loses it (Alt+Tab, Ctrl+Esc, a dialog, another program's window coming to the front) the desktop comes back and the callback hears ExclusiveSuspendedE; when the window has the focus again the mode is set again and it hears ExclusiveActiveE. Draw the whole frame then: the screen starts black.</li>
<li>While the game has the screen, every key but Alt+Tab, Ctrl+Esc and Alt+F4 goes to its window, as in a game input window; the mouse cursor is not drawn; onMouse gets whole-screen coordinates.</li>
<li>dvxExclusivePresent8 shows a picture of palette numbers on the whole screen, scaled by the largest whole factor that fits and centred on black, in any pixel format. At 8 bits per pixel the palette is the screen's own: dvxExclusiveSetPalette changes colours at once, so palette animation costs nothing, and a hardware model of up to 16 palettes of 16 colours fits in the 256 entries.</li>
<li>To draw yourself, draw into dvxExclusiveDisplay(ctx)-&gt;backBuf (DVX's draw functions work on it with dvxExclusiveBlitOps) and call dvxExclusiveFlush. dvxExclusiveWaitRetrace waits for the vertical retrace before a flush, so the frame is not torn.</li>
<li>dvxExclusiveEnd gives the screen back. Closing the window, the program ending and End Task do too.</li>
</ul>
</div>
<div class="topic" id="games.sound">
<h1>Sound, Music and MIDI</h1>
<p>Games play sound on whatever the computer has: a Sound Blaster family card, an FM synthesizer chip, a MIDI port, the PC speaker, or nothing. Nothing fails because of missing hardware: audioOpen returns NULL (or the device plays tones only), and the game carries on silently. Several programs can play at once, and everything a program opened is stopped and released when it ends.</p>
@ -7361,7 +7437,7 @@ End Sub</code></pre>
<h1>Porting a Game Library to DVX</h1>
<p>Portable game libraries for DOS-era machines usually keep everything machine-specific behind a small hardware layer: start and stop, show a frame, set the palette, read input, wait for the next frame, play sound, and find files. A DVX port replaces that layer. The rest of the library, including drawing code, sprite compilers that write x86 code into memory, and asset formats built for DOS, works unchanged, because a DVX program is 32-bit protected-mode x86 code in the same flat memory as a DOS program built with the same compiler.</p>
<h2>The Model: a Window, a Task, a Virtual Frame Rate</h2>
<p>The game becomes a DVX program with its own main loop: appMain calls the game's main() (compile the game with -Dmain=gameMain, say, and call gameMain from appMain). The library's start-up creates a game input window whose content area is the game's screen at a whole scale; its frame wait yields until the next frame on the high-resolution clock. The game shares the screen with the desktop, so there is no mode change, no direct palette hardware and no vertical retrace; the port converts the library's back buffer into the window each frame.</p>
<p>The game becomes a DVX program with its own main loop: appMain calls the game's main() (compile the game with -Dmain=gameMain, say, and call gameMain from appMain). The library's start-up creates a game input window whose content area is the game's screen at a whole scale; its frame wait yields until the next frame on the high-resolution clock. In a window the game shares the screen with the desktop, so there is no mode change, no direct palette hardware and no vertical retrace; the port converts the library's back buffer into the window each frame. On the whole screen (see The Whole Screen) the game has its own mode, its own palette and the vertical retrace, while the other programs run on.</p>
<h2>Mapping a Hardware Layer to DVX</h2>
<pre> Hardware layer DVX
-------------- ---
@ -7369,9 +7445,11 @@ End Sub</code></pre>
Shut-down dvxDestroyWindow, return from appMain
Error text, debug log dvxLog (DVX.LOG)
Present the back buffer colour table -&gt; win-&gt;contentBuf, dvxInvalidateRect; onPaint
Palette packColor, gamePalNearest, GamePalCacheT
Present, whole screen dvxExclusivePresent8, or dvxExclusiveDisplay + dvxExclusiveFlush
Full screen on and off dvxExclusiveBegin, dvxExclusiveEnd
Palette packColor, gamePalNearest, GamePalCacheT; whole screen: dvxExclusiveSetPalette
Palette per scan line one colour table per palette, chosen per row
Wait for vertical blank gameFrameInit, gameFrameWait
Wait for vertical blank gameFrameInit, gameFrameWait; whole screen: dvxExclusiveWaitRetrace
Frame counter, frame rate GameFramePacerT.frame, the rate given to gameFrameInit
Milliseconds platformMicros() / 1000
Held keys onRawKey, gameKeyIsDown, platformKeyStateGet
@ -7397,8 +7475,8 @@ End Sub</code></pre>
<li>Start-up: make the window not resizable, with its content area the game's screen at a whole scale that fits the display; give it onClose and onPaint. audioOpen returning NULL (no sound device) is not an error.</li>
<li>Shut-down: close what the port opened and return. Anything left (sounds, music, joystick handles, timers) is released when the program ends.</li>
<li>Present: expand the rows or word bands that changed through the colour table into the content buffer, scaled, and invalidate them; onPaint does the whole screen. See The Screen: Framebuffers and Palettes.</li>
<li>Palette: scale 12-bit or 18-bit colour to 0..255 per channel before looking it up. Never touch the VGA palette registers: other programs share the screen.</li>
<li>Vertical blank: there is none in a window. The frame rate is a virtual one: use the rate the library reports (60 or 70 frames a second); truncate the frame counter to the library's width.</li>
<li>Palette: scale 12-bit or 18-bit colour to 0..255 per channel before looking it up. Never touch the VGA palette registers: other programs share the screen, and on the whole screen dvxExclusiveSetPalette does it (and puts the desktop's palette back when the game lets go).</li>
<li>Vertical blank: there is none in a window. The frame rate is a virtual one: use the rate the library reports (60 or 70 frames a second); truncate the frame counter to the library's width. On the whole screen dvxExclusiveWaitRetrace waits for the real one.</li>
<li>Keyboard: raw codes are set-1 scancodes with KEY_RAW_EXT for the keys sent behind 0xE0, so a DOS port's scancode table maps directly. Never hook the keyboard interrupt.</li>
<li>Mouse: divide content-area pixels by the scale and clamp.</li>
<li>Joystick: axes are -32768..32767 after calibration and dead zone; divide down to the library's range, or compare with a threshold for a digital stick. Buttons are bits. Never read the game port directly; calibration lives in the Control Panel.</li>
@ -7413,7 +7491,8 @@ End Sub</code></pre>
<li>Shared library or static: built as a DVX library (.lib, loaded when DVX starts) the port is shared by every game, but its global state (the stage, the key table) means one game at a time. Linked into each game's .app (the LIBS variable) every game has its own copy, several can run at once, and nothing is loaded while no game runs.</li>
<li>Paths: libraries that open &quot;DATA/...&quot; or write saves to a fixed folder need a run-time root (appDir and configDir) instead of compile-time paths.</li>
<li>Speed: every frame costs a conversion and a composite. Present only what changed; a 1x scale on a 486 and larger scales on faster machines.</li>
<li>Tearing: there is no vertical retrace in a window. Games that depend on raster timing need rework.</li>
<li>Tearing: there is no vertical retrace in a window. On the whole screen there is (dvxExclusiveWaitRetrace), but games that change registers mid-frame (raster effects) still need rework.</li>
<li>Whole screen: offer it (Alt+Enter is the custom) and handle ExclusiveSuspendedE by drawing in the window again: the player can Alt+Tab to the desktop at any time.</li>
<li>The 256-colour desktop: nearest colours only; say so in the game's help if it needs 15-bit colour or better.</li>
</ul>
<h2>Checking a Port</h2>
@ -8189,7 +8268,7 @@ if (!job) {
handshake Flow control mode (RS232_HANDSHAKE_*).</pre>
<p>Returns RS232_SUCCESS or a negative error code. The UART type is probed once here (RS232_ERR_NO_UART when nothing answers at the port address); only a 16550A gets its FIFO enabled and is fed 16 bytes per transmit interrupt, every other UART one byte.</p>
<h2>rs232Close</h2>
<p>Close a COM port and release its ISR resources.</p>
<p>Close a COM port and release its ISR resources. The modem control lines are left as they are: closing never drops DTR or RTS, so a modem stays connected and the next program to open the port can carry on. To hang up, call rs232SetDtr(com, false) before closing; the line stays down after the close. Opening a port raises DTR and RTS.</p>
<pre><code>int32_t rs232Close(int32_t com);</code></pre>
<h2>rs232Read</h2>
<p>Non-blocking read from the receive ring buffer.</p>
@ -8247,9 +8326,9 @@ if (!job) {
rs232SetHandshake(com, handshake) Change flow control mode. Releases any throttle the old mode applied (raises RTS/DTR or sends XON) and starts the transmit side from the live CTS/DSR line.
rs232SetIrq(com, irq) Move an open port to another IRQ (for a wrong auto-detection). Interrupt enables and DTR/RTS are kept.
rs232SetMcr(com, mcr) Set modem control register directly.
rs232SetDtr(com, dtr) Set DTR line state.
rs232SetDtr(com, dtr) Set DTR line state. The state is kept when the port is closed (dropping DTR is how a modem is hung up).
rs232SetBreak(com, on) Hold the transmit line in the break (spacing) state (on = true) or release it. The caller times the break, typically 250-500 ms.
rs232SetRts(com, rts) Set RTS line state.
rs232SetRts(com, rts) Set RTS line state. The state is kept when the port is closed.
rs232SetFifoThreshold(com, thr) Set FIFO trigger level (1, 4, 8, or 14 bytes) or 0 to turn the FIFO off. Levels other than 0 return RS232_ERR_INVALID_FIFO unless the UART is a 16550A.</pre>
<h2>Buffer Management</h2>
<pre> Function Description

BIN
screenshots/86box/fullmode-fm-13h.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
screenshots/86box/fullmode-fm-desk.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
screenshots/86box/fullmode-fm-modex.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
screenshots/86box/fullmode-fm-vbe.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
screenshots/86box/fullscr-fs-desk.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
screenshots/86box/fullscr-fs-game.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
screenshots/dosbox/fullmode-fm-13h.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
screenshots/dosbox/fullmode-fm-desk.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
screenshots/dosbox/fullmode-fm-modex.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
screenshots/dosbox/fullmode-fm-vbe.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
screenshots/dosbox/fullscr-fs-desk.png (Stored with Git LFS) Normal file

Binary file not shown.

BIN
screenshots/dosbox/fullscr-fs-game.png (Stored with Git LFS) Normal file

Binary file not shown.

View file

@ -0,0 +1,4 @@
# The first coin: its palette entry glows, so its colour depends on the
# frame the game was paused in (full screen in mode 13h: game pixels are
# screen pixels)
184 96 8 8

View file

@ -135,6 +135,13 @@ typedef struct {
int (*getIrq)(int);
int (*setBase)(int, int);
int (*setIrq)(int, int);
int (*getCts)(int);
int (*getDcd)(int);
int (*getDsr)(int);
int (*getDtr)(int);
int (*getRts)(int);
int (*setDtr)(int, bool);
int (*setRts)(int, bool);
} SerApiT;
static SerApiT sSerApi;
@ -143,6 +150,7 @@ static bool sSerApiResolved = false;
// Per-COM raw serial state: open tracking and terminal attach
#define SER_RECV_BUF 4096
#define SER_BASIC_TRUE (-1) // BASIC's True, for the Ser functions that return one
typedef struct {
bool opened; // SerOpen succeeded and SerClose has not run
@ -626,14 +634,23 @@ void SerClose(int32_t com);
void SerDetach(int32_t com);
void SerFlush(int32_t com);
int32_t SerGetBase(int32_t com);
int32_t SerGetCts(int32_t com);
int32_t SerGetDcd(int32_t com);
int32_t SerGetDsr(int32_t com);
int32_t SerGetDtr(int32_t com);
int32_t SerGetIrq(int32_t com);
int32_t SerGetRts(int32_t com);
int32_t SerGetUart(int32_t com);
static void serIdlePoll(void *ctx);
static int32_t serLineGet(int (*const *get)(int), int32_t com);
static void serLineSet(int (*const *set)(int, bool), int32_t com, int32_t on);
int32_t SerOpen(int32_t com, int32_t baud, int32_t dataBits, const char *parity, int32_t stopBits, int32_t handshake);
const char *SerRead(int32_t com);
static bool serResolveApi(void);
void SerSetBase(int32_t com, int32_t base);
void SerSetDtr(int32_t com, int32_t on);
void SerSetIrq(int32_t com, int32_t irq);
void SerSetRts(int32_t com, int32_t on);
static int32_t serTermRead(void *ctx, uint8_t *buf, int32_t maxLen);
static int32_t serTermWrite(void *ctx, const uint8_t *data, int32_t len);
int32_t SerWrite(int32_t com, const char *data);
@ -6104,6 +6121,26 @@ int32_t SerGetBase(int32_t com) {
}
int32_t SerGetCts(int32_t com) {
return serLineGet(&sSerApi.getCts, com);
}
int32_t SerGetDcd(int32_t com) {
return serLineGet(&sSerApi.getDcd, com);
}
int32_t SerGetDsr(int32_t com) {
return serLineGet(&sSerApi.getDsr, com);
}
int32_t SerGetDtr(int32_t com) {
return serLineGet(&sSerApi.getDtr, com);
}
int32_t SerGetIrq(int32_t com) {
if (!serResolveApi() || !sSerApi.getIrq) {
return 0;
@ -6113,6 +6150,11 @@ int32_t SerGetIrq(int32_t com) {
}
int32_t SerGetRts(int32_t com) {
return serLineGet(&sSerApi.getRts, com);
}
int32_t SerGetUart(int32_t com) {
if (!serResolveApi() || !sSerApi.getUartType) {
return 0;
@ -6134,6 +6176,29 @@ static void serIdlePoll(void *ctx) {
}
// A modem line of an open port as BASIC's True / False (False on an error
// or a port that is not open). get points at the sSerApi field, read
// after serResolveApi has filled it.
static int32_t serLineGet(int (*const *get)(int), int32_t com) {
if (!serResolveApi() || !*get) {
return 0;
}
return (*get)(com - 1) > 0 ? SER_BASIC_TRUE : 0;
}
// Raise (on nonzero) or drop a modem line of an open port. The line keeps
// its state when the port is closed.
static void serLineSet(int (*const *set)(int, bool), int32_t com, int32_t on) {
if (!serResolveApi() || !*set) {
return;
}
(*set)(com - 1, on != 0);
}
int32_t SerOpen(int32_t com, int32_t baud, int32_t dataBits, const char *parity, int32_t stopBits, int32_t handshake) {
if (!serResolveApi()) {
return 0;
@ -6153,7 +6218,7 @@ int32_t SerOpen(int32_t com, int32_t baud, int32_t dataBits, const char *parity,
sSerAttach[idx].opened = true;
}
return -1;
return SER_BASIC_TRUE;
}
@ -6192,6 +6257,13 @@ static bool serResolveApi(void) {
sSerApi.getIrq = dlsym(NULL, DVX_SYM("rs232GetIrq"));
sSerApi.setBase = dlsym(NULL, DVX_SYM("rs232SetBase"));
sSerApi.setIrq = dlsym(NULL, DVX_SYM("rs232SetIrq"));
sSerApi.getCts = dlsym(NULL, DVX_SYM("rs232GetCts"));
sSerApi.getDcd = dlsym(NULL, DVX_SYM("rs232GetDcd"));
sSerApi.getDsr = dlsym(NULL, DVX_SYM("rs232GetDsr"));
sSerApi.getDtr = dlsym(NULL, DVX_SYM("rs232GetDtr"));
sSerApi.getRts = dlsym(NULL, DVX_SYM("rs232GetRts"));
sSerApi.setDtr = dlsym(NULL, DVX_SYM("rs232SetDtr"));
sSerApi.setRts = dlsym(NULL, DVX_SYM("rs232SetRts"));
return sSerApi.open != NULL;
}
@ -6206,6 +6278,11 @@ void SerSetBase(int32_t com, int32_t base) {
}
void SerSetDtr(int32_t com, int32_t on) {
serLineSet(&sSerApi.setDtr, com, on);
}
void SerSetIrq(int32_t com, int32_t irq) {
if (!serResolveApi() || !sSerApi.setIrq) {
return;
@ -6215,6 +6292,11 @@ void SerSetIrq(int32_t com, int32_t irq) {
}
void SerSetRts(int32_t com, int32_t on) {
serLineSet(&sSerApi.setRts, com, on);
}
// Raw serial terminal callbacks
static int32_t serTermRead(void *ctx, uint8_t *buf, int32_t maxLen) {
SerAttachT *sa = (SerAttachT *)ctx;

View file

@ -548,6 +548,8 @@ Pictures are printed in black and white, as large as fits on the page. Printing
.index Running Programs
.index Starting Programs
.index Switching Programs
.index Full screen
.index Whole screen
.index Program Help
.index About
@ -579,6 +581,10 @@ Most programs can only run once at a time. If you start a program that is alread
.item Press Ctrl+Esc to open the Task Manager, pick the program and click Switch To.
.endlist
.h2 Programs That Use the Whole Screen
Some programs, games especially, can fill the whole screen instead of a window (many use Alt+Enter to switch). Your other programs keep running while they do. Alt+Tab and Ctrl+Esc still work and bring the desktop back; when you return to the program, it fills the screen again.
.h2 Closing a Program
Close the program's main window, or choose Exit from its File menu. A program that has unsaved work usually asks whether you want to save it first. See Working with Windows for all the ways to close a window.

View file

@ -84,7 +84,7 @@ DECLARE LIBRARY "basrt"
' Open a raw serial port. Returns True on success.
DECLARE FUNCTION SerOpen(BYVAL com AS LONG, BYVAL baud AS LONG, BYVAL dataBits AS LONG, BYVAL parity AS STRING, BYVAL stopBits AS LONG, BYVAL handshake AS LONG) AS LONG
' Close a raw serial port.
' Close a raw serial port. DTR and RTS keep their state.
DECLARE SUB SerClose(BYVAL com AS LONG)
' Write a string to the serial port. Returns True on success.
@ -105,6 +105,25 @@ DECLARE LIBRARY "basrt"
' Detach a raw serial port from a Terminal widget.
DECLARE SUB SerDetach(BYVAL com AS LONG)
' =========================================================
' Modem Control Lines (raw serial port must be open)
' =========================================================
' Raise (True) or drop (False) DTR or RTS. SerOpen raises both.
' SerClose leaves them as they are, so closing never hangs up a
' modem: to hang up, call SerSetDtr(com, False) before SerClose.
DECLARE SUB SerSetDtr(BYVAL com AS LONG, BYVAL state AS LONG)
DECLARE SUB SerSetRts(BYVAL com AS LONG, BYVAL state AS LONG)
' True when the line is up. DTR and RTS are ours; CTS (clear to
' send), DSR (data set ready) and DCD (carrier detect) come from the
' other end.
DECLARE FUNCTION SerGetDtr(BYVAL com AS LONG) AS LONG
DECLARE FUNCTION SerGetRts(BYVAL com AS LONG) AS LONG
DECLARE FUNCTION SerGetCts(BYVAL com AS LONG) AS LONG
DECLARE FUNCTION SerGetDsr(BYVAL com AS LONG) AS LONG
DECLARE FUNCTION SerGetDcd(BYVAL com AS LONG) AS LONG
' =========================================================
' Packet Serial (reliable delivery + channel multiplexing)
' =========================================================

View file

@ -32,6 +32,8 @@
# dist/DVX.ISO ISO 9660 image of dist/cd
# dist/floppy/DISKnn.IMG floppy images: Disk 1 = SETUP.EXE, README.TXT
# and DVX.P01; Disk n = DVX.P0n
# dist/sdk.zip the SDK (bin/sdk) in DVX-SDK/, with the
# technical and BASIC references in docs/
#
# FLOPPY: 360K, 720K, 1.2M, 1.44M (default), 2.88M -- images of that
# format -- or a number of bytes: the room on each disk, and folders
@ -41,8 +43,9 @@
# DVX_DIST_BIN / DVX_DIST_OUT replace bin/ and dist/ (the DOS test
# harness builds from its bin snapshot into its run folder).
#
# Needs mtools (mformat, mcopy, mdir) and mkisofs on the build machine
# (the DOS test harness already needs mtools; mkcd.sh mkisofs).
# Needs mtools (mformat, mcopy, mdir), mkisofs and zip on the build
# machine (the DOS test harness already needs mtools; mkcd.sh mkisofs;
# zip keeps the SDK tools' execute bits).
set -euo pipefail
@ -82,6 +85,8 @@ format() {
}
command -v zip > /dev/null || fail "zip is missing (it packs dist/sdk.zip)"
for f in "${TOOL}" "${SETUP}"; do
[ -x "${f}" ] || [ -f "${f}" ] || fail "${f} is missing (run make first)"
done
@ -148,6 +153,15 @@ done
rm -rf "${STAGE}"
# The SDK: bin/sdk and the references (make all writes docs/) in DVX-SDK/
SDK_STAGE="${DIST}/sdkstage"
mkdir -p "${SDK_STAGE}/DVX-SDK/docs"
cp -r "${BIN}/sdk/." "${SDK_STAGE}/DVX-SDK/"
cp "${ROOT}/docs/dvx_system_reference.html" "${ROOT}/docs/dvx_basic_reference.html" "${SDK_STAGE}/DVX-SDK/docs/"
(cd "${SDK_STAGE}" && zip -q -r -X "${DIST}/sdk.zip" DVX-SDK)
rm -rf "${SDK_STAGE}"
echo "mkDist: SETUP.EXE ${SETUP_SIZE} bytes; package $(stat -c %s "${DIST}/dvx.zip") bytes"
echo "mkDist: CD: ${DIST}/DVX.ISO (SETUP.EXE $(stat -c %s "${DIST}/cd/SETUP.EXE") bytes with the package)"
echo "mkDist: floppies (${FLOPPY}): ${DISKS} disk(s) in ${DIST}/floppy, ${ROOM1} bytes of the package on Disk 1"
echo "mkDist: SDK: ${DIST}/sdk.zip ($(stat -c %s "${DIST}/sdk.zip") bytes)"

View file

@ -833,7 +833,9 @@ static void secureStart(ConnT *c, uint32_t nowMs) {
}
// Closing the port drops DTR, which also hangs up a modem.
// Ending the connection drops DTR, which also hangs up a modem. Closing
// the port alone would leave DTR as it is; with DTR down when the port
// closes, it stays down.
static void serialClose(ConnT *c) {
if (!c->portOpen) {
return;
@ -843,6 +845,7 @@ static void serialClose(ConnT *c) {
rs232SetBreak(c->cfg.comPort, false);
}
rs232SetDtr(c->cfg.comPort, false);
rs232Close(c->cfg.comPort);
c->portOpen = false;
}

View file

@ -2330,6 +2330,15 @@ Install an icon that is already in display pixel format (for example from dvxLoa
.index dvxResLoadData
.index dvxTextHash
.index dvxChangeVideoMode
.index dvxExclusiveBegin
.index dvxExclusiveEnd
.index dvxExclusiveDisplay
.index dvxExclusiveBlitOps
.index dvxExclusiveFlush
.index dvxExclusivePresent8
.index dvxExclusiveSetPalette
.index dvxExclusiveWaitRetrace
.index Full-screen exclusive mode
.h1 dvxApp.h -- Layer 5: Application API
@ -3131,6 +3140,79 @@ void dvxSetSoundHook(DvxSoundHookT fn);
The function that plays the sounds, set by the sound library when it loads (NULL: silent). Programs do not call it.
.h2 Full-Screen Exclusive Mode
A window's program takes the whole screen in a video mode of its own (a game at 320x200, say) while every other program keeps running: windows keep painting into their own buffers, and the desktop is drawn again when the program lets go. The owner's window must have the focus: when it loses it (Alt+Tab, Ctrl+Esc, a dialog, another window coming to the front) the desktop comes back and onChange hears ExclusiveSuspendedE; when the window has the focus again the mode is set again and onChange hears ExclusiveActiveE (draw the whole frame: the screen and the backbuffer start black). While active every key but Alt+Tab, Ctrl+Esc and Alt+F4 goes to the window as in game input mode, the mouse cursor is not drawn, onMouse gets full-screen coordinates, dvxScreenshot saves the full screen, and dvxChangeVideoMode fails. Closing the window, the program ending or being ended, and dvxExclusiveEnd give the screen back. See the Writing Games guide.
.code
typedef enum {
ExclusiveActiveE, // the screen is the program's: draw the whole frame
ExclusiveSuspendedE, // its window lost the focus: the desktop is back until it has the focus again
ExclusiveEndedE // the mode could not be set again: full screen is over
} ExclusiveStateE;
typedef void (*ExclusiveFnT)(WindowT *win, ExclusiveStateE state, void *userData);
.endcode
.h3 dvxExclusiveBegin
.code
int32_t dvxExclusiveBegin(AppContextT *ctx, WindowT *win, int32_t width, int32_t height, int32_t bpp, ExclusiveFnT onChange, void *userData);
.endcode
Take the whole screen for win (raised and focused): the smallest mode of at least width x height, at bpp when there is one (0: any depth). At 8 bpp (or 0) a request that fits 320x200 gets VGA mode 13h, and one that fits 320x240 VGA Mode X (square pixels, two pages flipped at the retrace), unless the card has a VBE mode no larger. onChange (may be NULL) hears ExclusiveActiveE before this returns. A window that already has full screen gets the new size.
Returns: DVX_EXCL_OK; DVX_EXCL_ERR_ARGS (no such window, or a bad size); DVX_EXCL_ERR_BUSY (another window has full screen); DVX_EXCL_ERR_MODE (no mode could be set; the desktop is back).
.h3 dvxExclusiveEnd
.code
void dvxExclusiveEnd(AppContextT *ctx, WindowT *win);
.endcode
Give the screen back: the desktop's mode, palette and mouse, and a full repaint. onChange is not called. Nothing when win does not have full screen.
.h3 dvxExclusiveDisplay / dvxExclusiveBlitOps
.code
DisplayT *dvxExclusiveDisplay(AppContextT *ctx);
const BlitOpsT *dvxExclusiveBlitOps(AppContextT *ctx);
.endcode
The full-screen display (its size may be larger than asked for) and the span operations for its pixel format while active; NULL while suspended or without full screen. Draw into the display's backBuf, with DVX's draw functions if you like, then dvxExclusiveFlush.
.h3 dvxExclusiveFlush
.code
void dvxExclusiveFlush(AppContextT *ctx, const RectT *r);
.endcode
Copy a rectangle of the backbuffer to the screen (NULL: all of it). Nothing when not active. In Mode X a whole-screen flush goes to the hidden page, which is shown from the next vertical retrace (the call waits for it): no tearing; a smaller rectangle goes to the page on show.
.h3 dvxExclusivePresent8
.code
bool dvxExclusivePresent8(AppContextT *ctx, const uint8_t *src, int32_t srcW, int32_t srcH, int32_t srcPitch);
.endcode
Show an 8-bit picture of palette indexes on the whole screen: scaled by the largest whole factor that fits, centred on black, through the palette in any pixel format, and flushed (the whole screen, so Mode X flips pages). Returns false (and does nothing) when not active, so a caller falls back to drawing in its window.
.h3 dvxExclusiveSetPalette
.code
void dvxExclusiveSetPalette(AppContextT *ctx, const uint8_t *pal, int32_t first, int32_t count);
.endcode
Set palette entries first..first+count-1 (pal is the whole 256-entry table, R, G, B bytes per entry). At 8 bpp they go to the DAC at once, so a picture on screen changes colour; at other depths the next dvxExclusivePresent8 uses them. The palette is kept while suspended and set again on resuming; the desktop's palette comes back whenever the desktop does.
.h3 dvxExclusiveWaitRetrace
.code
void dvxExclusiveWaitRetrace(void);
.endcode
Wait for the start of the next vertical retrace (at most two frames' time), before a flush, so the frame is not torn.
.h2 Accelerators
.h3 dvxCreateAccelTable

View file

@ -242,6 +242,12 @@ static bool drawPopupRow(AppContextT *ctx, DisplayT *d, const BlitOpsT *ops, int
static void drawSysMenu(AppContextT *ctx, DisplayT *d, const BlitOpsT *ops, const RectT *clipTo);
static void entropyFeed(const uint8_t *event, int32_t len);
static void enumModeCb(int32_t w, int32_t h, int32_t bpp, void *userData);
static bool exclusiveEnter(AppContextT *ctx);
static void exclusiveLeave(AppContextT *ctx);
static void exclusiveLutSet(ExclusiveStateT *ex, int32_t first, int32_t count);
static void exclusiveMouse(AppContextT *ctx);
static void exclusiveNotify(AppContextT *ctx, WindowT *win, ExclusiveFnT fn, void *userData, ExclusiveStateE state);
static void exclusiveSync(AppContextT *ctx);
static void executeSysMenuCmd(AppContextT *ctx, int32_t cmd);
static WindowT *findWindowById(AppContextT *ctx, int32_t id);
static uint8_t *finishLoadImage(const DisplayT *d, uint8_t *rgb, int32_t imgW, int32_t imgH, int32_t *outW, int32_t *outH, int32_t *outPitch);
@ -289,13 +295,15 @@ static void refreshMinimizedIcons(AppContextT *ctx);
static void releaseKeyPressedButton(AppContextT *ctx);
static int32_t remapIndexForLower(int32_t index, int32_t lowered);
static void repositionWindow(AppContextT *ctx, WindowT *win, int32_t x, int32_t y, int32_t w, int32_t h);
static int32_t saveImageBusy(AppContextT *ctx, const uint8_t *data, int32_t w, int32_t h, int32_t pitch, const char *path);
static int32_t saveImageAs(const DisplayT *fmt, const uint8_t *data, int32_t w, int32_t h, int32_t pitch, const char *path);
static int32_t saveImageBusy(AppContextT *ctx, const DisplayT *fmt, const uint8_t *data, int32_t w, int32_t h, int32_t pitch, const char *path);
static int32_t scrollToShow(int32_t value, int32_t pos, int32_t size, int32_t view);
static void scrollWidgetIntoView(AppContextT *ctx, WindowT *win, WidgetT *w);
static void sendMouseToWindow(AppContextT *ctx, WindowT *win, int32_t mx, int32_t my, int32_t buttons);
static void setWindowScroll(AppContextT *ctx, WindowT *win, ScrollbarT *sb, int32_t value);
static const char *splitMenuLabel(const char *label, char leftBuf[MAX_MENU_LABEL]);
static inline void storePixel(uint8_t *row, int32_t x, int32_t bytesPerPx, uint32_t px);
static bool takesGameKeys(const AppContextT *ctx, const WindowT *win);
static int32_t tileSlotPos(int32_t slot, int32_t tile, int32_t screen);
static void toggleMaximize(AppContextT *ctx, WindowT *win);
static void updateCursorShape(AppContextT *ctx);
@ -1096,6 +1104,14 @@ static void compositeAndFlush(AppContextT *ctx) {
DirtyListT *dl = &ctx->dirty;
WindowStackT *ws = &ctx->stack;
// A full-screen program has the screen: nothing is drawn (windows keep
// painting into their own buffers), and giving the screen back
// repaints all of it.
if (ctx->exclusive.active) {
dirtyListClear(dl);
return;
}
// Merge overlapping dirty rects to reduce flush count
dirtyListMerge(dl);
@ -1472,6 +1488,10 @@ static void destroyWindowPrep(AppContextT *ctx, WindowT *win) {
sOpenSoundWinId = 0;
}
// A window going away gives full screen back (its program is told
// nothing: it is closing the window or being ended).
dvxExclusiveEnd(ctx, win);
// Clear the modal pointer if it was this window, even when deferring
// -- a hidden pending-destroy modal must not keep eating input.
if (ctx->modalWindow == win) {
@ -1647,6 +1667,12 @@ static void dispatchEvents(AppContextT *ctx) {
ctx->modalWindow = NULL;
}
// Full screen: the mouse is the program's alone.
if (ctx->exclusive.active) {
exclusiveMouse(ctx);
return;
}
// Mouse movement always dirties old and new cursor positions
if (mx != ctx->prevMouseX || my != ctx->prevMouseY) {
dirtyCursorArea(ctx, ctx->prevMouseX, ctx->prevMouseY);
@ -2020,7 +2046,7 @@ static void dispatchKey(AppContextT *ctx, int32_t scancode, int32_t ascii, int32
// Game input: the three keys above are the only ones DVX keeps; every
// other key goes straight to the game, unless a menu or the keyboard
// move/size mode (opened with the mouse) is taking keys.
if (win && win->gameInput && !ctx->popup.active && !ctx->sysMenu.active && ctx->kbMoveResize.mode == KbModeNoneE) {
if (win && takesGameKeys(ctx, win) && !ctx->popup.active && !ctx->sysMenu.active && ctx->kbMoveResize.mode == KbModeNoneE) {
if (win->onKey) {
WIN_CALLBACK(ctx, win, win->onKey(win, key, shiftFlags));
}
@ -2158,7 +2184,7 @@ static void dispatchRawKey(AppContextT *ctx, const RawKeyEventT *evt) {
WIN_CALLBACK(ctx, win, win->onKeyUp(win, scan | KEY_EXT_FLAG, platformKeyboardGetModifiers()));
}
if (win->gameInput && win->onRawKey && !win->destroyPending) {
if (takesGameKeys(ctx, win) && win->onRawKey && !win->destroyPending) {
WIN_CALLBACK(ctx, win, win->onRawKey(win, evt));
}
}
@ -2488,6 +2514,12 @@ void dvxCascadeWindows(AppContextT *ctx) {
// screen are clamped. On failure, the old mode is restored.
int32_t dvxChangeVideoMode(AppContextT *ctx, int32_t requestedW, int32_t requestedH, int32_t preferredBpp) {
// Not while a full-screen program has the screen (the desktop mode is
// set again when it lets go).
if (ctx->exclusive.active) {
return -1;
}
// Save old state for rollback
DisplayT oldDisplay = ctx->display;
@ -2801,6 +2833,177 @@ void dvxDestroyWindowNow(AppContextT *ctx, WindowT *win) {
}
// ============================================================
// dvxExclusiveBegin / dvxExclusiveEnd and the full-screen display
// ============================================================
//
// The owner's window must have the focus; exclusiveSync (every dvxUpdate)
// suspends and resumes full screen as the focus leaves and comes back.
int32_t dvxExclusiveBegin(AppContextT *ctx, WindowT *win, int32_t width, int32_t height, int32_t bpp, ExclusiveFnT onChange, void *userData) {
ExclusiveStateT *ex = &ctx->exclusive;
if (!win || windowIndex(ctx, win) < 0 || win->destroyPending || width <= 0 || height <= 0) {
return DVX_EXCL_ERR_ARGS;
}
if (ex->owner && ex->owner != win) {
return DVX_EXCL_ERR_BUSY;
}
// A new size for the owner: let go of the old mode first.
exclusiveLeave(ctx);
if (!ex->owner) {
dvxGeneratePalette(ex->palette);
}
ex->owner = win;
ex->reqW = width;
ex->reqH = height;
ex->reqBpp = bpp;
ex->onChange = onChange;
ex->userData = userData;
if (win->minimized) {
dvxRestoreWindow(ctx, win);
}
raiseAndFocus(ctx, windowIndex(ctx, win));
if (!exclusiveEnter(ctx)) {
ex->owner = NULL;
return DVX_EXCL_ERR_MODE;
}
return DVX_EXCL_OK;
}
const BlitOpsT *dvxExclusiveBlitOps(AppContextT *ctx) {
return ctx->exclusive.active ? &ctx->exclusive.blitOps : NULL;
}
DisplayT *dvxExclusiveDisplay(AppContextT *ctx) {
return ctx->exclusive.active ? &ctx->exclusive.display : NULL;
}
void dvxExclusiveEnd(AppContextT *ctx, WindowT *win) {
if (!win || ctx->exclusive.owner != win) {
return;
}
exclusiveLeave(ctx);
ctx->exclusive.owner = NULL;
}
void dvxExclusiveFlush(AppContextT *ctx, const RectT *r) {
DisplayT *d = &ctx->exclusive.display;
if (!ctx->exclusive.active) {
return;
}
RectT screen = { 0, 0, d->width, d->height };
RectT clip = screen;
if (r && !rectIntersect(r, &screen, &clip)) {
return;
}
platformVideoExclusiveFlush(d, &clip);
}
// Whole-factor scaling, one source row expanded once and copied for the
// repeated rows. The backbuffer is cleared (borders black) whenever the
// picture's place changes. The whole screen is flushed, so a
// page-flipped mode shows the frame at once and without tearing.
bool dvxExclusivePresent8(AppContextT *ctx, const uint8_t *src, int32_t srcW, int32_t srcH, int32_t srcPitch) {
ExclusiveStateT *ex = &ctx->exclusive;
DisplayT *d = &ex->display;
if (!ex->active || !src || srcW <= 0 || srcH <= 0) {
return false;
}
int32_t scale = DVX_MAX(1, DVX_MIN(d->width / srcW, d->height / srcH));
int32_t outW = DVX_MIN(srcW * scale, d->width);
int32_t outH = DVX_MIN(srcH * scale, d->height);
int32_t bpp = d->format.bytesPerPixel;
RectT place = { (d->width - outW) / 2, (d->height - outH) / 2, outW, outH };
if (memcmp(&place, &ex->presentRect, sizeof(place)) != 0) {
memset(d->backBuf, 0, (size_t)d->pitch * (size_t)d->height);
ex->presentRect = place;
}
for (int32_t y = 0; y < outH; y++) {
uint8_t *dst = d->backBuf + (size_t)(place.y + y) * (size_t)d->pitch + (size_t)place.x * (size_t)bpp;
if (y % scale != 0) {
memcpy(dst, dst - d->pitch, (size_t)outW * (size_t)bpp);
continue;
}
const uint8_t *row = src + (size_t)(y / scale) * (size_t)srcPitch;
if (bpp == 1 && scale == 1) {
memcpy(dst, row, (size_t)outW);
continue;
}
int32_t x = 0;
for (int32_t sx = 0; x < outW; sx++) {
uint32_t px = bpp == 1 ? row[sx] : ex->lut[row[sx]];
for (int32_t k = 0; k < scale && x < outW; k++, x++) {
storePixel(dst, x, bpp, px);
}
}
}
dvxExclusiveFlush(ctx, NULL);
return true;
}
void dvxExclusiveSetPalette(AppContextT *ctx, const uint8_t *pal, int32_t first, int32_t count) {
ExclusiveStateT *ex = &ctx->exclusive;
if (!pal || first < 0 || first >= PAL_ENTRIES) {
return;
}
count = DVX_MIN(count, PAL_ENTRIES - first);
if (count <= 0) {
return;
}
memcpy(ex->palette + first * PAL_CHANNELS, pal + first * PAL_CHANNELS, (size_t)count * PAL_CHANNELS);
if (!ex->active) {
return;
}
if (ex->display.format.bitsPerPixel == PALETTE_BPP) {
platformVideoSetPalette(ex->palette, first, count);
} else {
exclusiveLutSet(ex, first, count);
}
}
void dvxExclusiveWaitRetrace(void) {
platformVideoWaitRetrace();
}
void dvxFitWindow(AppContextT *ctx, WindowT *win) {
fitWindow(ctx, win, true, true);
}
@ -3568,28 +3771,11 @@ void dvxRun(AppContextT *ctx) {
// dvxWindowScreenshot are convenience wrappers around it.
int32_t dvxSaveImage(const AppContextT *ctx, const uint8_t *data, int32_t w, int32_t h, int32_t pitch, const char *path) {
if (!ctx || !data || !path || w <= 0 || h <= 0) {
if (!ctx) {
return -1;
}
uint8_t *rgb = bufferToRgb(&ctx->display, data, w, h, pitch);
if (!rgb) {
return -1;
}
const char *ext = strrchr(path, '.');
int ok;
if (ext && strcasecmp(ext, BMP_EXT) == 0) {
ok = stbi_write_bmp(path, w, h, RGB_CHANNELS, rgb);
} else {
ok = stbi_write_png(path, w, h, RGB_CHANNELS, rgb, w * RGB_CHANNELS);
}
free(rgb);
return ok ? 0 : -1;
return saveImageAs(&ctx->display, data, w, h, pitch, path);
}
@ -3633,9 +3819,26 @@ bool dvxSaveTheme(const AppContextT *ctx, const char *filename) {
// LFB since we only write to the LFB, never read.
int32_t dvxScreenshot(AppContextT *ctx, const char *path) {
DisplayT *d = &ctx->display;
if (!ctx->exclusive.active) {
const DisplayT *d = &ctx->display;
return saveImageBusy(ctx, d->backBuf, d->width, d->height, d->pitch, path);
return saveImageBusy(ctx, d, d->backBuf, d->width, d->height, d->pitch, path);
}
// While a program has full screen the screen is its display: read
// back from video memory where the backbuffer is not a copy of it.
const DisplayT *d = &ctx->exclusive.display;
uint8_t *screen = (uint8_t *)malloc((size_t)d->width * (size_t)d->height);
int32_t rc;
if (screen && platformVideoExclusiveCapture(d, screen)) {
rc = saveImageBusy(ctx, d, screen, d->width, d->height, d->width, path);
} else {
rc = saveImageBusy(ctx, d, d->backBuf, d->width, d->height, d->pitch, path);
}
free(screen);
return rc;
}
@ -3895,6 +4098,7 @@ void dvxShowWindow(AppContextT *ctx, WindowT *win) {
void dvxShutdown(AppContextT *ctx) {
dvxExclusiveEnd(ctx, ctx->exclusive.owner);
platformTimerShutdown();
platformKeyHookShutdown();
@ -4135,6 +4339,7 @@ bool dvxUpdate(AppContextT *ctx) {
pollMouse(ctx);
pollKeyboard(ctx);
dispatchEvents(ctx);
exclusiveSync(ctx);
updateTooltip(ctx);
pollWidgets(ctx);
@ -4209,7 +4414,7 @@ int32_t dvxWindowScreenshot(AppContextT *ctx, WindowT *win, const char *path) {
ctx->dispatchDepth++;
flushWindowPaint(ctx, win);
int32_t rc = (win->destroyPending || !win->contentBuf) ? -1 : saveImageBusy(ctx, win->contentBuf, win->contentW, win->contentH, win->contentPitch, path);
int32_t rc = (win->destroyPending || !win->contentBuf) ? -1 : saveImageBusy(ctx, &ctx->display, win->contentBuf, win->contentW, win->contentH, win->contentPitch, path);
ctx->dispatchDepth--;
@ -4267,6 +4472,158 @@ static void enumModeCb(int32_t w, int32_t h, int32_t bpp, void *userData) {
}
// ============================================================
// Full screen: taking and giving back the screen
// ============================================================
// Set the owner's mode and hand it the screen. False (the desktop is back
// and repainted) when no mode could be set.
static bool exclusiveEnter(AppContextT *ctx) {
ExclusiveStateT *ex = &ctx->exclusive;
DisplayT *d = &ex->display;
if (platformVideoExclusiveBegin(d, ex->reqW, ex->reqH, ex->reqBpp) != 0) {
dvxLog("Video: no %ldx%ld full-screen mode", (long)ex->reqW, (long)ex->reqH);
platformVideoExclusiveEnd(d, &ctx->display);
dirtyListInit(&ctx->dirty);
dirtyListAdd(&ctx->dirty, 0, 0, ctx->display.width, ctx->display.height);
return false;
}
dvxLog("Video: full screen %ldx%ld at %ld bpp", (long)d->width, (long)d->height, (long)d->format.bitsPerPixel);
// Menus, a drag or a tooltip would be invisible and in the way.
closeWindowTransients(ctx, NULL);
dvxInvalidateTooltip(ctx, NULL);
if (ctx->stack.dragWindow >= 0) {
wmDragEnd(&ctx->stack);
}
if (ctx->stack.resizeWindow >= 0) {
wmResizeEnd(&ctx->stack);
}
d->palette = ex->palette;
drawInit(&ex->blitOps, d);
if (d->format.bitsPerPixel == PALETTE_BPP) {
platformVideoSetPalette(ex->palette, 0, PAL_ENTRIES);
} else {
exclusiveLutSet(ex, 0, PAL_ENTRIES);
}
memset(&ex->presentRect, 0, sizeof(ex->presentRect));
ex->savedMouseX = ctx->mouseX;
ex->savedMouseY = ctx->mouseY;
platformMouseInit(d->width, d->height);
platformMouseWarp(d->width / 2, d->height / 2);
ctx->mouseX = d->width / 2;
ctx->mouseY = d->height / 2;
ctx->prevMouseX = ctx->mouseX;
ctx->prevMouseY = ctx->mouseY;
ex->active = true;
exclusiveNotify(ctx, ex->owner, ex->onChange, ex->userData, ExclusiveActiveE);
return true;
}
// Give the desktop back (the owner keeps full screen, suspended): the
// desktop's mode, mouse and a full repaint. Nothing when not active.
static void exclusiveLeave(AppContextT *ctx) {
ExclusiveStateT *ex = &ctx->exclusive;
if (!ex->active) {
return;
}
ex->active = false;
if (platformVideoExclusiveEnd(&ex->display, &ctx->display) != 0) {
dvxLog("Video: the desktop mode could not be set again");
}
platformMouseInit(ctx->display.width, ctx->display.height);
platformMouseWarp(ex->savedMouseX, ex->savedMouseY);
ctx->mouseX = ex->savedMouseX;
ctx->mouseY = ex->savedMouseY;
ctx->prevMouseX = ctx->mouseX;
ctx->prevMouseY = ctx->mouseY;
resetClipRect(&ctx->display);
dirtyListInit(&ctx->dirty);
dirtyListAdd(&ctx->dirty, 0, 0, ctx->display.width, ctx->display.height);
}
// Palette entries first..first+count-1 as pixels of the full-screen format
// (above 8 bpp), for dvxExclusivePresent8.
static void exclusiveLutSet(ExclusiveStateT *ex, int32_t first, int32_t count) {
for (int32_t i = first; i < first + count; i++) {
const uint8_t *rgb = ex->palette + i * PAL_CHANNELS;
ex->lut[i] = packColor(&ex->display, rgb[0], rgb[1], rgb[2]);
}
}
// Full screen: any mouse change goes to the owner in screen coordinates.
static void exclusiveMouse(AppContextT *ctx) {
WindowT *win = ctx->exclusive.owner;
if (ctx->mouseX == ctx->prevMouseX && ctx->mouseY == ctx->prevMouseY && ctx->mouseButtons == ctx->prevMouseButtons) {
return;
}
if (win->onMouse && !win->destroyPending) {
WIN_CALLBACK(ctx, win, win->onMouse(win, ctx->mouseX, ctx->mouseY, ctx->mouseButtons));
}
}
// Tell a full-screen owner what happened, under its app id.
static void exclusiveNotify(AppContextT *ctx, WindowT *win, ExclusiveFnT fn, void *userData, ExclusiveStateE state) {
if (fn && win && !win->destroyPending) {
WIN_CALLBACK(ctx, win, fn(win, state, userData));
}
}
// Once a frame: full screen follows the owner's focus. Losing it (Alt+Tab,
// Ctrl+Esc, a dialog, minimizing) suspends; getting it back sets the mode
// again, and when that fails full screen is over.
static void exclusiveSync(AppContextT *ctx) {
ExclusiveStateT *ex = &ctx->exclusive;
WindowT *win = ex->owner;
if (!win) {
return;
}
bool focused = focusedWindow(ctx) == win && win->visible && !win->minimized && !win->destroyPending;
if (focused == ex->active) {
return;
}
if (!focused) {
exclusiveLeave(ctx);
exclusiveNotify(ctx, win, ex->onChange, ex->userData, ExclusiveSuspendedE);
return;
}
if (!exclusiveEnter(ctx)) {
ExclusiveFnT fn = ex->onChange;
void *userData = ex->userData;
ex->owner = NULL;
exclusiveNotify(ctx, win, fn, userData, ExclusiveEndedE);
}
}
// ============================================================
// executeSysMenuCmd
// ============================================================
@ -5178,7 +5535,7 @@ static void interactiveSaveBuffer(AppContextT *ctx, const char *title, const uin
memcpy(snap, src, size);
if (dvxFileDialog(ctx, title, FD_SAVE, NULL, filters, DVX_ARRAY_LEN(filters), path, sizeof(path))) {
if (saveImageBusy(ctx, snap, w, h, pitch, path) != 0) {
if (saveImageBusy(ctx, &ctx->display, snap, w, h, pitch, path) != 0) {
dvxErrorBox(ctx, title, SCREENSHOT_FAIL_MSG);
}
}
@ -5273,7 +5630,7 @@ static void keyFocusSync(AppContextT *ctx) {
ctx->keyFocusWinId = winId;
if (old && old->gameInput && old->onRawKey && !old->destroyPending) {
if (old && takesGameKeys(ctx, old) && old->onRawKey && !old->destroyPending) {
uint8_t held[KEY_RAW_COUNT];
platformKeyStateGet(held);
@ -6069,14 +6426,42 @@ static void repositionWindow(AppContextT *ctx, WindowT *win, int32_t x, int32_t
}
// dvxSaveImage's body, for pixels in fmt's format (the desktop's, or a
// full-screen program's for dvxScreenshot).
static int32_t saveImageAs(const DisplayT *fmt, const uint8_t *data, int32_t w, int32_t h, int32_t pitch, const char *path) {
if (!data || !path || w <= 0 || h <= 0) {
return -1;
}
uint8_t *rgb = bufferToRgb(fmt, data, w, h, pitch);
if (!rgb) {
return -1;
}
const char *ext = strrchr(path, '.');
int ok;
if (ext && strcasecmp(ext, BMP_EXT) == 0) {
ok = stbi_write_bmp(path, w, h, RGB_CHANNELS, rgb);
} else {
ok = stbi_write_png(path, w, h, RGB_CHANNELS, rgb, w * RGB_CHANNELS);
}
free(rgb);
return ok ? 0 : -1;
}
// dvxSaveImage with the busy cursor shown, restoring the caller's busy
// state afterwards (a caller already busy stays busy).
static int32_t saveImageBusy(AppContextT *ctx, const uint8_t *data, int32_t w, int32_t h, int32_t pitch, const char *path) {
static int32_t saveImageBusy(AppContextT *ctx, const DisplayT *fmt, const uint8_t *data, int32_t w, int32_t h, int32_t pitch, const char *path) {
bool wasBusy = ctx->busy;
dvxSetBusy(ctx, true);
int32_t result = dvxSaveImage(ctx, data, w, h, pitch, path);
int32_t result = saveImageAs(fmt, data, w, h, pitch, path);
dvxSetBusy(ctx, wasBusy);
return result;
@ -6207,6 +6592,13 @@ static inline void storePixel(uint8_t *row, int32_t x, int32_t bytesPerPx, uint3
}
// Keys go to win as in game input mode: it asked for them, or it has full
// screen (menus there could not be seen).
static bool takesGameKeys(const AppContextT *ctx, const WindowT *win) {
return win->gameInput || win == ctx->exclusive.owner;
}
// Position of tile slot along an axis: slot * tile, pulled back so the
// tile stays on screen when minimum-size tiles overflow it (the last
// tiles then overlap instead of landing off-screen).
@ -6337,6 +6729,11 @@ static void updateTooltip(AppContextT *ctx) {
int32_t mx = ctx->mouseX;
int32_t my = ctx->mouseY;
// No tooltips over a full-screen program.
if (ctx->exclusive.active) {
return;
}
// Mouse moved or button pressed -- hide tooltip and reset timer
if (mx != ctx->prevMouseX || my != ctx->prevMouseY || ctx->mouseButtons) {
dvxInvalidateTooltip(ctx, NULL);

View file

@ -43,10 +43,41 @@
#include "dvxPlat.h"
#include "dvxVideo.h"
#include "dvxDraw.h"
#include "dvxPal.h"
#include "dvxComp.h"
#include "dvxWm.h"
// ============================================================
// Full-screen exclusive mode (state; the API is further down)
// ============================================================
// What happened to a program's full screen (its ExclusiveFnT is told).
typedef enum {
ExclusiveActiveE, // the screen is the program's: draw the whole frame
ExclusiveSuspendedE, // its window lost the focus: the desktop is back until it has the focus again
ExclusiveEndedE // the mode could not be set again: full screen is over
} ExclusiveStateE;
typedef void (*ExclusiveFnT)(WindowT *win, ExclusiveStateE state, void *userData);
typedef struct {
WindowT *owner; // window holding full screen (NULL: none)
bool active; // the screen is the owner's now (false: suspended)
int32_t reqW; // what the owner asked for
int32_t reqH;
int32_t reqBpp;
ExclusiveFnT onChange;
void *userData;
DisplayT display; // the full-screen display while active
BlitOpsT blitOps; // span operations for display's format
uint8_t palette[PAL_ENTRIES * PAL_CHANNELS]; // the owner's palette (the DAC at 8 bpp)
uint32_t lut[PAL_ENTRIES]; // palette index -> pixel, above 8 bpp
RectT presentRect; // where dvxExclusivePresent8 last put the picture
int32_t savedMouseX; // desktop mouse position to go back to
int32_t savedMouseY;
} ExclusiveStateT;
// ============================================================
// Application context
// ============================================================
@ -156,6 +187,7 @@ typedef struct AppContextT {
int32_t wallpaperPitch; // bytes per row
char wallpaperPath[DVX_MAX_PATH]; // source image path (empty = none)
WallpaperModeE wallpaperMode; // stretch, tile, or center
ExclusiveStateT exclusive; // full-screen exclusive mode (dvxExclusiveBegin)
} AppContextT;
// Initialize the entire GUI stack: video mode, input devices, font,
@ -223,6 +255,67 @@ void dvxPlaySoundWait(DvxSoundE event);
// The default beep: dvxPlaySound(SoundDefaultE).
void dvxBeep(void);
// ============================================================
// Full-screen exclusive mode
// ============================================================
//
// A window's program takes the whole screen in a video mode of its own
// (a game at 320x200, say). Every other program keeps running: windows
// keep painting into their own buffers, and the desktop is drawn again
// when the program lets go. The owner's window must have the focus:
// when it loses it (Alt+Tab, Ctrl+Esc, a dialog, any other window coming
// to the front) the desktop comes back and onChange hears
// ExclusiveSuspendedE; when the window has the focus again the mode is set
// again and onChange hears ExclusiveActiveE (draw the whole frame: the
// screen and the backbuffer start black). While active, every key but
// Alt+Tab, Ctrl+Esc and Alt+F4 goes to the window as in game input mode,
// the mouse cursor is not drawn, and onMouse gets full-screen coordinates.
// Closing the window, the program ending or being ended, and
// dvxExclusiveEnd all give the screen back.
#define DVX_EXCL_OK 0
#define DVX_EXCL_ERR_MODE (-1) // no video mode could be set
#define DVX_EXCL_ERR_BUSY (-2) // another window has full screen
#define DVX_EXCL_ERR_ARGS (-3) // no such window, or a bad size
// Take the whole screen for win: the smallest mode of at least width x
// height, at bpp when there is one (0: any depth); raises and focuses win.
// onChange (may be NULL) hears ExclusiveActiveE before this returns. A
// window that already has full screen gets the new size.
int32_t dvxExclusiveBegin(AppContextT *ctx, WindowT *win, int32_t width, int32_t height, int32_t bpp, ExclusiveFnT onChange, void *userData);
// The span operations for the full-screen display's pixel format (NULL
// when not active).
const BlitOpsT *dvxExclusiveBlitOps(AppContextT *ctx);
// The full-screen display while active, NULL otherwise (suspended or no
// full screen). Draw into its backBuf (DVX's draw functions work on it),
// then dvxExclusiveFlush. Its size may be larger than asked for.
DisplayT *dvxExclusiveDisplay(AppContextT *ctx);
// Give the screen back (no onChange call). Nothing when win does not
// have full screen.
void dvxExclusiveEnd(AppContextT *ctx, WindowT *win);
// Copy a rectangle of the backbuffer to the screen (NULL: all of it).
void dvxExclusiveFlush(AppContextT *ctx, const RectT *r);
// Show an 8-bit picture of palette indexes on the whole screen: scaled by
// the largest whole factor that fits, centred on black, through the
// palette (dvxExclusiveSetPalette) in any pixel format. False when not
// active.
bool dvxExclusivePresent8(AppContextT *ctx, const uint8_t *src, int32_t srcW, int32_t srcH, int32_t srcPitch);
// Set palette entries first..first+count-1 (R, G, B bytes per entry, the
// whole 256-entry table in pal). At 8 bpp they go to the DAC at once (a
// picture on screen changes colour); at other depths the next
// dvxExclusivePresent8 uses them. Kept across suspending.
void dvxExclusiveSetPalette(AppContextT *ctx, const uint8_t *pal, int32_t first, int32_t count);
// Wait for the next vertical retrace, before a flush, so the frame is not
// torn.
void dvxExclusiveWaitRetrace(void);
// ============================================================
// Color scheme
// ============================================================

View file

@ -65,7 +65,7 @@
// src/test/abi/abiSizes.txt (make -C src/test/abi update) in the same
// change; the abi test fails when a public struct changes size without a
// bump, and when the table was not regenerated after one.
#define DVX_ABI_VERSION 3
#define DVX_ABI_VERSION 4
// A module's stamp is named <file base name><DVX_ABI_SUFFIX>, e.g. the
// stamp of LIBS/KPUNCH/LIBDVX/LIBDVX.LIB is libdvxAbiVersion.

View file

@ -202,6 +202,44 @@ Palette animation (colour cycling, fades) works at every depth: change the game
Size the window so its content area is exactly the framebuffer times a whole scale that fits the screen (the template computes the scale from the display size and the chrome constants, then corrects with dvxResizeWindow after adding the menu bar). Copying only what changed matters on slower machines: a full 320x200 frame at 2x on a 16-bit desktop is 256 KB of pixels to convert and composite.
.topic games.fullscreen
.title The Whole Screen
.toc 1 The Whole Screen
.index Full screen
.index Exclusive mode
.index dvxExclusiveBegin
.index dvxExclusivePresent8
.index Alt+Enter
.h1 The Whole Screen
A game may also take the whole screen in a video mode of its own, as the template does with Alt+Enter. Every other program keeps running while it does: their windows go on painting into their own buffers, their timers fire, their sound plays, and the desktop is drawn again, as it is now, when the game lets go.
.code
static void fullChanged(WindowT *win, ExclusiveStateE state, void *userData) {
// Active: draw the whole frame. Suspended: draw in the window again.
// Ended: the mode could not be set again; full screen is over.
}
if (dvxExclusiveBegin(ctx, win, 320, 200, 8, fullChanged, NULL) == DVX_EXCL_OK) {
dvxExclusiveSetPalette(ctx, gamePal, 0, 256);
}
// every frame
if (!dvxExclusivePresent8(ctx, fb, 320, 200, 320)) {
// not on the whole screen: present into the window as usual
}
.endcode
.list
.item dvxExclusiveBegin sets the smallest mode of at least the size asked for, at the depth asked for when there is one (0: any). At 8 bits per pixel a request that fits 320x200 gets VGA mode 13h, and one that fits 320x240 gets VGA Mode X (square pixels, and two pages: a whole-screen flush is shown from the next retrace, so frames never tear), unless the card has a VBE mode no larger. Either way the game draws into an ordinary linear backbuffer. The display may be larger than asked for; dvxExclusiveDisplay says what it is.
.item The game's window must have the focus. When it loses it (Alt+Tab, Ctrl+Esc, a dialog, another program's window coming to the front) the desktop comes back and the callback hears ExclusiveSuspendedE; when the window has the focus again the mode is set again and it hears ExclusiveActiveE. Draw the whole frame then: the screen starts black.
.item While the game has the screen, every key but Alt+Tab, Ctrl+Esc and Alt+F4 goes to its window, as in a game input window; the mouse cursor is not drawn; onMouse gets whole-screen coordinates.
.item dvxExclusivePresent8 shows a picture of palette numbers on the whole screen, scaled by the largest whole factor that fits and centred on black, in any pixel format. At 8 bits per pixel the palette is the screen's own: dvxExclusiveSetPalette changes colours at once, so palette animation costs nothing, and a hardware model of up to 16 palettes of 16 colours fits in the 256 entries.
.item To draw yourself, draw into dvxExclusiveDisplay(ctx)->backBuf (DVX's draw functions work on it with dvxExclusiveBlitOps) and call dvxExclusiveFlush. dvxExclusiveWaitRetrace waits for the vertical retrace before a flush, so the frame is not torn.
.item dvxExclusiveEnd gives the screen back. Closing the window, the program ending and End Task do too.
.endlist
.topic games.sound
.title Sound, Music and MIDI
.toc 1 Sound, Music and MIDI
@ -327,7 +365,7 @@ Portable game libraries for DOS-era machines usually keep everything machine-spe
.h2 The Model: a Window, a Task, a Virtual Frame Rate
The game becomes a DVX program with its own main loop: appMain calls the game's main() (compile the game with -Dmain=gameMain, say, and call gameMain from appMain). The library's start-up creates a game input window whose content area is the game's screen at a whole scale; its frame wait yields until the next frame on the high-resolution clock. The game shares the screen with the desktop, so there is no mode change, no direct palette hardware and no vertical retrace; the port converts the library's back buffer into the window each frame.
The game becomes a DVX program with its own main loop: appMain calls the game's main() (compile the game with -Dmain=gameMain, say, and call gameMain from appMain). The library's start-up creates a game input window whose content area is the game's screen at a whole scale; its frame wait yields until the next frame on the high-resolution clock. In a window the game shares the screen with the desktop, so there is no mode change, no direct palette hardware and no vertical retrace; the port converts the library's back buffer into the window each frame. On the whole screen (see The Whole Screen) the game has its own mode, its own palette and the vertical retrace, while the other programs run on.
.h2 Mapping a Hardware Layer to DVX
@ -338,9 +376,11 @@ The game becomes a DVX program with its own main loop: appMain calls the game's
Shut-down dvxDestroyWindow, return from appMain
Error text, debug log dvxLog (DVX.LOG)
Present the back buffer colour table -> win->contentBuf, dvxInvalidateRect; onPaint
Palette packColor, gamePalNearest, GamePalCacheT
Present, whole screen dvxExclusivePresent8, or dvxExclusiveDisplay + dvxExclusiveFlush
Full screen on and off dvxExclusiveBegin, dvxExclusiveEnd
Palette packColor, gamePalNearest, GamePalCacheT; whole screen: dvxExclusiveSetPalette
Palette per scan line one colour table per palette, chosen per row
Wait for vertical blank gameFrameInit, gameFrameWait
Wait for vertical blank gameFrameInit, gameFrameWait; whole screen: dvxExclusiveWaitRetrace
Frame counter, frame rate GameFramePacerT.frame, the rate given to gameFrameInit
Milliseconds platformMicros() / 1000
Held keys onRawKey, gameKeyIsDown, platformKeyStateGet
@ -369,8 +409,8 @@ The game becomes a DVX program with its own main loop: appMain calls the game's
.item Start-up: make the window not resizable, with its content area the game's screen at a whole scale that fits the display; give it onClose and onPaint. audioOpen returning NULL (no sound device) is not an error.
.item Shut-down: close what the port opened and return. Anything left (sounds, music, joystick handles, timers) is released when the program ends.
.item Present: expand the rows or word bands that changed through the colour table into the content buffer, scaled, and invalidate them; onPaint does the whole screen. See The Screen: Framebuffers and Palettes.
.item Palette: scale 12-bit or 18-bit colour to 0..255 per channel before looking it up. Never touch the VGA palette registers: other programs share the screen.
.item Vertical blank: there is none in a window. The frame rate is a virtual one: use the rate the library reports (60 or 70 frames a second); truncate the frame counter to the library's width.
.item Palette: scale 12-bit or 18-bit colour to 0..255 per channel before looking it up. Never touch the VGA palette registers: other programs share the screen, and on the whole screen dvxExclusiveSetPalette does it (and puts the desktop's palette back when the game lets go).
.item Vertical blank: there is none in a window. The frame rate is a virtual one: use the rate the library reports (60 or 70 frames a second); truncate the frame counter to the library's width. On the whole screen dvxExclusiveWaitRetrace waits for the real one.
.item Keyboard: raw codes are set-1 scancodes with KEY_RAW_EXT for the keys sent behind 0xE0, so a DOS port's scancode table maps directly. Never hook the keyboard interrupt.
.item Mouse: divide content-area pixels by the scale and clamp.
.item Joystick: axes are -32768..32767 after calibration and dead zone; divide down to the library's range, or compare with a threshold for a digital stick. Buttons are bits. Never read the game port directly; calibration lives in the Control Panel.
@ -387,7 +427,8 @@ The game becomes a DVX program with its own main loop: appMain calls the game's
.item Shared library or static: built as a DVX library (.lib, loaded when DVX starts) the port is shared by every game, but its global state (the stage, the key table) means one game at a time. Linked into each game's .app (the LIBS variable) every game has its own copy, several can run at once, and nothing is loaded while no game runs.
.item Paths: libraries that open "DATA/..." or write saves to a fixed folder need a run-time root (appDir and configDir) instead of compile-time paths.
.item Speed: every frame costs a conversion and a composite. Present only what changed; a 1x scale on a 486 and larger scales on faster machines.
.item Tearing: there is no vertical retrace in a window. Games that depend on raster timing need rework.
.item Tearing: there is no vertical retrace in a window. On the whole screen there is (dvxExclusiveWaitRetrace), but games that change registers mid-frame (raster effects) still need rework.
.item Whole screen: offer it (Alt+Enter is the custom) and handle ExclusiveSuspendedE by drawing in the window again: the player can Alt+Tab to the desktop at any time.
.item The 256-colour desktop: nearest colours only; say so in the game's help if it needs 15-bit colour or better.
.endlist

View file

@ -288,6 +288,28 @@ void platformVideoEnumModes(void (*cb)(int32_t w, int32_t h, int32_t bpp, void *
// firstEntry..firstEntry+count-1 are written, clamped to the table.
void platformVideoSetPalette(const uint8_t *pal, int32_t firstEntry, int32_t count);
// VGA mode 13h: the boot splash, and the full-screen fallback
#define VGA13_W 320
#define VGA13_H 200
// Full-screen exclusive video (dvxExclusiveBegin). Begin sets the
// smallest mode of at least requestedW x requestedH (preferredBpp first,
// any depth for 0; on DOS VGA mode 13h up to 320x200 and Mode X up to
// 320x240 at 8 bpp when no VBE mode is that small) and fills d with its
// own linear backbuffer; the desktop's display and buffers are left as
// they are. End frees d's buffers and sets the desktop's mode (and 8-bit
// palette) again; call it after a failed Begin too. Flush puts a
// backbuffer rectangle on the screen (Mode X: a whole-screen flush flips
// pages at the next retrace and waits for it). Capture reads what is on
// the screen into out (width x height bytes) where the backbuffer is not
// a copy of it (Mode X), else returns false. WaitRetrace waits for the
// start of the next vertical retrace.
int32_t platformVideoExclusiveBegin(DisplayT *d, int32_t requestedW, int32_t requestedH, int32_t preferredBpp);
bool platformVideoExclusiveCapture(const DisplayT *d, uint8_t *out);
int32_t platformVideoExclusiveEnd(DisplayT *d, const DisplayT *desktop);
void platformVideoExclusiveFlush(const DisplayT *d, const RectT *r);
void platformVideoWaitRetrace(void);
// ============================================================
// Framebuffer flush
// ============================================================
@ -788,8 +810,8 @@ void platformOutOfMemory(size_t bytes) __attribute__((noreturn));
// SPLASH.RAW format (written by the bmp2raw tool): SPLASH_PAL_BYTES of
// palette (R, G, B per entry, 6-bit DAC values), then SPLASH_PIXEL_BYTES
// of 8-bit pixels, top-to-bottom scanlines.
#define SPLASH_W 320
#define SPLASH_H 200
#define SPLASH_W VGA13_W
#define SPLASH_H VGA13_H
#define SPLASH_BPP 8
#define SPLASH_PAL_ENTRIES 256
#define SPLASH_PAL_CHANNELS 3

View file

@ -300,11 +300,57 @@
// the handlers are small, a generous span guarantees full coverage.
#define ISR_LOCK_SIZE 2048
// Sentinel for sLfbLinearAddr/sLfbMappedSize meaning "no LFB currently mapped".
// Sentinel for LfbMapT.linearAddr meaning "no LFB currently mapped".
#define LFB_NOT_MAPPED 0
// VGA splash screen (mode 13h) constants
#define SPLASH_VGA_ADDR 0xA0000
// VGA mode 13h: 320x200 at 8 bpp, linear at A0000h (the boot splash, and
// the full-screen fallback when the card has no VBE mode that small)
#define VGA13_ADDR 0xA0000
// Full-screen mode choice (scoreExclusive): the requested depth first,
// then the least wasted area. AREA_MAX is above any mode's pixel count.
#define EXCL_SCORE_AREA_MAX (1 << 22)
#define EXCL_SCORE_BPP_MATCH (EXCL_SCORE_AREA_MAX * 2)
// Mode X: VGA 256 colours unchained, 320x240 with square pixels, pixel
// (x, y) in plane x & 3 at byte y * 80 + x / 4. Two pages of
// MODEX_PAGE_BYTES in the 64K window: a whole-screen flush fills the
// hidden one and flips to it at the retrace.
#define MODEX_W 320
#define MODEX_H 240
#define MODEX_PLANES 4
#define MODEX_PLANE_MASK (MODEX_PLANES - 1)
#define MODEX_ROW_BYTES (MODEX_W / MODEX_PLANES)
#define MODEX_PAGE_BYTES (MODEX_ROW_BYTES * MODEX_H)
#define MODEX_PAGES 2
#define MODEX_ALL_PLANES 0x0F
#define VGA_WINDOW_BYTES 0x10000
#define VGA_SEQ_INDEX 0x3C4
#define VGA_GC_INDEX 0x3CE
#define VGA_CRTC_INDEX 0x3D4
#define VGA_CRTC_DATA 0x3D5
#define VGA_MISC_WRITE 0x3C2
#define SEQ_RESET 0x00
#define SEQ_MAP_MASK 0x02
#define SEQ_MEMORY_MODE 0x04
#define SEQ_RESET_SYNC 0x01 // synchronous reset (while the clock changes)
#define SEQ_RESET_RUN 0x03 // sequencer running
#define SEQ_MEM_UNCHAINED 0x06 // chain-4 and odd/even off, 256K
#define GC_READ_MAP 0x04
#define CRTC_START_HIGH 0x0C
#define CRTC_START_LOW 0x0D
#define CRTC_VRETRACE_END 0x11
#define CRTC_UNPROTECT_MASK 0x7F // clears the write-protect bit of CRTC 0-7
#define MISC_MODEX 0xE3 // 25 MHz dot clock, 480-line sync polarities
#define REG_DATA_SHIFT 8 // outportw: index in the low byte, data in the high
#define BYTE_MASK 0xFF
// Vertical retrace: VGA input status 1, bit 3 is set during retrace. The
// wait gives up after RETRACE_WAIT_MAX_US (two frames at 50 Hz), so a card
// without the bit never hangs a game.
#define VGA_INPUT_STATUS 0x3DA
#define VGA_STATUS_RETRACE 0x08
#define RETRACE_WAIT_MAX_US 40000
// Max VBE mode numbers we read from the BIOS mode list
#define MAX_VBE_MODES 256
@ -343,6 +389,19 @@ typedef struct {
uint8_t bluePos;
} VbeModeT;
// A DPMI physical mapping of a linear framebuffer: the RAW linear address
// from __dpmi_physical_address_mapping (NOT d->lfb, which has
// __djgpp_conventional_base folded in) and its size, kept so the mapping
// can be unlocked and freed. LFB_NOT_MAPPED means nothing mapped.
typedef struct {
uint32_t linearAddr;
uint32_t size;
} LfbMapT;
// Scores a VBE mode against a request (-1: unusable); findBestMode keeps
// the highest.
typedef int32_t (*ModeScoreFnT)(const VbeModeT *m, int32_t requestedW, int32_t requestedH, int32_t preferredBpp);
// ============================================================
// Prototypes
@ -352,19 +411,22 @@ static uint8_t biosVideoMode(void);
static bool cpuHasTsc(void);
static uint32_t estimateClockMhz(void);
static uint32_t farPtrToLinear(uint32_t addr);
static int32_t findBestMode(int32_t requestedW, int32_t requestedH, int32_t preferredBpp, uint16_t *outMode, DisplayT *d);
static int32_t findBestMode(int32_t requestedW, int32_t requestedH, int32_t preferredBpp, ModeScoreFnT score, uint16_t *outMode, DisplayT *d);
static bool hasCpuid(void);
static void installCritErrorHandler(void);
static void int8Handler(void);
static void int9Handler(void);
static void irqServicesArmAtExit(void);
static void irqServicesAtExit(void);
static int32_t mapLfb(DisplayT *d, uint32_t physAddr);
static int32_t mapLfb(DisplayT *d, uint32_t physAddr, LfbMapT *map);
static void modeXCopy(const DisplayT *d, const RectT *r, uint32_t pageOffset);
static void modeXSet(void);
static uint32_t pagesToKb(unsigned long pages);
static uint32_t pitLatchElapsed(void);
static void platformCrashHandler(int sig);
ISR_INLINE uint64_t readTsc(void);
static bool regionFromNear(const void *addr, uint32_t size, bool isCode, __dpmi_meminfo *out);
static int32_t scoreExclusive(const VbeModeT *m, int32_t requestedW, int32_t requestedH, int32_t preferredBpp);
static int32_t scoreMode(const VbeModeT *m, int32_t requestedW, int32_t requestedH, int32_t preferredBpp);
static dxe_symbol_table sDxeExportTable[];
static void setBiosMode(uint16_t mode);
@ -373,7 +435,7 @@ static void sysInfoAppend(const char *fmt, ...) __attribute__
static void sysInfoDrives(void);
static bool timerLock(void);
static void timerUnlock(void);
static void unmapLfb(DisplayT *d);
static void unmapLfb(DisplayT *d, LfbMapT *map);
static uint16_t vbeGetControllerInfo(void);
static bool vbeQueryMode(uint16_t mode, uint16_t vbeVersion, VbeModeT *out);
static int32_t vbeReadModeList(uint16_t *modes);
@ -500,13 +562,34 @@ static jmp_buf *sPlatformCrashJmp = NULL;
static volatile int *sPlatformCrashSignal = NULL;
static PlatformLogFnT sPlatformLogFn = NULL;
// Linear address and size of the currently mapped physical LFB, saved by
// mapLfb so platformVideoFreeBuffers / platformVideoShutdown can unlock and
// free the DPMI physical mapping. This is the RAW linear address returned by
// __dpmi_physical_address_mapping, NOT d->lfb (which has
// __djgpp_conventional_base folded in). LFB_NOT_MAPPED means nothing mapped.
static uint32_t sLfbLinearAddr = LFB_NOT_MAPPED;
static uint32_t sLfbMappedSize = 0;
// The desktop's LFB mapping (platformVideoInit .. FreeBuffers) and a
// full-screen program's (platformVideoExclusiveBegin .. End). The desktop
// mapping stays while a program has the screen, so returning needs only
// the mode set again.
static LfbMapT sDesktopLfb = { LFB_NOT_MAPPED, 0 };
static LfbMapT sExclusiveLfb = { LFB_NOT_MAPPED, 0 };
// The desktop's VBE mode, set again when a full-screen program lets go.
static uint16_t sDesktopMode = 0;
// Full screen in Mode X, and the page on show (0 or 1).
static bool sModeX = false;
static int32_t sModeXShown = 0;
// Mode X's CRTC registers over mode 13h's (index in the low byte, value in
// the high): 240 lines of one scan line each, byte addressing.
static const uint16_t sModeXCrtc[] = {
0x0D06, // vertical total
0x3E07, // overflow
0x4109, // maximum scan line: one scan line per pixel row doubled
0xEA10, // vertical retrace start
0xAC11, // vertical retrace end (write protect off)
0xDF12, // vertical display end
0x0014, // underline location: doubleword mode off
0xE715, // vertical blank start
0x0616, // vertical blank end
0xE317 // mode control: byte mode
};
// True from a successful platformVideoInit until platformVideoShutdown: a
// DVX screen mode is up (a failed live mode change restores it), so video
@ -610,7 +693,7 @@ static uint32_t farPtrToLinear(uint32_t addr) {
// real-mode interrupts via __dpmi_int(), so all data must pass through
// the transfer buffer.
static int32_t findBestMode(int32_t requestedW, int32_t requestedH, int32_t preferredBpp, uint16_t *outMode, DisplayT *d) {
static int32_t findBestMode(int32_t requestedW, int32_t requestedH, int32_t preferredBpp, ModeScoreFnT score, uint16_t *outMode, DisplayT *d) {
uint16_t vbeVersion = vbeGetControllerInfo();
// VBE 2.0+ is required for LFB (Linear Frame Buffer) support.
@ -636,10 +719,10 @@ static int32_t findBestMode(int32_t requestedW, int32_t requestedH, int32_t pref
continue;
}
int32_t score = scoreMode(&candidate, requestedW, requestedH, preferredBpp);
int32_t s = score(&candidate, requestedW, requestedH, preferredBpp);
if (score > bestScore) {
bestScore = score;
if (s > bestScore) {
bestScore = s;
bestMode = modes[i];
best = candidate;
}
@ -823,7 +906,7 @@ static void irqServicesAtExit(void) {
// is the offset DJGPP applies to convert linear addresses to near
// pointer addresses (compensating for the DS segment base).
static int32_t mapLfb(DisplayT *d, uint32_t physAddr) {
static int32_t mapLfb(DisplayT *d, uint32_t physAddr, LfbMapT *map) {
__dpmi_meminfo info;
uint32_t fbSize = (uint32_t)d->pitch * (uint32_t)d->height;
@ -853,8 +936,8 @@ static int32_t mapLfb(DisplayT *d, uint32_t physAddr) {
}
// Save the RAW linear address (not d->lfb) so we can later unlock/free it.
sLfbLinearAddr = info.address;
sLfbMappedSize = fbSize;
map->linearAddr = info.address;
map->size = fbSize;
// Convert linear address to near pointer by adding the DS base offset
d->lfb = (uint8_t *)(info.address + __djgpp_conventional_base);
@ -863,6 +946,66 @@ static int32_t mapLfb(DisplayT *d, uint32_t physAddr) {
}
// Backbuffer rectangle -> Mode X memory at pageOffset, a plane at a time.
// Whole rows go four pixels to a doubleword write.
static void modeXCopy(const DisplayT *d, const RectT *r, uint32_t pageOffset) {
uint8_t *vram = (uint8_t *)(VGA13_ADDR + __djgpp_conventional_base) + pageOffset;
int32_t right = r->x + r->w;
bool whole = r->x == 0 && r->w == MODEX_W;
for (int32_t plane = 0; plane < MODEX_PLANES; plane++) {
int32_t first = r->x + ((plane - r->x) & MODEX_PLANE_MASK);
if (first >= right) {
continue;
}
outportw(VGA_SEQ_INDEX, (uint16_t)(SEQ_MAP_MASK | ((1 << plane) << REG_DATA_SHIFT)));
for (int32_t y = r->y; y < r->y + r->h; y++) {
const uint8_t *src = d->backBuf + (size_t)y * (size_t)d->pitch;
uint8_t *dst = vram + y * MODEX_ROW_BYTES;
if (whole) {
uint32_t *dst32 = (uint32_t *)dst;
for (int32_t x = plane; x < MODEX_W; x += MODEX_PLANES * (int32_t)sizeof(uint32_t)) {
*dst32++ = (uint32_t)src[x] | ((uint32_t)src[x + MODEX_PLANES] << BYTE_BITS) | ((uint32_t)src[x + 2 * MODEX_PLANES] << (2 * BYTE_BITS)) | ((uint32_t)src[x + 3 * MODEX_PLANES] << (3 * BYTE_BITS));
}
continue;
}
for (int32_t x = first; x < right; x += MODEX_PLANES) {
dst[x / MODEX_PLANES] = src[x];
}
}
}
}
// Mode 13h unchained into Mode X (Abrash's sequence): chain-4 off, the
// 25 MHz clock under a sequencer reset, the 240-line CRTC timings, page 0
// on show, all of video memory cleared.
static void modeXSet(void) {
setBiosMode(BIOS_MODE_VGA13);
outportw(VGA_SEQ_INDEX, (uint16_t)(SEQ_MEMORY_MODE | (SEQ_MEM_UNCHAINED << REG_DATA_SHIFT)));
outportw(VGA_SEQ_INDEX, (uint16_t)(SEQ_RESET | (SEQ_RESET_SYNC << REG_DATA_SHIFT)));
outportb(VGA_MISC_WRITE, MISC_MODEX);
outportw(VGA_SEQ_INDEX, (uint16_t)(SEQ_RESET | (SEQ_RESET_RUN << REG_DATA_SHIFT)));
outportb(VGA_CRTC_INDEX, CRTC_VRETRACE_END);
outportb(VGA_CRTC_DATA, inportb(VGA_CRTC_DATA) & CRTC_UNPROTECT_MASK);
for (size_t i = 0; i < sizeof(sModeXCrtc) / sizeof(sModeXCrtc[0]); i++) {
outportw(VGA_CRTC_INDEX, sModeXCrtc[i]);
}
outportw(VGA_SEQ_INDEX, (uint16_t)(SEQ_MAP_MASK | (MODEX_ALL_PLANES << REG_DATA_SHIFT)));
memset((uint8_t *)(VGA13_ADDR + __djgpp_conventional_base), 0, VGA_WINDOW_BYTES);
sModeXShown = 0;
}
// Convert a DPMI page count to KB, mapping the "unavailable" sentinel to 0.
static uint32_t pagesToKb(unsigned long pages) {
if (pages == DPMI_INFO_UNAVAILABLE) {
@ -2140,7 +2283,7 @@ void platformSplashFillRect(int32_t x, int32_t y, int32_t w, int32_t h, uint8_t
continue;
}
dosmemput(rowBuf, spanW, SPLASH_VGA_ADDR + row * SPLASH_W + x0);
dosmemput(rowBuf, spanW, VGA13_ADDR + row * SPLASH_W + x0);
}
}
@ -2182,7 +2325,7 @@ bool platformSplashLoadRaw(const char *path) {
}
// One block move per scanline (rep movs) instead of 320 byte pokes
dosmemput(rowBuf, SPLASH_W, SPLASH_VGA_ADDR + y * SPLASH_W);
dosmemput(rowBuf, SPLASH_W, VGA13_ADDR + y * SPLASH_W);
}
fclose(fp);
@ -2441,6 +2584,143 @@ void platformVideoEnumModes(void (*cb)(int32_t w, int32_t h, int32_t bpp, void *
}
// A program takes the whole screen: the smallest VBE mode of at least the
// requested size (the requested depth first, any depth for 0), or a VGA
// mode for a request at 8 bpp (or any depth) that fits one when no VBE
// mode is that small: mode 13h up to 320x200, Mode X up to 320x240. d gets its own backbuffer; the desktop's
// buffers and LFB mapping are left alone. On failure the screen may be in
// any mode: the caller calls platformVideoExclusiveEnd, as after success.
int32_t platformVideoExclusiveBegin(DisplayT *d, int32_t requestedW, int32_t requestedH, int32_t preferredBpp) {
uint16_t mode;
bool vgaDepth = preferredBpp == 0 || preferredBpp == BPP_8;
bool fitsVga13 = vgaDepth && requestedW <= VGA13_W && requestedH <= VGA13_H;
bool fitsModeX = vgaDepth && requestedW <= MODEX_W && requestedH <= MODEX_H;
int32_t vgaArea = fitsVga13 ? VGA13_W * VGA13_H : MODEX_W * MODEX_H;
bool found;
memset(d, 0, sizeof(*d));
sModeX = false;
found = findBestMode(requestedW, requestedH, preferredBpp, scoreExclusive, &mode, d) == 0;
// A request that fits 320x200 (mode 13h) or 320x240 (Mode X) at 8 bpp
// gets that VGA mode rather than a larger VBE mode: the true low
// resolution and far fewer bytes a frame.
if (found && !(fitsModeX && d->width * d->height > vgaArea)) {
uint32_t physAddr = (uint32_t)(uintptr_t)d->lfb;
d->lfb = NULL;
if (setVesaMode(mode) != 0 || mapLfb(d, physAddr, &sExclusiveLfb) != 0) {
return -1;
}
} else if (fitsVga13) {
memset(d, 0, sizeof(*d));
setBiosMode(BIOS_MODE_VGA13);
d->width = VGA13_W;
d->height = VGA13_H;
d->pitch = VGA13_W;
d->format.bitsPerPixel = BPP_8;
d->format.bytesPerPixel = 1;
d->lfb = (uint8_t *)(VGA13_ADDR + __djgpp_conventional_base);
dvxLog("Video: VGA mode 13h");
} else if (fitsModeX) {
// Planar: no linear framebuffer; platformVideoExclusiveFlush
// writes the planes.
memset(d, 0, sizeof(*d));
modeXSet();
sModeX = true;
d->width = MODEX_W;
d->height = MODEX_H;
d->pitch = MODEX_W;
d->format.bitsPerPixel = BPP_8;
d->format.bytesPerPixel = 1;
dvxLog("Video: VGA Mode X (planar, two pages)");
} else {
return -1;
}
d->backBuf = (uint8_t *)calloc(1, (size_t)d->pitch * (size_t)d->height);
if (!d->backBuf) {
return -1;
}
d->clipW = d->width;
d->clipH = d->height;
return 0;
}
// Mode X only: the page on show, read back plane by plane, into out
// (width x height bytes). False in a linear mode: the backbuffer is what
// is on the screen.
bool platformVideoExclusiveCapture(const DisplayT *d, uint8_t *out) {
if (!sModeX) {
return false;
}
const uint8_t *vram = (const uint8_t *)(VGA13_ADDR + __djgpp_conventional_base) + sModeXShown * MODEX_PAGE_BYTES;
for (int32_t plane = 0; plane < MODEX_PLANES; plane++) {
outportw(VGA_GC_INDEX, (uint16_t)(GC_READ_MAP | (plane << REG_DATA_SHIFT)));
for (int32_t y = 0; y < d->height; y++) {
for (int32_t x = plane; x < d->width; x += MODEX_PLANES) {
out[y * d->width + x] = vram[y * MODEX_ROW_BYTES + x / MODEX_PLANES];
}
}
}
return true;
}
// The full-screen program lets go: its buffers and mapping are freed and
// the desktop's mode (and, at 8 bpp, its palette) is set again. The
// desktop's backbuffer is untouched; the caller repaints the screen.
int32_t platformVideoExclusiveEnd(DisplayT *d, const DisplayT *desktop) {
free(d->backBuf);
d->backBuf = NULL;
sModeX = false;
unmapLfb(d, &sExclusiveLfb);
if (setVesaMode(sDesktopMode) != 0) {
return -1;
}
if (desktop->format.bitsPerPixel == BPP_8 && desktop->palette) {
platformVideoSetPalette(desktop->palette, 0, PAL_ENTRIES);
}
return 0;
}
// A linear mode copies the rectangle. Mode X: a whole-screen flush fills
// the hidden page, makes it the one on show and waits for the retrace that
// shows it (no tearing); a smaller one writes the page on show.
void platformVideoExclusiveFlush(const DisplayT *d, const RectT *r) {
if (!sModeX) {
platformFlushRect(d, r);
return;
}
if (r->x != 0 || r->y != 0 || r->w != d->width || r->h != d->height) {
modeXCopy(d, r, (uint32_t)(sModeXShown * MODEX_PAGE_BYTES));
return;
}
int32_t hidden = (sModeXShown + 1) % MODEX_PAGES;
uint32_t offset = (uint32_t)(hidden * MODEX_PAGE_BYTES);
modeXCopy(d, r, offset);
outportw(VGA_CRTC_INDEX, (uint16_t)(CRTC_START_HIGH | (offset & (BYTE_MASK << REG_DATA_SHIFT))));
outportw(VGA_CRTC_INDEX, (uint16_t)(CRTC_START_LOW | ((offset & BYTE_MASK) << REG_DATA_SHIFT)));
platformVideoWaitRetrace();
sModeXShown = hidden;
}
// Tears down the graphics mode and restores standard 80x25 text
// mode (BIOS mode 3) so the user gets their DOS prompt back.
// Also frees the backbuffer, palette, and disables near pointers
@ -2457,7 +2737,7 @@ void platformVideoFreeBuffers(DisplayT *d) {
d->palette = NULL;
}
unmapLfb(d);
unmapLfb(d, &sDesktopLfb);
}
@ -2482,7 +2762,7 @@ int32_t platformVideoInit(DisplayT *d, int32_t requestedW, int32_t requestedH, i
memset(d, 0, sizeof(*d));
// Find the best VESA mode
if (findBestMode(requestedW, requestedH, preferredBpp, &bestMode, d) != 0) {
if (findBestMode(requestedW, requestedH, preferredBpp, scoreMode, &bestMode, d) != 0) {
return -1;
}
@ -2497,10 +2777,12 @@ int32_t platformVideoInit(DisplayT *d, int32_t requestedW, int32_t requestedH, i
// A failure past this point leaves the new mode set: at startup
// videoError puts the machine back in text mode for its message, and a
// live mode change restores the old mode.
if (mapLfb(d, physAddr) != 0) {
if (mapLfb(d, physAddr, &sDesktopLfb) != 0) {
return -1;
}
sDesktopMode = bestMode;
// Allocate backbuffer
uint32_t fbSize = (uint32_t)d->pitch * (uint32_t)d->height;
d->backBuf = (uint8_t *)calloc(1, fbSize);
@ -2580,6 +2862,20 @@ void platformVideoShutdown(DisplayT *d) {
}
// Wait for the start of the next vertical retrace (a retrace already under
// way is let finish first), so a frame copied now is not torn. Gives up
// after RETRACE_WAIT_MAX_US.
void platformVideoWaitRetrace(void) {
uint64_t start = platformMicros();
while ((inportb(VGA_INPUT_STATUS) & VGA_STATUS_RETRACE) && platformMicros() - start < RETRACE_WAIT_MAX_US) {
}
while (!(inportb(VGA_INPUT_STATUS) & VGA_STATUS_RETRACE) && platformMicros() - start < RETRACE_WAIT_MAX_US) {
}
}
// Cooperative yield to the DPMI host. In a multitasking DOS
// environment (Windows 3.x DOS box or OS/2 VDM),
// this gives other tasks a chance to run. Under a single-tasking
@ -2617,6 +2913,24 @@ static bool regionFromNear(const void *addr, uint32_t size, bool isCode, __dpmi_
}
// Scores a mode for a full-screen program: the requested depth (any when
// preferredBpp is 0) outweighs size; then the smaller the mode (the less
// wasted around the program's picture), the better. -1 below the request.
static int32_t scoreExclusive(const VbeModeT *m, int32_t requestedW, int32_t requestedH, int32_t preferredBpp) {
if (m->width < requestedW || m->height < requestedH) {
return -1;
}
int32_t waste = m->width * m->height - requestedW * requestedH;
if (waste >= EXCL_SCORE_AREA_MAX) {
waste = EXCL_SCORE_AREA_MAX - 1;
}
return (m->bpp == preferredBpp ? EXCL_SCORE_BPP_MATCH : 0) + EXCL_SCORE_AREA_MAX - waste;
}
// Scores a supported mode against the requested parameters:
//
// Base score by bpp: 16-bit=100, 15-bit=90, 32-bit=85, 8-bit=70
@ -2776,20 +3090,20 @@ static void timerUnlock(void) {
}
// Unlocks and frees the DPMI physical LFB mapping recorded by mapLfb.
// Safe to call when nothing is mapped (sLfbLinearAddr == LFB_NOT_MAPPED)
// Unlocks and frees a DPMI physical LFB mapping recorded by mapLfb.
// Safe to call when nothing is mapped (map->linearAddr == LFB_NOT_MAPPED)
// and idempotent: the sentinel is reset so a second call is a no-op.
// Uses the raw linear address, NOT d->lfb (which has the conventional
// base folded in). Clears d->lfb so callers do not keep a stale pointer.
static void unmapLfb(DisplayT *d) {
if (sLfbLinearAddr != LFB_NOT_MAPPED) {
static void unmapLfb(DisplayT *d, LfbMapT *map) {
if (map->linearAddr != LFB_NOT_MAPPED) {
__dpmi_meminfo info;
info.address = sLfbLinearAddr;
info.size = sLfbMappedSize;
info.address = map->linearAddr;
info.size = map->size;
__dpmi_unlock_linear_region(&info);
__dpmi_free_physical_address_mapping(&info);
sLfbLinearAddr = LFB_NOT_MAPPED;
sLfbMappedSize = 0;
map->linearAddr = LFB_NOT_MAPPED;
map->size = 0;
}
d->lfb = NULL;
@ -3115,10 +3429,15 @@ DXE_EXPORT_TABLE(sDxeExportTable)
DXE_EXPORT(platformUnlockRegion)
DXE_EXPORT(platformValidateFilename)
DXE_EXPORT(platformVideoEnumModes)
DXE_EXPORT(platformVideoExclusiveBegin)
DXE_EXPORT(platformVideoExclusiveCapture)
DXE_EXPORT(platformVideoExclusiveEnd)
DXE_EXPORT(platformVideoExclusiveFlush)
DXE_EXPORT(platformVideoFreeBuffers)
DXE_EXPORT(platformVideoInit)
DXE_EXPORT(platformVideoSetPalette)
DXE_EXPORT(platformVideoShutdown)
DXE_EXPORT(platformVideoWaitRetrace)
DXE_EXPORT(platformYield)
// --- DPMI/go32 (needed by rs232 serial driver) ---

View file

@ -150,7 +150,7 @@ static const int32_t sModeList[][2] = {
static const int32_t sBppList[] = { HOST_BPP_8, HOST_BPP_16, HOST_BPP_32 };
// Most recently initialised display, for hostSavePng.
static DisplayT *sDisplay = NULL;
static const DisplayT *sDisplay = NULL;
// Palette as last programmed by platformVideoSetPalette (8-bit modes).
static uint8_t sPalette[PAL_ENTRIES * PAL_CHANNELS];
@ -925,6 +925,46 @@ void platformVideoEnumModes(void (*cb)(int32_t w, int32_t h, int32_t bpp, void *
}
// The host has no modes to set: the full-screen display is a pair of
// buffers like the desktop's (bpp 0 = 8), and hostVideoFailNext fails it.
// The palette is the caller's, as on DOS.
int32_t platformVideoExclusiveBegin(DisplayT *d, int32_t requestedW, int32_t requestedH, int32_t preferredBpp) {
int32_t rc = platformVideoInit(d, requestedW, requestedH, preferredBpp == 0 ? HOST_BPP_8 : preferredBpp);
free(d->palette);
d->palette = NULL;
return rc;
}
// The host's full-screen display is linear: the backbuffer is the screen.
bool platformVideoExclusiveCapture(const DisplayT *d, uint8_t *out) {
(void)d;
(void)out;
return false;
}
int32_t platformVideoExclusiveEnd(DisplayT *d, const DisplayT *desktop) {
free(d->backBuf);
d->backBuf = NULL;
free(d->lfb);
d->lfb = NULL;
sDisplay = desktop;
if (desktop->format.bitsPerPixel == HOST_BPP_8 && desktop->palette) {
platformVideoSetPalette(desktop->palette, 0, PAL_ENTRIES);
}
return 0;
}
void platformVideoExclusiveFlush(const DisplayT *d, const RectT *r) {
platformFlushRect(d, r);
}
void platformVideoFreeBuffers(DisplayT *d) {
free(d->backBuf);
d->backBuf = NULL;
@ -1009,6 +1049,11 @@ void platformVideoShutdown(DisplayT *d) {
}
// No retrace to wait for on the host.
void platformVideoWaitRetrace(void) {
}
// Nothing else is running in a test process; the fake clock moves on by
// the yield step (hostClockSetYieldStep), in real-time mode not at all.
void platformYield(void) {

View file

@ -162,6 +162,9 @@ DVX_ABI_STAMP(serial);
#define UART_READ_LCR(C) inportb((C)->base + UART_LCR)
#define UART_READ_LSR(C) ((C)->lsr = inportb((C)->base + UART_LSR))
#define UART_READ_MSR(C) ((C)->msr = inportb((C)->base + UART_MSR))
// The live MCR, for the modem lines another program left set before this
// port was opened (the cached mcr only knows what this library wrote).
#define UART_READ_MCR(C) inportb((C)->base + UART_MCR)
#define UART_READ_BPS(C, D) \
{ outportb((C)->base + UART_LCR, inportb((C)->base + UART_LCR) | LCR_DLAB); \
(D) = (uint16_t)inportw((C)->base + UART_DLW); \
@ -256,6 +259,9 @@ DVX_ABI_STAMP(serial);
#define MCR_OUT2 0x08
#define MCR_LOOPBACK 0x10
#define MCR_MASK (MCR_DTR | MCR_RTS | MCR_OUT1 | MCR_OUT2 | MCR_LOOPBACK)
// The modem control lines: never changed by opening or closing a port,
// only by rs232SetDtr / rs232SetRts / rs232SetMcr and flow control.
#define MCR_LINES (MCR_DTR | MCR_RTS)
// LSR bits
#define LSR_DATA_READY 0x01
@ -732,8 +738,9 @@ static int32_t findIrq(Rs232StateT *com) {
int32_t bit;
uint32_t flags;
// Set up the UART
UART_WRITE_MCR(com, MCR_OUT2);
// Set up the UART (OUT2 gates the interrupt line; DTR and RTS stay as
// they are, so probing never hangs up a modem)
UART_WRITE_MCR(com, (UART_READ_MCR(com) & MCR_LINES) | MCR_OUT2);
UART_WRITE_FCR(com, 0);
// Wait until TX hold register is empty
@ -986,7 +993,11 @@ int32_t rs232Close(int32_t com) {
flags = irqSave();
port->isOpen = 0;
UART_WRITE_IER(port, 0);
UART_WRITE_MCR(port, 0);
// DTR and RTS are left as they are: closing a port must never hang up
// a modem or signal the other end. Only OUT2 (the interrupt gate),
// OUT1 and loopback are cleared. To hang up, drop DTR first
// (rs232SetDtr); the line stays down after the close.
UART_WRITE_MCR(port, port->mcr & MCR_LINES);
UART_WRITE_FCR(port, 0);
drainRxFifo(port);
UART_READ_IIR(port);

View file

@ -177,7 +177,7 @@ Returns RS232_SUCCESS or a negative error code. The UART type is probed once her
.h2 rs232Close
Close a COM port and release its ISR resources.
Close a COM port and release its ISR resources. The modem control lines are left as they are: closing never drops DTR or RTS, so a modem stays connected and the next program to open the port can carry on. To hang up, call rs232SetDtr(com, false) before closing; the line stays down after the close. Opening a port raises DTR and RTS.
.code
int32_t rs232Close(int32_t com);
@ -276,9 +276,9 @@ rs232GetParity returns a lowercase character on success. Callers comparing again
rs232SetHandshake(com, handshake) Change flow control mode. Releases any throttle the old mode applied (raises RTS/DTR or sends XON) and starts the transmit side from the live CTS/DSR line.
rs232SetIrq(com, irq) Move an open port to another IRQ (for a wrong auto-detection). Interrupt enables and DTR/RTS are kept.
rs232SetMcr(com, mcr) Set modem control register directly.
rs232SetDtr(com, dtr) Set DTR line state.
rs232SetDtr(com, dtr) Set DTR line state. The state is kept when the port is closed (dropping DTR is how a modem is hung up).
rs232SetBreak(com, on) Hold the transmit line in the break (spacing) state (on = true) or release it. The caller times the break, typically 250-500 ms.
rs232SetRts(com, rts) Set RTS line state.
rs232SetRts(com, rts) Set RTS line state. The state is kept when the port is closed.
rs232SetFifoThreshold(com, thr) Set FIFO trigger level (1, 4, 8, or 14 bytes) or 0 to turn the FIFO off. Levels other than 0 return RS232_ERR_INVALID_FIFO unless the UART is a 16550A.
.endtable

View file

@ -27,6 +27,18 @@ packaging. Technical Reference > Writing Games for DVX
covers game input, timing, the screen and palettes, sound, music,
joysticks, data and save files, and porting a game library to DVX.
Documentation
-------------
docs/dvx_system_reference.html The DVX help as one page: the user guide,
the SDK Quick Start, Writing Games for
DVX, and the reference for every header,
library and widget
docs/dvx_basic_reference.html DVX BASIC: the language, the IDE, the
controls and the include libraries
Inside DVX the same text is in the Help program.
Directory Structure
-------------------
@ -68,9 +80,9 @@ Requirements
- The DJGPP cross compiler (i586-pc-msdosdjgpp-gcc) and its dxe3gen.
Projects find it through DJGPP_PREFIX (environment or make command
line); the default is toolchains/djgpp in the DVX source tree the SDK
was built in (../../toolchains/djgpp from this directory), which
./installToolchain.sh sets up.
line), e.g. make DJGPP_PREFIX=/opt/djgpp. Without it the SDK looks in
../../toolchains/djgpp from this directory: inside a DVX source tree
that is the toolchain ./installToolchain.sh sets up.
- A Linux host for the tools in bin/. On DVX itself the same tools are
SYSTEM/DVXRES.EXE, SYSTEM/DVXHLPC.EXE and SYSTEM/BASCOMP.EXE.

View file

@ -54,6 +54,8 @@
// The game's screen
#define FB_W 320
#define FB_H 200
#define FULL_BPP 8 // full screen: 256 colours, the game's palette in the DAC
#define SCAN_ALT_ENTER 0xA6 // BIOS scancode of Alt with the keypad Enter
#define SCALE_MAX 3
#define FRAME_HZ 60
#define STEPS_MAX 4 // frames made up at once after a stall
@ -117,6 +119,7 @@
#define CMD_GAME_PAUSE 101
#define CMD_GAME_MUSIC 102
#define CMD_GAME_EXIT 103
#define CMD_GAME_FULL 104
#define CMD_HELP_CONTENTS 200
#define CMD_HELP_ABOUT 201
@ -154,6 +157,7 @@ static int32_t sScale = 1;
static bool sQuit = false;
static bool sPaused = false;
static bool sMusicOn = true;
static bool sFull = false;
static GameStateT sGame;
// The game's screen and palette, and the desktop pixel for each colour
@ -181,6 +185,8 @@ static void expandRow(uint8_t *dst, const uint8_t *src, int32_t count, int32
static void fbFill(int32_t x, int32_t y, int32_t w, int32_t h, uint8_t color);
static void fbFrame(int32_t x, int32_t y, int32_t size, uint8_t fill, uint8_t edge);
static void fbSky(int32_t x, int32_t y, int32_t w, int32_t h);
static void fullChanged(WindowT *win, ExclusiveStateE state, void *userData);
static void fullSet(bool on);
static void gameNew(void);
static void gameUpdate(uint32_t steps);
static void loadBest(void);
@ -238,6 +244,7 @@ static void coinGlow(void) {
sGamePal[COL_COIN * PAL_CHANNELS + 0] = glow;
sGamePal[COL_COIN * PAL_CHANNELS + 1] = glow;
lutSetRange(COL_COIN, 1);
dvxExclusiveSetPalette(sAc, sGamePal, COL_COIN, 1);
}
@ -347,6 +354,45 @@ static void fbSky(int32_t x, int32_t y, int32_t w, int32_t h) {
}
// Full screen came, went away for a while (another window has the focus:
// the game draws in its window again) or ended.
static void fullChanged(WindowT *win, ExclusiveStateE state, void *userData) {
(void)win;
(void)userData;
if (state == ExclusiveEndedE) {
sFull = false;
wmMenuItemSetChecked(sWin->menuBar, CMD_GAME_FULL, false);
}
if (state == ExclusiveActiveE) {
dvxExclusiveSetPalette(sAc, sGamePal, 0, PAL_ENTRIES);
}
dvxLog("tplName: full screen %s", state == ExclusiveActiveE ? "on" : state == ExclusiveSuspendedE ? "suspended" : "ended");
dirtyAdd(0, 0, FB_W, FB_H);
}
// Alt+Enter: the whole screen at 320x200 (or the nearest mode DVX has),
// or back to the window. The other programs run on either way.
static void fullSet(bool on) {
if (on && dvxExclusiveBegin(sAc, sWin, FB_W, FB_H, FULL_BPP, fullChanged, NULL) != DVX_EXCL_OK) {
dvxLog("tplName: no full-screen mode");
on = false;
}
if (!on && sFull) {
dvxExclusiveEnd(sAc, sWin);
dirtyAdd(0, 0, FB_W, FB_H);
dvxLog("tplName: full screen off");
}
sFull = on;
wmMenuItemSetChecked(sWin->menuBar, CMD_GAME_FULL, on);
}
static void gameNew(void) {
sGame.playerX = (FB_W - PLAYER_SIZE) / 2 - FIRST_COIN_DX / 2;
sGame.playerY = (FB_H - PLAYER_SIZE) / 2;
@ -470,10 +516,15 @@ static void onClose(WindowT *win) {
// In a game input window every key but Alt+Tab, Ctrl+Esc and Alt+F4
// comes here, F1 included: the game decides what it does.
// comes here, F1 included: the game decides what it does. Alt+Enter
// comes as Enter or as a bare Enter scancode (the main or keypad key).
static void onKey(WindowT *win, int32_t key, int32_t mod) {
(void)win;
(void)mod;
if ((mod & KEY_MOD_ALT) && (key == KEY_ENTER || key == (KEY_SCAN_ENTER | KEY_EXT_FLAG) || key == (SCAN_ALT_ENTER | KEY_EXT_FLAG))) {
fullSet(!sFull);
return;
}
switch (key) {
case KEY_F1:
@ -513,6 +564,10 @@ static void onMenu(WindowT *win, int32_t menuId) {
musicApply();
break;
case CMD_GAME_FULL:
fullSet(!sFull);
break;
case CMD_GAME_EXIT:
sQuit = true;
break;
@ -566,8 +621,14 @@ static void paletteInit(void) {
}
// Copy this frame's changed rectangles to the window and tell DVX.
// Copy this frame's changed rectangles to the window and tell DVX. In
// full screen the whole frame goes to the screen instead.
static void present(void) {
if (sDirtyCount > 0 && dvxExclusivePresent8(sAc, sFb, FB_W, FB_H, FB_W)) {
sDirtyCount = 0;
return;
}
for (int32_t i = 0; i < sDirtyCount; i++) {
const GameRectT *r = &sDirty[i];
@ -799,6 +860,7 @@ static bool windowCreate(void) {
wmAddMenuItem(gameMenu, "&New Game\tF2", CMD_GAME_NEW);
wmAddMenuCheckItem(gameMenu, "&Pause\tP", CMD_GAME_PAUSE, false);
wmAddMenuCheckItem(gameMenu, "&Music", CMD_GAME_MUSIC, true);
wmAddMenuCheckItem(gameMenu, "&Full Screen\tAlt+Enter", CMD_GAME_FULL, false);
wmAddMenuSeparator(gameMenu);
wmAddMenuItem(gameMenu, "E&xit", CMD_GAME_EXIT);
}

View file

@ -42,7 +42,8 @@ While TplName is the active window it has the keyboard to itself: keys that usua
P Pause, or carry on.
F1 Open this help.
F2 Start a new round.
Alt+Tab Switch to another program.
Alt+Enter Play on the whole screen, or back in the window.
Alt+Tab Switch to another program (from the whole screen too: TplName comes back to it when you return).
Alt+F4 Close TplName.
Ctrl+Esc Open the Task Manager.
.endtable
@ -61,6 +62,7 @@ While TplName is the active window it has the keyboard to itself: keys that usua
Game New Game Start a new round.
Game Pause Pause the game, or carry on.
Game Music Turn the music on or off.
Game Full Screen Play on the whole screen (Alt+Enter), or back in the window.
Game Exit Close TplName.
Help Contents Open this help.
Help About TplName Show the program name.

View file

@ -2,12 +2,12 @@
# (i386 DJGPP), recorded at the DVX_ABI_VERSION below. Generated by
# `make -C src/test/abi update`; read abiCheck.py and DVX_ABI_VERSION in
# src/libs/kpunch/libdvx/dvxTypes.h before changing it.
version 3
version 4
AccelEntryT 12
AccelTableT 12
AnsiTermApiT 36
AnsiTermExApiT 16
AppContextT 1460
AppContextT 3400
AppDescriptorT 80
AudioDriverT 28
AudioDrvConfigT 16
@ -43,6 +43,7 @@ DvxResDirEntryT 48
DvxResFooterT 16
DvxResHandleT 268
DxeAppContextT 2216
ExclusiveStateT 1940
FileFilterT 4
FileOpsCallbacksT 12
FileOpsEntryT 20

View file

@ -0,0 +1,14 @@
[Project]
Name = Ticker
Author = Scott Duensing
Publisher = Kangaroo Punch Studios
Version = 1.00
Copyright = Copyright 2026 Scott Duensing
Description = DOS harness fixture: a timer counting in the caption, to show a program runs on in the background
[Forms]
File0 = ticker.frm
Count = 1
[Settings]
StartupForm = Ticker

View file

@ -0,0 +1,60 @@
VERSION DVX 1.00
Begin Form Ticker
Caption = "Counting 0"
Layout = VBox
AutoSize = False
Resizable = False
Width = 200
Height = 60
Begin Label lblNote
Caption = "Counts to 100"
Weight = 1
End
Begin Timer tmrTick
Interval = 100
Enabled = True
End
End
' The MIT License (MIT)
'
' Copyright (C) 2026 Scott Duensing
'
' Permission is hereby granted, free of charge, to any person obtaining a copy
' of this software and associated documentation files (the "Software"), to
' deal in the Software without restriction, including without limitation the
' rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
' sell copies of the Software, and to permit persons to whom the Software is
' furnished to do so, subject to the following conditions:
'
' The above copyright notice and this permission notice shall be included in
' all copies or substantial portions of the Software.
'
' THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
' IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
' FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
' AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
' LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
' FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
' IN THE SOFTWARE.
' ticker -- DOS harness fixture. Counts to 100, ten a second, in the
' window caption, then shows "Counted 100". The fullscr script starts it,
' puts a game on the whole screen and waits for "Counted 100" while the
' game still has the screen: the program ran on in the background.
Dim Shared ticks As Integer
Sub tmrTick_Timer ()
ticks = ticks + 1
If ticks >= 100 Then
tmrTick.Enabled = False
Ticker.Caption = "Counted 100"
Else
Ticker.Caption = "Counting " & LTrim$(Str$(ticks))
End If
End Sub

View file

@ -1416,12 +1416,14 @@ for script in "${SCRIPTS[@]}"; do
SCRIPT_DOSBOX_MACHINE="${DOSBOX_OLD_MACHINE}"
fi
# "# LOG: text": DVX.LOG must contain text (checked after the run).
# "# LOG: text": DVX.LOG must contain text (checked after the run);
# "# LOG(emulator): text" only on that emulator (e.g. a mode one of
# the emulated video cards does not offer).
LOG_EXPECTS=()
while IFS= read -r expect; do
LOG_EXPECTS+=("${expect}")
done < <(sed -n 's/^#[[:space:]]*LOG:[[:space:]]*\(.*[^[:space:]]\)[[:space:]]*$/\1/p' "${script}")
done < <(sed -n -e 's/^#[[:space:]]*LOG:[[:space:]]*\(.*[^[:space:]]\)[[:space:]]*$/\1/p' -e "s/^#[[:space:]]*LOG(${EMU}):[[:space:]]*\(.*[^[:space:]]\)[[:space:]]*$/\1/p" "${script}")
# "# DOSBOX-CPU: type": DOSBox Staging's cputype (e.g. pentium, which
# has a time-stamp counter); 86Box keeps its 486.

View file

@ -133,6 +133,7 @@ typedef struct {
static bool addStep(ParseStateT *ps, const ScriptStepT *step);
static bool cmdButton(ParseStateT *ps, ScriptStepT *step, const TokenT *tok, int32_t count);
static bool cmdFullScreen(ParseStateT *ps, ScriptStepT *step, const TokenT *tok, int32_t count);
static bool cmdKey(ParseStateT *ps, ScriptStepT *step, const TokenT *tok, int32_t count);
static bool cmdLaunch(ParseStateT *ps, ScriptStepT *step, const TokenT *tok, int32_t count);
static bool cmdLog(ParseStateT *ps, ScriptStepT *step, const TokenT *tok, int32_t count);
@ -229,34 +230,42 @@ static const RawKeyNameT sRawKeyNames[] = {
#define RAW_KEY_NAME_COUNT ((int32_t)(sizeof(sRawKeyNames) / sizeof(sRawKeyNames[0])))
// FULLSCREEN depths
#define SCRIPT_BPP_8 8
#define SCRIPT_BPP_15 15
#define SCRIPT_BPP_16 16
#define SCRIPT_BPP_32 32
static const CommandT sCommands[] = {
{ "ASSERTNOWIN", ScriptOpAssertNoWinE, cmdTitle },
{ "ASSERTWIN", ScriptOpAssertWinE, cmdTitle },
{ "CLICK", ScriptOpClickE, cmdPoint },
{ "CLOCKMARK", ScriptOpClockMarkE, cmdNoArgs },
{ "DBLCLICK", ScriptOpDblClickE, cmdPoint },
{ "DRAG", ScriptOpDragE, cmdPoint },
{ "KEY", ScriptOpKeyE, cmdKey },
{ "KEYDOWN", ScriptOpKeyDownE, cmdRawKey },
{ "KEYUP", ScriptOpKeyUpE, cmdRawKey },
{ "LAUNCH", ScriptOpLaunchE, cmdLaunch },
{ "LOG", ScriptOpLogE, cmdLog },
{ "LOGWINDOWS", ScriptOpLogWindowsE, cmdNoArgs },
{ "MOUSEDOWN", ScriptOpMouseDownE, cmdButton },
{ "MOUSEUP", ScriptOpMouseUpE, cmdButton },
{ "MOVE", ScriptOpMoveE, cmdPoint },
{ "OUTDIR", ScriptOpLogE, cmdOutDir },
{ "QUIT", ScriptOpQuitE, cmdQuit },
{ "SHOT", ScriptOpShotE, cmdShot },
{ "TIMEOUT", ScriptOpLogE, cmdTimeout },
{ "TYPE", ScriptOpTypeE, cmdType },
{ "WAIT", ScriptOpWaitE, cmdWait },
{ "WAITEXIT", ScriptOpWaitExitE, cmdTitle },
{ "WAITHOST", ScriptOpWaitHostE, cmdTitle },
{ "WAITLOG", ScriptOpWaitLogE, cmdTitle },
{ "WAITMS", ScriptOpWaitMsE, cmdWait },
{ "WAITNOWIN", ScriptOpWaitNoWinE, cmdTitle },
{ "WAITWIN", ScriptOpWaitWinE, cmdTitle }
{ "ASSERTNOWIN", ScriptOpAssertNoWinE, cmdTitle },
{ "ASSERTWIN", ScriptOpAssertWinE, cmdTitle },
{ "CLICK", ScriptOpClickE, cmdPoint },
{ "CLOCKMARK", ScriptOpClockMarkE, cmdNoArgs },
{ "DBLCLICK", ScriptOpDblClickE, cmdPoint },
{ "DRAG", ScriptOpDragE, cmdPoint },
{ "FULLSCREEN", ScriptOpFullScreenE, cmdFullScreen },
{ "KEY", ScriptOpKeyE, cmdKey },
{ "KEYDOWN", ScriptOpKeyDownE, cmdRawKey },
{ "KEYUP", ScriptOpKeyUpE, cmdRawKey },
{ "LAUNCH", ScriptOpLaunchE, cmdLaunch },
{ "LOG", ScriptOpLogE, cmdLog },
{ "LOGWINDOWS", ScriptOpLogWindowsE, cmdNoArgs },
{ "MOUSEDOWN", ScriptOpMouseDownE, cmdButton },
{ "MOUSEUP", ScriptOpMouseUpE, cmdButton },
{ "MOVE", ScriptOpMoveE, cmdPoint },
{ "OUTDIR", ScriptOpLogE, cmdOutDir },
{ "QUIT", ScriptOpQuitE, cmdQuit },
{ "SHOT", ScriptOpShotE, cmdShot },
{ "TIMEOUT", ScriptOpLogE, cmdTimeout },
{ "TYPE", ScriptOpTypeE, cmdType },
{ "WAIT", ScriptOpWaitE, cmdWait },
{ "WAITEXIT", ScriptOpWaitExitE, cmdTitle },
{ "WAITHOST", ScriptOpWaitHostE, cmdTitle },
{ "WAITLOG", ScriptOpWaitLogE, cmdTitle },
{ "WAITMS", ScriptOpWaitMsE, cmdWait },
{ "WAITNOWIN", ScriptOpWaitNoWinE, cmdTitle },
{ "WAITWIN", ScriptOpWaitWinE, cmdTitle },
{ "WINDOWED", ScriptOpWindowedE, cmdNoArgs }
};
#define COMMAND_COUNT ((int32_t)(sizeof(sCommands) / sizeof(sCommands[0])))
@ -298,6 +307,22 @@ static bool cmdButton(ParseStateT *ps, ScriptStepT *step, const TokenT *tok, int
// KEY key [key ...]: one step per key
// FULLSCREEN width height bpp (bpp 0, 8, 15, 16 or 32)
static bool cmdFullScreen(ParseStateT *ps, ScriptStepT *step, const TokenT *tok, int32_t count) {
bool ok = count == 3 && parseInt(tok[0].text, &step->from.x) && parseInt(tok[1].text, &step->from.y) && parseInt(tok[2].text, &step->count);
if (!ok || step->from.x <= 0 || step->from.y <= 0) {
return fail(ps, "expected width, height and bits per pixel");
}
if (step->count != 0 && step->count != SCRIPT_BPP_8 && step->count != SCRIPT_BPP_15 && step->count != SCRIPT_BPP_16 && step->count != SCRIPT_BPP_32) {
return fail(ps, "bits per pixel must be 0, 8, 15, 16 or 32");
}
return addStep(ps, step);
}
static bool cmdKey(ParseStateT *ps, ScriptStepT *step, const TokenT *tok, int32_t count) {
if (count < 1) {
return fail(ps, "KEY needs at least one key name");
@ -358,6 +383,8 @@ static bool cmdLog(ParseStateT *ps, ScriptStepT *step, const TokenT *tok, int32_
// A command without arguments (LOGWINDOWS)
static bool cmdNoArgs(ParseStateT *ps, ScriptStepT *step, const TokenT *tok, int32_t count) {
(void)tok;

View file

@ -60,6 +60,7 @@ typedef enum {
ScriptOpClockMarkE, // CLOCKMARK: send "CLOCK <DOS time>" to the host
ScriptOpDblClickE, // DBLCLICK pos
ScriptOpDragE, // DRAG [button] pos pos
ScriptOpFullScreenE, // FULLSCREEN w h bpp: the script's own window takes the whole screen with a test pattern
ScriptOpKeyDownE, // KEYDOWN key: press it at the keyboard (one step per key)
ScriptOpKeyE, // KEY keyspec (one step per key)
ScriptOpKeyUpE, // KEYUP key: release it at the keyboard (one step per key)
@ -78,7 +79,8 @@ typedef enum {
ScriptOpWaitLogE, // WAITLOG "text" [seconds]
ScriptOpWaitMsE, // WAITMS milliseconds
ScriptOpWaitNoWinE, // WAITNOWIN "title" [seconds]
ScriptOpWaitWinE // WAITWIN "title" [seconds]
ScriptOpWaitWinE, // WAITWIN "title" [seconds]
ScriptOpWindowedE // WINDOWED: FULLSCREEN's window gives the screen back and closes
} ScriptOpE;
// How a coordinate is measured. Screen coordinates are always
@ -114,10 +116,10 @@ typedef struct {
char text[SCRIPT_TEXT_MAX]; // title, path, text or file name
char args[SCRIPT_TEXT_MAX]; // LAUNCH arguments
ScriptKeyT key; // KEY; KEYDOWN / KEYUP: scancode is the raw key code
ScriptPointT from; // position; DRAG start
ScriptPointT from; // position; DRAG start; FULLSCREEN size (x, y)
ScriptPointT to; // DRAG end
int32_t button; // MOUSE_LEFT / MOUSE_RIGHT / MOUSE_MIDDLE
int32_t count; // frames, or a time in ms
int32_t count; // frames, or a time in ms; FULLSCREEN bits per pixel
bool pass; // QUIT status
} ScriptStepT;

View file

@ -44,6 +44,12 @@
#define CLOCK_LINE_MAX 48
#define CS_PER_SECOND 100 // CLOCKMARK reports hundredths of a second
#define US_PER_CS 10000
#define FULL_TITLE "Full Screen Test"
#define FULL_WIN_W 160
#define FULL_WIN_H 120
#define FULL_SQUARE_SHIFT 4 // test pattern: 16x16 squares
#define FULL_SQUARES_ROW 16 // palette entries per row of squares
#define WHITE 255
// Phases of the multi-frame mouse steps
#define PHASE_MOVE 0
@ -71,9 +77,11 @@ typedef enum {
static uint32_t elapsedMs(PlatformTicksT since);
static StepResultE execClockMark(ScriptRunT *run, const ScriptStepT *step);
static StepResultE execFullScreen(ScriptRunT *run, const ScriptStepT *step);
static StepResultE execMouse(ScriptRunT *run, const ScriptStepT *step);
static StepResultE execStep(ScriptRunT *run, const ScriptStepT *step);
static StepResultE execWait(ScriptRunT *run, const ScriptStepT *step);
static void execWindowed(ScriptRunT *run);
static StepResultE failStep(ScriptRunT *run, const ScriptStepT *step, const char *fmt, ...) __attribute__((format(printf, 3, 4)));
static WindowT *findWindow(const AppContextT *ctx, const char *pattern);
static void finishRun(ScriptRunT *run, bool pass, const char *detail);
@ -114,6 +122,53 @@ static StepResultE execClockMark(ScriptRunT *run, const ScriptStepT *step) {
}
// FULLSCREEN: the script's own window takes the whole screen and shows a
// test pattern in the DVX palette: 16x16 squares of every entry, and a
// white diagonal one pixel wide, so a plane, page or scaling mix-up shows
// in a screenshot.
static StepResultE execFullScreen(ScriptRunT *run, const ScriptStepT *step) {
AppContextT *ctx = run->env.ctx;
int32_t w = step->from.x;
int32_t h = step->from.y;
uint8_t pal[PAL_ENTRIES * PAL_CHANNELS];
if (!run->fullWin) {
run->fullWin = dvxCreateWindow(ctx, FULL_TITLE, 0, 0, FULL_WIN_W, FULL_WIN_H, false);
if (!run->fullWin) {
return failStep(run, step, "no window for FULLSCREEN");
}
}
int32_t rc = dvxExclusiveBegin(ctx, run->fullWin, w, h, step->count, NULL, NULL);
if (rc != DVX_EXCL_OK) {
return failStep(run, step, "FULLSCREEN %" PRId32 "x%" PRId32 " at %" PRId32 " bpp failed (%" PRId32 ")", w, h, step->count, rc);
}
uint8_t *pic = (uint8_t *)malloc((size_t)w * (size_t)h);
if (!pic) {
return failStep(run, step, "out of memory for the FULLSCREEN pattern");
}
dvxGeneratePalette(pal);
uint8_t white = dvxNearestPalEntry(pal, WHITE, WHITE, WHITE);
for (int32_t y = 0; y < h; y++) {
for (int32_t x = 0; x < w; x++) {
pic[y * w + x] = x == y ? white : (uint8_t)((y >> FULL_SQUARE_SHIFT) * FULL_SQUARES_ROW + (x >> FULL_SQUARE_SHIFT));
}
}
dvxExclusiveSetPalette(ctx, pal, 0, PAL_ENTRIES);
dvxExclusivePresent8(ctx, pic, w, h, w);
free(pic);
return StepYieldE;
}
// MOVE, CLICK, DBLCLICK, DRAG, MOUSEDOWN, MOUSEUP. One mouse state per
// tick: move first, then each press and release on its own frame.
static StepResultE execMouse(ScriptRunT *run, const ScriptStepT *step) {
@ -219,6 +274,13 @@ static StepResultE execStep(ScriptRunT *run, const ScriptStepT *step) {
case ScriptOpClockMarkE:
return execClockMark(run, step);
case ScriptOpFullScreenE:
return execFullScreen(run, step);
case ScriptOpWindowedE:
execWindowed(run);
return StepYieldE;
case ScriptOpKeyDownE:
case ScriptOpKeyUpE:
if (run->phase == 0) {
@ -370,6 +432,18 @@ static StepResultE execWait(ScriptRunT *run, const ScriptStepT *step) {
}
// WINDOWED: FULLSCREEN's window gives the screen back and closes.
static void execWindowed(ScriptRunT *run) {
if (!run->fullWin) {
return;
}
dvxExclusiveEnd(run->env.ctx, run->fullWin);
dvxDestroyWindow(run->env.ctx, run->fullWin);
run->fullWin = NULL;
}
static StepResultE failStep(ScriptRunT *run, const ScriptStepT *step, const char *fmt, ...) {
char reason[SCRIPT_DETAIL_MAX];
char detail[SCRIPT_DETAIL_MAX];

View file

@ -86,6 +86,7 @@ typedef struct {
int32_t fromY;
int32_t toX;
int32_t toY;
WindowT *fullWin; // FULLSCREEN's window (NULL: none)
bool mouseKnown; // mouseX/Y seeded from the context
bool finished;
bool passed;

View file

@ -0,0 +1,33 @@
# fullmode.scr -- every kind of full-screen mode shows its picture.
#
# FULLSCREEN puts the script's own window on the whole screen with a test
# pattern (16x16 squares of every palette entry and a white diagonal one
# pixel wide). 320x240 at 8 bpp goes to VGA Mode X: planar, two pages,
# each frame flipped at the retrace; its screenshot is read back from
# video memory, so it shows what the planes hold. 320x200 at 8 bpp goes
# to VGA mode 13h; 640x480 at 16 bpp to a VBE mode, the pattern shown
# through the palette. WINDOWED gives the desktop back. 86Box's S3 card
# has VBE modes of exactly 320x240 and 320x200 at 8 bpp, which win over
# the VGA modes there; DOSBox's has neither, so it runs Mode X and 13h.
# LOG(dosbox): Video: VGA Mode X (planar, two pages)
# LOG(dosbox): Video: VGA mode 13h
# LOG: Video: full screen 320x240 at 8 bpp
# LOG: Video: full screen 320x200 at 8 bpp
# LOG: Video: full screen 640x480 at 16 bpp
TIMEOUT 60
WAITWIN "Program Manager"
MOVE 639 479
FULLSCREEN 320 240 8
WAIT 5
SHOT fm-modex
FULLSCREEN 320 200 8
WAIT 5
SHOT fm-13h
FULLSCREEN 640 480 16
WAIT 5
SHOT fm-vbe
WINDOWED
WAIT 10
ASSERTNOWIN "Full Screen Test"
SHOT fm-desk
QUIT

View file

@ -0,0 +1,50 @@
# fullscr.scr -- a game on the whole screen while another program runs on.
#
# TEST\TICKER.APP (fixtures/ticker) counts to 100 in its caption, ten a
# second. The SDK's C game template starts, pauses (so nothing moves) and
# takes the whole screen with Alt+Enter at 320x200 in 256 colours. While
# the game has the screen the ticker must reach "Counted 100" (it ran on in
# the background) and the screenshot is the game's own screen (the coin's
# glow is masked). Alt+Tab gives the desktop back (the game hears
# "suspended"): the desktop, at 256 colours, must look right again (its
# palette restored). Shift+Alt+Tab brings the game back to the front, and
# with it the whole screen; Alt+Enter goes back to the window, and Alt+F4
# ends the game.
# SDK-GAMES
# INI: video bpp 8
# LOG: sdkGame: started, 320x200 at 1x on a 8-bit desktop
# LOG: sdkGame: full screen on
# LOG: sdkGame: full screen suspended
# LOG: sdkGame: full screen on
# LOG: sdkGame: full screen off
# LOG: sdkGame: ended
TIMEOUT 120
WAITWIN "Program Manager"
MOVE 639 479
LAUNCH TEST/TICKER.APP
WAITWIN "Counting *"
LAUNCH apps/sdktest/sdkGame/sdkGame.app
WAITLOG "sdkGame: started"
WAITWIN "SdkGame - Score 0 - Time *"
KEY p
WAITWIN "SdkGame - Score 0 - Paused"
KEY Alt+Enter
WAITLOG "sdkGame: full screen on"
ASSERTWIN "Counting *"
WAITWIN "Counted 100"
SHOT fs-game
KEY Alt+Tab
WAITLOG "sdkGame: full screen suspended"
WAIT 20
SHOT fs-desk
KEY Shift+Alt+Tab
WAITLOG "sdkGame: full screen on"
WAIT 10
KEY Alt+Enter
WAITLOG "sdkGame: full screen off"
WAIT 10
KEY Alt+F4
WAITLOG "sdkGame: ended"
WAITNOWIN "SdkGame*"
ASSERTWIN "Counted 100"
QUIT

View file

@ -48,6 +48,8 @@ typedef struct {
int32_t txLen;
int32_t base;
int32_t irq;
bool dtr;
bool rts;
} StubPortT;
typedef struct {
@ -69,13 +71,20 @@ const FrtStubApiT *frtStubApi(void);
int32_t rs232ClearRxBuffer(int32_t com);
int32_t rs232Close(int32_t com);
int32_t rs232GetBase(int32_t com);
int32_t rs232GetCts(int32_t com);
int32_t rs232GetDcd(int32_t com);
int32_t rs232GetDsr(int32_t com);
int32_t rs232GetDtr(int32_t com);
int32_t rs232GetIrq(int32_t com);
int32_t rs232GetRts(int32_t com);
int32_t rs232GetRxBuffered(int32_t com);
int32_t rs232GetUartType(int32_t com);
int32_t rs232Open(int32_t com, int32_t bps, int32_t dataBits, char parity, int32_t stopBits, int32_t handshake);
int32_t rs232Read(int32_t com, char *data, int32_t len);
int32_t rs232SetBase(int32_t com, int32_t base);
int32_t rs232SetDtr(int32_t com, bool dtr);
int32_t rs232SetIrq(int32_t com, int32_t irq);
int32_t rs232SetRts(int32_t com, bool rts);
int32_t rs232Write(int32_t com, const char *data, int32_t len);
int32_t rs232WriteBuf(int32_t com, const char *data, int32_t len);
void secLinkClose(void *link);
@ -103,6 +112,7 @@ static const char *stubHlpcLastOutput(void);
static void stubHlpcRc(int32_t rc);
static int32_t stubIdleCount(void);
static void stubIdleRun(void);
static int32_t stubLine(int32_t com, bool up);
static void stubReset(void);
static void stubSerFail(bool openFails, bool writeFails);
static bool stubSerIsOpen(int32_t com);
@ -203,6 +213,28 @@ int32_t rs232GetBase(int32_t com) {
}
// The stub modem: CTS and DSR up, no carrier. Lines of a closed port
// read as the rs232 "not open" error.
int32_t rs232GetCts(int32_t com) {
return stubLine(com, true);
}
int32_t rs232GetDcd(int32_t com) {
return stubLine(com, false);
}
int32_t rs232GetDsr(int32_t com) {
return stubLine(com, true);
}
int32_t rs232GetDtr(int32_t com) {
return stubLine(com, com >= 0 && com < FRT_STUB_PORTS && sStubPorts[com].dtr);
}
int32_t rs232GetIrq(int32_t com) {
if (com < 0 || com >= FRT_STUB_PORTS) {
return 0;
@ -212,6 +244,11 @@ int32_t rs232GetIrq(int32_t com) {
}
int32_t rs232GetRts(int32_t com) {
return stubLine(com, com >= 0 && com < FRT_STUB_PORTS && sStubPorts[com].rts);
}
int32_t rs232GetRxBuffered(int32_t com) {
if (com < 0 || com >= FRT_STUB_PORTS) {
return 0;
@ -239,6 +276,8 @@ int32_t rs232Open(int32_t com, int32_t bps, int32_t dataBits, char parity, int32
}
sStubPorts[com].open = true;
sStubPorts[com].dtr = true;
sStubPorts[com].rts = true;
return 0;
}
@ -267,6 +306,16 @@ int32_t rs232SetBase(int32_t com, int32_t base) {
}
int32_t rs232SetDtr(int32_t com, bool dtr) {
if (com < 0 || com >= FRT_STUB_PORTS || !sStubPorts[com].open) {
return -1;
}
sStubPorts[com].dtr = dtr;
return 0;
}
int32_t rs232SetIrq(int32_t com, int32_t irq) {
if (com >= 0 && com < FRT_STUB_PORTS) {
sStubPorts[com].irq = irq;
@ -276,6 +325,16 @@ int32_t rs232SetIrq(int32_t com, int32_t irq) {
}
int32_t rs232SetRts(int32_t com, bool rts) {
if (com < 0 || com >= FRT_STUB_PORTS || !sStubPorts[com].open) {
return -1;
}
sStubPorts[com].rts = rts;
return 0;
}
int32_t rs232Write(int32_t com, const char *data, int32_t len) {
return rs232WriteBuf(com, data, len);
}
@ -546,6 +605,16 @@ static void stubIdleRun(void) {
}
// A modem line of an open stub port (1 up, 0 down), -1 when not open.
static int32_t stubLine(int32_t com, bool up) {
if (com < 0 || com >= FRT_STUB_PORTS || !sStubPorts[com].open) {
return -1;
}
return up ? 1 : 0;
}
static void stubReset(void) {
memset(sStubPorts, 0, sizeof(sStubPorts));
memset(sStubLinks, 0, sizeof(sStubLinks));

View file

@ -152,12 +152,19 @@ void SerClose(int32_t com);
void SerDetach(int32_t com);
void SerFlush(int32_t com);
int32_t SerGetBase(int32_t com);
int32_t SerGetCts(int32_t com);
int32_t SerGetDcd(int32_t com);
int32_t SerGetDsr(int32_t com);
int32_t SerGetDtr(int32_t com);
int32_t SerGetIrq(int32_t com);
int32_t SerGetRts(int32_t com);
int32_t SerGetUart(int32_t com);
int32_t SerOpen(int32_t com, int32_t baud, int32_t dataBits, const char *parity, int32_t stopBits, int32_t handshake);
const char *SerRead(int32_t com);
void SerSetBase(int32_t com, int32_t base);
void SerSetDtr(int32_t com, int32_t on);
void SerSetIrq(int32_t com, int32_t irq);
void SerSetRts(int32_t com, int32_t on);
int32_t SerWrite(int32_t com, const char *data);
int32_t SQLAffected(int32_t db);
void SQLClose(int32_t db);
@ -4366,6 +4373,22 @@ static void caseSerialLoopback(void) {
TEST_EQ(SerGetBase(1), 0x2F8);
SerSetIrq(1, 3);
TEST_EQ(SerGetIrq(1), 3);
// Modem lines: DTR/RTS up after the open, settable; CTS/DSR up and no
// carrier on the stub modem; BASIC True is -1.
TEST_EQ(SerGetDtr(1), -1);
TEST_EQ(SerGetRts(1), -1);
TEST_EQ(SerGetCts(1), -1);
TEST_EQ(SerGetDsr(1), -1);
TEST_EQ(SerGetDcd(1), 0);
SerSetDtr(1, 0);
SerSetRts(1, 0);
TEST_EQ(SerGetDtr(1), 0);
TEST_EQ(SerGetRts(1), 0);
SerSetDtr(1, -1);
TEST_EQ(SerGetDtr(1), -1);
TEST_EQ(SerGetCts(4), 0);
SerSetRts(4, -1);
SerAttach(9, "Term1");
SerAttach(1, "NoSuchTerm");

View file

@ -328,7 +328,11 @@ static void caseParseErrors(void) {
{ "OK\n", 1 },
{ "WAIT 1\nWAIT 1\nDRAG 1 2 3", 3 },
{ "WAITEXIT \"x\"", 1 },
{ "WAITEXIT 1 2", 1 }
{ "WAITEXIT 1 2", 1 },
{ "FULLSCREEN 320 240", 1 },
{ "FULLSCREEN 0 240 8", 1 },
{ "FULLSCREEN 320 240 24", 1 },
{ "WINDOWED 1", 1 }
};
tCase("parse errors");
@ -353,6 +357,26 @@ static void caseParseErrors(void) {
}
static void caseParseFullScreen(void) {
ScriptT s;
tCase("parse FULLSCREEN and WINDOWED");
if (!TEST_TRUE(scriptParse(&s, "FULLSCREEN 320 240 8\nFULLSCREEN 640 480 0\nWINDOWED", ""))) {
return;
}
TEST_EQ(s.count, 3);
TEST_EQ(s.steps[0].op, ScriptOpFullScreenE);
TEST_EQ(s.steps[0].from.x, 320);
TEST_EQ(s.steps[0].from.y, 240);
TEST_EQ(s.steps[0].count, 8);
TEST_EQ(s.steps[1].count, 0);
TEST_EQ(s.steps[2].op, ScriptOpWindowedE);
scriptFree(&s);
}
// KEYDOWN / KEYUP names, and the keys going into the keyboard hook's
// decoder (held-key table); CLOCKMARK sends a CLOCK line to the host.
static void caseRawKeys(void) {
@ -764,6 +788,7 @@ int main(void) {
caseKeyNames();
caseParseCommands();
caseParseErrors();
caseParseFullScreen();
caseRawKeys();
caseRunnerFailures();
caseRunnerInput();

View file

@ -65,6 +65,7 @@ typedef struct {
bool open;
bool dcd; // carrier detect as the test set it
bool breakOn; // line held in the break state
bool dtr; // DTR as the library set it (kept by rs232Close, like the real driver)
int32_t writeCap; // max bytes accepted per rs232Write (LOOP_CAP_NONE = all)
uint8_t rx[RX_QUEUE_SIZE];
uint32_t rxHead;
@ -156,6 +157,11 @@ bool loopGetBreak(int com) {
}
bool loopGetDtr(int com) {
return portValid(com) && sPorts[com].dtr;
}
void loopGetStats(int com, LoopStatsT *stats) {
if (!portValid(com) || !stats) {
return;
@ -349,6 +355,7 @@ int rs232Open(int com, int32_t bps, int dataBits, char parity, int stopBits, int
}
sPorts[com].open = true;
sPorts[com].breakOn = false;
sPorts[com].dtr = true;
sPorts[com].rxHead = 0;
sPorts[com].rxTail = 0;
sPorts[com].rxCount = 0;
@ -406,6 +413,18 @@ int32_t rs232SetBreak(int32_t com, bool on) {
}
int32_t rs232SetDtr(int32_t com, bool dtr) {
if (!portValid(com)) {
return RS232_ERR_INVALID_PORT;
}
if (!sPorts[com].open) {
return RS232_ERR_NOT_OPEN;
}
sPorts[com].dtr = dtr;
return RS232_SUCCESS;
}
int rs232Write(int com, const char *data, int len) {
if (!portValid(com)) {
return RS232_ERR_INVALID_PORT;

View file

@ -91,6 +91,7 @@ typedef void (*LoopIdleHookT)(void *ctx);
clock_t loopClock(void);
void loopClockAdvance(clock_t ticks);
bool loopGetBreak(int com);
bool loopGetDtr(int com);
void loopGetStats(int com, LoopStatsT *stats);
bool loopIsOpen(int com);
int loopPeer(int com);

View file

@ -286,6 +286,7 @@ static void caseModem(void) {
connClose(c);
TEST_TRUE("modem hung up", strcmp(connStatus(c), "Hung up") == 0);
TEST_TRUE("modem hangup closes port", !loopIsOpen(LOCAL_COM));
TEST_TRUE("modem hangup drops DTR", !loopGetDtr(LOCAL_COM));
connDestroy(c);
}

View file

@ -104,6 +104,7 @@ int main(void);
int32_t rs232GetDcd(int32_t com);
int32_t rs232GetTxBuffered(int32_t com);
int32_t rs232SetBreak(int32_t com, bool on);
int32_t rs232SetDtr(int32_t com, bool dtr);
int32_t rs232WriteBuf(int32_t com, const char *data, int32_t len);
@ -520,6 +521,14 @@ int32_t rs232SetBreak(int32_t com, bool on) {
}
// A socket has no DTR line.
int32_t rs232SetDtr(int32_t com, bool dtr) {
(void)com;
(void)dtr;
return RS232_SUCCESS;
}
int32_t rs232WriteBuf(int32_t com, const char *data, int32_t len) {
return rs232Write(com, data, len);
}

View file

@ -37,6 +37,7 @@ BINDIR = $(DVXTEST_BINDIR)/wm
SUITE = $(BINDIR)/wmSuite
SUITE2 = $(BINDIR)/wmSuite2
SUITE3 = $(BINDIR)/wmFixes
SUITE4 = $(BINDIR)/wmExcl
.PHONY: all run clean
@ -47,12 +48,13 @@ $(BINDIR)/%: %.c $(DVX_MAKEFILES) $(DVXTEST_ARCHIVES) | dvxtest-libs
@mkdir -p $(BINDIR)
$(HOSTCC) $(DVXTEST_CFLAGS) -o $@ $< $(DVXTEST_LINK)
-include $(SUITE).d $(SUITE2).d $(SUITE3).d
-include $(SUITE).d $(SUITE2).d $(SUITE3).d $(SUITE4).d
run: $(SUITE) $(SUITE2) $(SUITE3)
run: $(SUITE) $(SUITE2) $(SUITE3) $(SUITE4)
cd $(BINDIR) && ASAN_OPTIONS=detect_leaks=1:allocator_may_return_null=1 ./wmSuite
cd $(BINDIR) && ASAN_OPTIONS=detect_leaks=1:allocator_may_return_null=1 ./wmSuite2
cd $(BINDIR) && ASAN_OPTIONS=detect_leaks=1:allocator_may_return_null=1 ./wmFixes
cd $(BINDIR) && ASAN_OPTIONS=detect_leaks=1:allocator_may_return_null=1 ./wmExcl
clean:
rm -rf $(BINDIR)

481
src/test/wm/wmExcl.c Normal file
View file

@ -0,0 +1,481 @@
// The MIT License (MIT)
//
// Copyright (C) 2026 Scott Duensing
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to
// deal in the Software without restriction, including without limitation the
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
// sell copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
// IN THE SOFTWARE.
// wmExcl.c -- full-screen exclusive mode (dvxExclusive*)
//
// Taking and giving back the screen, suspending when the owner loses the
// focus and resuming when it gets it back, other windows painting on
// while the desktop is not drawn, input going to the owner, the 8-bit
// picture scaled through the palette, screenshots of the full screen,
// and the failure paths (busy, bad arguments, no mode, no mode again on
// resuming, the owner closing).
#include "dvxtest.h"
#include "dvxPlat.h"
#include "dvxPlatHost.h"
#include <inttypes.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#define SCREEN_W 640
#define SCREEN_H 480
#define SCREEN_BPP 32
#define WIN_A_X 40
#define WIN_A_Y 40
#define WIN_B_X 300
#define WIN_B_Y 200
#define WIN_W 200
#define WIN_H 150
#define FULL_W 320
#define FULL_H 200
#define BPP_8 8
#define BPP_16 16
#define PIC_W 160 // a quarter-size picture: scaled by 2
#define PIC_H 100
#define SQUARE 100 // a square picture: scaled by 2, centred
#define MOUSE_X 50
#define MOUSE_Y 60
#define PAL_TEST_INDEX 7
#define PAL_TEST_R 200
#define PAL_TEST_G 100
#define PAL_TEST_B 50
#define SETTLE_FRAMES 3
#define SHOT_FILE "wmExcl.png"
#define SCAN_X 0x2D
#define ASCII_X 'x'
static int32_t sActiveCount = 0;
static int32_t sSuspendedCount = 0;
static int32_t sEndedCount = 0;
static int32_t sMouseCount = 0;
static int32_t sMouseX = -1;
static int32_t sMouseY = -1;
static int32_t sKeyCount = 0;
static int32_t sLastKey = 0;
static int32_t sPaintCount = 0;
static void caseBeginEnd(void);
static void caseErrors(void);
static void caseInput(void);
static void caseOthersRunOn(void);
static void casePresentScaled(void);
static void caseResumeFails(void);
static void caseSuspendResume(void);
static void countsReset(void);
static void onChange(WindowT *win, ExclusiveStateE state, void *userData);
static void onKey(WindowT *win, int32_t key, int32_t mod);
static void onMouse(WindowT *win, int32_t x, int32_t y, int32_t buttons);
static void onPaintCount(WindowT *win, RectT *dirty);
static WindowT *ownerCreate(void);
// Taking the screen: the mode asked for, the desktop left as it was, the
// mouse in the middle; giving it back restores both, without a callback.
static void caseBeginEnd(void) {
tCase("dvxExclusiveBegin takes the screen, dvxExclusiveEnd gives it back");
tInit(SCREEN_W, SCREEN_H, SCREEN_BPP);
countsReset();
AppContextT *ctx = tCtx();
WindowT *a = ownerCreate();
tPump(SETTLE_FRAMES);
tMouseMove(MOUSE_X, MOUSE_Y);
tPump(1);
TEST_EQ(dvxExclusiveBegin(ctx, a, FULL_W, FULL_H, BPP_8, onChange, NULL), DVX_EXCL_OK);
TEST_EQ(sActiveCount, 1);
DisplayT *d = dvxExclusiveDisplay(ctx);
TEST_TRUE(d != NULL);
TEST_TRUE(dvxExclusiveBlitOps(ctx) != NULL);
TEST_EQ(d->width, FULL_W);
TEST_EQ(d->height, FULL_H);
TEST_EQ(d->format.bitsPerPixel, BPP_8);
TEST_EQ(ctx->display.width, SCREEN_W);
TEST_EQ(ctx->display.height, SCREEN_H);
TEST_EQ(ctx->mouseX, FULL_W / 2);
TEST_EQ(ctx->mouseY, FULL_H / 2);
TEST_EQ(dvxChangeVideoMode(ctx, SCREEN_W, SCREEN_H, BPP_16), -1);
// A screenshot is of the full screen.
char path[DVX_MAX_PATH];
snprintf(path, sizeof(path), "%s/%s", tTempDir(), SHOT_FILE);
TEST_EQ(dvxScreenshot(ctx, path), 0);
struct stat st;
TEST_TRUE(stat(path, &st) == 0 && st.st_size > 0);
tPump(SETTLE_FRAMES);
TEST_EQ(sSuspendedCount, 0);
dvxExclusiveEnd(ctx, a);
TEST_TRUE(dvxExclusiveDisplay(ctx) == NULL);
TEST_TRUE(dvxExclusiveBlitOps(ctx) == NULL);
TEST_TRUE(ctx->exclusive.owner == NULL);
TEST_EQ(ctx->mouseX, MOUSE_X);
TEST_EQ(ctx->mouseY, MOUSE_Y);
TEST_EQ(ctx->dirty.count, 1);
TEST_EQ(sSuspendedCount + sEndedCount, 0);
tPump(SETTLE_FRAMES);
tShutdown();
}
// Busy, bad arguments, no mode; closing the owner gives the screen back.
static void caseErrors(void) {
tCase("dvxExclusiveBegin errors, and the owner closing");
tInit(SCREEN_W, SCREEN_H, SCREEN_BPP);
countsReset();
AppContextT *ctx = tCtx();
WindowT *a = ownerCreate();
WindowT *b = dvxCreateWindow(ctx, "B", WIN_B_X, WIN_B_Y, WIN_W, WIN_H, false);
tPump(SETTLE_FRAMES);
TEST_EQ(dvxExclusiveBegin(ctx, NULL, FULL_W, FULL_H, BPP_8, onChange, NULL), DVX_EXCL_ERR_ARGS);
TEST_EQ(dvxExclusiveBegin(ctx, a, 0, FULL_H, BPP_8, onChange, NULL), DVX_EXCL_ERR_ARGS);
hostVideoFailNext(1);
TEST_EQ(dvxExclusiveBegin(ctx, a, FULL_W, FULL_H, BPP_8, onChange, NULL), DVX_EXCL_ERR_MODE);
TEST_TRUE(ctx->exclusive.owner == NULL);
TEST_EQ(sActiveCount, 0);
TEST_EQ(dvxExclusiveBegin(ctx, a, FULL_W, FULL_H, BPP_8, onChange, NULL), DVX_EXCL_OK);
TEST_EQ(dvxExclusiveBegin(ctx, b, FULL_W, FULL_H, BPP_8, onChange, NULL), DVX_EXCL_ERR_BUSY);
// The owner asking again gets the new size.
TEST_EQ(dvxExclusiveBegin(ctx, a, FULL_W * 2, FULL_H * 2, BPP_16, onChange, NULL), DVX_EXCL_OK);
TEST_EQ(dvxExclusiveDisplay(ctx)->width, FULL_W * 2);
TEST_EQ(dvxExclusiveDisplay(ctx)->format.bitsPerPixel, BPP_16);
dvxExclusiveEnd(ctx, b);
TEST_TRUE(dvxExclusiveDisplay(ctx) != NULL);
dvxDestroyWindow(ctx, a);
tPump(SETTLE_FRAMES);
TEST_TRUE(dvxExclusiveDisplay(ctx) == NULL);
TEST_TRUE(ctx->exclusive.owner == NULL);
TEST_EQ(sSuspendedCount + sEndedCount, 0);
tShutdown();
}
// Mouse changes reach the owner in full-screen coordinates; keys reach it
// as in game input mode although it did not ask for that.
static void caseInput(void) {
tCase("full screen: mouse and keys go to the owner");
tInit(SCREEN_W, SCREEN_H, SCREEN_BPP);
countsReset();
AppContextT *ctx = tCtx();
WindowT *a = ownerCreate();
tPump(SETTLE_FRAMES);
TEST_EQ(dvxExclusiveBegin(ctx, a, FULL_W, FULL_H, BPP_8, onChange, NULL), DVX_EXCL_OK);
TEST_TRUE(!a->gameInput);
tMouseMove(MOUSE_X, MOUSE_Y);
tPump(1);
TEST_TRUE(sMouseCount > 0);
TEST_EQ(sMouseX, MOUSE_X);
TEST_EQ(sMouseY, MOUSE_Y);
tKey(SCAN_X, ASCII_X, 0);
tPump(1);
TEST_EQ(sKeyCount, 1);
TEST_EQ(sLastKey, ASCII_X);
// F10 would open a menu nobody can see: it goes to the owner too.
tFKey(10);
tPump(1);
TEST_EQ(sKeyCount, 2);
TEST_TRUE(!ctx->popup.active);
dvxExclusiveEnd(ctx, a);
tShutdown();
}
// Other windows paint on into their own buffers; the desktop's screen is
// not drawn until the owner lets go, then all of it is.
static void caseOthersRunOn(void) {
tCase("full screen: other windows paint on, the desktop waits");
tInit(SCREEN_W, SCREEN_H, SCREEN_BPP);
countsReset();
AppContextT *ctx = tCtx();
WindowT *b = dvxCreateWindow(ctx, "B", WIN_B_X, WIN_B_Y, WIN_W, WIN_H, false);
WindowT *a = ownerCreate();
b->onPaint = onPaintCount;
tPump(SETTLE_FRAMES);
size_t screenBytes = (size_t)ctx->display.pitch * (size_t)ctx->display.height;
const uint8_t *screen = hostFramebuffer(&ctx->display);
uint8_t *before = (uint8_t *)malloc(screenBytes);
TEST_TRUE(before != NULL);
memcpy(before, screen, screenBytes);
TEST_EQ(dvxExclusiveBegin(ctx, a, FULL_W, FULL_H, BPP_8, onChange, NULL), DVX_EXCL_OK);
sPaintCount = 0;
dvxInvalidateWindow(ctx, b);
dvxSetTitle(ctx, b, "B busy");
tPump(SETTLE_FRAMES);
TEST_TRUE(sPaintCount > 0);
TEST_EQ(ctx->dirty.count, 0);
TEST_TRUE(memcmp(before, screen, screenBytes) == 0);
dvxExclusiveEnd(ctx, a);
tPump(1);
TEST_TRUE(memcmp(before, screen, screenBytes) != 0);
TEST_TRUE(memcmp(ctx->display.backBuf, screen, screenBytes) == 0);
free(before);
tShutdown();
}
// An 8-bit picture: whole-factor scaled and centred on black, through the
// palette at 16 bpp, straight at 8 bpp.
static void casePresentScaled(void) {
tCase("dvxExclusivePresent8 scales, centres and converts");
tInit(SCREEN_W, SCREEN_H, SCREEN_BPP);
countsReset();
AppContextT *ctx = tCtx();
WindowT *a = ownerCreate();
uint8_t pal[PAL_ENTRIES * PAL_CHANNELS];
uint8_t pic[PIC_W * PIC_H];
memset(pal, 0, sizeof(pal));
pal[PAL_TEST_INDEX * PAL_CHANNELS] = PAL_TEST_R;
pal[PAL_TEST_INDEX * PAL_CHANNELS + 1] = PAL_TEST_G;
pal[PAL_TEST_INDEX * PAL_CHANNELS + 2] = PAL_TEST_B;
memset(pic, PAL_TEST_INDEX, sizeof(pic));
tPump(SETTLE_FRAMES);
TEST_TRUE(!dvxExclusivePresent8(ctx, pic, PIC_W, PIC_H, PIC_W));
// 16 bpp: a quarter-size picture fills the screen at twice the size.
TEST_EQ(dvxExclusiveBegin(ctx, a, FULL_W, FULL_H, BPP_16, onChange, NULL), DVX_EXCL_OK);
dvxExclusiveSetPalette(ctx, pal, 0, PAL_ENTRIES);
TEST_TRUE(dvxExclusivePresent8(ctx, pic, PIC_W, PIC_H, PIC_W));
DisplayT *d = dvxExclusiveDisplay(ctx);
uint16_t want = (uint16_t)packColor(d, PAL_TEST_R, PAL_TEST_G, PAL_TEST_B);
uint16_t last = ((const uint16_t *)(d->backBuf + (size_t)(FULL_H - 1) * (size_t)d->pitch))[FULL_W - 1];
TEST_EQ(((const uint16_t *)d->backBuf)[0], want);
TEST_EQ(last, want);
TEST_TRUE(memcmp(d->backBuf, hostFramebuffer(d), (size_t)d->pitch * (size_t)d->height) == 0);
// A square picture: twice the size, centred, black either side.
TEST_TRUE(dvxExclusivePresent8(ctx, pic, SQUARE, SQUARE, PIC_W));
int32_t left = (FULL_W - SQUARE * 2) / 2;
TEST_EQ(((const uint16_t *)d->backBuf)[left - 1], 0);
TEST_EQ(((const uint16_t *)d->backBuf)[left], want);
TEST_EQ(((const uint16_t *)d->backBuf)[left + SQUARE * 2 - 1], want);
TEST_EQ(((const uint16_t *)d->backBuf)[left + SQUARE * 2], 0);
// 8 bpp: the indexes themselves, the palette in the DAC.
TEST_EQ(dvxExclusiveBegin(ctx, a, FULL_W, FULL_H, BPP_8, onChange, NULL), DVX_EXCL_OK);
d = dvxExclusiveDisplay(ctx);
TEST_TRUE(dvxExclusivePresent8(ctx, pic, PIC_W, PIC_H, PIC_W));
TEST_EQ(d->backBuf[0], PAL_TEST_INDEX);
TEST_EQ(d->palette[PAL_TEST_INDEX * PAL_CHANNELS], PAL_TEST_R);
dvxExclusiveEnd(ctx, a);
tShutdown();
}
// Losing the focus suspends; when the mode cannot be set again on the
// way back, full screen ends and the owner hears so.
static void caseResumeFails(void) {
tCase("full screen ends when the mode cannot be set again");
tInit(SCREEN_W, SCREEN_H, SCREEN_BPP);
countsReset();
AppContextT *ctx = tCtx();
WindowT *b = dvxCreateWindow(ctx, "B", WIN_B_X, WIN_B_Y, WIN_W, WIN_H, false);
WindowT *a = ownerCreate();
tPump(SETTLE_FRAMES);
TEST_EQ(dvxExclusiveBegin(ctx, a, FULL_W, FULL_H, BPP_8, onChange, NULL), DVX_EXCL_OK);
dvxRaiseWindow(ctx, b);
tPump(1);
TEST_EQ(sSuspendedCount, 1);
hostVideoFailNext(1);
dvxRaiseWindow(ctx, a);
tPump(1);
TEST_EQ(sEndedCount, 1);
TEST_TRUE(ctx->exclusive.owner == NULL);
TEST_TRUE(dvxExclusiveDisplay(ctx) == NULL);
TEST_EQ(ctx->display.width, SCREEN_W);
tShutdown();
}
// Alt+Tab away suspends (the desktop is drawn again); coming back sets the
// mode again and the owner redraws.
static void caseSuspendResume(void) {
tCase("full screen suspends on Alt+Tab and resumes on the focus");
tInit(SCREEN_W, SCREEN_H, SCREEN_BPP);
countsReset();
AppContextT *ctx = tCtx();
WindowT *b = dvxCreateWindow(ctx, "B", WIN_B_X, WIN_B_Y, WIN_W, WIN_H, false);
WindowT *a = ownerCreate();
tPump(SETTLE_FRAMES);
TEST_EQ(dvxExclusiveBegin(ctx, a, FULL_W, FULL_H, BPP_8, onChange, NULL), DVX_EXCL_OK);
tAltTab();
tPump(1);
TEST_EQ(sSuspendedCount, 1);
TEST_TRUE(dvxExclusiveDisplay(ctx) == NULL);
TEST_TRUE(ctx->exclusive.owner == a);
TEST_TRUE(!ctx->exclusive.active);
TEST_EQ(ctx->dirty.count, 0);
TEST_TRUE(memcmp(ctx->display.backBuf, hostFramebuffer(&ctx->display), (size_t)ctx->display.pitch * (size_t)ctx->display.height) == 0);
// Minimized, it stays suspended even with the focus nominally back.
dvxMinimizeWindow(ctx, a);
tPump(1);
TEST_EQ(sActiveCount, 1);
dvxRestoreWindow(ctx, a);
dvxRaiseWindow(ctx, a);
tPump(1);
TEST_EQ(sActiveCount, 2);
TEST_TRUE(dvxExclusiveDisplay(ctx) != NULL);
TEST_TRUE(b->visible);
dvxExclusiveEnd(ctx, a);
tShutdown();
}
static void countsReset(void) {
sActiveCount = 0;
sSuspendedCount = 0;
sEndedCount = 0;
sMouseCount = 0;
sMouseX = -1;
sMouseY = -1;
sKeyCount = 0;
sLastKey = 0;
sPaintCount = 0;
}
static void onChange(WindowT *win, ExclusiveStateE state, void *userData) {
(void)win;
(void)userData;
if (state == ExclusiveActiveE) {
sActiveCount++;
} else if (state == ExclusiveSuspendedE) {
sSuspendedCount++;
} else {
sEndedCount++;
}
}
static void onKey(WindowT *win, int32_t key, int32_t mod) {
(void)win;
(void)mod;
sKeyCount++;
sLastKey = key;
}
static void onMouse(WindowT *win, int32_t x, int32_t y, int32_t buttons) {
(void)win;
(void)buttons;
sMouseCount++;
sMouseX = x;
sMouseY = y;
}
static void onPaintCount(WindowT *win, RectT *dirty) {
(void)win;
(void)dirty;
sPaintCount++;
}
// The window that takes full screen (a menu bar, so F10 has a menu to
// open when it is not taken by full screen).
static WindowT *ownerCreate(void) {
AppContextT *ctx = tCtx();
WindowT *win = dvxCreateWindow(ctx, "Game", WIN_A_X, WIN_A_Y, WIN_W, WIN_H, false);
MenuBarT *bar = wmAddMenuBar(win);
MenuT *m = wmAddMenu(bar, "&Game");
wmAddMenuItem(m, "&New", 1);
win->onKey = onKey;
win->onMouse = onMouse;
return win;
}
int main(void) {
platformSetLogPath("wmExcl.log");
tSuiteBegin("wm suite (full-screen exclusive)");
int32_t loaded = tProcessInit(DVXTEST_WIDGET_DIR);
printf("widgets loaded: %" PRId32 "\n", loaded);
TEST_TRUE(loaded > 0);
caseBeginEnd();
caseErrors();
caseInput();
caseOthersRunOn();
casePresentScaled();
caseResumeFails();
caseSuspendResume();
return tSuiteEnd();
}