diff --git a/Makefile b/Makefile
index 1a1f1933..b28feaf3 100644
--- a/Makefile
+++ b/Makefile
@@ -231,7 +231,7 @@ deploy-sdk: tools apps installer
@echo "Building SDK..."
@# Start from an empty tree so a header dropped from the lists goes away.
rm -rf $(SDKDIR)
- @mkdir -p $(SDKDIR)/bin $(SDKDIR)/include/core $(SDKDIR)/include/shell $(SDKDIR)/include/tasks $(SDKDIR)/include/sql $(SDKDIR)/include/texthelp $(SDKDIR)/include/listhelp $(SDKDIR)/include/richtext $(SDKDIR)/include/fileops $(SDKDIR)/include/hlpfile $(SDKDIR)/include/xfer $(SDKDIR)/include/conn $(SDKDIR)/include/print $(SDKDIR)/include/game $(SDKDIR)/include/joystick $(SDKDIR)/include/audio $(SDKDIR)/include/music $(SDKDIR)/include/pkg $(SDKDIR)/include/json $(SDKDIR)/include/http $(SDKDIR)/include/pkgnet $(SDKDIR)/include/net $(SDKDIR)/include/basic $(SDKDIR)/samples/basic/basdemo $(SDKDIR)/samples/basic/widshow
+ @mkdir -p $(SDKDIR)/bin $(SDKDIR)/include/core $(SDKDIR)/include/shell $(SDKDIR)/include/tasks $(SDKDIR)/include/sql $(SDKDIR)/include/serial $(SDKDIR)/include/crypto $(SDKDIR)/include/seclink $(SDKDIR)/include/texthelp $(SDKDIR)/include/listhelp $(SDKDIR)/include/richtext $(SDKDIR)/include/fileops $(SDKDIR)/include/hlpfile $(SDKDIR)/include/xfer $(SDKDIR)/include/conn $(SDKDIR)/include/print $(SDKDIR)/include/game $(SDKDIR)/include/joystick $(SDKDIR)/include/audio $(SDKDIR)/include/music $(SDKDIR)/include/pkg $(SDKDIR)/include/json $(SDKDIR)/include/http $(SDKDIR)/include/pkgnet $(SDKDIR)/include/net $(SDKDIR)/include/basic $(SDKDIR)/samples/basic/basdemo $(SDKDIR)/samples/basic/widshow
cp $(SDK_CORE_H) $(SDKDIR)/include/core/
cp $(SDK_SHELL_H) $(SDKDIR)/include/shell/
cp $(SDK_TASKS_H) $(SDKDIR)/include/tasks/
@@ -259,9 +259,9 @@ deploy-sdk: tools apps installer
@for h in $(SDK_NET_STD_H); do \
mkdir -p $(SDKDIR)/include/net/$$(dirname $$h) && cp $(SDK_NET_DIR)/$$h $(SDKDIR)/include/net/$$h || exit 1; \
done
- @for h in $(SDK_SERIAL_H); do \
- mkdir -p $(SDKDIR)/include/serial/$$(dirname $$h) && cp $(SDK_SERIAL_DIR)/$$h $(SDKDIR)/include/serial/$$h || exit 1; \
- done
+ cp $(SDK_SERIAL_H) $(SDKDIR)/include/serial/
+ cp $(SDK_CRYPTO_H) $(SDKDIR)/include/crypto/
+ cp $(SDK_SECLINK_H) $(SDKDIR)/include/seclink/
@# Widget headers -- one subdir per widget
@for h in $(SDK_WIDGET_H); do \
wgt=$$(basename $$(dirname $$h)); \
diff --git a/analyze.sh b/analyze.sh
index 4ef750db..eb1ba087 100755
--- a/analyze.sh
+++ b/analyze.sh
@@ -1,4 +1,26 @@
#!/bin/bash
+# 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.
+
#
# analyze.sh -- run the DJGPP build under gcc's -fanalyzer.
#
diff --git a/config/README.md b/config/README.md
index 794b58aa..b2eb3125 100644
--- a/config/README.md
+++ b/config/README.md
@@ -178,6 +178,8 @@ ignored. Names are case-insensitive.
| `dvxshell.dep` | dvxshell.lib | libtasks, libdvx, texthelp, listhelp |
| `basrt.dep` | basrt.lib | libtasks, libdvx |
| `serial.dep` | serial.lib | libtasks, libdvx |
+| `crypto.dep` | crypto.lib | libdvx |
+| `seclink.dep` | seclink.lib | crypto, serial, libdvx |
### Widget Dependencies
@@ -186,7 +188,7 @@ ignored. Names are case-insensitive.
| `textinpt.dep` | textinpt.wgt | texthelp |
| `combobox.dep` | combobox.wgt | texthelp, listhelp |
| `spinner.dep` | spinner.wgt | texthelp |
-| `terminal.dep` | terminal.wgt | texthelp |
+| `ansiterm.dep` | ansiterm.wgt | texthelp |
| `dropdown.dep` | dropdown.wgt | listhelp |
| `listbox.dep` | listbox.wgt | listhelp |
| `listview.dep` | listview.wgt | listhelp |
diff --git a/cppcheck.sh b/cppcheck.sh
index b46d613f..01ccf1d8 100755
--- a/cppcheck.sh
+++ b/cppcheck.sh
@@ -1,4 +1,26 @@
#!/bin/bash
+# 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.
+
#
# cppcheck.sh -- static analysis for the DVX source tree.
#
@@ -28,6 +50,8 @@ INCLUDES=(
-Isrc/libs/kpunch/dvxshell
-Isrc/libs/kpunch/taskmgr
-Isrc/libs/kpunch/serial
+ -Isrc/libs/kpunch/crypto
+ -Isrc/libs/kpunch/seclink
-Isrc/widgets/kpunch
-Isrc/apps/kpunch/dvxbasic
-Isrc/apps/kpunch/dvxbasic/compiler
diff --git a/docs/dvx_basic_reference.html b/docs/dvx_basic_reference.html
index 992a7e70..a192bb5e 100644
--- a/docs/dvx_basic_reference.html
+++ b/docs/dvx_basic_reference.html
@@ -196,6 +196,8 @@ img { max-width: 100%; }
@@ -1978,7 +2040,7 @@ End Sub
TreeView Hierarchical tree control.
VBox Vertical layout container.
WrapBox Flow layout container that wraps items to the next row.
-
Image and ImageButton controls are not in the Toolbox; they can still be written into a .frm file by hand and are then edited in the Properties panel like any other control.
+
A new Image or ImageButton starts empty, 32 by 32 pixels so it can be seen and selected: set its Picture property to show a picture.
@@ -3429,6 +3491,7 @@ ErrorHandler:
11 Division by zero
13 Type mismatch (text that is not a number where a number is needed) / not an array / not a TYPE instance
14 Out of string space (no memory to build a string)
+ 18 User interrupt occurred (a control's code ran too long without returning and was stopped)
20 RESUME without error
26 FOR loop nesting too deep
28 Out of stack space (calls or expressions nested too deeply)
@@ -3462,9 +3525,10 @@ ErrorHandler:
SHELL
Executes an operating-system command.
SHELL "command"
-
When used as a function, SHELL returns the exit code of the command.
+
When used as a function, SHELL returns the exit code of the command, or -1 when it did not run.
DVX steps aside while the command runs, as it does for a DOS program started from Program Manager: the screen goes back to text, and the command has the keyboard, the mouse, and the hardware to itself. Nothing of DVX runs meanwhile, so the other programs that are running are named first and asked to close (your program stays and waits); choosing No, or a program that keeps a window open, means the command does not run (-1). When the command ends, the desktop comes back and your program goes on. A command that prints and ends at once is gone from the screen as soon as it ends: ShellRunDos (shell.bas) can wait for a key first.
The command goes to DOS as it is written. DOS commands read / as the start of a switch (DIR /B), so write the paths in a command with \ (SHELL "DEL C:\TEMP\X.TXT"); FileOpsPathNative$ (fileops.bas) turns a path built with / into that form. Everywhere else in DVX BASIC a path may use / or \.
SLEEP
Pauses execution for the given number of seconds. With no argument, sleeps for 1 second. The desktop and the program's own forms keep running while it sleeps, SLEEP 0 just lets pending events run, and closing the program ends the sleep.
@@ -3508,15 +3572,18 @@ PLAY "T160 O3 L8 E D C D E E E4"
MF Music foreground: PLAY, SOUND, and BEEP wait until the sound has finished (the default).
MB Music background: the program goes on while the music plays; it only waits when more than 32 notes are waiting.
I n DVX's own: play the notes that follow on General MIDI instrument n (1 to 128; 1 is the piano, 41 the violin, 74 the flute) through the music synthesizer. I0 goes back to plain tones (the default).
+ V n Volume of the notes that follow, 0 to 100 percent (default 100), as in QB64. On an instrument it sets how hard the note is played.
+ @ n Waveform of the tones that follow, as in QB64: @1 square (the default, like the PC speaker), @3 triangle (softer), @5 noise (drums, explosions). QB64's @2 and @4 are not available.
X "X" + VARPTR$(s$) plays the commands in s$ at that point (s$ may use X itself, up to 16 levels deep).
= Any number above may be written as "=" + VARPTR$(n), n a numeric expression: "T=" + VARPTR$(tempo).
A string with a mistake in it plays nothing and raises error 5 (Illegal function call). X and "=" work as in QBasic, with VARPTR$ as for DRAW (see the program screen's DRAW): the value is taken when VARPTR$ runs. A whole tune may be at most 32767 characters once X and "=" are filled in.
verse$ = "L8 E D C D E E E4"
tempo = 160
PLAY "T=" + VARPTR$(tempo) + "O3 X" + VARPTR$(verse$) + "X" + VARPTR$(verse$)
-
Building the string with + works too, such as PLAY "O" + STR$(n). Without I, the notes are square-wave tones like the PC speaker's; with it, they play on the same synthesizer as MIDI music.
+
Building the string with + works too, such as PLAY "O" + STR$(n). Without I, the notes are tones -- square waves like the PC speaker's unless @ chooses another -- and with it, they play on the same synthesizer as MIDI music. The PC speaker plays only square tones at one volume: there V and @ make no difference, except that V0 is silent.
PLAY "MB T120 L4 CDEF GAB>C" ' a scale in the background
-PLAY "I74 O4 L8 GAB>C" ' on the flute
+PLAY "I74 O4 L8 GAB>C" ' on the flute
+PLAY "@3 V60 L8 CEG>C" ' softer, quieter
PLAY Function
PLAY(n) returns how many notes are still waiting to be played in the background (n is ignored). A game can use it to keep its music going:
If PLAY(0) < 4 Then PLAY "MB " + tune$
@@ -3582,7 +3649,9 @@ Loop
FULLSCREEN
Shows the program's screen on the whole screen, or back in its window. Alt+Enter in the window does the same. While the program has the whole screen, Alt+Tab brings the desktop back (the program keeps running in its window) and switching back to the program returns it to the whole screen. Other programs keep running either way. When the computer has no suitable video mode, the screen stays in its window.
+FULLSCREEN expression ' True: whole screen, False: window
+isFull = FULLSCREEN ' True while the program's screen has the whole screen
+
Used as a value, FULLSCREEN tells whether the program's screen has the whole screen (also while the desktop is shown for another program, since the screen comes back with the focus): False when no suitable video mode could be set, or before SCREEN or any drawing made a screen. A form takes the whole screen with its FullScreen property instead.
Text: CLS, LOCATE, COLOR, CSRLIN, POS, SCREEN( )
CLS [n]
LOCATE [row] [, [column] [, cursor]]
@@ -3824,7 +3893,7 @@ pos = SEEK(channel) ' Function: get current position
NAME oldName$ AS newName$
Name "draft.txt" As "final.txt"
FILECOPY
-
Copies a file. If the destination already exists it is overwritten.
+
Copies a file. If the destination already exists it is overwritten. While a big file copies, the rest of DVX and the program's own events go on, as with DoEvents (in a control's code they wait until the copy ends).
FILECOPY source$, destination$
FileCopy "data.db", "data.bak"
MKDIR / RMDIR
@@ -3857,7 +3926,7 @@ End If
SetAttr "secret.dat", vbHidden + vbReadOnly
CURDIR$ / DIR$
-
CURDIR$ returns the current directory. DIR$ returns the first file matching a pattern; subsequent calls with no arguments return additional matches (or "" when done). DIR$ returns only files: never ".", "..", or subdirectories. DIR$("C:\*.*") lists the root of C: (DIR$("C:*.*") the drive's current folder).
+
CURDIR$ returns the current directory. DIR$ returns the first file matching a pattern; subsequent calls with no arguments return additional matches (or "" when done). DIR$ returns only files: never ".", "..", or subdirectories. The pattern uses DOS's wildcards, as the file dialog does: * stands for any characters and ? for one, case does not matter, and *.* is every file, with or without an extension. DIR$("C:\*.*") lists the root of C: (DIR$("C:*.*") the drive's current folder). Looking through a big folder for a rare match lets the rest of DVX and the program's own events go on, as with DoEvents; if one of those events starts a new DIR$ listing, the call returns "" (in a control's code nothing else runs meanwhile).
Function Returns Description
-------- ------- -----------
ASC(s$) Integer ASCII code of the first character of s$ (error 5 for an empty string)
+ ASCW(s$) Long Unicode code point of the first character of s$ (error 5 for an empty string)
CHR$(n) String Character with ASCII code n
+ CHRW$(n) String Character for Unicode code point n ("?" when DVX has no such character; error 5 outside 0 to &H10FFFF)
FILTER(arr, match$ [, include [, compare]]) String array The elements of arr that contain match$ (or, with include False, that do not)
FORMAT$(value [, fmt$]) String Formats a number or a date using a format string
HEX$(n) String Hexadecimal representation of n (uppercase, no leading &H)
@@ -4000,7 +4071,7 @@ Print s$ ' Hello, DVX--!
SIN(n) Double Sine of n (n in radians)
SQR(n) Double Square root of n
TAN(n) Double Tangent of n (n in radians)
- TIMER Double Seconds since midnight
+ TIMER Double Seconds since midnight, to a fraction of a millisecond (it keeps rising between the clock's 55 ms ticks)
RND follows QBasic: RND or RND(n) with n > 0 returns the next number in the sequence, RND(0) repeats the last number, and RND(n) with n < 0 reseeds the generator from n (the same n always gives the same number). The result is a Double, 0 <= RND < 1.
INT, FIX, and SGN return an Integer, widening to Long or Double when the value does not fit. EXP, ^, and the other functions raise error 6 (Overflow) when the result leaves the numeric range, and error 5 when it is undefined (for example a negative number to a fractional power).
Note: Use RANDOMIZE or RANDOMIZE TIMER to seed the generator so you don't get the same sequence each run.
LOADPICTURE(file$) reads a picture file (BMP, PNG, JPEG, or GIF) -- or the icon of a DVX program, widget, or library file (.app, .wgt, .lib) -- and returns a Picture object (TypeName "Picture") that can be given to a picture property: a control's DragIcon, the Picture of an Image or ImageButton. Use SET, or a plain assignment, as with a file name; file names still work too. LOADPICTURE() or LOADPICTURE("") gives an empty picture, which clears the property. A missing file raises error 53 (File not found), a file that is not a picture error 481 (Invalid picture), so the check happens where the picture is loaded. Loading the same file again gives the same Picture object. Reading a picture property back gives a Picture object too (TypeName "Picture"), as in Visual Basic; used as a value (printed, joined to text, compared) it stands for its file name, so code that treats the property as a file name keeps working. The name of a program, widget, or library file works wherever a picture file's name does (a Picture property, DragIcon, PaintPicture, a Toolbar's AddButton) and shows its icon; such a file without an icon is not a picture (error 481).
+
LOADPICTURE(file$) reads a picture file (BMP, PNG, JPEG, or GIF) -- or the icon of a DVX program, widget, or library file (.app, .wgt, .lib) -- and returns a Picture object (TypeName "Picture") that can be given to a picture property: a control's DragIcon, the Picture of an Image or ImageButton. Use SET, or a plain assignment, as with a file name; file names still work too. LOADPICTURE() or LOADPICTURE("") gives an empty picture, which clears the property. A missing file raises error 53 (File not found), a file that is not a picture error 481 (Invalid picture), so the check happens where the picture is loaded. The check reads the picture's header only (the control that shows it reads the rest): a picture file damaged after its header shows nothing. Loading the same file again gives the same Picture object. Reading a picture property back gives a Picture object too (TypeName "Picture"), as in Visual Basic; used as a value (printed, joined to text, compared) it stands for its file name, so code that treats the property as a file name keeps working. The name of a program, widget, or library file works wherever a picture file's name does (a Picture property, DragIcon, PaintPicture, a Toolbar's AddButton) and shows its icon; such a file without an icon is not a picture (error 481).
The optional size and colordepth arguments are Visual Basic's: size is vbLPSmall (0), vbLPLarge (1), vbLPSmallShell (2), vbLPLargeShell (3), or vbLPCustom (4, with x and y giving the size), colordepth vbLPDefault (0), vbLPMonochrome (1), vbLPVGAColor (2), or vbLPColor (3). They choose among the images of an icon file; every DVX picture is a single bitmap, so, as for a bitmap in Visual Basic, they are checked and otherwise make no difference. A value outside these, or a negative x or y, raises error 5.
A Picture object has three properties, which can only be read:
Property Type Description
@@ -4166,6 +4237,12 @@ Set Label1.DragIcon = LoadPicture() ' back to the dotted outline
Dim pic As Object
Set pic = LoadPicture("logo.bmp")
Print Me.ScaleX(pic.Width, vbHimetric, vbPixels); "x"; Me.ScaleY(pic.Height, vbHimetric, vbPixels)
+
LOADPICTUREDATA
+
LOADPICTUREDATA(bytes$) does what LOADPICTURE does for a picture held in a string rather than in a file: the bytes of a BMP, PNG, JPEG, or GIF file, such as HttpBody$ gives for a picture downloaded from the web, or ResGetData$ for a picture resource of a DVX program, widget, or library file (any of its resources, not only its icon). The Picture object it returns goes wherever a LOADPICTURE picture goes: the Picture of an Image, an ImageButton, or a PictureBox, a control's DragIcon, a form's Icon, and PaintPicture; reading such a property back gives the same object. Its Width, Height, and Type are a loaded picture's; used as a value it stands for "", since it has no file name. Bytes that are not a picture raise error 481 (Invalid picture); LOADPICTUREDATA("") gives an empty picture. Giving one to a control that shows no picture (a Label's Picture, for example) stops the program with an error.
+
A Picture object made this way keeps its bytes until the program ends, and the same bytes give the same object, so loading a picture again costs nothing. To show many different pictures one after another (a slide show, pictures from the web), use the SetPictureData method of the Image, ImageButton, or PictureBox instead: Image1.SetPictureData bytes$ shows the picture without keeping it, and returns True, or False when the bytes are not a picture.
+
Image1.Picture = LoadPictureData(HttpBody$(req))
+Set Label1.DragIcon = LoadPictureData(ResGetData$("mygame.app", "card")) ' resource.bas in the project
+If Not Picture1.SetPictureData(HttpBody$(req)) Then MsgBox "That is not a picture."
COMMAND$
COMMAND$ returns the program's arguments exactly as they were given, as in Visual Basic (QBasic turned them into capitals; DVX keeps them as they are). Arguments are separated by blanks; an argument with blanks in it is written in double quotes. COMMAND$(n) picks out one argument with its quotes removed, so a program that opens the document it was started with only needs:
IF COMMAND$(1) <> "" THEN
@@ -4671,6 +4748,14 @@ fruit.Remove 1 ' removes Banana, now the first item
Put text values in single quotes, as in the examples. Text in double quotes is read as a column name, so "Scott" in place of 'Scott' gives the error "no such column: Scott".
+
SQLEscape$
+
Returns text with every single quote doubled, the way SQL writes a quote inside a quoted value. Use it on text the user typed (or read from a file) before you put it between quotes in a statement: a name such as O'Brien would otherwise end the value early and break the statement.
SQLExec runs every statement in sql$, one after another, before it returns.
+
Long Statements
+
A statement can take a while: one that changes many rows, CREATE INDEX on a big table, or the first SQLNext of a query that sorts or groups, which reads every row first. While a statement runs, DVX and the other programs keep going, and your program's events run as they do during DoEvents. Ending the program (End, or closing its last form) stops the statement, and what it changed is undone.
+
An event that runs meanwhile cannot use the database the statement is running on: SQLExec, SQLQuery, and SQLNext return False or 0, and SQLError$ says "A statement is still running on this database". Its field functions return "" or 0. SQLFreeResult and SQLClose wait until the statement ends. Other databases can be used as always.
+
Code in a control file (a control written in BASIC) does not wait like this: its statements run to the end.
SQLAffected
Returns the number of rows affected by the last INSERT, UPDATE, or DELETE.
count = SQLAffected(db)
@@ -5093,7 +5178,7 @@ End Sub
---- --------
commdlg.bas File Open/Save dialogs (DlgFileOpenIn$ and DlgFileSaveIn$ start in a folder you name), the folder dialog (DlgFolder$, for a Browse button), input dialogs, choice dialogs, save-prompt dialogs, the color dialog, the font dialog (DlgFont), message boxes with your own buttons (DlgButtons)
sql.bas SQLite database access (SQLOpen, SQLQuery, SQLNext, SQLField$, etc.)
- comm.bas Serial-port I/O (raw and packet) and optional encrypted links; port settings read back and changed, why a call failed, breaks, ring, and line errors (SerOpen, SerSet, SerLastError, CommOpen, CommHandshakeStep, ...)
+ comm.bas Serial-port I/O (raw and packet) and optional encrypted links; port settings read back and changed, why a call failed, breaks, ring, and line errors (SerOpen, SerSet, SerLastError, CommOpen, CommOpenTcp, CommHandshakeStep, ...)
conn.bas Connections the way Terminal makes them: a serial port, a modem that dials, the secure link to the DVX host proxy, or Telnet, shown on a Terminal control, with capture, answerback, and file transfers (ConnCreate, ConnOpen, ConnPoll, ConnAttach, ConnSendFiles, ...)
xfer.bas File transfers with XMODEM, XMODEM-1K, YMODEM, and ZMODEM, on a serial port or a connection, with their progress and questions (XferSendFile, XferStatus, XferBytes, XferAnswer, ...)
prefs.bas Settings files (INI): load one, read and change its settings, list the keys of a section, save it; DVX's own settings file, the screen mode it starts in, the names its choices are stored as and its default choices, and choices stored by names of your own (PrefsLoad, PrefsGet$, PrefsSet, PrefsKeyName$, PrefsSaveAs, PrefsDvxIni$, PrefsVideoWidth, PrefsChoiceLabel$, PrefsChoiceDefault, PrefsGetChoiceFrom, ...)
@@ -5103,7 +5188,7 @@ End Sub
net.bas Network connections: TCP (TcpConnect, TcpListen, TcpSend, TcpRecv$, TcpPeer$, ...), UDP messages and name lookups, any bytes; the network settings and status (NetConfigLoad, NetConfigSet, NetConfigStore, NetRestart, NetStatus$, ...)
print.bas Printing: text with the page setup and pictures, with or without the Print dialog; a form; the Page Setup dialog; the printer and page settings; the printer ports (PrintModelLabel$, PrintSettingsStore, PrintPageSetting, PrintChoose, PrintForm, PrintPortStatus, ...)
listhelp.bas Lists and icon views you draw yourself (a UserControl, a PictureBox) that behave like ListBox and Program Manager: selection with Shift and Ctrl, the arrow keys in a list or a grid, Ctrl+A, Space, type-ahead, where a dragged item lands, and Program Manager's icon view rules -- its cells, the grid with a scroll bar, names on two lines, dragging and dropping (ListSelCreate, ListSelClick, ListSelNavigate, ListTypeAhead, ListIconMetric, ListIconLabel$, ...)
- security.bas Security: random bytes for keys, a key exchange, a cipher, SHA-256 hashes, HMAC signatures, key making, and a safe compare (SecRandom$, SecDhCreate, SecCipherCrypt$, SecSha256$, SecHmac$, SecHkdf$, ...)
+ security.bas Security: random bytes for keys, key exchanges, ciphers, sealing, SHA-256 hashes, HMAC and public-key signatures, key making, and a safe compare (SecRandom$, SecX25519$, SecAeadLock$, SecEd25519Sign$, SecSha256$, SecHkdf$, ...)
game.bas Games: whether a key is being held down right now, forgetting held keys, frames FRAMEWAIT skipped, waiting for the screen's retrace (GameKeyDown, GameKeyClear, FrameMissed, GameWaitRetrace)
joystick.bas Joysticks: how many there are, their names, where each stick points, and which buttons are pressed (JoyCount, JoyX, JoyY, JoyButton, ...); setting them up: calibration, dead zone, game port mode, keyboard joystick keys (JoySetCalibration, JoyCalibration, JoyRescan, JoyKeyIndex, JoySettingsStore, ...)
sound.bas Sound: play WAV and VOC files, tones and noise, set the volume (PlayWave, PlayTone, StopWave, SoundInfo$, ...); sounds loaded once and played with volume, pan, and pitch, sound made by the program as it goes, recording from a microphone or line input (RecordStart, RecordStop, RecordLevel, ...), the FM synthesizer chip, a wavetable card's voices and memory (SoundLoad, SoundPlay, SoundSetPitch, SoundCreate, SoundStreamOpen, SoundStreamWrite, FmAlloc, FmWrite, WtAlloc, WtStart, ...); DVX's own event sounds (SoundPlayEvent), master volume, sound driver, and MIDI settings (SoundEventFile$, SoundSetMasterVolume, SoundSettingsStore, ...)
@@ -5116,8 +5201,8 @@ End Sub
compiler.bas The BASIC compiler: compile a project or files you give it, find compile errors in their files, list a program's procedures and the language's keywords, build an .app or a control file; put a project's UserControls on a form designer's toolbox; read source text as the compiler does (tokens, code or string or comment, renaming a control in code, WithEvents variables, class Events), F1 help topics; open, change, and save project files and UserControl headers (CompSourceFromProject, CompSourceAdd, CompCompile, CompError$, CompMapFile$, CompProcList$, CompBuildApp, CompDesignControls, CompTokens$, CompRenameInCode$, CompHelpTopic$, ProjOpen, ProjSave, ProjCtlSetHeader$, ...)
debug.bas Run a compiled program inside yours and debug it: breakpoints, stepping, its variables, expressions, Immediate-window lines, its PRINT output (RunCreate, RunStart, RunPoll, DbgSetBreakpoints, DbgStepOver, DbgLocals$, DbgEval$, DbgExec$, ...)
designer.bas Form designers: the control types, whether each holds other controls and can be placed, the names of new controls, default events, toolbox tooltips and icons, and each type's properties (with their types and value names), methods, and events (with each event's parameters); the names in .frm and class module text; .frm files as data to read, edit, and write, controls copied and pasted as text, and a loaded form written back as .frm text (CtlTypeCount, CtlTypeName$, CtlTypeFlags, CtlPropCount, CtlPropName$, CtlPropType, CtlEventList$, CtlEventParams$, CtlTypeEventParams$, FrmFormName$, FrmControlNames$, ClsClassName$, FrmOpen, FrmText$, FrmCtlAdd, FrmCtlText$, FrmCtlPaste, FrmMenuAdd, FormText$, ...)
- shell.bas DVX itself, everything Program Manager and Task Manager do: start programs (DVX's and DOS programs) and open documents, command lines (split into their parts) and the Run dialog, the installed programs and their names, file types, the running programs (find one, bring it to the front, ask it to close, or end it), this program's priority, keys that work in every program, Task Manager, system information, Exit DVX, the desktop and the program started after it, the DVX log; Program Manager's program groups with their items, shortcut keys, and icons (ShellRun, ShellRunProgram, ShellRunDos, ShellProgramScan, ShellAssocCount, ShellRunningId, ShellRunningActivate, ShellRunningClose, ShellArg$, ShellSetPriority, HotkeyAdd, ShellExitDvx, ShellGroupsLoad, ShellItemGet$, ShellItemIcon$, ...)
- pkg.bas Add-on packages: the packages installed in DVX and their details, files, and needs; reading .DPK files and planning what installing them takes; installing and removing as a job with progress; trusting a publisher's key; and making packages, signing them, and making keys (PkgOpen, PkgInstalledRows$, PkgOfferFile, PkgPlan, PkgInstallStart, PkgRunFor, PkgRemoveStart, PkgBuild, PkgSign, PkgKeygen, ...)
+ shell.bas DVX itself, everything Program Manager and Task Manager do: start programs (DVX's and DOS programs) and open documents, command lines (split into their parts) and the Run dialog, the installed programs and their names, file types, the running programs (find one, bring it to the front, ask it to close, or end it, and their priority), this program's priority, keys that work in every program, Task Manager, system information, Exit DVX, the desktop and the program started after it, the DVX log; Program Manager's program groups with their items, shortcut keys, and icons (ShellRun, ShellRunProgram, ShellRunDos, ShellProgramScan, ShellAssocCount, ShellRunningId, ShellRunningActivate, ShellRunningClose, ShellArg$, ShellSetPriority, HotkeyAdd, ShellExitDvx, ShellGroupsLoad, ShellItemGet$, ShellItemIcon$, ...)
+ pkg.bas Add-on packages: the packages installed in DVX and their details, files, and needs; reading .DPK files, installing, removing, making, and signing packages as jobs with progress, and planning what installing them takes; trusting a publisher's key; and making keys (PkgOpen, PkgInstalledRows$, PkgOfferFileStart, PkgPlan, PkgInstallStart, PkgRunFor, PkgRemoveStart, PkgBuildStart, PkgSignStart, PkgKeygen, ...)
json.bas JSON: read a document (a web service's reply, a settings file) and find values in it by path such as packages[3].version, and quote text for JSON of your own (JsonParse, JsonText$, JsonNumber, JsonCount, JsonKey$, JsonQuote$, ...)
http.bas Web requests: GET a page, a file, or a web service's answer, or POST to one, over the network a little at a time with progress, to memory or to a file (resuming one left part-way), following redirects (HttpOpen, HttpStart, HttpRunFor, HttpStatus, HttpBody$, HttpSaveTo, ...)
pkgnet.bas A package server: what it offers, what it has that is newer than what is installed, and fetching packages with what they need, for pkg.bas to install (PkgNetOpen, PkgNetCatalogue, PkgNetUpdates, PkgNetFetch, PkgNetRunFor, PkgNetRows$, ...)
@@ -5182,7 +5267,7 @@ If Not DlgInputCancelled() Then
SetZoom n
End If
Resources in Files
-
With resource.bas in the project, a program reads and writes the named resources DVX keeps at the end of program, widget, and library files: icons, text, and any bytes. ResOpen, ResCount, ResName$, ResType, and ResSize list them; ResGetText$ and ResAddText read and write text; ResAddFile and ResExtract copy a resource from or to a file; ResRemove takes one out. ResGetData$(file, name) gives every byte of a resource as a String (CHR$(0) included), and ResAddData file, name, type, bytes adds or replaces a resource of any bytes (type RES_TYPE_BINARY, RES_TYPE_TEXT, RES_TYPE_ICON, or RES_TYPE_FONT, a .DFN font the program carries; see Fonts). RES_NAME_ICON ("icon32") and RES_NAME_WIDGET_ICON ("icon24") are the names DVX gives a program's and a widget's icon; a name is at most RES_NAME_MAX - 1 characters.
+
With resource.bas in the project, a program reads and writes the named resources DVX keeps at the end of program, widget, and library files: icons, text, and any bytes. ResOpen, ResCount, ResName$, ResType, and ResSize list them; ResGetText$ and ResAddText read and write text; ResAddFile and ResExtract copy a resource from or to a file; ResRemove takes one out. ResGetData$(file, name) gives every byte of a resource as a String (CHR$(0) included) -- LoadPictureData(ResGetData$(file, name)) shows a picture resource -- and ResAddData file, name, type, bytes adds or replaces a resource of any bytes (type RES_TYPE_BINARY, RES_TYPE_TEXT, RES_TYPE_ICON, or RES_TYPE_FONT, a .DFN font the program carries; see Fonts). RES_NAME_ICON ("icon32") and RES_NAME_WIDGET_ICON ("icon24") are the names DVX gives a program's and a widget's icon; a name is at most RES_NAME_MAX - 1 characters.
Dim settings As String
settings = Chr$(1) + Chr$(0) + Chr$(255)
@@ -5201,7 +5286,11 @@ Else
Print "Handshake failed"
End If
After the handshake, data that arrives unencrypted is ignored unless that channel is opened for it with CommSetPlain link, channel, True.
-
CommHandshake waits until the key exchange is done. CommHandshakeStep(link, passphrase) does one step of it and returns at once, so a program can keep working: call it from a Timer until it returns something other than COMM_PENDING -- COMM_OK when the link is ready, else a COMM_ERR_ code. CommLastError(link) gives the COMM_ code of the last CommHandshake, CommHandshakeStep, CommSend, CommSetPlain, or CommPoll, which tells a wrong passphrase (COMM_ERR_AUTH) from a line that does not answer (COMM_ERR_TIMEOUT). CommDropped(link) counts arriving packets thrown away because they were tampered with or came unencrypted where that is refused. Channels are numbered 0 to COMM_MAX_CHANNEL.
+
The same link runs over the network: with net.bas in the project too, CommOpenTcp(conn) opens it on a connection from TcpConnect or TcpAccept (both ends do), with every channel, the handshake, and encryption as on a COM port. The connection stays the program's: close it with TcpClose after CommClose.
CommHandshake waits until the key exchange is done (up to 30 seconds), and CommSend waits while the link has as much in flight as it can; the program's events run while they wait, as they do in DoEvents. CommHandshakeStep(link, passphrase) does one step of it and returns at once, so a program can keep working: call it from a Timer until it returns something other than COMM_PENDING -- COMM_OK when the link is ready, else a COMM_ERR_ code. CommLastError(link) gives the COMM_ code of the last CommHandshake, CommHandshakeStep, CommSend, CommSetPlain, or CommPoll, which tells a wrong passphrase (COMM_ERR_AUTH) from a line that does not answer (COMM_ERR_TIMEOUT). CommDropped(link) counts arriving packets thrown away because they were tampered with or came unencrypted where that is refused. Channels are numbered 0 to COMM_MAX_CHANNEL.
Sub Timer1_Timer ()
Select Case CommHandshakeStep(link, txtPass.Text)
Case COMM_PENDING
@@ -5298,12 +5387,12 @@ Sub cmdOK_Click ()
PrefsClose prefs
End Sub
Files and Folders
-
With fileops.bas in the project, a program can show and change what is on the disks. FileOpsList(folder, filter) reads a folder once -- its folders and the files matching filter -- and returns a listing (a number; FileOpsListFree frees it). FileOpsListRows$ gives the whole listing as name, size, type, and date columns, ready for a ListView's SetRows, so even a folder of hundreds of files fills the list at once; FileOpsListSort re-sorts it by name, size, type, or date with folders first. FileOpsListName$, FileOpsListIsFolder, and FileOpsListSize read single entries, and FileOpsListFind finds one by name.
+
With fileops.bas in the project, a program can show and change what is on the disks. FileOpsList(folder, filter) reads a folder once -- its folders and the files matching filter -- and returns a listing (a number; FileOpsListFree frees it). FileOpsListRows$ gives the whole listing as name, size, type, and date columns, ready for a ListView's SetRows, so even a folder of hundreds of files fills the list at once; FileOpsListSort re-sorts it by name, size, type, or date with folders first. FileOpsListName$, FileOpsListIsFolder, and FileOpsListSize read single entries, and FileOpsListFind finds one by name. FileOpsListFolders(listing) makes a new listing of just the folders in one, sorted by name, without reading the disk again (handy for a folder tree next to a file list).
Paths: FileOpsPathNormalize$ (the full path of an existing folder, or ""), FileOpsPathParent$, FileOpsPathJoin$, FileOpsPathSame, FileOpsPathIsRoot, FileOpsPathIsAbsolute (True for a path that is complete by itself, such as "C:\DOCS", False for one that counts from the current folder), FileOpsPathIsInside(path, folder) (True when path is folder itself or anywhere inside it: check it before copying or moving a folder, so it is never copied into itself), FileOpsPathSep$ (the character between folder names, "\" on DOS), FileOpsPathNative$ (a path written with that character, for showing it: "C:/DOCS/A.TXT" becomes "C:\DOCS\A.TXT"), FileOpsBaseName$, and FileOpsExtension$. Every one of them takes / and \ alike and gives back \. FileOpsFilterMatch(itemName, filter) tells whether a name matches a filter the way the file dialogs match it: wildcards * and ?, several patterns separated by ; such as "*.bmp;*.png", case not mattering.
Files: FileOpsMakeDir and FileOpsRename (FileOpsError$ says why one failed), FileOpsTempPath$ and FileOpsReplaceFile (save safely: write the new file to FileOpsTempPath$(path$), then FileOpsReplaceFile swaps it in; the old file comes back if that fails), FileOpsValidName(folder, itemName) (whether a name can be used for a new file or folder in folder, by the rules of its disk; FileOpsError$ says why not), FileOpsValidLongName and FileOpsValidShortName (the long-name or the 8.3 rules by themselves, whatever the disk takes), FileOpsShortPath$ (the short 8.3 form of an existing path, for an old program that cannot take long names or blanks), FileOpsMakePath (a folder and every missing folder above it, which MKDIR does not make), FileOpsIsFolder, FileOpsGetAttr, and FileOpsSetAttr (the FILEOPS_ATTR_ bits), FileOpsModified$ and FileOpsSetModified (when a file was last changed, read and set in the form "YYYY-MM-DD HH:MM"; FileOpsSetModified returns False for a date that does not exist), FileOpsTreeSize with FileOpsTreeBytes, FileOpsTreeFiles, and FileOpsTreeFolders (the size of a whole folder), and FileOpsFormatSize$.
Drives: FileOpsDriveList$ (the drive letters), FileOpsDriveType (FILEOPS_DRIVE_FIXED for a hard disk, FILEOPS_DRIVE_REMOVABLE for a floppy disk drive, FILEOPS_DRIVE_REMOTE for a network drive or a CD-ROM; asking never makes a drive spin up), FileOpsBootDrive$ (the drive the computer started from), FileOpsFreeBytes, FileOpsFreeKb, and FileOpsTotalBytes (the size of the disk; right for disks past 2 GB too), FileOpsLongNames(path) (True when the disk holding path takes long file names: up to 255 characters, with spaces and several dots; otherwise names are 8.3). FileOpsIsWritable(folder) is False where files cannot be made (a CD-ROM, a write-protected floppy), and FileOpsReadOnlySystem() is True when nothing can be saved: DVX runs from such a disk and there is no TEMP folder to keep the user folder in. FileOpsTempDir$() is the folder for scratch files ("" when there is none).
DVX's folders: FileOpsUserDir$() is the user folder (USER in the DVX folder, or the folder DVXUSER names), where everything DVX and its programs save goes; FileOpsUserPath$(name) is a file or folder in it, for example FileOpsUserPath$("DOWNLOAD"); FileOpsUserInTemp() is True when DVX runs from a disk it cannot write to and keeps the user folder in the TEMP folder. A program's own folder there is App.User. The fonts, sounds, themes, desktop pictures, MIDI instrument banks, and screen savers come in two folders each, DVX's own (and add-on packages') in SHARED and the user's in the user folder; the kinds are FILEOPS_KIND_FONTS, FILEOPS_KIND_SOUNDS, FILEOPS_KIND_THEMES, FILEOPS_KIND_WPAPER, FILEOPS_KIND_BANKS, and FILEOPS_KIND_SAVERS. An add-on's files are in a folder of its vendor's inside a kind's folder and are named VENDOR\NAME (for example ACME\SPACE.THM). FileOpsKindDir$(kind, user) is DVX's folder of a kind (user False) or the user's (True); FileOpsKindBrowseDir$(kind) is the one a file dialog starts in (the user's when it exists); FileOpsKindFind$(kind, fileName) finds a file by name, the user's first ("" when there is none); FileOpsKindName$(kind, path) is the name alone of a file in one of the two folders, the way settings store it ("" for any other path); FileOpsKindList(kind) lists both folders as one, sorted by name, a user's file hiding DVX's of the same name (a list for FileOpsListCount, FileOpsListName$, and the rest; FileOpsListFree when done). Programs read DVX's SHARED folder but never write in it.
-
FileOpsCopyTree, FileOpsMoveTree, and FileOpsDeleteTree do a whole folder at once. For a long copy, move, or delete the program stays usable when it runs the work as a job, a little at a time from a Timer: FileOpsJobCreate, then FileOpsJobAddSource for each file or folder, then FileOpsJobRunFor from the timer until it returns FILEOPS_STEP_DONE or FILEOPS_STEP_CANCELLED. When it returns FILEOPS_STEP_ASK (a file already exists, a file is read-only) or FILEOPS_STEP_ERROR, show FileOpsJobMessage$ and pass the user's choice to FileOpsJobAnswer. FileOpsJobCurrent$, FileOpsJobFilesDone, FileOpsJobFilesTotal, FileOpsJobPercent, FileOpsJobBytesDone, and FileOpsJobBytesTotal show the progress, and FileOpsJobCancel stops the job. Files copied from a CD-ROM are read-only; FileOpsJobClearAttr job, FILEOPS_ATTR_READONLY before the job runs makes every copy changeable (FILEOPS_ATTR_EDITABLE clears all four attribute bits). Whatever a program leaves open is freed when it ends.
+
FileOpsCopyTree, FileOpsMoveTree, and FileOpsDeleteTree do a whole folder in one call, as does FileOpsTreeSize. While one works, the rest of DVX goes on and so do the program's own events, as with DoEvents (in a control's code they wait until the call ends); if the program is ended meanwhile, the call stops and returns False. For a long copy, move, or delete with progress and questions, run the work as a job, a little at a time from a Timer: FileOpsJobCreate, then FileOpsJobAddSource for each file or folder, then FileOpsJobRunFor from the timer until it returns FILEOPS_STEP_DONE or FILEOPS_STEP_CANCELLED. When it returns FILEOPS_STEP_ASK (a file already exists, a file is read-only) or FILEOPS_STEP_ERROR, show FileOpsJobMessage$ and pass the user's choice to FileOpsJobAnswer. FileOpsJobCurrent$, FileOpsJobFilesDone, FileOpsJobFilesTotal, FileOpsJobFoldersTotal, FileOpsJobPercent, FileOpsJobBytesDone, and FileOpsJobBytesTotal show the progress, and FileOpsJobCancel stops the job. A FILEOPS_MEASURE job only counts: the files (FileOpsJobFilesTotal), the folders inside what was added (FileOpsJobFoldersTotal), and the bytes (FileOpsJobBytesTotal), changing nothing and asking nothing; FileOpsJobUnread says how many folders could not be read. A Properties dialog counts a big folder this way without stopping the program. Files copied from a CD-ROM are read-only; FileOpsJobClearAttr job, FILEOPS_ATTR_READONLY before the job runs makes every copy changeable (FILEOPS_ATTR_EDITABLE clears all four attribute bits). Whatever a program leaves open is freed when it ends.
Dim job As Long
Sub cmdCopy_Click ()
@@ -5330,7 +5419,7 @@ Sub Timer1_Timer ()
End Sub
Working with DVX
With shell.bas in the project, a program works with DVX the way Program Manager does -- a whole desktop can be written in BASIC.
-
Starting programs. ShellRun program, args starts a program (args is what it gets as COMMAND$), runs a DOS program, or opens a document in the program set for its type; it returns True when it started (or the DOS program will run). ShellRunProgram(program, args, folder) does the same the way a Program Manager item does: file names in args, and a document named without a folder, are looked for in folder first ("" for none), and it returns the new program's id (SHELL_RUN_DOS for a DOS program, 0 when it did not start; DVX has already said why). DOS programs are .EXE, .COM, and .BAT files (ShellIsDosProgram(path) says whether a file is one). ShellRunDos program, args, folder, pause runs one with DVX stepped aside: once your procedure has returned, DVX names the programs running besides the desktop, this one included, and asks them to close (each asks about unsaved work; No, or a program kept open, cancels the run); the screen then becomes the DOS screen until the program ends, and DVX comes back with the desktop. folder is where it runs ("" for its own folder); pause True shows "Press any key to return to DVX." when it ends. ShellMinimizeApp id runs that program minimized: its windows become icons, including the ones it opens while it starts. ShellRunCommand(commandLine) runs a whole command line (a program, in double quotes when its name has a blank, then its arguments) and returns the id too. ShellRunDialog$(history) shows DVX's Run dialog offering history (earlier commands, newest first, one per line) and returns what the user chose, "" for Cancel. ShellOpenDocument path opens a document in the program set for its type in the Control Panel.
+
Starting programs. ShellRun program, args starts a program (args is what it gets as COMMAND$), runs a DOS program, or opens a document in the program set for its type; it returns True when it started (or the DOS program will run). ShellRunProgram(program, args, folder) does the same the way a Program Manager item does: file names in args, and a document named without a folder, are looked for in folder first ("" for none), and it returns the new program's id (SHELL_RUN_DOS for a DOS program, 0 when it did not start; DVX has already said why). DOS programs are .EXE, .COM, and .BAT files (ShellIsDosProgram(path) says whether a file is one). ShellRunDos program, args, folder, pause runs one with DVX stepped aside: once your procedure has returned, DVX names the programs running besides the desktop, this one included, and asks them to close (each asks about unsaved work; No, or a program kept open, cancels the run); the screen then becomes the DOS screen until the program ends, and DVX comes back with the desktop. folder is where it runs ("" for its own folder); pause True shows "Press any key to return to DVX." when it ends. ShellRunDosWait(program, args, folder, pause) does the same and waits for it, as SHELL does: your program is not asked to close, its events run meanwhile, and the DOS program's exit code (its ERRORLEVEL) comes back, -1 when it did not run. ShellMinimizeApp id runs that program minimized: its windows become icons, including the ones it opens while it starts. ShellRunCommand(commandLine) runs a whole command line (a program, in double quotes when its name has a blank, then its arguments) and returns the id too. ShellRunDialog$(history) shows DVX's Run dialog offering history (earlier commands, newest first, one per line) and returns what the user chose, "" for Cancel. ShellOpenDocument path opens a document in the program set for its type in the Control Panel.
Installed programs. ShellProgramScan(folder, skipPath) finds every program in folder and its subfolders ("" for the APPS folder) and returns a list: ShellProgramCount(list) programs, ShellProgramPath$(list, index) each one's path (index from 0), ShellProgramFree list when done (a list not freed is freed when the program ends). Give ShellAppPath$() as skipPath to leave this program out. ShellProgramName$(path) and ShellProgramDescription$(path) are the name and description a program shows, and ShellIsProgram(path) tells a program from a document. LOADPICTURE of a program gives its icon, so a PictureBox can show a grid of programs with PaintPicture. ShellProgramsFolder$() is the folder the installed programs are in (where ShellProgramScan("") looks), for example to start a Browse for a program there with DlgFileOpenIn$.
File types. ShellAssocProgram$(extension) tells which program opens files with that extension (with ",arguments" after it when it is given any), ShellAssocCommand$(document) just the program that opens a document. ShellAssocCount() file types are set; ShellAssocExt$(index) and ShellAssocValue$(index) read them (index from 0). ShellAssocSet(extension, program) sets one and ShellAssocRemove(extension) removes one, saved at once; ShellAssocNormalize$(extension) is the extension as stored (".txt" gives "TXT"; "" when it cannot be one).
Running programs. ShellRunningCount() says how many programs are running, and ShellRunningId(index) is the id of each one (index from 0; the same id ShellRunProgram returns). ShellAppId() is this program's own id. ShellRunningName$(id) and ShellRunningPath$(id) are the name a running program shows and the path of its .app ("" when it is not running), and ShellRunningFind(program) is the id of a running copy of a program, 0 when it is not running. ShellRunningActivate id brings a program to the front the way Task Manager's Switch To does (a minimized window is restored). ShellRunningClose id asks a program to close as if the user had closed its windows: it may ask to save its work and the user may cancel, and it is asked once your code has returned to DVX, so check from a Timer whether it is still running. ShellRunningKill id ends a program at once without asking it, the way Task Manager's End Task does: whatever it had not saved is lost, so keep it for a program that does not respond. ShellAppIsHung(id) is True when a program has been busy for 5 seconds or longer ("Not responding" in Task Manager). A program cannot end itself with ShellRunningKill (END does that). ShellAppMemory(id) is how many bytes of memory a program is using. ShellChangeCount() changes whenever a program starts or ends: check it from a Timer and update what you show when it is not the value you saw last. ShellTaskManager opens the Task Manager, ShellSystemInfo$() describes the computer (several lines), and ShellExitDvx exits DVX: every program is asked to close, each asking about unsaved work, and a program that stays open stops the exit (ask the user first).
@@ -5388,22 +5477,34 @@ End Sub
Loop
End Sub
Add-on Packages
-
With pkg.bas in the project, a program installs, removes, and makes add-on packages -- the .DPK files Software installs -- the way Software and Package Builder (both written in BASIC) do. PkgOpen() starts a session (a number; PkgClose ends it), and PkgError$(session) says why the last call that failed did. Lists come back as rows for a ListView's SetRows: cells separated by TAB, each row ending in a line feed.
+
With pkg.bas in the project, a program installs, removes, and makes add-on packages -- the .DPK files Software installs -- the way Software and Package Builder (both written in BASIC) do. PkgOpen() starts a session (a number; PkgClose ends it), and PkgError$(session) says why the last call that failed did. PkgOpenIn(root) starts one on another DVX folder (a copy being made ready on another disk, say): its installed packages and the keys its user folder trusts. Lists come back as rows for a ListView's SetRows: cells separated by TAB, each row ending in a line feed.
What is installed. PkgInstalledRows$ lists every installed package (DVX itself is kpunch.dvx): id, name, version, kind, and summary. A package is named by its id, vendor.name (acme.chess). PkgField$(s, id, field) gives one detail (PKG_FIELD_NAME, PKG_FIELD_VERSION, PKG_FIELD_PUBLISHER, PKG_FIELD_SIGNER_NAME, ...); PkgFilesRows$, PkgDependsRows$, and PkgNeededBy$ its files, what it needs, and what needs it. PkgFileOwner$(s, fileName) is the other way: the id of the package that put a file there (its name from the DVX folder, such as "SHARED\FONTS\KPUNCH\ARIMO.DFN"; "" when none did).
-
Installing. PkgOfferFile(s, path) reads a package file and every package beside it (what it may need) as offers, numbered from 0, and returns the number of the one at path (-1 when it cannot be read). PkgOfferField$ and PkgOfferFilesRows$ describe an offer before it is installed; PKG_FIELD_TRUST says whether DVX trusts who signed it. PkgPlan(s, "0") plans installing it with everything it needs, checked against this DVX and the packages installed (False, with PkgError$ saying why, when it cannot be installed); PkgPlanRows$ lists what will go in, PkgPlanBytes the disk space, and PkgPlanRestart whether libraries or widgets are among them (they work from the next time DVX starts). PkgInstallStart(s) then starts the job, and PkgRunFor(s, milliseconds) runs it a little at a time, returning PKG_RUN_BUSY until it is PKG_RUN_DONE or PKG_RUN_FAILED (PkgRunMessage$ says why); PkgRunPercent and PkgRunCurrent$ show how far it is.
-
offer = PkgOfferFile(s, path)
-If offer >= 0 And PkgPlan(s, CStr(offer)) Then
- If PkgInstallStart(s) Then
- Do
- rc = PkgRunFor(s, 60)
- pbJob.Value = PkgRunPercent(s)
- DoEvents
- Loop While rc = PKG_RUN_BUSY
+
Jobs. Reading package files, installing, removing, and making and signing packages each take a while (checking a signature alone takes a moment on a 486), so each is a job: a ...Start function starts it, and PkgRunFor(s, milliseconds) runs it a little at a time, returning PKG_RUN_BUSY until it is PKG_RUN_DONE or PKG_RUN_FAILED (PkgRunMessage$ says why); PkgRunPercent and PkgRunCurrent$ show how far it is. A session runs one job at a time.
+
Installing. PkgOfferFileStart(s, path) reads a package file and every package beside it (what it may need) as offers, numbered from 0; the one at path is 0, and the job fails when it cannot be read. PkgOfferField$ and PkgOfferFilesRows$ describe an offer before it is installed; PKG_FIELD_TRUST says whether DVX trusts who signed it. PkgPlan(s, "0") plans installing it with everything it needs, checked against this DVX and the packages installed (False, with PkgError$ saying why, when it cannot be installed); PkgPlanRows$ lists what will go in, PkgPlanBytes the disk space, and PkgPlanRestart whether libraries or widgets are among them (they work from the next time DVX starts). PkgInstallStart(s) then starts installing.
+
Function RunJob(s As Long) As Long
+ Dim rc As Long
+ Do
+ rc = PkgRunFor(s, 60)
+ pbJob.Value = PkgRunPercent(s)
+ DoEvents
+ Loop While rc = PKG_RUN_BUSY
+ RunJob = rc
+End Function
+
+If PkgOfferFileStart(s, path) Then
+ If RunJob(s) = PKG_RUN_DONE And PkgPlan(s, "0") Then
+ If PkgInstallStart(s) Then
+ rc = RunJob(s)
+ End If
End If
End If
Removing. PkgDependents$(s, ids) lists the installed packages that need the ones going (they must go too), and PkgOrphans$(s, ids) those only they needed. PkgRemoveStart(s, ids, removeChanged) starts removing, run with PkgRunFor like an install; files changed since they were installed stay unless removeChanged, and PkgRunKept$ lists them.
-
Trust. A publisher signs a package with a key so people can tell it came from them. PkgTrustKey(s, keyPath) trusts a publisher's key file (.KEY) from then on.
-
From a package server. With pkgnet.bas in the project too, a program gets packages from a package server the way Software does. PkgNetOpen("") returns a handle on the server DVX's settings name, with the account they give; PkgNetClose frees it. PkgNetServer$, PkgNetPort, PkgNetUser$, and PkgNetPassword$ read those settings (PkgNetUrl$ is the address they make), and PkgNetSaveSettings(server, port, user, password) changes them. A job is started with PkgNetCatalogue (every package the server offers), PkgNetUpdates (what it has that is newer than the packages installed, DVX itself among them), or PkgNetFetch(n, ids) (packages by id, with everything they need that is not installed), and run with PkgNetRunFor(n, milliseconds) until it is PKGNET_DONE or PKGNET_FAILED (PkgNetError$ says why); PkgNetPercent and PkgNetCurrent$ show how far a fetch is. PkgNetRows$ lists the answer (id, name, version, kind, summary, size, and the version installed, if any), and PkgNetField$ and PkgNetDependsRows$ describe one package in it. Each package fetched is checked against the size and fingerprint the server gives, and the server's signature of it comes with it, so it is trusted or not exactly as a package file is. What was fetched is in PkgNetDir$: PkgOfferFolder(s, folder) reads every package there as the offers, and PkgOfferIndex(s, id) gives the number of the one wanted, to plan and install as above; PkgNetClear removes them afterwards. With an account (the settings', or PkgNetAccount(n, email, password) for another server), PkgNetUpload(n, path) publishes a package file for the server's review, and PkgNetMine(n) asks what the account has sent: PkgNetMineRows$ lists it (id, name, version, state, and the review's note); both sign in first.
+
Stopping. PkgRunCancel(s) stops a job after the package it is on: one still being unpacked is left as it was, and one whose files are already in is finished first, so keep calling PkgRunFor until it stops saying PKG_RUN_BUSY. A package being made or signed stops at once, and nothing of it is left behind.
+
Trust. A publisher signs a package with a key so people can tell it came from them. PkgTrustKey(s, keyPath) trusts a publisher's key file (.KEY) from then on. PkgKeysRows$(s) lists the keys DVX trusts (name, id, and "system" for DVX's own or "user" for one trusted this way), and PkgUntrustKey(s, keyId) stops trusting one of the user's.
+
Checking a package. PkgVerifyStart(s, path, keyPath) checks a package file without installing it, a job run with PkgRunFor: every file in it against the size and fingerprint its manifest lists, so a damaged or changed package is found. With keyPath (a .KEY file) the package must also be signed by that key, in it or beside it, or nothing starts.
+
Signing any data. PkgSignData$(s, bytes, secretPath) signs any bytes (a file read into a string, a message) with a secret key file, giving the signature as text; PkgCheckData(s, bytes, signature, keyPath) says whether a signature was made over those bytes by the key in keyPath, or with keyPath "" by a key DVX trusts.
+
Plain ZIP files. A session also lists, unpacks, and makes ordinary .ZIP files, as jobs run with PkgRunFor: PkgZipListStart(s, zipPath) reads one to its end, every file in it checked; PkgZipExtractStart(s, zipPath, folder) unpacks it into folder (a name that would leave folder stops it); PkgZipCreateStart(s, zipPath, files) makes one from rows of a name in it and the file it comes from (as PkgFolderRows$ gives them). PkgZipRows$(s) lists what the last of them read or packed: name, size, date, and "folder" for a folder.
+
From a package server. With pkgnet.bas in the project too, a program gets packages from a package server the way Software does. PkgNetOpen("") returns a handle on the server DVX's settings name, with the account they give; PkgNetClose frees it. PkgNetServer$, PkgNetPort, PkgNetUser$, and PkgNetPassword$ read those settings (PkgNetUrl$ is the address they make), and PkgNetSaveSettings(server, port, user, password) changes them. A job is started with PkgNetCatalogue (every package the server offers), PkgNetUpdates (what it has that is newer than the packages installed, DVX itself among them), or PkgNetFetch(n, ids) (packages by id, with everything they need that is not installed), and run with PkgNetRunFor(n, milliseconds) until it is PKGNET_DONE or PKGNET_FAILED (PkgNetError$ says why); PkgNetPercent and PkgNetCurrent$ show how far a fetch is. PkgNetRows$ lists the answer (id, name, version, kind, summary, size, and the version installed, if any), and PkgNetField$ and PkgNetDependsRows$ describe one package in it (PKG_FIELD_SHA256 is its file's fingerprint, PKG_FIELD_REPO_SIGNED whether the repository signed it). PkgNetOpenIn(server, root, downloadDir) works for another DVX folder and fetches into another folder. Each package fetched is checked against the size and fingerprint the server gives, and the server's signature of it comes with it, so it is trusted or not exactly as a package file is. What was fetched is in PkgNetDir$: PkgOfferFolderStart(s, folder) reads every package there as the offers (a job), and PkgOfferIndex(s, id) gives the number of the one wanted, to plan and install as above; PkgNetClear removes them afterwards. With an account (the settings', or PkgNetAccount(n, email, password) for another server), PkgNetUpload(n, path) publishes a package file for the server's review, and PkgNetMine(n) asks what the account has sent: PkgNetMineRows$ lists it (id, name, version, state, and the review's note); both sign in first.
n = PkgNetOpen("")
If PkgNetFetch(n, "acme.chess") Then
Do
@@ -5412,13 +5513,14 @@ If PkgNetFetch(n, "acme.chess") Then
DoEvents
Loop While rc = PKGNET_BUSY
End If
-If rc = PKGNET_DONE And PkgOfferFolder(s, PkgNetDir$(n)) > 0 Then
+If rc = PKGNET_DONE And PkgOfferFolderStart(s, PkgNetDir$(n)) Then
+ rc = RunJob(s)
offer = PkgOfferIndex(s, "acme.chess")
' plan and install offer as above, then PkgNetClear n
End If
-
Making packages. PkgBuild(s, template, files, out, secret) builds a package: template is a settings file with a [package] section (id, name, version, kind, and summary, publisher, author, licence, homepage, dvx as wanted) and a [depends] section (id = oldest version for each package it needs); files are rows of a name in the package (from the DVX folder, such as APPS\ACME\CHESS\CHESS.APP) and the file it is made from; secret is the secret key file (.SEC) to sign with, or "". PkgFolderRows$(s, folder, prefix) gives those rows for every file in a folder, and PkgKindFolder$(kind, id) the folder a package of that kind keeps its files in. PkgSign signs a package already built, PkgKeygen(s, name, base) makes a key pair (base.KEY to give out, base.SEC to keep private), and PkgKeyInfo$ reads a key file's name and id. PkgIdValid and PkgVersionValid check an id and a version before building; PkgKindName$ lists the kinds.
+
Making packages. PkgBuildStart(s, template, files, out, secret) starts building a package, a job run with PkgRunFor like an install (PkgRunCurrent$ is the file it is on): template is a settings file with a [package] section (id, name, version, kind, and summary, publisher, author, licence, homepage, dvx as wanted) and a [depends] section (id = oldest version for each package it needs); files are rows of a name in the package (from the DVX folder, such as APPS\ACME\CHESS\CHESS.APP) and the file it is made from; secret is the secret key file (.SEC) to sign with, or "". PkgFolderRows$(s, folder, prefix) gives those rows for every file in a folder, and PkgKindFolder$(kind, id) the folder a package of that kind keeps its files in. PkgSignStart(s, path, secret, detached) signs a package already built: into it, a job run the same way, or beside it (NAME.SIG) when detached, done at once. PkgKeygen(s, name, base) makes a key pair (base.KEY to give out, base.SEC to keep private), and PkgKeyInfo$ reads a key file's name and id. PkgIdValid and PkgVersionValid check an id and a version before building; PkgVersionCompare(a, b) says which of two versions is newer (-1, 0, or 1: "1.10.0" is newer than "1.9.2"); PkgKindName$ lists the kinds.
Web Requests and JSON
-
With http.bas in the project, a program asks web servers for pages, files, and web services' answers. HttpOpen(method, url) makes a request ("GET" to fetch, "POST" to send) and returns a handle; HttpClose frees it. Before starting it, HttpAddHeader adds a header, HttpSetBody gives a body to send with its type ("application/json", say), HttpSaveTo writes the answer's body to a file instead of keeping it (with keepPart True, a file left part-way is finished rather than started again), and HttpSetTimeout sets how many seconds without progress mean the server is not answering (30 to start with). HttpStart starts it (False for an address that is not http://; there is no encryption), and HttpRunFor(h, milliseconds) runs it a little at a time, returning HTTP_BUSY until it is HTTP_DONE or HTTP_FAILED:
+
With http.bas in the project, a program asks web servers for pages, files, and web services' answers. HttpOpen(method, url) makes a request ("GET" to fetch, "POST" to send) and returns a handle; HttpClose frees it. Before starting it, HttpAddHeader adds a header, HttpSetBody gives a body to send with its type ("application/json", say), HttpSetBodyFile sends a file's bytes instead (a piece at a time, so a file of any size), HttpSaveTo writes the answer's body to a file instead of keeping it (with keepPart True, a file left part-way is finished rather than started again), and HttpSetTimeout sets how many seconds without progress mean the server is not answering (30 to start with). HttpStart starts it (False for an address that is not http://; there is no encryption), and HttpRunFor(h, milliseconds) runs it a little at a time, returning HTTP_BUSY until it is HTTP_DONE or HTTP_FAILED:
h = HttpOpen("GET", "http://example.com/news.txt")
IF HttpStart(h) THEN
DO
@@ -5431,7 +5533,7 @@ IF rc = HTTP_DONE AND HttpStatus(h) = 200 THEN txtNews.Text = HttpBody$(h)
IF rc = HTTP_FAILED THEN MsgBox HttpError$(h)
HttpClose h
HTTP_DONE means the server answered: HttpStatus is how (200 OK, 404 not found, ...), HttpBody$ what it sent, HttpReplyHeader$ one of its headers, and HttpUrl$ where the answer came from when the server sent the request elsewhere (redirects are followed). HTTP_FAILED means no answer came, and HttpError$ says why. HttpBytes and HttpTotal (-1 until the server says) show progress.
-
With json.bas in the project, JsonParse(text) reads JSON -- what most web services answer with -- and returns a handle (0 when it is not JSON: JsonError$ says why and on which line); JsonFree frees it. The other functions find a value by its path from the top: member names and item numbers from 0, such as "packages[3].version". JsonType says what is there (JSON_STRING, JSON_NUMBER, JSON_ARRAY, ..., JSON_NONE for nothing), JsonText$ gives a string, JsonNumber a number, JsonBool True or False, JsonCount how many items an array or members an object has, and JsonKey$(h, path, i) the name of an object's member i. JsonQuote$(text) puts text in quotes with its special characters escaped, for building JSON to send:
+
With json.bas in the project, JsonParse(text) reads JSON -- what most web services answer with -- and returns a handle (0 when it is not JSON: JsonError$ says why and on which line); JsonFree frees it. The other functions find a value by its path from the top: member names and item numbers from 0, such as "packages[3].version". JsonType says what is there (JSON_STRING, JSON_NUMBER, JSON_ARRAY, ..., JSON_NONE for nothing), JsonText$ gives a string, JsonNumber a number, JsonBool True or False, JsonCount how many items an array or members an object has, and JsonKey$(h, path, i) the name of an object's member i. JsonQuote$(text) puts text in quotes with its special characters escaped, for building JSON to send. Text comes and goes as DVX characters: an accented letter or a line-drawing character in a reply is the same character in JsonText$ ("?" stands for one DVX does not have), and JsonQuote$ writes such characters so that any web service reads them right:
h = JsonParse(HttpBody$(req))
FOR i = 0 TO JsonCount(h, "packages") - 1
lstPackages.AddItem JsonText$(h, "packages[" & CStr(i) & "].name")
@@ -5448,7 +5550,7 @@ body = "{" & JsonQuote$("name") & ": " &am
Mouse: SysSetMouse wheelDir, dblClickMs, accel, mickeys, wheelStep applies a whole mouse setup at once: the scroll wheel's direction (1 or -1, the values of PREFS_CHOICE_WHEEL), the double-click time in milliseconds, the acceleration (a value of PREFS_CHOICE_ACCEL), how far the mouse moves for 8 pixels of pointer movement (8 is normal, higher is slower), and the lines scrolled per wheel notch (1 to 10). An accel or mickeys of 0 leaves that one as it is. SysMouseWheelDir, SysMouseDoubleClick, SysMouseAccel, SysMouseMickeys, and SysMouseWheelStep read the setup in effect. MOUSE_WHEEL_STEP_MIN and MOUSE_WHEEL_STEP_MAX are the limits of the wheel step, and MOUSE_WHEEL_STEP_DEFAULT, MOUSE_DBLCLICK_DEFAULT_MS, and MOUSE_SPEED_DEFAULT are DVX's defaults (what a Default button puts back).
The pointer and the keyboard: SysMouseX() and SysMouseY() are where the mouse pointer is on the screen, and SysMouseButtons() the buttons held (vbLeftButton, vbRightButton, and vbMiddleButton bits) -- also while the pointer is over another program's window, where your MouseMove events do not see it. SysSetMousePos x, y moves the pointer (it is kept on the screen). SysShiftState() is the Shift, Ctrl, and Alt keys held now (vbShiftMask, vbCtrlMask, and vbAltMask bits). SysSendKey(keyCode, shift) types a key into the window that has the focus as if the user pressed it, like SendKeys in other BASICs: keyCode as a KeyDown event gets it -- a character code such as ASC("a") or 13 for Enter, or a vbKey constant such as vbKeyF1 -- and shift the vbShiftMask, vbCtrlMask, and vbAltMask bits held with it (SysSendKey ASC("s"), vbCtrlMask is Ctrl+S). The keys arrive in order, one at a time, once your code has returned to DVX; up to 64 can wait, and SysSendKey returns False when that many are waiting. A program cannot press the mouse buttons: to "click" a control, call its event procedure or set its value.
Memory: SysMemoryTotalKb() is the computer's memory in KB and SysMemoryFreeKb() how much of it is free (-1 when it cannot be told).
-
Libraries: some parts of DVX are optional (Setup's "What to install" page; Software lists each as one package), and so are their libraries: Networking has dvxnet, http, and pkgnet (net.bas, http.bas, pkgnet.bas), Databases has dvxsql (sql.bas), and Communications has conn and xfer (conn.bas, xfer.bas). SysLibraryLoaded(libName) is True when the library a DECLARE LIBRARY line names (any case) is installed and was started with DVX. A program that can do without such a library asks first and leaves out what needs it: calling a function of a library that is not there stops the program with "External function not found". Software, for one, keeps its package server pages off without pkgnet.
+
Libraries: some parts of DVX are optional (Setup's "What to install" page; Software lists each as one package), and so are their libraries: Networking has dvxnet, http, and pkgnet (net.bas, http.bas, pkgnet.bas), Databases has dvxsql (sql.bas), and Communications has conn and xfer (conn.bas, xfer.bas). SysLibraryLoaded(libName) is True when the library a DECLARE LIBRARY line names (any case) is installed (DVX loads a library when it is first needed, so asking loads it). A program that can do without such a library asks first and leaves out what needs it: calling a function of a library that is not there stops the program with "External function not found". Software, for one, keeps its package server pages off without pkgnet.
If SysLibraryLoaded("pkgnet") Then
handle = PkgNetOpen("")
Else
@@ -5555,6 +5657,22 @@ Loop Until quitting
shipX = shipX + JoyX(1) \ 8192
If JoyButton(1, 1) Then FireShot
End Sub
+
A program can also hear of every change instead of reading the joystick: JoyWatch joy, True starts keeping that joystick's changes (False stops), and JoyEvent() gives the next one since it was last asked, each once and in order (the last 64 are kept): JOY_EVENT_AXIS (an axis moved), JOY_EVENT_BUTTON_DOWN, JOY_EVENT_BUTTON_UP, JOY_EVENT_HAT, or JOY_EVENT_NONE when nothing has changed. JoyEventJoy() and JoyEventIndex() say which joystick and which axis, button, or hat, and JoyEventValue() an axis's new position or a hat's direction. A button pressed and let go between two Timer ticks still gives both.
+
Sub Form1_Load ()
+ ok = JoyWatch(1, True)
+End Sub
+
+Sub Timer1_Timer ()
+ Dim kind As Long
+
+ kind = JoyEvent()
+
+ Do While kind <> JOY_EVENT_NONE
+ If kind = JOY_EVENT_BUTTON_DOWN And JoyEventIndex() = 1 Then FireShot
+ If kind = JOY_EVENT_AXIS And JoyEventIndex() = 1 Then shipSpeed = JoyEventValue() \ 8192
+ kind = JoyEvent()
+ Loop
+End Sub
When no joystick is connected the keyboard stands in for one (the arrow keys, with Left Ctrl, Space, Left Shift, and Z as buttons), and it only answers while one of the program's windows is active. Joysticks are calibrated, and the keyboard joystick's keys chosen, on the Control Panel's Game Controllers tab. QBasic's STICK and STRIG work too, without joystick.bas.
joystick.bas can also set joysticks up, as the Control Panel does. JoyHats(joy) counts the hats, JoyAxisPresent(joy, axis) is True when an axis answers, JoyConnected(joy) is False once a joystick stops answering, and JoyCanCalibrate(joy) is True for one with analog axes. For calibrating, JoyRawAxis(joy, axis) gives an axis's raw reading and JoyRawButtons(joy) the buttons held (1 for button 1, 2 for button 2, 4 for button 3, ... added up); JoySetCalibration(joy, axis, minV, centerV, maxV) takes the readings at one end, the middle, and the other end, and JoyClearCalibration(joy, axis) lets the axis find its own range again. JoyDeadZone(joy) and JoySetDeadZone(joy, percent) are the dead zone round the middle (0 to 50 percent). JoyChoiceCount, JoyChoiceLabel$, JoyChoice, and JoySetChoice list and change the game port's mode (JOY_CHOICE_GAMEPORT) and when the keyboard joystick stands in (JOY_CHOICE_FALLBACK); JoyKeyCount and JoyKeyName$(index) list the keys the keyboard joystick can use, and JoyKbdSlot(slot) and JoySetKbdSlot(slot, keyIndex) its keys for up, down, left, right, and buttons 1 to 4 (slots 0 to 7). These lists are numbered from 0, so they fill a DropDown in order. JoyKeyIndex(keyName) gives a key's number by its name (-1 when unknown), JoyKeyCode(index) its raw key code, which GameKeyDown in game.bas takes, and JoyKeyNameOf$(rawCode) the name of a raw key code. JoyIsKeyboard(joy) is True for the keyboard standing in for a joystick, and JoyCalibration(joy, axis, part) reads back an axis's calibration: part JOY_CAL_MIN, JOY_CAL_CENTER, or JOY_CAL_MAX of the readings set with JoySetCalibration, or of the range the axis found by itself (0 for a joystick that cannot be calibrated). JoySetting$(key, default) and JoySetSetting key, value reach any other joystick setting; JOY_KEY_POLL_HZ is the key of how often joysticks are read, JOY_POLL_HZ_MIN to JOY_POLL_HZ_MAX times a second (JOY_POLL_HZ_DEFAULT normally), and dead zones go up to JOY_DEADZONE_MAX percent (JOY_DEADZONE_DEFAULT normally). After a new game port mode, JoyRescan() looks for joysticks again; it returns the new count, or -1 while another program is using a joystick. Every change applies at once; JoySettingsStore handle puts them all into a settings handle from prefs.bas for the program to save, and JoySettingsLoad handle takes them back from one, to undo.
Sub cmdCenter_Click ()
@@ -5581,8 +5699,17 @@ Sub cmdFire_Click ()
End If
End Sub
WAV files must be uncompressed (8 or 16 bits, mono or stereo); VOC files may be 8- or 16-bit. QBasic's BEEP, SOUND, and PLAY work too, without sound.bas.
-
PlayTriangle(frequency, milliseconds, volume) plays a triangle tone, softer than PlayTone's square tone. SoundGetVolume() gives the volume of everything the program plays. SoundCaps(field) tells what the sound device can do, by name in any case: "dac" (it plays recorded sound), "synth" (it has a music synthesizer), "tone" (it plays tones on a tone generator), "running", and "record" (it records sound) are True or False; "voices" (how many sounds play at once), "rate" (hertz), "bits", "channels", and "recordsources" (the inputs it records from) are numbers. SoundPlayEvent eventIdx, wait plays one of DVX's own event sounds (SOUND_EVENT_DEFAULT, SOUND_EVENT_ERROR, SOUND_EVENT_WARNING, ...), the one the settings choose; with wait True it returns once the sound has played.
-
A sound played often is best loaded once. SoundLoad(fileName) loads a WAV or VOC file and returns a sound number, or 0 (SoundError$() says why); SoundCreate(data, rate, bits, channels) makes a sound from raw sound data in a string: 8-bit samples (0 to 255, 128 is silence) or 16-bit ones (two bytes each, the low byte first), mono or stereo (left, right, left, ...), rate times a second (SOUND_RATE_MIN to SOUND_RATE_MAX). SoundPlay(sound, volume, pan, loopIt) plays it on a new voice and returns the voice number; pan goes from SOUND_PAN_LEFT (-100) through SOUND_PAN_CENTER (0) to SOUND_PAN_RIGHT (100). SoundLength(sound) is its length in milliseconds and SoundFree sound stops its voices and forgets it. While a voice plays, SoundSetVolume voice, volume, SoundSetPan voice, pan, and SoundSetPitch voice, percent change it (pitch is a percent of the recorded speed: SOUND_PITCH_NORMAL is 100, 200 is an octave higher, from SOUND_PITCH_MIN 25 to SOUND_PITCH_MAX 400), and SoundSetFrequency voice, hertz changes a tone's frequency. SoundPause voice holds a sound's voice where it is, silent, and SoundResume voice goes on from there (a tone cannot be paused). A program's sounds are freed when it ends.
+
PlayTriangle(frequency, milliseconds, volume) plays a triangle tone, softer than PlayTone's square tone. SoundGetVolume() gives the volume of everything the program plays. SoundCaps(field) tells what the sound device can do, by name in any case: "dac" (it plays recorded sound), "synth" (it has a music synthesizer), "tone" (it plays tones on a tone generator), "running", and "record" (it records sound) are True or False; "voices" (how many sounds play at once), "rate" (hertz), "bits", "channels", and "recordsources" (the inputs it records from) are numbers. SoundDriver$() is the short name of the sound driver playing ("sb", "speaker", ...; "" when none is), and SoundStats(field) what the mixer has done since the device started: "renders" (blocks of sound made), "frames" (samples made), "underruns" (stream samples that were due but not yet written), "clipped" (samples too loud), "dropped" (sounds dropped because too many played at once), and "playing" (voices playing now). SoundPlayEvent eventIdx, wait plays one of DVX's own event sounds (SOUND_EVENT_DEFAULT, SOUND_EVENT_ERROR, SOUND_EVENT_WARNING, ...), the one the settings choose; with wait True it returns once the sound has played (DVX and the program's events go on meanwhile, as in DoEvents; in a control file's code, and once the program is ending, it does not wait).
+
A sound played often is best loaded once. SoundLoad(fileName) loads a WAV or VOC file and returns a sound number, or 0 (SoundError$() says why); it reads and decodes the whole file before it returns, so for a long recording (a minute or more) use MusicLoad and MusicPlay (music.bas), which play a WAV or VOC file as they read it; SoundCreate(data, rate, bits, channels) makes a sound from raw sound data in a string: 8-bit samples (0 to 255, 128 is silence) or 16-bit ones (two bytes each, the low byte first), mono or stereo (left, right, left, ...), rate times a second (SOUND_RATE_MIN to SOUND_RATE_MAX). SoundLoadData(bytes) loads one from the bytes of a WAV or VOC file held in a string, such as ResGetData$ (resource.bas) or HttpBody$ (http.bas) gives. SoundPlay(sound, volume, pan, loopIt) plays it on a new voice and returns the voice number; pan goes from SOUND_PAN_LEFT (-100) through SOUND_PAN_CENTER (0) to SOUND_PAN_RIGHT (100). SoundLength(sound) is its length in milliseconds, SoundRate(sound), SoundBits(sound), and SoundChannels(sound) how it was recorded, and SoundFree sound stops its voices and forgets it. While a voice plays, SoundSetVolume voice, volume, SoundSetPan voice, pan, and SoundSetPitch voice, percent change it (pitch is a percent of the recorded speed: SOUND_PITCH_NORMAL is 100, 200 is an octave higher, from SOUND_PITCH_MIN 25 to SOUND_PITCH_MAX 400), and SoundSetFrequency voice, hertz changes a tone's frequency. SoundPause voice holds a sound's voice where it is, silent, and SoundResume voice goes on from there (a tone cannot be paused). A program's sounds are freed when it ends.
+
To hear of every sound ending, call SoundEnded() from a Timer: it gives the next of the program's voices that ended since it was last asked (each once, in the order they ended; the last 64 are kept), or 0 when none has, and SoundEndReason() says why that one ended: SOUND_END_FINISHED (it played to its end), SOUND_END_STOPPED (StopWave, StopAllSounds, or its sound was freed), or SOUND_END_DROPPED (too many sounds played at once and it was the quietest).
+
Sub Timer1_Timer ()
+ Dim v As Long
+ v = SoundEnded()
+ Do While v <> 0
+ If v = engine And SoundEndReason() = SOUND_END_FINISHED Then engine = SoundPlay(idle, 60, 0, True)
+ v = SoundEnded()
+ Loop
+End Sub
Dim Shared laser As Long
Sub Form1_Load ()
@@ -5601,13 +5728,18 @@ End Sub
pending = Mid$(pending, taken + 1)
If Len(pending) = 0 Then pending = MakeMoreSound()
End Sub
-
A program can record sound from the sound card's microphone, line, or CD input into a WAV file. SoundCaps("record") says whether the card can, and SoundCaps("recordsources") which inputs it has (the bit 2 ^ RECORD_MIC, RECORD_LINE, or RECORD_CD set for each). RecordStart(fileName, rate, bits, channels, inputSource) starts recording (0 for 22,050 Hz, 16-bit, mono) and returns True, or False with SoundError$ saying why; the card records at its nearest, which RecordRate(), RecordBits(), and RecordChannels() tell. While it records, RecordLength() is how long the recording is (milliseconds) and RecordLevel() the loudest sound since it was last asked (percent: a level meter); RecordLost() counts samples dropped because DVX was too busy. RecordStop() ends it and returns True when the file was all written; Recording() is True in between. Most sound cards record or play, not both, so nothing plays while a program records, and one program records at a time. A program's recording ends when the program does.
+
A melody of tones -- a game's tune or a sound effect -- plays without a gap or a stumble in a note queue. NotesOpen(wave, volume) opens one in a waveform (SOUND_WAVE_SQUARE, SOUND_WAVE_TRIANGLE, or SOUND_WAVE_NOISE) and returns its number, or 0. NotesAdd(queue, hertz, milliseconds) adds a tone (0 hertz for a rest; fractions count) and returns True, or False when 256 are already waiting; each lasts exactly as long as asked, as long as the next is added before the one playing ends. NotesSetTone(queue, wave, volume) changes the waveform and volume (percent of the queue's) of the tones added from then on, so they change in time with the tune. NotesQueued(queue) is how many tones and rests have not finished (0 once it has gone quiet) and NotesSpace(queue) how many more fit; NotesVoice(queue) is its voice, for SoundSetVolume and SoundSetPan. NotesClear queue silences it and forgets what it holds, and NotesClose queue closes it. PLAY's V and @ commands give PLAY tunes a volume and a waveform too.
+
q = NotesOpen(SOUND_WAVE_TRIANGLE, 80)
+For f = 200 To 800 Step 50
+ ok = NotesAdd(q, f, 15) ' a rising sweep, 15 ms a step
+Next f
+
A program can record sound from the sound card's microphone, line, or CD input into a WAV file, or into memory. SoundCaps("record") says whether the card can, and SoundCaps("recordsources") which inputs it has (the bit 2 ^ RECORD_MIC, RECORD_LINE, or RECORD_CD set for each). RecordStart(fileName, rate, bits, channels, inputSource) starts recording (0 for 22,050 Hz, 16-bit, mono) and returns True, or False with SoundError$ saying why; the card records at its nearest, which RecordRate(), RecordBits(), and RecordChannels() tell. While it records, RecordLength() is how long the recording is (milliseconds) and RecordLevel() the loudest sound since it was last asked (percent: a level meter); RecordLost() counts samples dropped because DVX was too busy. RecordStop() ends it and returns True when the file was all written; Recording() is True in between. With a file name of "", RecordStart records into memory instead: RecordRead$(maxBytes) takes up to maxBytes of what was recorded and not yet taken, as raw sound data in the recording's format (as SoundCreate and SoundStreamWrite take it), and RecordQueued() is how many bytes are waiting. Take them often, from a Timer: about two seconds are kept, and what does not fit is lost (RecordLost()). A program can show the sound as it comes, change it, or send it over the network. Most sound cards record or play, not both, so nothing plays while a program records, and one program records at a time. A program's recording ends when the program does.
If RecordStart("NOTE.WAV", 0, 0, 0, RECORD_MIC) Then
' ... a Timer shows RecordLength() and RecordLevel()
ok = RecordStop()
End If
On a computer with an FM synthesizer chip (AdLib and most Sound Blasters), a program can play instruments of its own. FmInfo$(field) describes the chip: "description" (like "OPL3 FM synthesizer at 388h", "" when there is none), "voices", "free" (voices no program has), "stereo", and "owned" (one program has the whole chip). FmAlloc() takes one of its two-operator voices (0 when none is free), FmWrite(voice, register, value) sets one of the voice's registers (the FM_ constants: FM_MOD_CHAR, FM_CAR_LEVEL, FM_FNUM_LOW, FM_KEY_BLOCK with FM_KEY_ON, ...; True when it worked), and FmFree voice gives it back. A player of chip music files can take the whole chip with FmRawAcquire() (False while another program has it), set any register with FmRawWrite(register, value) (bank * FM_BANK_REGS + register; the second bank only on an OPL3) and give it back with FmRawRelease. A frequency number for FM_FNUM_LOW and FM_KEY_BLOCK is hertz * 2 ^ 20 / (FM_CHIP_HZ * 2 ^ block).
-
On a wavetable sound card (a Gravis UltraSound, a Sound Blaster AWE32 or AWE64), a program can play recorded sounds on the card's own voices from the card's memory. WtInfo$(field) describes the card: "description" ("" when there is none), "voices", "free" (voices no program has), "memory" and "freememory" (bytes), "level" (how many centibels louder a voice at full volume is than the loudest recorded sound the card plays: 0 when it is the same or not known), and what the card does itself ("envelopes", "lfos", "filter", "effects", "8bit", "rom"; True or False as text). WtMemAlloc(bytes, bits) takes memory for samples of 8 or 16 bits (its address, -1 when there is no room), WtUpload(address, pcm, bits) copies 16-bit mono samples there (a binary string, two bytes a sample; True when it worked), and WtMemFree address gives it back. WtAlloc() takes a voice (0 when none is free); WtStart(voice, address, startFrame, endFrame, loopStart, loopEnd, loopMode, bits, pitch, attenuation, pan) plays a sample on it from startFrame (0: its first; loopMode 0 plays once, 1 loops, 3 loops until WtRelease; pitch in cents, 0 for 44,100 samples a second and 1200 an octave higher; attenuation in centibels, 0 loudest and 60 half as loud; pan -100 to 100). WtSetPitch, WtSetAttenuation, and WtSetPan change a playing voice, WtStop silences it, WtActive(voice) is True while it sounds, and WtFree voice gives it back. Everything a program took is given back when it ends.
+
On a wavetable sound card (a Gravis UltraSound, a Sound Blaster AWE32 or AWE64), a program can play recorded sounds on the card's own voices from the card's memory. WtInfo$(field) describes the card: "description" ("" when there is none), "voices", "free" (voices no program has), "memory", "freememory" (the largest free block), and "freetotal" (all free, in blocks maybe apart; bytes), "level" (how many centibels louder a voice at full volume is than the loudest recorded sound the card plays: 0 when it is the same or not known), and what the card does itself ("envelopes", "lfos", "filter", "effects", "8bit", "rom"; True or False as text). WtMemAlloc(bytes, bits) takes memory for samples of 8 or 16 bits (its address, -1 when there is no room), WtUpload(address, pcm, bits) copies 16-bit mono samples there (a binary string, two bytes a sample; True when it worked), and WtMemFree address gives it back. WtAlloc() takes a voice (0 when none is free); WtStart(voice, address, startFrame, endFrame, loopStart, loopEnd, loopMode, bits, pitch, attenuation, pan) plays a sample on it from startFrame (0: its first; loopMode 0 plays once, 1 loops, 3 loops until WtRelease; pitch in cents, 0 for 44,100 samples a second and 1200 an octave higher; attenuation in centibels, 0 loudest and 60 half as loud; pan -100 to 100); on a card with a General MIDI sample ROM (WtInfo$("rom")) the address may be WT_ADDR_ROM plus the bytes from the ROM's start. WtSetPitch, WtSetAttenuation, and WtSetPan change a playing voice, WtStop silences it, WtActive(voice) is True while it sounds, and WtFree voice gives it back. WtShape(voice, field, value) shapes the voice's notes as a SoundFont instrument does, one value by name, where the card can (WtInfo$ "envelopes", "lfos", "filter", "effects"): the volume envelope ("voldelay", "volattack", "volhold", "voldecay", "volsustain", "volrelease"), the modulation envelope ("moddelay" to "modrelease", "modenvpitch", "modenvfilter"), the modulation and vibrato LFOs ("modlfodelay", "modlfofreq", "modlfopitch", "modlfofilter", "modlfovolume", "viblfodelay", "viblfofreq", "viblfopitch"), the filter ("filterfc", "filterq"), and the effects ("reverb", "chorus"), in the units sound.bas lists. A value is kept for the voice's next WtStart; the filter, the effects, and the LFO depths also change a note that is sounding. Everything a program took is given back when it ends.
sound.bas also changes DVX's own sound settings, as the Control Panel's Sounds page does. DVX plays a sound for each of SoundEventCount() events, numbered from 0: SoundEventLabel$(eventIdx) names one ("Start DVX", "Error", ...) and SoundEventKey$(eventIdx) gives its key in DVX.INI. With a settings handle from prefs.bas (PrefsLoad(PrefsDvxIni$())), SoundEventFile$(handle, eventIdx) is the event's sound file ("" for none) and SoundSetEventFile handle, eventIdx, fileName changes it; after the program saves the file, SoundEventsReload makes DVX play the new sounds. SoundTestFile(fileName) plays a file the way DVX plays its event sounds (0 when it cannot, and SoundError$() says why), and SoundFolder$() is the folder of DVX's own sounds. SoundMasterVolume() and SoundSetMasterVolume percent are the volume of all sound from every program. SoundDriverCount() reads the list of sound drivers and SoundDriverName$(index) names each; SoundDriverAuto$() is the setting that means "find the best one". SoundMidiCount() and SoundMidiLabel$(index) list where MIDI music can play, SoundMidiInCount() and SoundMidiInLabel$(index) where MIDI input can come from, and SoundModulesCount() and SoundModulesLabel$(index) where music modules can play. SoundSettingDriver$(handle), SoundSettingMidi(handle), SoundSettingModules(handle), and SoundSettingVolume(handle) read the sound driver, MIDI choice, modules choice, and master volume from the settings, and SoundSettingsStore handle, driver, midi, modules, volume stores them (a new driver or MIDI choice takes effect when DVX next starts, a modules choice with the next music); SoundSettingMidiIn(handle) and SoundSettingMidiInStore handle, midiIn read and store the MIDI input choice (it takes effect when DVX next starts).
Dim ini As Long
Dim i As Long
@@ -5620,7 +5752,7 @@ Next i
With music.bas in the project, a program plays music files: tracker modules (MOD, S3M, XM, and IT files, made of recorded instrument sounds) wherever the computer can play sampled sound (on a Gravis UltraSound's or Sound Blaster AWE's own voices, which takes the computer little time), MIDI files (.MID, types 0 and 1) on the computer's music synthesizer, or on a built-in synthesizer that sounds much the same when there is none, and WAV and VOC sound files wherever the computer can play sampled sound (a long WAV recording is read from the disk as it plays, so it needs no memory). Several programs can play music at once, and everything a program plays stops when it ends.
-
MusicLoad(fileName) loads a music file and returns its music number, or MUSIC_FAILED (0) when it could not be loaded; MusicError$() then says why. MusicPlay(music, loopIt) plays it from where it is (the start after loading or stopping); with loopIt True it starts over at the end until it is stopped. MusicStop(music) stops and goes back to the start, MusicPause(music) and MusicResume(music) pause and go on, and MusicPlaying(music) is True while it plays. MusicVolume(music, volume) sets its volume in percent (0 to 100). MusicPosition(music) and MusicLength(music) give the place and the length in milliseconds, and MusicSeek(music, milliseconds) goes to a place (a module goes to the start of the part that holds it). MusicFree(music) stops a music and forgets it.
+
MusicLoad(fileName) loads a music file and returns its music number, or MUSIC_FAILED (0) when it could not be loaded; MusicError$() then says why. MusicLoadData(bytes) does the same with the bytes of a music or sound file held in a string (from ResGetData$ or HttpBody$, say). MusicPlay(music, loopIt) plays it from where it is (the start after loading or stopping); with loopIt True it starts over at the end until it is stopped. MusicStop(music) stops and goes back to the start, MusicPause(music) and MusicResume(music) pause and go on, and MusicPlaying(music) is True while it plays. MusicEnded(music) is True once after the music played to its end (not when it was stopped) since MusicPlay started it: a Timer that asks it can start the next tune on time. MusicVolume(music, volume) sets its volume in percent (0 to 100). MusicPosition(music) and MusicLength(music) give the place and the length in milliseconds, and MusicSeek(music, milliseconds) goes to a place (a module goes to the start of the part that holds it). MusicFree(music) stops a music and forgets it.
MusicLoadMacro(macro) makes a music from a music macro string, the kind PLAY takes, with its notes on the General MIDI instruments its I commands name (the piano before any I): the tune can then be played in the background, paused, stopped, and made quieter like any music. MIDI music plays on the instruments chosen in Control Panel; MusicSetBank music, bank plays one music on another instrument bank, named as in the banks folders ("" is DVX's standard bank), before the music first plays (MusicError$ says when the bank is missing or damaged). A SoundFont (.SF2 or .SBK) gives the recorded instruments for a wavetable card or the built-in SoundFont synthesizer, as can a Gravis UltraSound's own patches: MusicPatchSet$() names them (their ULTRASND.INI, "" when there are none), and MusicRomBank$() names the instruments built into a Sound Blaster AWE card ("" when Creative's software is not installed).
tune = MusicLoadMacro("T150 I41 O4 L8 CEG>C4")
MusicSetBank tune, "DMXOPL.OP2"
@@ -5709,16 +5841,24 @@ Sub Timer1_Timer ()
End Sub
Function Description
-------- -----------
- HostLookup$(hostName) The address of a name, such as "192.0.2.10", or "" when it is not found.
+ HostLookup$(hostName) The address of a name, such as "192.0.2.10", or "" when it is not found. Waits for the answer; other programs keep running.
+ HostLookupStart(hostName) The same without waiting: a lookup handle (0 when too many are under way).
+ HostLookupPoll(lookup) NET_BUSY while the lookup goes on, NET_DONE once found, NET_FAILED (NetError$ says why).
+ HostLookupResult$(lookup) The address found, once HostLookupPoll says NET_DONE.
+ HostLookupClose lookup Free a lookup handle (or give the lookup up).
HostName$(address) The name of an address; the address itself when no name is known for it.
NetAddress$() This computer's address ("" while it has none).
NetError$() Why the last call failed, in plain words.
TcpConnect(host, port) Connect; a handle, or 0. Waits up to 20 seconds; other programs keep running.
+ TcpConnectStart(host, port) The same without waiting: the connection's handle at once (0 when it cannot start).
+ TcpConnectPoll(handle) NET_BUSY while the host is looked up and the connection made, NET_DONE once it is (the handle then works as TcpConnect's), NET_FAILED (close it).
TcpListen(port) Wait for connections on port; a listening handle, or 0.
TcpAccept(handle) A connection that arrived on a listening handle, or 0 when none is waiting.
- TcpSend(handle, bytes) Send text or any other bytes; the bytes sent, or -1.
+ TcpSend(handle, bytes) Send text or any other bytes; the bytes sent, or -1. Waits while the connection's buffer is full.
+ TcpSendSome(handle, bytes) Send what the connection takes now, never waiting: the bytes taken from the start (0 while it is full), or -1.
TcpPending(handle) Bytes waiting to be read (-1 for a bad handle).
TcpRecv$(handle) What has arrived (up to 4096 bytes), or "".
+ TcpPeek$(handle) What has arrived (up to 4096 bytes), left waiting for the next TcpRecv$: look for a line's end or a message's length first.
TcpConnected(handle) 1 while the connection is open, 0 once the other side closed it.
TcpShutdown(handle) Tell the other side nothing more will be sent; what it sends can still be read.
TcpPeer$(handle) "address:port" of the other side ("" when not connected).
@@ -5732,6 +5872,25 @@ End Sub
UdpFrom$() "address:port" of the sender of the last UdpRecv$ message.
UdpClose handle Close a UDP handle.
The strings sent and received may hold any bytes, CHR$(0) too, so binary protocols work as well as text. Every connection a program opens is closed for it when the program ends.
+
HostLookup$, TcpConnect, and TcpSend wait until they are done. Other programs keep running meanwhile, but yours does not: its windows do not answer, and the first lookup after DVX starts can also wait for the network to get its settings (DHCP). To stay responsive, start the lookup or connection and check on it from a Timer:
+
Dim conn As Long
+
+Sub cmdConnect_Click ()
+ conn = TcpConnectStart("bbs.example.org", 23)
+ Timer1.Enabled = conn <> 0
+End Sub
+
+Sub Timer1_Timer ()
+ Select Case TcpConnectPoll(conn)
+ Case NET_DONE
+ Timer1.Enabled = False
+ lblStatus.Caption = "Connected"
+ Case NET_FAILED
+ Timer1.Enabled = False
+ lblStatus.Caption = "Could not connect: " & NetError$()
+ TcpClose conn
+ End Select
+End Sub
A program can also edit the network settings the way the Control Panel's Network page does, and show the network's state. The settings are kept in DVX's settings file, so NetConfigLoad and NetConfigStore take a handle from prefs.bas (add prefs.bas to the project too). NetConfigLoad gives a settings handle (0 when none is free); read and change its fields by name, check them, store them, and restart the network only when they changed:
Dim prefs As Long
Dim cfg As Long
@@ -5770,6 +5929,7 @@ End Sub
NetVectorCount() / NetVector(index) The packet driver interrupts to choose from: index 0 is 0 (find it), then each one the settings accept.
NetRestart() Stop the network and start it with the saved settings. True when it is running.
NetStop Stop the network (every program's connections are closed) until NetRestart.
+ NetState() The network's state as a number: NET_STATE_STOPPED, NET_STATE_NO_DRIVER (no network card driver found), NET_STATE_CONFIGURING (waiting for an address from the network), or NET_STATE_UP.
NetStatus$(field) The network's state as text: "message" (in plain words), "up" and "link" ("-1" / "0"), "dhcp", "address", "netmask", "gateway", "dns1", "dns2", "hostname", "driver" (the network card's driver), "mac" (its hardware address), "vector", "sockets", "rxpackets", "rxbytes", "rxdropped", "txpackets", "txbytes". "" for an unknown field.
Settings handles a program leaves open are freed for it when it ends.
CommRecvEncrypted(handle, channel) tells whether the data the last CommRecv$ on that channel returned can be trusted. It returns True when every byte of it arrived encrypted and authenticated, and also when that CommRecv$ returned nothing or CommRecv$ has not been called on the channel yet. It returns False when any byte arrived unencrypted, which can only happen before CommHandshake or on a channel opened with CommSetPlain, and for an invalid handle or a channel outside 0-127.
@@ -5797,7 +5957,7 @@ Sub Timer1_Timer ()
If ConnZmodemRequest(conn) Then download = ConnReceiveFiles(conn, XFER_ZMODEM, "DOWNLOAD", XFER_EXIST_RENAME, False)
End Sub
File Transfers
-
With xfer.bas in the project, a program sends and receives files with XMODEM, XMODEM-1K, YMODEM, or ZMODEM. A transfer is a number: XferSendFile and XferReceive start one on a serial port opened with SerOpen (comm.bas), and ConnSendFiles and ConnReceiveFiles start one on a connection (conn.bas), with the options Terminal has: escaping every control character, asking the receiver to continue interrupted files, and what to do with a file that is already there. On a serial port XferPoll moves the transfer along; on a connection ConnPoll does. XferStatus tells where it is (XFER_RUNNING until it ends with XFER_DONE, XFER_FAILED, or XFER_CANCELLED), and XferBytes, XferSize, XferFile$, XferFileIndex, XferFileCount, XferBatchBytes, XferBatchSize, XferResumedAt, and XferErrors show its progress; XferMessage$ says how it ended. A receive with XFER_EXIST_ASK stops in XFER_ASKING when a file already exists: XferAskSize, XferAskRemoteSize (the size the sender offers, -1 when it did not say), and XferCanResume describe it, and XferAnswer continues with an XFER_EXIST_ choice. XferCancel stops a transfer and XferClose frees it.
+
With xfer.bas in the project, a program sends and receives files with XMODEM, XMODEM-1K, YMODEM, or ZMODEM. A transfer is a number: XferSendFile and XferReceive start one on a serial port opened with SerOpen (comm.bas), and ConnSendFiles and ConnReceiveFiles start one on a connection (conn.bas). XferSendFiles and XferReceiveFiles (a serial port) and the Conn ones take the options Terminal has: a batch of files (one path per line), escaping every control character, asking the receiver to continue interrupted files, and what to do with a file that is already there. On a serial port XferPoll moves the transfer along; on a connection ConnPoll does. XferStatus tells where it is (XFER_RUNNING until it ends with XFER_DONE, XFER_FAILED, or XFER_CANCELLED), and XferBytes, XferSize, XferFile$, XferFileIndex, XferFileCount, XferBatchBytes, XferBatchSize, XferResumedAt, and XferErrors show its progress; XferMessage$ says how it ended. A receive with XFER_EXIST_ASK stops in XFER_ASKING when a file already exists: XferAskSize, XferAskRemoteSize (the size the sender offers, -1 when it did not say), and XferCanResume describe it, and XferAnswer continues with an XFER_EXIST_ choice. XferCancel stops a transfer and XferClose frees it.
XferCrc16(bytes) and XferCrc32(bytes) give the check values the protocols use (CRC-16 as XMODEM computes it, 0 to 65535; the CRC-32 of ZMODEM and ZIP files as a Long, negative when its top bit is set) for any bytes. XferSafeName$(name, folder) gives the name a receive into folder stores a sender's file name as: no folder or device name; a long name, its case kept, where folder's disk takes long names (a character a name cannot hold becomes _), else an 8.3 name of characters every disk accepts.
Lists You Draw Yourself
With listhelp.bas in the project, a list or icon view a program draws itself -- in a UserControl or a PictureBox -- follows the same rules as ListBox and ListView. ListSelCreate(multi, count) makes a selection for count items (several at a time when multi is True) and returns a handle; it remembers which items are selected and which one is current (the one the keyboard moves). Items are numbered from 0. Give it the clicks and keys, and ask it what to draw:
@@ -5858,26 +6018,34 @@ End Sub
Function Description
-------- -----------
SecRandom$(count) count random bytes, good for keys and salts (RND is not). SecAddEntropy bytes stirs something unpredictable (key timings, mouse positions) into the generator.
- SecDhCreate() A key exchange with fresh keys (a few seconds on a slow computer); a handle, or 0.
+ SecX25519PublicKey$(secretKey) The public key to send the other side, for a secret key of SEC_X25519_KEY_SIZE random bytes (SecRandom$).
+ SecX25519$(secretKey, otherPublicKey) The secret both sides now share, which nobody listening can work out ("" for a key the other side should not have sent). About a sixth of a second on a slow computer. Make keys from it with SecHkdf$.
+ SecX25519Start(secretKey, otherPublicKey) The same without the wait: SecX25519Step(h) then does a ninth of the work a call, returning SEC_X25519_PENDING until it is done (SEC_OK); SecX25519End$(h) gives the secret (with otherPublicKey "", the public key) and frees the handle.
+ SecAeadLock$(key, nonce, extra, bytes) bytes sealed: encrypted with a SEC_AEAD_KEY_SIZE-byte key and a SEC_AEAD_NONCE_SIZE-byte nonce, followed by a tag that proves them, and extra (sent in the clear), unchanged. Use a new nonce for every message: SecRandom$(SEC_AEAD_NONCE_SIZE) is safe.
+ SecAeadUnlock$(key, nonce, extra, sealed) What SecAeadLock$ sealed, or "" when anything was changed (or the key, nonce, or extra differs).
+ SecEd25519KeyPair$(seed) A signing key made from SecRandom$(SEC_ED25519_SEED_SIZE); keep it private. SecEd25519PublicKey$(key) is the key to give out.
+ SecEd25519Sign$(key, bytes) The signature of bytes; SecEd25519Check(signature, publicKey, bytes) is True only for exactly those bytes signed with that key.
+ SecDhCreate() The older key exchange (Diffie-Hellman), with fresh keys (a few seconds on a slow computer); a handle, or 0.
+ SecDhCreateStart() The same without the wait: SecDhStep(dh) then computes the keys a slice at a time, returning SEC_DH_PENDING until they are ready (SEC_OK).
SecDhPublicKey$(dh) This side's public key (SEC_DH_KEY_SIZE bytes), to send to the other side.
SecDhComputeSecret(dh, otherPublicKey) Combine with the other side's public key: SEC_OK, or SEC_ERR_PARAM for a key that is not acceptable.
+ SecDhComputeStart(dh, otherPublicKey) The same a slice at a time: SEC_DH_PENDING (or SEC_ERR_PARAM at once), then SecDhStep as above.
SecDhSecret$(dh) The secret both sides now share, which nobody listening can work out. Make keys from it with SecHkdf$; never use it as a key itself. SecDhFree dh frees the handle.
- SecCipherCreate(key) A cipher with a SEC_XTEA_KEY_SIZE-byte key; 0 for a key of another length.
+ SecCipherCreate(key) The older cipher (XTEA), with a SEC_XTEA_KEY_SIZE-byte key; 0 for a key of another length. It does not show tampering: add SecHmac$, or seal instead.
SecCipherCrypt$(cipher, bytes) Encrypt -- or decrypt, the same call -- bytes. SecCipherSetCounter cipher, n sets the position in the key stream; the same key and position must never encrypt two different messages. SecCipherFree cipher frees it.
SecSha256$(bytes) The SHA-256 hash (SEC_SHA256_SIZE bytes). For data in pieces: SecSha256Open, SecSha256Add h, piece, then SecSha256End$(h), which also frees the handle.
SecHmac$(key, bytes) The HMAC-SHA256 signature of bytes with key; SecHmacOpen(key), SecHmacAdd, and SecHmacEnd$ for pieces.
SecHkdf$(salt, secret, info, length) length bytes (up to SEC_HKDF_MAX_OUT) of keys made from secret; a different info gives different keys.
SecEqual(a, b) True when a and b hold the same bytes, compared in the same time wherever they differ (for signature checks).
-
' Both sides: make keys, swap public keys (here over a TCP connection), agree a cipher key
-Dim dh As Long, cipher As Long, key As String
-dh = SecDhCreate()
-TcpSend conn, SecDhPublicKey$(dh)
-' ... receive the other side's key into theirs ...
-If SecDhComputeSecret(dh, theirs) = SEC_OK Then
- key = SecHkdf$("", SecDhSecret$(dh), "chat cipher", SEC_XTEA_KEY_SIZE)
- cipher = SecCipherCreate(key)
-End If
-SecDhFree dh
+
' Both sides: make keys, swap public keys (here over a TCP connection), agree a sealing key
+Dim mine As String, theirs As String, key As String, nonce As String
+mine = SecRandom$(SEC_X25519_KEY_SIZE)
+TcpSend conn, SecX25519PublicKey$(mine)
+' ... receive the other side's public key into theirs ...
+key = SecHkdf$("", SecX25519$(mine, theirs), "chat", SEC_AEAD_KEY_SIZE)
+' Send a message: the nonce, then the sealed bytes
+nonce = SecRandom$(SEC_AEAD_NONCE_SIZE)
+TcpSend conn, nonce & SecAeadLock$(key, nonce, "", "Hello")
Handles a program leaves open are freed, and their keys wiped, when it ends.
Settings Files
With prefs.bas in the project, a program keeps its settings in an INI file the way DVX's own programs do. PrefsLoad(path) reads the file once (a file that is not there reads as empty); PrefsGet$, PrefsGetInt, and PrefsGetBool read a key of a section (each with the value to use when it is missing), and PrefsSet, PrefsSetInt, PrefsSetBool, and PrefsRemove change it in memory. PrefsSave writes it back, keeping comments and every line it did not change. To write a whole new file -- dropping sections that are no longer wanted -- fill a PrefsCreate handle and save it with PrefsSaveAs: the old file is replaced only once the new one is completely written. PrefsClose frees a handle. IniRead and IniWrite (built in) do one value at a time and read or write the whole file each time; prefs.bas is the faster choice for many settings.
@@ -5895,7 +6063,7 @@ PrefsClose ini
Next i
cboAccel.ListIndex = PrefsChoiceIndexOfValue(PREFS_CHOICE_ACCEL, SysMouseAccel())
Help Files
-
With help.bas in the project, HelpCompile(source, output) compiles a help source file (.dhs) into a help file (.hlp), True when it worked; HelpCompileMessages$() gives the compiler's errors and warnings, one per line, each with the file and line ("error: NOTES.DHS:1: directive .h1 outside of a topic"). HelpView file opens a help file in the DVX Help Viewer (a file named without a folder is looked for beside the program; "" opens the DVX user guide). HelpViewTopic file, topicId opens it at a topic, the way F1 does. A Help Viewer that is already open shows the file in its window instead of a second copy starting.
+
With help.bas in the project, HelpCompile(source, output) compiles a help source file (.dhs) into a help file (.hlp), True when it worked; its .image pictures are looked for in the source's folder. HelpCompileMessages$() gives the compiler's errors and warnings, one per line, each with the file and line ("error: NOTES.DHS:1: directive .h1 outside of a topic"). While a big file compiles, the rest of DVX and the program's own events go on, as with DoEvents: one compile runs at a time, so a second HelpCompile meanwhile returns False at once, and the compile stops if the program ends. HelpView file opens a help file in the DVX Help Viewer (a file named without a folder is looked for beside the program; "" opens the DVX user guide). HelpViewTopic file, topicId opens it at a topic, the way F1 does. A Help Viewer that is already open shows the file in its window instead of a second copy starting.
With hlpfile.bas, a program reads help files itself -- everything a help viewer needs. HlpOpen(path) opens a help file and returns a handle (0 when it is not one); HlpClose closes it (a program's open files are closed when it ends). Topics are numbered from 0: HlpTopicCount, HlpTopicTitle$ and HlpTopicId$ (the name links and HelpTopic use), HlpFindTopic(handle, topicId) (-1 when there is none), and HlpStartTopic(handle, topicId), the topic to show first (topicId when the file has it, else the file's own start page). HlpTocCount, HlpTocTitle$, HlpTocDepth, HlpTocTopic, and HlpTocExpanded give the table of contents in order -- a depth of 0 is the top level, a topic of -1 a heading that only groups others -- ready to fill a TreeView. HlpIndexCount, HlpIndexKeyword$, and HlpIndexTopic give the keyword index (sorted), for a list or DlgChoice. HlpSearch(handle, words) searches the titles, text, and keywords as the Help Viewer does and returns how many topics have every word; HlpSearchTopic(handle, n) gives them.
To show a topic, put a HelpPane control on the form and call HelpPane1.ShowTopic handle, topic: headings, paragraphs, lists, notes, code, tables, pictures, and links look as they do in the Help Viewer, and the text wraps to the control's width. A click on a link, or Enter on one reached with Tab, fires the HelpPane's LinkClick event with the ID of the topic the link names; the program decides what to do, usually show that topic and remember it for a Back button. A program that draws topics itself reads them with HlpLoadTopic(handle, topic) and HlpRecordType, HlpRecordFlags, HlpRecordText$, and HlpRecordLink$ (the HLP_REC_ constants), saves a picture for LOADPICTURE with HlpRecordImageSave, and wraps text with HlpWrap$.
Dim hlp As Long
@@ -6051,7 +6219,7 @@ End Sub
ToolTipText String R/W Tooltip text shown after the mouse hovers over the control. Empty string or unset = no tooltip.
HelpTopic String R/W Help topic opened by F1 while the control has the focus. Empty = the form's HelpTopic. F1 uses the project's help file.
DragMode Integer R/W vbManual (0, the default): the program starts a drag with the Drag method. vbAutomatic (1): pressing the left mouse button on the control starts a drag. See Drag and Drop below.
- DragIcon Picture R/W Picture shown under the mouse while the control is dragged: a picture file name (BMP, PNG, JPEG, or GIF) or a Picture object from LOADPICTURE (Set works too). Reading it gives a Picture object, which stands for the file name where a value is needed. Empty = a dotted outline the size of the control.
+ DragIcon Picture R/W Picture shown under the mouse while the control is dragged: a picture file name (BMP, PNG, JPEG, or GIF) or a Picture object from LOADPICTURE or LOADPICTUREDATA (Set works too). Reading it gives a Picture object, which stands for the file name where a value is needed. Empty = a dotted outline the size of the control.
Parent Form R The form the control is on (Text1.Parent.Caption).
Tag String R/W Any text the program wants to keep with the control (DVX does nothing with it), as in Visual Basic. Also in the .frm file.
MousePointer Integer R/W The mouse pointer shown over the control (and the controls inside it that have none of their own): vbDefault (0, the control's own pointer), vbArrow (1), vbCrosshair (2), vbSizeNESW (6), vbSizeNS (7), vbSizeNWSE (8), vbSizeWE (9), vbHourglass (11), or vbNoDrop (12). A form's MousePointer is used where no control has one, and Screen.MousePointer over every form of the program. Change it in MouseMove to show different pointers over parts of a control. Also in the .frm file; another value is an error.
@@ -6102,7 +6270,7 @@ End Sub
With DragMode set to vbAutomatic, pressing the left mouse button on the control starts the drag, and the control gets no Click, MouseDown, or MouseUp from the left button. With DragMode vbManual, start the drag yourself, usually in MouseDown. A drag started while the mouse button is up ends at the next click. During a drag no mouse events other than DragOver and DragDrop are raised.
Drop targets only react: nothing is moved or copied for you. Use Source in DragDrop to decide what to do, for example read its Name or Caption, or check what kind of control it is with TypeOf Source Is TextBox (see Operators).
Note that vbCancel is the MsgBox Cancel value (2), which as a Drag action means vbEndDrag; cancel a drag with Drag 0.
-
DragIcon takes a file name or a picture loaded with LOADPICTURE: Set Label1.DragIcon = LoadPicture("drag.bmp"). A missing file or one that is not a picture raises its error at LOADPICTURE; a file name that cannot be read when the drag starts shows the outline instead.
+
DragIcon takes a file name or a picture loaded with LOADPICTURE or LOADPICTUREDATA: Set Label1.DragIcon = LoadPicture("drag.bmp"). A missing file or one that is not a picture raises its error at LOADPICTURE; a file name that cannot be read when the drag starts shows the outline instead.
' Label1 has DragMode = 1 (vbAutomatic) in the form designer.
Sub List1_DragOver(Source As Control, X As Integer, Y As Integer, State As Integer)
If State = vbEnter Then Label2.Caption = "Drop to add " & Source.Caption
@@ -6190,7 +6358,7 @@ End Sub
vbDstInvert &H550009 NOT destination
vbGrayCopy &H1000000 The picture greyed, as DVX shows a disabled picture (DVX only)
vbTransparentCopy &H2000000 Every pixel of the picture but the key colour: a colour given after the opcode, else the colour of the picture's top-left pixel (DVX only)
-
vbGrayCopy and vbTransparentCopy copy the picture at its own size (width and height are not used): Picture1.PaintPicture "ship.bmp", x, y, 32, 32, 0, 0, 32, 32, vbTransparentCopy, vbMagenta draws a sprite.
+
vbGrayCopy and vbTransparentCopy copy the picture at its own size (width and height are not used): Picture1.PaintPicture "ship.bmp", x, y, 32, 32, 0, 0, 32, 32, vbTransparentCopy, vbMagenta draws a sprite. A picture file (or a LOADPICTURE picture) drawn again and again, a sprite in a game loop say, is read from the disk only the first time while the program runs and the file does not change (about a megabyte of pictures is kept that way).
Any of the 256 raster operations of Windows can be given by its number; the bits work on the pixel values, so with fewer than 32 bits a pixel the results follow the display's colour format.
Picture1.PaintPicture Picture2, 0, 0, 32, 32, 0, 0, 32, 32, vbSrcInvert ' XOR the picture in
Picture1.PaintPicture Picture2, 0, 0, 32, 32, 0, 0, 32, 32, vbSrcInvert ' and out again
@@ -6556,12 +6724,16 @@ End Sub
WindowState Integer 0 vbNormal (0), vbMinimized (1, shown only as an icon at the bottom of the screen), or vbMaximized (2, filling the screen). Set on a hidden form (or in the .frm file), it takes effect when the form is shown; reading it then gives the state the form will have.
Controls Collection (none) The form's controls (read-only), in the order they were made: Me.Controls.Count, Me.Controls(1) (the first), Me.Controls("Text1"), For Each c In Me.Controls. Elements of a control array are in it too but have no name key. Each use makes a new Collection; changing it does not change the form.
DesignMode Boolean False True puts the form in design mode, for a form designer: see Designing Forms below. Not in .frm files.
- Icon Picture (DVX's) The picture shown for the window while it is minimized: a picture file name (BMP, PNG, JPEG, or GIF) or a picture from LOADPICTURE (Set works too); "" puts DVX's back. Reading it gives a Picture object. Also in the .frm file. A file that is not a picture is an error.
+ Icon Picture (DVX's) The picture shown for the window while it is minimized: a picture file name (BMP, PNG, JPEG, or GIF) or a picture from LOADPICTURE or LOADPICTUREDATA (Set works too); "" puts DVX's back. Reading it gives a Picture object. Also in the .frm file. A file that is not a picture is an error.
Tag String "" Any text the program wants to keep with the form, as for controls.
MousePointer Integer 0 The mouse pointer over the form where its controls have none (see MousePointer under Common Properties).
FontName String "System" The form's font: the font of every control on it that has none of its own, and of the form's Print and TextWidth. FontSize, FontBold, FontItalic, FontUnderline, and FontStrikethru go with it, as for controls (see Fonts under Common Properties). Also in the .frm file.
ActiveControl Control (none) The control of the form that has the keyboard focus (read-only), Nothing when none of them has it: Me.ActiveControl.SelText for an Edit menu's Copy. Screen.ActiveControl is the focused control of the whole program. Not in .frm files.
- DesignSelection String "" The name of the control (Text1, or Text1(2) in a control array) whose selection handles are drawn while the form is in design mode; "" for none. Not in .frm files.
+ DesignSelection String "" The name of the control (Text1, or Text1(2) in a control array) whose selection handles are drawn while the form is in design mode; "" for none. Not in .frm files.
+ MaxWidth Integer 0 The widest the user can make the window, by dragging or maximizing; 0 is the screen's width. A window wider than a new MaxWidth shrinks to it. Also in the .frm file.
+ MaxHeight Integer 0 The tallest the window can be made, as MaxWidth; 0 is the screen's height.
+ GameInput Boolean False True for a game: while the form has the keyboard, every key goes to it -- none to F1 (help), F10 or Alt (the menu bar), menu shortcuts, or DVX's shortcut keys. Only Alt+Tab, Ctrl+Esc, and Alt+F4 still reach DVX. Also in the .frm file.
+ FullScreen Boolean False True gives the form the whole screen: a video mode as large as the form's inside (its controls and drawing, without the title bar), the form shown on it at the largest whole multiple of its size that fits, centred on black, kept up to date as it changes. The mouse works on it as in the window, and every key goes to it as with GameInput. A hidden form is shown first. When the user switches to another program (Alt+Tab, Ctrl+Esc), or the program shows a message box or another form, the desktop comes back; the form gets the whole screen again with the focus. Reads False when no video mode could be set (the form stays in its window). False gives the desktop back. Run time only; see the FullScreen event.
Maximizing a window, or restoring a maximized one, changes its size, so Resize fires. Minimizing does not change the size and fires no Resize. A minimized window keeps its maximized state: set WindowState = vbMaximized to bring it back filling the screen, vbNormal to bring it back at its normal size.
frmTerminal.Show
frmTerminal.WindowState = vbMinimized ' starts as an icon
@@ -6578,6 +6750,7 @@ frmTerminal.WindowState = vbMinimized ' starts as an icon
DragDrop Source As Control, X As Integer, Y As Integer Fires when a dragged control is dropped on an empty part of the form. X and Y are where, in the form.
DragOver Source As Control, X As Integer, Y As Integer, State As Integer Fires while a dragged control moves over an empty part of the form. State is vbEnter, vbLeave, or vbOver.
NewArgs (none) Fires when the running program is handed new arguments: COMMAND$ has changed (see New Arguments below).
+ FullScreen State As Integer With FullScreen True: dvxFullScreenActive (1) when the form has the whole screen (draw anything that must look new then), dvxFullScreenSuspended (2) when the desktop came back while another window has the focus, dvxFullScreenEnded (0) when the form's video mode could not be set again (FullScreen is False now).
DesignMouse Action As Integer, Button As Integer, X As Integer, Y As Integer, CtrlName As String, Handle As Integer In design mode: the mouse was pressed, moved, released, or double-clicked in the form. See Designing Forms below.
DesignKey KeyCode As Integer, Shift As Integer In design mode: a key was pressed in the form (KeyCode and Shift as in KeyDown).
DesignPropertyChanged CtrlName As String, PropertyName As String In design mode: the code of a UserControl on the form called PropertyChanged. See Designing Forms below.
@@ -6802,6 +6975,7 @@ Count = 1
Property Type Description
---------- ------- -------------------------------------------
Cols Integer Number of character columns (1-512). Setting Cols or Rows gives the terminal a new size, starting as a new terminal: empty screen and scrollback, modes the host set reset (its colors and connection stay); set the form's AutoSize to True afterwards to fit the window to it.
+ Music Boolean Whether ANSI music sequences (ESC[M, ESC[N, ESC[| followed by PLAY text, as BBSes send) are recognised (True at first): each raises the Music event instead of being shown. False: ESC[M is always Delete Line, and ESC[N and ESC[| do nothing.
Rows Integer Number of character rows (1-512).
Scrollback Integer Maximum number of scrollback lines. Older lines are dropped (default 500). Setting it discards the existing history.
Type-Specific Methods
@@ -6809,12 +6983,18 @@ Count = 1
------ -----------
Clear Clear the terminal screen and reset the cursor to the home position.
Copy() Copy the selected text to the clipboard and clear the selection. Returns False when nothing is selected.
- Paste Send the clipboard's text to the attached connection as if it were typed (new lines as Enter), as Ctrl+V does.
+ Paste Send the clipboard's text to the attached connection as if it were typed (new lines as Enter), as Ctrl+V does. With no connection attached it goes to the Send event.
Poll Poll the attached communication channel for incoming bytes and feed them into the ANSI parser.
ResetColors Back to the standard 16 colors.
SetColor index%, color& The color (RGB(), as ForeColor takes it) of text attribute index (0-15, in CGA order: black, blue, green, cyan, red, magenta, brown, light gray, then their bright forms). An index outside 0-15 is ignored.
- Write text$ Write a string to the terminal with full ANSI escape processing.
-
No default event. Uses common events only.
+ Write text$ Write a string to the terminal with full ANSI escape processing. Every byte of the string is written, CHR$(0) included (a CHR$(0) itself is padding and shows nothing, as on other terminals), so text read from a connection of the program's own can be written as it came. A very long text appears over the next few moments, a part at a time, so the program and the rest of DVX keep going; Clear after it still clears all of it.
+
Type-Specific Events
+
Event Description
+ ----- -----------
+ Bell A bell character (BEL, CHR$(7)) arrived. The terminal makes no sound itself: the program decides (Beep, a sound of its own, or nothing).
+ Music (Macro As String) An ANSI music sequence arrived (Music True): Macro is its PLAY text, to play with PLAY or MusicLoadMacro.
+ Send (Bytes As String) The terminal has bytes for the host and no connection is attached: the keys the user typed, a paste, or the terminal's answer to the host asking where the cursor is. A program with a connection of its own (a TCP socket, a modem it drives itself) sends Bytes on. Bytes may hold any character, CHR$(0) included.
+
No default event. The common events are raised too.
Serial Communication
The conn.bas library connects a Terminal the way the Terminal program does -- a serial port, a modem, the secure link, or Telnet -- with capture, answerback, and file transfers (ConnAttach; see Include Libraries). For a packet link of your own, use the comm.bas library:
Dim link As Long
@@ -6827,6 +7007,15 @@ If link <> 0 Then
End If
End If
The passphrase is optional. Both ends must give the same passphrase, or both give none; otherwise CommHandshake returns False. See comm.bas for the full communications API.
+
A Connection of Your Own
+
Without an attached connection, Write shows what the program received and the Send event hands over what the terminal would send back:
+
Sub Sock_Data ()
+ Terminal1.Write ReceivedText$
+End Sub
+
+Sub Terminal1_Send (Bytes As String)
+ SendToHost Bytes
+End Sub
Example
Begin Terminal Terminal1
MinWidth = 640
@@ -6955,7 +7144,8 @@ End Sub
-------- ---- -----------
BorderStyle Enum 0 - None: no frame, the control is exactly the size of the drawing. 1 - FixedSingle (the default): a sunken frame 2 pixels wide around the drawing. Mouse coordinates and drawing positions start inside the frame either way.
ImageHeight Integer Height of the drawing in pixels (read-only). After Load it is the loaded image's height.
- ImageWidth Integer Width of the drawing in pixels (read-only). After Load it is the loaded image's width.
+ ImageWidth Integer Width of the drawing in pixels (read-only). After Load it is the loaded image's width.
+ Picture String The picture file the canvas shows ("" when it shows none). Setting it to a file name, or to LoadPicture(path$), loads that file as Load does; LoadPictureData(bytes$) shows a picture from bytes; setting it to "" (or LoadPicture("")) clears the canvas to white. Clear and Resize wipe the picture, so Picture becomes "". The default property: Picture1 = "logo.bmp" loads it.
Load and Save return 0 on success and -1 on failure (missing or unreadable file, unsupported format, path that cannot be written). Call them as functions to check the result, or as statements to ignore it. A failed Load leaves the drawing unchanged.
Type-Specific Methods
Method Description
@@ -6972,6 +7162,7 @@ End Sub
Save(path$) Save the canvas: BMP when path$ ends in .bmp, PNG otherwise. Returns 0 on success, -1 on failure.
SetPenColor color% Set the drawing color for subsequent operations.
SetPenSize size% Set the pen/brush size in pixels (1 to 255; larger values use 255).
+ SetPictureData(bytes$ [, name$]) Show the picture held in bytes$ (the bytes of a BMP, PNG, JPEG, or GIF file, such as HttpBody$ or ResGetData$ gives) as Load shows a file, without keeping a Picture object as Picture = LoadPictureData(bytes$) does. Picture then reads name$ ("" without it). Returns True, or False (the drawing unchanged) when the bytes are not a picture.
SetPixel x%, y%, color% Set the color of a single pixel.
Default Event: Click
Example
@@ -7016,7 +7207,7 @@ End Sub
ComboBox
VB Equivalent: ComboBox -- DVX Widget: combobox (editable text field + drop-down list, default maxLen 256)
-
A combination of a single-line text input and a drop-down list. The user can either type a value or pick one from the list. Selecting a list item copies its text into the edit field. Supports the same AddItem / RemoveItem / Clear / List methods as ListBox.
+
A combination of a single-line text input and a drop-down list. The user can either type a value or pick one from the list. Selecting a list item copies its text into the edit field. Supports the same AddItem / RemoveItem / Clear / SetItems / List methods as ListBox.
Its font must be fixed-pitch, every character the same width; the designer's Font dialog offers only those families for it. Given a proportional font it draws in the system font instead.
Type-Specific Properties
Property Type Description
@@ -7031,7 +7222,8 @@ End Sub
Clear Remove all items.
List(index%) Return the text of the item at the given index.
ListCount() Return the number of items in the list.
- RemoveItem index% Remove the item at the given index. The selection stays on the same item; removing the selected item leaves no selection (the text is kept).
+ RemoveItem index% Remove the item at the given index. The selection stays on the same item; removing the selected item leaves no selection (the text is kept).
+ SetItems items$ Replace all the items at once from a "|"-separated list ("Red|Green|Blue"; every "|" separates two items, "" empties the list). Nothing is selected afterwards. The text is kept.
Default Event: Click
Example
Begin ComboBox Combo1
@@ -7053,6 +7245,13 @@ End Sub
Data
VB Equivalent: Data -- DVX Widget: data | Name Prefix: Data
A data access control that connects to a SQLite database and provides record navigation. Other controls bind to a Data control via their DataSource and DataField properties. The Data control cache is populated by calling Refresh. Move operations and Refresh auto-save the current row if dirty; a move is refused while the save fails, and Refresh discards an edit that cannot be saved. Updates find the record by the key value it was loaded with, so the key column itself can be edited; a key stored as a number in a column without a declared type, or a missing (NULL) key, is still found as long as it identifies exactly one record. Writing a field back with the value it already has is not an edit and does not fire Validate. While a Validate handler runs, MoveFirst, MoveLast, MoveNext, MovePrevious, AddNew, Refresh, and Update on the same Data control are ignored (they would save, and validate, the same record again); Delete is allowed. A RecordSource that is a SELECT query is read-only, and edits made to its records are dropped when the current record changes.
+
A big record set is read a part at a time: Refresh reads the first 64 records, and the rest arrive while your program and DVX go on, so RecordCount grows until every record is in. MoveLast, AddNew, Update, and Delete read the rest first, and MoveNext and AbsolutePosition read as far as they need. Until every record is in, other programs (and SQLExec) cannot change that database: their changes fail with "database is locked".
+
EOF and BOF work as in Visual Basic, so a loop like this reads every record once:
+
Data1.Refresh
+Do Until Data1.EOF
+ total = total + Val(Data1.GetField("amount"))
+ Data1.MoveNext
+Loop
Property Type Description
@@ -7061,19 +7260,19 @@ End Sub
RecordSource String Table name or SQL SELECT query for the recordset.
KeyColumn String Primary key column name (used for UPDATE/DELETE operations).
Caption String Text displayed on the navigator bar.
- BOF Boolean True if the current position is the first record or there are no records (read-only).
- EOF Boolean True if the current position is past the last record or there are no records (read-only).
+ BOF Boolean True after MovePrevious at the first record (which stays the current record), or when there are no records; moving onto a record clears it (read-only).
+ EOF Boolean True after MoveNext at the last record (which stays the current record), or when there are no records; moving onto a record clears it (read-only).
MasterSource String Name of a master Data control (for master-detail binding).
MasterField String Column in the master recordset to read for the filter value.
DetailField String Column in this recordset that matches the master field.
- RecordCount Long The number of records (read-only).
+ RecordCount Long The number of records read so far: all of them once the rest has arrived, or after MoveLast (read-only).
FieldCount Integer The number of columns (read-only).
AbsolutePosition Long The current record, 0 for the first (-1 when there are none). Setting it moves there (saving pending changes first, as the Move methods do) and fires Reposition; a value out of range does nothing.
Type-Specific Methods
Method Parameters Description
------------ ---------- -------------------------------------------
MoveFirst (none) Navigate to the first record.
- MoveLast (none) Navigate to the last record.
+ MoveLast (none) Navigate to the last record (every record is read first).
MoveNext (none) Navigate to the next record.
MovePrevious (none) Navigate to the previous record.
AddNew (none) Append a blank record and move to it.
@@ -7112,13 +7311,14 @@ End Sub
A data-bound grid that displays records from a Data control in a tabular format. Columns auto-populate from the query results. Bind the grid using the DataSource property. Click a column header to sort; drag a column border to resize. The highlighted row is always the bound Data control's current record: selecting a row moves the Data control's cursor, and navigating the Data control moves the highlight. Re-binding keeps the sort order and column settings, and a column width set by dragging a border is kept. Text wider than its column is clipped. The mouse wheel scrolls the rows without changing the current record.
+
A data-bound grid that displays records from a Data control in a tabular format. Columns auto-populate from the query results. Bind the grid using the DataSource property. Click a column header to sort; drag a column border to resize. The highlighted row is always the bound Data control's current record: selecting a row moves the Data control's cursor, and navigating the Data control moves the highlight. Re-binding keeps the sort order and column settings, and a column width set by dragging a border is kept. A column's automatic width fits its header and its first 200 records; text wider than its column is clipped. Records that arrive later (a Data control reads a big record set a part at a time) appear as they come, in the grid's sort order. The mouse wheel scrolls the rows without changing the current record.
Type-Specific Properties
Property Type Description
---------- ------- -------------------------------------------
DataSource String Name of the Data control that supplies records.
GridLines Boolean Show or hide grid lines between cells (default: True).
- SelectedRow Integer Index of the current record's row (-1 = no records). Setting it moves the Data control's cursor; negative values are ignored.
+ SelectedRow Integer Index of the current record's row (-1 = no records). Setting it moves the Data control's cursor; negative values are ignored.
+ Text String The current record's first visible column ("" with no record), read-only. It is the grid's default property, so Print DBGrid1 prints it. For another field, read the Data control's recordset or the field's bound control.
Type-Specific Methods
Method Parameters Description
---------------- ------------------------------- -------------------------------------------
@@ -7157,7 +7357,7 @@ End Sub
A read-only drop-down list. Unlike ComboBox, the user cannot type free text; they can only select from the provided items. Supports AddItem / RemoveItem / Clear / List. Use the Up and Down keys to change the choice. F4, Alt+Down, Space, or Enter opens the list; in the open list use the arrow keys to move and Enter (or F4, Alt+Down) to pick. Esc closes the list without changing the choice.
+
A read-only drop-down list. Unlike ComboBox, the user cannot type free text; they can only select from the provided items. Supports AddItem / RemoveItem / Clear / SetItems / List. Use the Up and Down keys to change the choice. F4, Alt+Down, Space, or Enter opens the list; in the open list use the arrow keys to move and Enter (or F4, Alt+Down) to pick. Esc closes the list without changing the choice.
Type-Specific Properties
Property Type Description
--------- ------- -------------------------------------------
@@ -7169,7 +7369,8 @@ End Sub
Clear Remove all items.
List(index%) Return the text of the item at the given index.
ListCount() Return the number of items in the list.
- RemoveItem index% Remove the item at the given index. The selection stays on the same item; removing the selected item leaves no selection.
+ RemoveItem index% Remove the item at the given index. The selection stays on the same item; removing the selected item leaves no selection.
+ SetItems items$ Replace all the items at once from a "|"-separated list ("Red|Green|Blue"; every "|" separates two items, "" empties the list). Nothing is selected afterwards.
Default Event: Change (fires when the user picks a new item; Click fires only when the arrow opens the popup)
Example
Begin DropDown DropDown1
@@ -7234,10 +7435,14 @@ End Sub
Type-Specific Properties
Property Type Description
----------- ------- -------------------------------------------
- Picture Picture Path to an image file to load (or a Picture object from LOADPICTURE); a program, widget, or library file gives its icon. Reads back as a Picture object for the file the image came from, which stands for the file name where a value is needed. Setting it to "" clears the image. A file that cannot be loaded leaves the image and Picture unchanged.
+ Picture Picture Path to an image file to load (or a Picture object from LOADPICTURE or LOADPICTUREDATA); a program, widget, or library file gives its icon. Reads back as a Picture object for the picture shown, which stands for the file name ("" for a picture from bytes) where a value is needed. Setting it to "" clears the image. A file that cannot be loaded leaves the image and Picture unchanged.
ImageWidth Integer Width of the loaded image in pixels (read-only).
ImageHeight Integer Height of the loaded image in pixels (read-only).
-
No type-specific methods or events.
+
Type-Specific Methods
+
Method Description
+ ------ -----------
+ SetPictureData(bytes$ [, name$]) Show the picture held in bytes$ (the bytes of a BMP, PNG, JPEG, or GIF file, such as HttpBody$ or ResGetData$ gives), as Picture = LoadPictureData(bytes$) does but without keeping a Picture object, so it suits showing many pictures in turn. Picture then reads name$ ("" without it). Returns True, or False (the picture unchanged) when the bytes are not a picture.
+
No type-specific events.
Default Event: Click
Example
Begin Toolbar Toolbar1
@@ -7261,13 +7466,17 @@ End Sub
Type-Specific Properties
Property Type Description
----------- ------- -------------------------------------------
- Picture Picture Path to an image file to load (or a Picture object from LOADPICTURE); a program, widget, or library file gives its icon. Reads back as a Picture object for the file the image came from, which stands for the file name where a value is needed. Setting it to "" clears the image. A file that cannot be loaded leaves the image and Picture unchanged. A disabled Image is drawn grayed (its transparent color stays transparent).
+ Picture Picture Path to an image file to load (or a Picture object from LOADPICTURE or LOADPICTUREDATA); a program, widget, or library file gives its icon. Reads back as a Picture object for the picture shown, which stands for the file name ("" for a picture from bytes) where a value is needed. Setting it to "" clears the image. A file that cannot be loaded leaves the image and Picture unchanged. A disabled Image is drawn grayed (its transparent color stays transparent).
Stretch Boolean When True, an image larger than the control is scaled down to fit, keeping its proportions; a smaller image stays at natural size. When False, the image is shown at natural size.
ImageWidth Integer Width of the loaded image in pixels (read-only).
ImageHeight Integer Height of the loaded image in pixels (read-only).
MaskColor Long The transparent colour (an RGB value): with UseMaskColor True, pixels of this colour are not drawn, so the form shows through.
UseMaskColor Boolean True leaves out the pixels of MaskColor (default False).
-
No type-specific methods or events.
+
Type-Specific Methods
+
Method Description
+ ------ -----------
+ SetPictureData(bytes$ [, name$]) Show the picture held in bytes$ (the bytes of a BMP, PNG, JPEG, or GIF file, such as HttpBody$ or ResGetData$ gives), as Picture = LoadPictureData(bytes$) does but without keeping a Picture object, so it suits showing many pictures in turn. Picture then reads name$ ("" without it). Returns True, or False (the picture unchanged) when the bytes are not a picture.
+
No type-specific events.
Default Event: Click
Example
Begin Image Image1
@@ -7305,7 +7514,8 @@ End
Type-Specific Properties
Property Type Description
--------- ------- -------------------------------------------
- ListIndex Integer Index of the currently selected item (-1 = no selection).
+ ListIndex Integer Index of the currently selected item (-1 = no selection).
+ Text String The selected item ("" with none); the default property, so Print List1 prints it. Setting it selects the first item that is that text (case ignored); with none, the selection stays as it is.
Type-Specific Methods
Method Parameters Description
--------------- --------------------------------------- -------------------------------------------
@@ -7345,9 +7555,11 @@ End Sub
VB Equivalent: ListView -- DVX Widget: listview
A multi-column list with column headers. Supports sorting, multi-select, and drag-to-reorder.
Type-Specific Properties
-
Property Type R/W Description
- --------- ------- --- -------------------------------------------
- ListIndex Integer R/W Index of the currently selected row (-1 = none).
+
Property Type R/W Description
+ ---------- ------- --- -------------------------------------------
+ ListIndex Integer R/W Index of the currently selected row (-1 = none).
+ SortColumn Integer R/W The column the list is sorted by, -1 when it is unsorted: what SetSort or the user's last click on a column header chose. Setting it sorts by that column in the current SortOrder (ascending when the list was unsorted); -1 unsorts the list.
+ SortOrder Integer R/W The order of the sort: 0 = none, 1 = ascending, 2 = descending (a click on the same header again reverses it). Setting it re-sorts the SortColumn in that order; 0 unsorts the list, and an unsorted list stays unsorted.
Type-Specific Methods
Method Description
------ -----------
@@ -7371,7 +7583,7 @@ End Sub
Type-Specific Events
Event Parameters Description
----------- ---------------------- -------------------------------------------
- ColumnClick Column As Integer A column header was clicked (Column 0 is the first). Without a handler the list sorts itself by that column's text (the same column again reverses); a program that orders the rows itself calls SetSort -1, 0 in the handler and fills the list in its own order.
+ ColumnClick Column As Integer A column header was clicked (Column 0 is the first). The list has already sorted itself by that column's text (the same column again reverses), with or without a handler; a program that orders the rows itself calls SetSort -1, 0 in the handler (or SetSort for another order) and fills the list in its own order.
Method Parameters Description
----------- ----------------- -------------------------------------------
+ GetSelected (none) Returns the index (as OptionIndex counts) of the selected OptionButton of this button's container, or -1 when none is: one call instead of reading every button's Value.
SetSelected Index As Integer Select the OptionButton at the given index within this button's container.
@@ -7439,15 +7652,15 @@ ListView2.SetRows "a.txt" & Chr$(9) & "120" & Ch
CanRedo Boolean An undone edit can be done again (read-only).
Text (a common property) is the plain text, a line per paragraph; setting it replaces everything with plain text. ReadOnly lets the user select and copy but not change the text.
Type-Specific Methods
-
Method Description
- ------ -----------
- Undo Undo the last edit (typing a run of characters is one edit). Up to 100 edits are kept.
- Redo Do an undone edit again.
- Cut, Copy, Paste The clipboard. Text copied from a RichEdit pastes back into a RichEdit with its looks; text from elsewhere takes the look of the text typed next.
- SelectAll Select all the text.
- Find(text) Select the next place text is (any case), from the selection on and around past the end; returns where it starts, or -1.
- LoadFile(path) Load a file: RTF when it is RTF, else plain text. Returns 0, or -1 when it cannot be read.
- SaveFile(path) Save the text: as RTF when path ends in .rtf, else as plain text. Returns 0, or -1 when it cannot be written.
+
Method Description
+ ------ -----------
+ Undo Undo the last edit (typing a run of characters is one edit). Up to 100 edits are kept.
+ Redo Do an undone edit again.
+ Cut, Copy, Paste The clipboard. Text copied from a RichEdit pastes back into a RichEdit with its looks; text from elsewhere takes the look of the text typed next.
+ SelectAll Select all the text.
+ Find(text[, matchCase]) Select the next place text is, from the selection on and around past the end; returns where it starts, or -1. Any case unless matchCase is True.
+ LoadFile(path) Load a file: RTF when it is RTF, else plain text. Returns 0, or -1 when it cannot be read.
+ SaveFile(path) Save the text: as RTF when path ends in .rtf, else as plain text. Returns 0, or -1 when it cannot be written.
Type-Specific Events
Event Parameters Description
----- ---------- -----------
@@ -7560,8 +7773,12 @@ End Sub
Line
VB Equivalent: Line -- DVX Widget: separator
-
A visual separator line. The underlying widget supports both horizontal and vertical orientations. A Line placed in a horizontal container (HBox, Toolbar) is vertical; anywhere else it is horizontal.
-
No type-specific properties, events, or methods.
+
A visual separator line, horizontal or vertical. A Line placed in a horizontal container (HBox, Toolbar) starts vertical; anywhere else it starts horizontal. Set Vertical to turn it the other way, for example a vertical Line between two side-by-side groups inside a VBox.
+
Type-Specific Properties
+
Property Type Description
+ -------- ------- -------------------------------------------
+ Vertical Boolean True for a vertical line, False for a horizontal one.
@@ -7623,6 +7840,10 @@ End Sub
-------- ------- -------------------------------------------
Position Integer Position of the divider in pixels from the left (or, when Vertical is False, from the top).
Vertical Boolean True (the default): panes side by side. False: one pane above the other. Can be set in the designer or at run time.
+
Type-Specific Events
+
Event Description
+ ----- -----------
+ Change The user moved the divider: raised as it is dragged, Position giving where it is. Not raised for a Position set in code.
A horizontal container styled as a status bar, typically placed at the bottom of a form. Put Labels (or other controls) inside it as its panes: they sit side by side, each drawn with a sunken-panel border for the classic segmented status-bar look. Give the pane that should take the spare room Weight = 100. Panes can be nested in the designer or the .frm, or added at run time with CreateControl(form, "Label", name$, StatusBar1).
-
No type-specific properties, events, or methods.
Container: Yes
+
Type-Specific Properties
+
Property Type Description
+ --------- ------- -------------------------------------------
+ Alignment Enum Where the panes sit along the bar when they do not fill it: Left (the default), Center, or Right.
+ Padding Integer Space in pixels between the edge of the bar and the panes: 1 unless set; 0 is none.
+ Spacing Integer Space in pixels between one pane and the next: 2 unless set; 0 is none.
+
No type-specific events or methods.
Example
Begin StatusBar Status1
Begin Label lblStatus
@@ -7652,16 +7879,19 @@ End
Type-Specific Properties
Property Type Description
-------- ------- -------------------------------------------
- TabIndex Integer Index of the active tab (0-based). Note: this property name collides with the common VB-compatibility TabIndex property, which shadows it at runtime. Use the SetActive method instead to switch tabs.
+ TabIndex Integer Index of the active tab (0-based). Setting it switches tabs as SetActive does.
Type-Specific Methods
Method Description
------ -----------
AddPage title$ Add a new tab page with the given title.
GetActive() Returns the index of the active tab.
- SetActive index% Switch to the tab at the given index. This is the recommended way to change tabs at runtime (the TabIndex property is shadowed by the common property handler). A negative index selects the first tab and an index past the last tab selects the last tab. Switching closes a drop-down list opened from the page being hidden and moves the focus to the tab strip if it was on a control of the page being hidden. The Click event is not raised for a switch made in code.
+ SetActive index% Switch to the tab at the given index (the same as setting TabIndex). A negative index selects the first tab and an index past the last tab selects the last tab. Switching closes a drop-down list opened from the page being hidden and moves the focus to the tab strip if it was on a control of the page being hidden. No event is raised for a switch made in code.
+
Type-Specific Events
+
Event Description
+ ----- -----------
+ Change The user switched tabs, with the mouse or the keys (not raised for a switch made in code). GetActive (or TabIndex) says which tab is active now.
Container: Yes
Default Event: Click
-
Warning: The TabIndex property is shadowed by the common property handler at runtime. Use the SetActive method to change tabs programmatically.
@@ -7744,13 +7974,13 @@ End Sub
SetSyntaxMode mode$ Activate syntax highlighting. Modes: "dhs" (help source), "bas" (BASIC, coloured as the DVX BASIC IDE colours it), "custom" (the program colours each line in the Colorize event). Pass "" to disable.
SetTabWidth width% Set the tab stop width in characters.
SetUseTabChar useChar When True, Tab inserts a tab character; when False, inserts spaces.
- Undo Take back the last change (Ctrl+Z); a second Undo puts it back.
+ Undo Take back the last change (Ctrl+Z; characters typed one after another are one change); a second Undo puts it back.
Cut, Paste, and Undo do nothing in a read-only area.
Type-Specific Events
Event Parameters Description
----------- ----------------------- -------------------------------------------
GutterClick LineNumber As Integer The line-number gutter (shown by SetShowLineNumbers) was clicked beside a line: toggle a breakpoint there. A click in the gutter does not move the cursor.
- Colorize LineText As String, Colors As String With SetSyntaxMode "custom", for every line as it is drawn: Colors comes with one "0" for each character of LineText; set each to the digit of a syntax colour (vbSyntaxText 0 to vbSyntaxType 6, colours as SetSyntaxColor sets them). Characters it leaves out stay plain. It runs whenever lines are drawn, so it must be quick and must not change the area.
+ Colorize LineText As String, Colors As String With SetSyntaxMode "custom", for every line as it is drawn: Colors comes with one "0" for each character of LineText; set each to the digit of a syntax colour (vbSyntaxText 0 to vbSyntaxType 6, colours as SetSyntaxColor sets them). Characters it leaves out stay plain. It runs when a line is drawn with text it has not coloured lately (the colours it gave are kept with the text, so a line drawn again unchanged does not ask again), so it must be quick and must not change the area; its colours should depend only on LineText. SetSyntaxMode "custom" again colours every line afresh (after the program changes how it colours).
TextArea1.SetSyntaxMode "custom"
Sub TextArea1_Colorize (LineText As String, Colors As String)
@@ -7792,7 +8022,8 @@ End Sub
Property Type Description
-------- ------- -------------------------------------------
Enabled Boolean True to start the timer, False to stop it.
- Interval Integer Timer interval in milliseconds. Setting it restarts the countdown. An Interval of 0 (or less) disables the Timer event.
+ Interval Integer Timer interval in milliseconds. Setting it restarts the countdown. An Interval of 0 (or less) disables the Timer event.
+ Repeat Boolean True (the default) to fire every Interval; False for a one-shot timer that fires once and stops itself (Enabled turns False), until Start or Enabled = True runs it again.
Type-Specific Methods
Method Parameters Description
------ ---------- -------------------------------------------
@@ -7822,7 +8053,13 @@ End Sub
VB Equivalent: Toolbar -- DVX Widget: toolbar
A horizontal container styled as a toolbar, with compact padding and spacing. Place buttons, labels, or other controls inside.
Container: Yes
-
No type-specific properties, events, or methods.
+
Type-Specific Properties
+
Property Type Description
+ --------- ------- -------------------------------------------
+ Alignment Enum Where the controls sit along the toolbar when they do not fill it: Left (the default), Center, or Right.
+ Padding Integer Space in pixels between the edge of the toolbar and the controls: 2 unless set; 0 is none.
+ Spacing Integer Space in pixels between one control and the next: 2 unless set; 0 is none.
@@ -7845,9 +8082,13 @@ End Sub
IsExpanded(index%) Returns True if the node at the given index is expanded.
IsItemSelected(index%) Returns True if the node at the given index is selected.
ItemCount() Returns the total number of nodes (all levels).
+ ItemDepth(index%) Returns how many nodes the node is under: 0 for a root-level node, 1 for its children, and so on (-1 when there is no such node).
+ ItemParent(index%) Returns the index of the node's parent, or -1 for a root-level node (or when there is no such node).
+ RemoveItem index% Remove the node and every node under it. The nodes after them move up.
SetExpanded index%, expanded Expand or collapse the node at the given index. Collapsing a node that hides the current item moves the current item to that node.
SetItemSelected index%, selected Select or deselect the node at the given index. Without multi-select this sets (or clears) the single selected node.
SetItemTag index%, tag$ Keep any text with the node (like VB's Node.Tag): a file path, a key. "" removes it.
+ SetItemText index%, text$ Change the text of the node at the given index.
SetMultiSelect multi Enable or disable multi-select mode. Turning it off clears the multi-selection.
SetReorderable reorderable Enable or disable node reordering.
The Change event fires when the current node changes (a click, the arrow keys, or ListIndex set by the program).
DVX Extension -- DVX Widget: wrapbox | Name Prefix: WrapBox
A container that arranges children in a flowing layout, wrapping to the next row when the available width is exceeded. Similar to CSS flexbox with flex-wrap: wrap.
Type-Specific Properties
-
Property Type Description
- --------- ---- -------------------------------------------
- Alignment Enum Horizontal alignment of items: Left, Center, or Right.
+
Property Type Description
+ --------- ------- -------------------------------------------
+ Alignment Enum Horizontal alignment of items: Left, Center, or Right.
+ Padding Integer Space in pixels between the edge of the box and the items: 2 unless set; 0 is none.
+ Spacing Integer Space in pixels between one item and the next, and between rows: 4 unless set; 0 is none.
When you exit DVX, each program is closed as if you had closed it yourself. If you answer Cancel when a program asks to save its work, DVX stays running. Save or close that program, then choose Exit DVX again.
Reporting a problem
DVX writes a record of what happened to the file DVX.LOG, in your USER folder (for example C:\DVX\USER\DVX.LOG). If a program crashes, this file contains details that help the program's author find the problem. The file is started fresh each time DVX starts, so copy it before starting DVX again.
+
DVX also remembers the programs' names and icons in PROGRAMS.CAC in the same folder, so Program Manager starts quickly. If a program shows an old name or icon after you changed it, delete PROGRAMS.CAC: DVX makes it again.
Function Purpose
-------- -------
dvxMemGetAppUsage Query current bytes allocated for an app
- dvxMemResetApp Free every tracked allocation charged to an app
+ dvxMemResetApp Start an app's accounting over (nothing is freed)
+ dvxMemSetApp Make an app the current one (a library's own memory: DVX_MEM_SYSTEM)
The dvxMemAppIdPtr pointer is set by the shell to &ctx->currentAppId so the allocator always knows which app to charge.
@@ -3017,10 +3091,10 @@ prefsClose(h);
Extension Directory Purpose Examples
--------- --------- ------- --------
.lib LIBS/ Core libraries loaded first. Provide infrastructure APIs. Each exports an ABI stamp. libtasks.lib, libdvx.lib, dvxshell.lib
- .wgt WIDGETS/ Widget type plugins. Each exports wgtRegister() and an ABI stamp. button.wgt, listview.wgt, terminal.wgt
+ .wgt WIDGETS/ Widget type plugins. Each exports wgtRegister() and an ABI stamp. button.wgt, listview.wgt, ansiterm.wgt
.app APPS/*/ Application modules. Each exports appDescriptor (which records its ABI) and appMain(). Loaded on demand by the shell. progman.app, notepad.app, cpanel.app
Binary Interface Version
-
DVX_ABI_VERSION (dvxTypes.h) is one number for the binary interface between all modules. Each .lib and .wgt exports <baseName>AbiVersion (written by DVX_ABI_STAMP(baseName) in its main source file); each app's AppDescriptorT starts with abiVersion (SHELL_APP_DESCRIPTOR fills it); each widget API struct starts with a WgtApiHeaderT; each widget class carries WGT_CLASS_VERSION, which is the same number. The loader checks every .lib and .wgt right after loading it and stops the boot with a message naming a module whose stamp is missing or different; the shell refuses a mismatched app with a message box; wgtGetApi and wgtRegisterClass refuse mismatched tables and classes. Details always go to DVX.LOG. The bump rule and the size-table test that enforces it are described with DVX_ABI_VERSION in the API reference (dvxTypes.h).
+
DVX_ABI_VERSION (dvxTypes.h) is one number for the binary interface between all modules. Each .lib and .wgt exports <baseName>AbiVersion (written by DVX_ABI_STAMP(baseName) in its main source file); each app's AppDescriptorT starts with abiVersion (SHELL_APP_DESCRIPTOR fills it); each widget API struct starts with a WgtApiHeaderT; each widget class carries WGT_CLASS_VERSION, which is the same number. The loader checks every .lib and .wgt right after loading it: at the start it stops the boot with a message naming a module whose stamp is missing or different, and a library loaded later (see Boot Sequence) is refused and logged; the shell refuses a mismatched app with a message box; wgtGetApi and wgtRegisterClass refuse mismatched tables and classes. Details always go to DVX.LOG. The bump rule and the size-table test that enforces it are described with DVX_ABI_VERSION in the API reference (dvxTypes.h).
|
+-- Read .dep files for each module (dependency base names)
|
- +-- Topological sort: load modules in dependency order
- | - dlopen() with RTLD_GLOBAL
- | - ABI stamp checked; a mismatch stops the boot
- | - Each .wgt that exports wgtRegister() has it called
+ +-- Index the names each library exports (its file's DXE header and
+ | export table only; a hash and the library for each name)
|
- +-- Find and call shellMain() (exported by dvxshell.lib)
+ +-- Load, each after the libraries it needs:
+ | - the libraries with constructors (code that runs as they load:
+ | audio, music, joystick, the network, Task Manager, texthelp)
+ | - the library with shellMain (dvxshell.lib)
+ | - every widget; each .wgt that exports wgtRegister() has it called
+ | - dlopen() with RTLD_GLOBAL; ABI stamp checked; a mismatch stops
+ | the boot
+ |
+ +-- Find and call shellMain()
+ |
+ +-- Every other library loads when something first needs one of
+ | its names: a module being opened (platformModuleOpen), or a
+ | dlsym(NULL) (BASIC's DECLARE LIBRARY, dvxLibraryLoaded)
|
+-- dvxInit() -- video mode, input, font, colors, cursors
|
+-- Load desktop app (progman.app)
|
+-- Main loop:
- dvxUpdate() -> tsYield() -> shellReapApps()
+ dvxUpdate() (yields) -> shellReapApps() -> close, exit,
+ and DOS-run requests -> screen saver
Application Lifecycle
Two kinds of DXE apps:
Callback-only (hasMainLoop = false)
appMain() creates windows, registers callbacks, and returns. The app lives through GUI callbacks driven by the shell's main loop. Lifecycle ends when the last window is closed. No extra task stack needed -- simpler and cheaper.
Main-loop (hasMainLoop = true)
-
A dedicated cooperative task is created. appMain() runs in that task with its own loop, calling tsYield() to share CPU. Needed for apps with continuous work (terminal emulators, games). Lifecycle ends when appMain() returns.
+
A dedicated cooperative task is created. appMain() runs in that task with its own loop, calling tsYield() (or dvxUpdate, which yields) to share CPU. Needed for apps with continuous work (terminal emulators, games). Lifecycle ends when appMain() returns.
+
Input is taken on the shell's own task (see Event Model), so every window callback -- a callback-only app's and a main-loop app's alike -- runs there, never on another program's stack; a main-loop app's own loop sees what its callbacks did when it next runs. A BASIC program's events are the exception: its form runtime queues those the shell's task dispatches and runs them on the program's own task at its next turn (DoEvents, its event loop), so a handler that waits (DoEvents, SHELL, a modal form) holds only its own program. Its UserControls' events (their mouse, keys, focus, Resize, and Paint) wait for that turn the same way: a program's code never runs on two tasks at once.
Crash Recovery
The platform layer installs signal handlers for SIGSEGV, SIGFPE, SIGILL. On crash, the handler logs platform-specific diagnostics (register dump on DJGPP), then longjmps back to the shell's main loop. The crashed app is killed; other apps and the shell survive. This provides Windows 3.1-style fault tolerance.
Per-App Memory Tracking
@@ -3056,16 +3142,16 @@ prefsClose(h);
Event Model
-
DVX uses a cooperative polling model. The main loop (dvxRun / dvxUpdate) runs this cycle each frame:
+
DVX uses a cooperative polling model. The main loop (dvxRun / dvxUpdate) runs this cycle each frame. The first four steps run only in a frame that takes input: the context's inputHere hook decides, and the shell lets only its own task take it (or any task, once the shell's has not for a quarter of a second, busy in a long call), so callbacks run on the shell's task; a frame on another task just paints and yields.
Poll mouse -- platformMousePoll() returns position and button bitmask. Compare with previous frame for press/release edge detection.
Poll keyboard -- platformKeyboardRead() returns ASCII + scancode. Non-blocking; returns false if buffer is empty. Shift/Ctrl/Alt+F-keys arrive as the plain F-key plus modifier flags. If an input entropy hook is installed (dvxSetInputEntropyHook), input events are timestamped and fed to it here.
Dispatch to focused window -- the event loop fires window callbacks (onKey, onMouse, etc.) on the focused window. If the window has a widget tree, the widget system's installed handlers dispatch to individual widgets.
Tooltips, widget polling (WCLASS_NEEDS_POLL), cursor blink, and widget timers.
-
Deferred paint flush -- every window whose paintNeeded was raised by wgtInvalidate / wgtInvalidatePaint during the frame is painted once (the highest requested level wins). Windows queued for destruction are skipped.
+
Deferred paint flush -- every window whose paintNeeded was raised by wgtInvalidate / wgtInvalidatePaint during the frame is painted once (the highest requested level wins). Windows queued for destruction are skipped. After a partial repaint (wgtInvalidatePaint only) the widget paint narrows the window's dirty area to the boxes of the widgets it repainted, joined and widened two pixels (a container that paints its children itself counts as its own box), so only that part of the screen is composited and flushed; a full repaint, an open popup list, and a window that paints itself composite the whole window.
Deferred destroy flush -- windows destroyed from inside callbacks are torn down once the dispatch depth is back to zero.
Compositor pass -- merge dirty rects, composite, flush to LFB.
-
Yield -- platformYield() or idle callback.
+
Yield -- the idle callback after a frame with nothing to paint, the busy callback after one that painted (the shell's run the idle handlers -- audio streams, network, serial pollers -- at least every 20 ms either way, and yield to the other tasks every frame), or platformYield() without them.
Smooth fonts where their face has the size smoothed, or stop; dvxFontSmooth says which it is (off at first). The fonts open are built again in place: the same BitmapFontT pointers with new glyphs and a new serial, so code that keeps measurements of a font compares its serial too, and text is laid out again. The fonts no one holds are unloaded. Programs call dvxSetFontSmoothing (dvxApp.h) instead, which follows the user's choice and the screen's depth.
Smooth fonts where their face has the size smoothed, or stop; dvxFontSmooth says which it is (off at first). The fonts open are built again in place: the same BitmapFontT pointers with new glyphs and a new serial, so code that keeps measurements of a font compares its serial too, and text is laid out again. The fonts no one holds are unloaded. dvxFontSetSmooth builds them all before it returns; dvxFontSmoothBegin only switches and marks them, and each dvxFontRebuildStep builds one of them again (15-150 ms a font), false when none is left (the fonts before the fonts with lines, which copy them). Programs call dvxSetFontSmoothing (dvxApp.h) instead, which follows the user's choice and the screen's depth and builds the fonts a frame at a time.
The texts that go with the face font was built from: its notice -- the family and style, the fonts it was converted from, and their copyright notices -- then a blank line and its licence. ASCII with CR LF line ends; malloc'd (the caller frees it).
Returns: The texts; NULL for the system font, and when the font's file cannot be read.
Declared in dvxFontFmt.h. DVX text and its fonts are code page 437. dfnCp437[c] is the Unicode code point of character c's glyph (0 for none), so the characters below 32 are the smileys, card suits, and arrows the fonts draw for them; dfnFromUnicode is the character whose glyph a code point is, -1 for none. For text, dfnTextFromUnicode and dfnTextToUnicode keep ASCII (line ends, tabs, and the other controls included) as itself and map the rest through code page 437; dfnTextFromUnicode is -1 for a code point DVX has no character for. The JSON library reads and writes Unicode this way, and BASIC's ChrW$ and AscW are these two.
Set a single color by ID. Repacks to native pixel format and invalidates the entire screen.
+
Set a single color by ID. Repacks to native pixel format; every window is painted again at the next frame (once, however many colors change before it) and the whole screen composited.
Smooth text, or stop (ctx->fontSmoothing; on after dvxInit). Text is smoothed when on and the screen has more than 8 bits per pixel (dvxFontSetSmooth, dvxText.h), in the faces and sizes that have a smoothed strike; dvxChangeVideoMode applies it again at the new depth. When what is drawn changes, every window is laid out and painted again.
+
Smooth text, or stop (ctx->fontSmoothing; on after dvxInit). Text is smoothed when on and the screen has more than 8 bits per pixel (dvxFontSmoothBegin, dvxText.h), in the faces and sizes that have a smoothed strike; dvxChangeVideoMode applies it again at the new depth. The fonts open are built again a font a frame (dvxUpdate calls dvxFontRebuildStep), and every window is laid out and painted again after the last.
A library that waits for something (a sound to end, as dvxPlaySoundWait does) calls dvxWaitPump between its checks: DVX goes on meanwhile, and so do the events of the BASIC program that waits, as in DoEvents (the hook the BASIC runtime sets with dvxSetWaitPump). It returns false when the wait should stop: the program is ending, or its code may not wait (a control written in BASIC). Without a hook it yields the processor and returns true.
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.
Full screen for a window's own content (a window of widgets, a drawing) rather than a picture the program draws: the smallest mode of at least the content's size in the desktop's pixel format, the content scaled by the largest whole factor that fits and centred on black, and shown again whenever the window paints (any dirty rectangle over its content). The mouse works on the content as in the window -- onMouse gets content coordinates -- and its cursor is drawn, in the shape the content asks for. Keys, suspending, resuming, and giving back are as for dvxExclusiveBegin; the BASIC form runtime's Form.FullScreen is this.
+
Returns: as dvxExclusiveBegin; DVX_EXCL_ERR_MODE also when there is no mode in the desktop's pixel format.
@@ -5209,7 +5314,7 @@ bool platformPathAbsolute(const char *path, char *out, int32_t outSize);platformPathAbsolute makes a path absolute against the current folder (it need not exist).
Optional Libraries: dvxLibraryLoaded
bool dvxLibraryLoaded(const char *name);
-
Whether the library (or widget) name, its file's base name in any case ("pkgnet", "dvxsql"), is loaded: installed and started with DVX, which loads every library and widget it has at start. It looks for the module's ABI stamp (DVX_ABI_STAMP: name and DVX_ABI_SUFFIX), which the loader checked before it took the module; a module that is not there, or did not load, has none. Some parts of DVX are optional (Setup's "What to install" page installs them as bundles: src/packages.ini), so a program that uses one of their libraries only when it is there asks first, and does not import it: an import of a missing library stops the program from loading. BASIC: SysLibraryLoaded (system.bas).
+
Whether the library (or widget) name, its file's base name in any case ("pkgnet", "dvxsql"), is installed and loaded. DVX loads a library when something first needs it, so asking loads one that is installed but not loaded yet. It looks for the module's ABI stamp (DVX_ABI_STAMP: name and DVX_ABI_SUFFIX), which the loader checks before it takes the module; a module that is not there, or did not load, has none. Some parts of DVX are optional (Setup's "What to install" page installs them as bundles: src/packages.ini), so a program that uses one of their libraries only when it is there asks first, and does not import it: an import of a missing library stops the program from loading. BASIC: SysLibraryLoaded (system.bas).
Idle Time: dvxIdleMs
uint32_t dvxIdleMs(const AppContextT *ctx);
The milliseconds since the last key or mouse input: a key (pressed, released, or typed), the mouse moving, a button, or the wheel, injected input (a test script, SysSendKey) included, and input that came while DVX was busy and was thrown away. DVX coming back from a DOS program or from full-screen mode counts as input too. The shell's screen saver starts after DVX.INI [desktop] saverWait minutes of it (shellSaver.h); BASIC: SysIdleSeconds. The time is kept in AppContextT.lastInputTime (platformClock ticks).
Load an image file (BMP, PNG, JPEG, GIF) and convert to the display's native pixel format. A file that is no image but a DVX program, widget, or library with an icon resource gives that icon (dvxResLoadIconData), so every picture that takes a file name can show a program's icon. Caller must free with dvxFreeImage().
+
Called from a program's own task (where the task takes no input: a BASIC program), the image loaders decode a big picture a part at a time, a frame of DVX's every 32 KB read, so the other programs go on; none of the caller's code runs meanwhile. On the task that takes the input they decode in one go.
typedef struct {
uint8_t type; // WGT_IFACE_*
+ int32_t len; // STRING: its length when it may hold NUL bytes; 0 = up to its NUL
union {
int32_t i; // INT, BOOL (0 / 1), ENUM
double f; // FLOAT, DOUBLE
const char *s; // STRING
};
} WgtValueT;
-
One property value, method argument or result, or event argument. WGT_IFACE_NONE (5) is "no value" (a void method's result). A STRING value never owns its text: an argument's text belongs to the caller for the duration of the call; a returned one (a property read, a method result) belongs to the widget and stays valid until the next call on that widget or until the widget changes or is destroyed -- copy it to keep it. wgtValueInt, wgtValueBool, wgtValueFloat, wgtValueDouble, and wgtValueStr (NULL becomes "") build values. FLOAT is BASIC's Single, DOUBLE its Double: a DOUBLE property or argument keeps every digit (a value through FLOAT is rounded to a float by a static widget's getter and setter, and the form runtime shows it as a Single); as text a DOUBLE has 15 significant digits.
+
One property value, method argument or result, or event argument. WGT_IFACE_NONE (5) is "no value" (a void method's result). A STRING value never owns its text: an argument's text belongs to the caller for the duration of the call; a returned one (a property read, a method result) belongs to the widget and stays valid until the next call on that widget or until the widget changes or is destroyed -- copy it to keep it. wgtValueInt, wgtValueBool, wgtValueFloat, wgtValueDouble, and wgtValueStr (NULL becomes "") build values; wgtValueBytes(s, len) builds a STRING of len bytes that may hold NUL bytes (s[len] must still be a NUL). A BASIC string holds any bytes, so the form runtime passes every string this way, and a STRING's length is wgtValueLen(v): its len, or up to its NUL when len is 0. A widget that takes binary text (Terminal Write) reads that length; one that wants C text reads s, which stops at the first NUL. FLOAT is BASIC's Single, DOUBLE its Double: a DOUBLE property or argument keeps every digit (a value through FLOAT is rounded to a float by a static widget's getter and setter, and the form runtime shows it as a Single); as text a DOUBLE has 15 significant digits.
Create an empty preferences handle with no associated file. Use this when building an INI from scratch before calling prefsSaveAs.
Returns: New handle, or NULL on allocation failure.
+
prefsDvxIniShared
+
PrefsHandleT *prefsDvxIniShared(void);
+
DVX.INI for reading, shared: parsed again only when the file's text changed (its text is read and compared, a fraction of a parse), for code that reads settings often (every sound, every song). Its users do not close it, change it, or keep it past their own call: the next call may read the file again.
prefsLoad
PrefsHandleT *prefsLoad(const char *filename);
Load an INI file into a new handle. If the file does not exist but a backup copy left by an interrupted save does (same name with the extension replaced by ~ and its first two characters, e.g. DVX.~IN), the backup is read instead, so the next save restores the file. Otherwise a missing file gives a valid empty handle. Either way the path is stored so prefsSave can write the file later.
Wraps malloc/free/calloc/realloc/strdup with a 16-byte header that records the owning app ID and allocation size. The Task Manager displays per-app memory usage, and leaks are detected when an app is terminated.
+
Wraps malloc/free/calloc/realloc/strdup with a 16-byte header that records the owning app ID and allocation size. The Task Manager displays per-app memory usage. The accounting is all it is: when an app ends its count starts over, and nothing it allocated is freed for it.
DXE code does NOT need to include this header for tracking to work. The DXE export table maps malloc/free/calloc/realloc/strdup to these wrappers transparently. Call sites that need the explicit dvxXxx names (e.g. dvxMemGetAppUsage) or the dvxMemAppIdPtr declaration should include dvxMem.h directly.
Globals
dvxMemAppIdPtr
@@ -6606,10 +6719,51 @@ extern const PrefsChoiceT prefsWheelDirChoices[PREFS_WHEEL_DIR_CHOICE_COUNT];Returns: Bytes allocated for this app.
dvxMemResetApp
void dvxMemResetApp(int32_t appId);
-
Free every tracked allocation charged to the given app. Called by the shell after an app exits or crashes to reclaim its memory.
+
Start the given app's accounting over: its usage reads 0, and a block it allocated that is freed later is not subtracted again (the block carries the old generation). Nothing is freed: a library's buffers made while the app ran stay valid. The shell calls it when an app ends.
Parameter Description
--------- -----------
appId App ID to flush
+
dvxMemSetApp
+
int32_t dvxMemSetApp(int32_t appId);
+
Make appId the current app (the one what is allocated from now is charged to, as the shell sees it too) and return the one before, to set back afterwards. A library keeps its own memory as DVX's (DVX_MEM_SYSTEM, 0) whichever app's call made it, so the Task Manager does not charge a cache or a font to the program that happened to ask first; the audio library calls a program's callback as that program. Without a shell (host tools) nothing changes and 0 comes back.
+
int32_t prev = dvxMemSetApp(DVX_MEM_SYSTEM);
+// ... allocations that belong to the library ...
+dvxMemSetApp(prev);
+
+
+
dvxEndian.h -- Byte Order
+
File formats and wire protocols fix their byte order. These inline functions read and write it a byte at a time, so they work at any alignment and on any computer. Header only: include it and call, nothing to link.
Get reads the value at p, high byte first (Be) or low byte first (Le); Put writes v at p the same way (dvxPutLe16 writes the low 16 bits of v). Cast a load to a signed type for a signed field, for example (int16_t)dvxGetLe16(p) for a 16-bit sample.
+
+
+
dvxHash.h -- Hashes, Checksums, and Hex Digits
+
Small inline functions every part of DVX uses for the same jobs: FNV-1a for hash tables and change detection, CRC-16-CCITT for serial packets and XMODEM / ZMODEM, and hex digits both ways. Header only: include it and call, nothing to link.
Start from DVX_FNV_BASIS and fold in bytes. dvxFnvByte takes one, so a caller can fold case or skip characters as it goes; dvxFnvBytes takes len bytes. Both return the hash carried on.
The CRC of len bytes (polynomial 0x1021, high bit first), carried on from crc, computed a byte at a time without a table. Seed 0 for XMODEM and ZMODEM, 0xFFFF for CCITT-FALSE (the serial packet layer's).
Monotonic tick counter for UI timing (double-click, tooltip delay, cursor blink, timers), advancing platformClockHz() ticks per second. PlatformTicksT is unsigned so "now - then" is wrap-safe: always compare intervals by subtraction, never raw tick values. PLATFORM_MS_TO_TICKS converts milliseconds to ticks at the platform rate, PLATFORM_TICKS_TO_MS ticks to milliseconds (the millisecond clock the conn and xfer engines take is PLATFORM_TICKS_TO_MS(platformClock())).
platformEntropyStamp
uint64_t platformEntropyStamp(void);
-
Fine-grained, free-running timestamp for input-timing entropy. It is not a clock: units and wrap point are backend-specific. On DOS it returns the CPU time-stamp counter (RDTSC) when CPUID reports one, otherwise the BIOS tick count shifted left 16 bits with the live PIT channel 0 counter (about 0.84 us resolution) in the low 16 bits. Used by dvxSetInputEntropyHook.
+
Fine-grained, free-running timestamp for entropy. It is not a clock: units and wrap point are backend-specific. On DOS it reads the live PIT channel 0 counter (about 0.84 us resolution, its own crystal) and returns it in the top 16 bits of the CPU time-stamp counter (RDTSC) when CPUID reports one, so a sample carries the jitter between the two clocks; otherwise the BIOS tick count shifted left 16 bits with the PIT counter in the low 16 bits. Used by dvxSetInputEntropyHook and by the security library's entropy gathering.
platformMicros
uint64_t platformMicros(void);
Monotonic microseconds, the high-resolution clock for games and measurements. On DOS it counts from the start of the timer service (dvxInit): the PIT input clocks the timer interrupt has counted plus the part of the running period, read from the latched channel 0 count (about 0.84 us steps), or on a CPU with a time-stamp counter the TSC cycles since the last interrupt at the rate measured over the interrupt period before it, so the clock never drifts from the PIT and a TSC whose rate changes (power saving, an emulator) does no harm. The host backend returns its fake clock. Not for use inside a timer callback.
The size stat reported for a file. A FAT32 file reaches 4 GB - 1 byte, but the DOS C library's st_size is a signed 32-bit off_t, so a file past 2 GB reads as negative there; platformStatSize reads it as the unsigned 32-bit size DOS keeps. Use it wherever a file size comes from stat or fstat.
+
platformFileStamp
+
typedef struct {
+ uint32_t size; // bytes (a file of 4 GB or more: 0xFFFFFFFF)
+ uint32_t time; // its date and time, as the platform keeps them
+} PlatformStampT;
+
+bool platformFileStamp(const char *path, PlatformStampT *out);
+
A file's size and time from its folder alone, for telling whether a file read before has changed: compare two stamps for equality only. On DOS it is one directory search; stat makes several DOS calls there, tens of milliseconds each on a slow disk. Elsewhere it is stat, with the time's nanoseconds folded in. False when path is no file, or holds a wildcard. DOS keeps a file's time to two seconds, so a file rewritten at the same size within two seconds keeps its stamp. The shell's program cache checks the programs it keeps with it.
platformMkdirRecursive
int32_t platformMkdirRecursive(const char *path);
Create a directory and all parent directories (like mkdir -p). Existing directories are not an error. Returns 0 on success, -1 on failure (including a path of DVX_MAX_PATH characters or more).
platformPathIsUnder: when path is dir or lies below it, the rest of path after dir and its separator ("" for dir itself); NULL otherwise.
platformPathJoin: dir and name with one separator between them (none added when dir ends in one), every separator native; an absolute name, or an empty dir, gives name alone. False when it does not fit.
platformPathDirName: the folder part of path in native separators: a root keeps its separator ("C:\\X.TXT" gives "C:\\", since "C:" alone is the drive's current folder), "C:X.TXT" gives "C:", a bare name gives ".". False when it does not fit.
+
platformPathParent: the folder holding path, a trailing separator ignored, in native separators ("C:/DVX/APPS/" gives "C:\\DVX", "/a" gives "/"). False for a root, a name without a folder, or when it does not fit.
+
platformPathIsRoot: true for a drive or file-system root: "C:", "C:\\", "/", or "\\".
platformPathTidy: platformPathNative, then each run of separators made one (a leading pair, a network "\\\\server", kept) and a trailing separator dropped unless it is a root's: the form for a path handed to DOS directly, without the C library in between. platformShortPath and the file operations library's attribute calls use it.
Write len bytes of data as the whole of path, all or nothing: into the PLATFORM_TMP_EXT sibling, then swapped in with platformReplaceFile, so a full disk or an I/O error leaves the file as it was. False when it was not done. The preferences system, the IDE's saves, Setup, and the package records use it.
Visit every file below dir, depth first, entering folders as they come and skipping names that start with '.'. A folder's names are read in full before any is visited (DOS has few file handles), and which are folders comes with them (platformDirEntryIsDir), not from a stat of each. fn gets each file's full path and its name; returning false stops the walk, and platformWalkDir then returns false too. A folder that cannot be read is skipped. The loader's module and help scans and the shell's program list use it.
Slurp a whole file into a freshly malloc'd, NUL-terminated buffer. Caller frees. Binary-safe: the NUL is past the end of the reported length.
@@ -7120,7 +7289,7 @@ char **platformKindReadDir(DvxKindE kind);
SHARED FONTS, SOUNDS, THEMES, WPAPER, BANKS, and SAVERS: what DVX and add-on packages ship for every program (an add-on's in a folder of its vendor's, such as FONTS\ACME)
USER (the user folder) DVX.INI, ASSOC.INI (the user's changes to SYSTEM's), DVX.LOG, the user's own FONTS, SOUNDS, THEMES, WPAPER, BANKS, and SAVERS, KEYS (publishers' keys the user trusts), APPS\<program path> (each program's settings and saved files), and DOWNLOAD
Only packages put files in SHARED: DVX's own, and add-on packages (pkg.lib), each in its vendor's folders. A program reads it but never writes or installs into it; a program's own files live with it (its DATA folder, or resources inside the .app), under its own vendor and program folder, so two programs' files never collide.
-
platformUserInit, called once by the loader, decides the user folder: the folder the DVXUSER environment variable names, else USER in the DVX folder, made when missing. When it cannot be written (DVX on a CD-ROM without DVXUSER), DVXUSER in the TEMP folder is used instead and platformUserInTemp is true; without a TEMP folder nothing can be saved and platformIsReadOnly is true. The first start copies SYSTEM\DVX.INI to the user folder. platformUserDir is the folder (DVXUSER or USER before platformUserInit has decided); platformUserPath joins a name to it; platformDvxIniPath is its DVX.INI. platformUserDirOf is the user folder of a DVX installed in another folder (DVXUSER, else that folder's USER), for Setup.
+
platformUserInit, called once by the loader (and not exported to modules), decides the user folder: the folder the DVXUSER environment variable names, else USER in the DVX folder, made when missing. When it cannot be written (DVX on a CD-ROM without DVXUSER), DVXUSER in the TEMP folder is used instead and platformUserInTemp is true; without a TEMP folder nothing can be saved and platformIsReadOnly is true. The first start copies SYSTEM\DVX.INI to the user folder. platformUserDir is the folder (DVXUSER or USER before platformUserInit has decided); platformUserPath joins a name to it; platformDvxIniPath is its DVX.INI. platformUserDirOf is the user folder of a DVX installed in another folder (DVXUSER, else that folder's USER), for Setup.
platformAppUserDir is a program's folder in the user folder: APPS\KPUNCH\TERMINAL\TERMINAL.APP gives USER\APPS\KPUNCH\TERMINAL; a program outside the DVX folder gives USER\APPS\<its file name without the extension>. It is not made here (the shell's userDir and BASIC's App.User are made on first use).
SHARED and the user folder have the same six kinds of folder (DvxKindE): fonts, sounds, themes, desktop pictures, MIDI instrument banks, and screen savers. Each may hold folders of add-on vendors (SHARED\FONTS\ACME), one level deep, whose files are named VENDOR\NAME. platformKindName is a kind's folder name; platformKindDir is SHARED's (user false) or the user's (user true); platformKindBrowseDir is where a file dialog for the kind starts (the user's when it exists, else SHARED's). platformKindFind finds a file of a kind by name (NAME or VENDOR\NAME, either separator), the user's first, so a user's file of the same name wins; any other name with a folder in it, or an absolute one, is taken as the path it is (a VENDOR\NAME not in the kind's folders is tried that way too). platformKindNameOf is the other way: a file in one of the kind's folders gives its name alone, or VENDOR\NAME in a vendor's folder, which is what settings store (DVX.INI [sounds], [desktop] wallpaper and saver, and [audio] midiBank), so moving the user folder or DVX keeps them working. platformKindReadDir lists both folders: the user's names (sorted, case ignored), then SHARED's not among them, with each vendor folder's files as VENDOR\NAME (the folders themselves are not listed); free it with dvxReadDirFree. BASIC: FileOpsUserDir$, FileOpsUserPath$, FileOpsUserInTemp, FileOpsKindDir$, FileOpsKindBrowseDir$, FileOpsKindFind$, FileOpsKindName$, and FileOpsKindList (fileops.bas, with FILEOPS_KIND_FONTS to FILEOPS_KIND_SAVERS), and App.User for a program's own folder.
Register platform and C runtime symbols with the dynamic module loader so DXE modules can resolve them at load time.
+
Register platform and C runtime symbols with the dynamic module loader so DXE modules can resolve them at load time. The loader's own, so not exported to modules.
Register function pointer overrides for subsequently loaded modules. entries is terminated by {NULL, NULL}. On DOS this wraps dlregsym(). Nothing in DVX itself calls it: it is SDK API for embedders and test harnesses that need to substitute a symbol (e.g. a stub) before loading a module.
+
Register function pointer overrides for subsequently loaded modules. entries is terminated by {NULL, NULL}. On DOS this wraps dlregsym(). Nothing in DVX itself calls it: it is SDK API for embedders and test harnesses that need to substitute a symbol (e.g. a stub) before loading a module, so code linked with the platform layer, not a module (it is not exported).
Interrupt-Safe Memory
platformLockRegion / platformUnlockRegion
int32_t platformLockRegion(const void *addr, uint32_t size, bool isCode);
@@ -7231,7 +7400,7 @@ if (voice < 0) {
}
...
audioClose(audio); // or let the program end
-
The first audioOpen starts the sound device and the last audioClose stops it. In between, after AUDIO_IDLE_CLOSE_SEC (3) seconds with nothing playing, the device's output is closed so it costs no CPU time (desktop sounds would otherwise keep it mixing silence), and the next sound opens it again with the same driver before it starts, so nothing of it is lost; audioGetInfo still reports the device. Functions returning int32_t give AUDIO_OK or a voice id (above 0) on success and a negative AUDIO_ERR_* code on failure; audioErrorText says what a code means. audioGetInfo describes the device: the driver's name and description, the mixing rate it achieved, its output bits and channels, the voice limit, and whether it plays sampled sound (hasDac), has a hardware synthesizer (hasSynth), or plays tones on a hardware tone generator (hasTone, the PC speaker). audioGetStats counts mixed blocks and frames, stream underruns, clipped samples, dropped voices, and the voices playing.
+
The first audioOpen starts the sound device and the last audioClose stops it; the driver modules it used stay loaded, and the next first audioOpen tries them first (no other driver is loaded or probed) while DVX.INI's driver choice is the same. In between, after AUDIO_IDLE_CLOSE_SEC (10) seconds with nothing playing, the device's output is closed so it costs no CPU time (desktop sounds would otherwise keep it mixing silence), and the next sound opens it again with the same driver before it starts, so nothing of it is lost; audioGetInfo still reports the device. Functions returning int32_t give AUDIO_OK or a voice id (above 0) on success and a negative AUDIO_ERR_* code on failure; audioErrorText says what a code means. audioGetInfo describes the device: the driver's name and description, the mixing rate it achieved, its output bits and channels, the voice limit, and whether it plays sampled sound (hasDac), has a hardware synthesizer (hasSynth), or plays tones on a hardware tone generator (hasTone, the PC speaker). audioGetStats counts mixed blocks and frames, stream underruns, clipped samples, dropped voices, and the voices playing.
None of the functions may be called from an interrupt (a timer-service callback).
Samples and Voices
audioSampleLoad (file), audioSampleLoadMem (memory), and audioSampleCreate (raw PCM in an AudioFormatT: rate, 8 bits unsigned or 16 bits signed, 1 or 2 channels) make an AudioSampleT; audioSampleFree frees it and stops the voices playing it. audioPlay(sample, volume, pan, loop) plays it on a new voice; audioPlayFile loads a file and plays it, freeing the sample when the voice ends.
@@ -7250,13 +7419,13 @@ audioClose(audio); // or let the program end
audioNotesOpen(audio, AUDIO_WAVE_SQUARE, AUDIO_VOLUME_MAX, &tune);
audioNotesAdd(tune, 440 * AUDIO_MILLIHZ_PER_HZ, 250000); // A for 1/4 s
audioNotesAdd(tune, 0, 50000); // a rest
-
A note queue plays tones and rests one after the other, each lasting exactly as long as asked (its length is counted in output frames from the time queued since the queue last went quiet, so many short notes do not drift). audioNotesAdd(notes, milliHz, us) appends a tone of milliHz thousandths of a hertz (AUDIO_TONE_MIN_HZ to AUDIO_TONE_MAX_HZ, or 0 for a rest) lasting us microseconds; up to AUDIO_NOTES_MAX entries may wait (AUDIO_ERR_BUSY beyond). audioNotesQueued says how many entries have not finished (and how many of them are tones), audioNotesSpace how many more fit, audioNotesClear silences the queue at once. The queue plays on its own voice (audioNotesVoice, for volume and pan) that the voice limit never drops, in the mixer or on the PC speaker's tone generator (never on the FM chip); it is silent while empty. music.lib plays QBasic's PLAY and SOUND through note queues.
+
A note queue plays tones and rests one after the other, each lasting exactly as long as asked (its length is counted in output frames from the time queued since the queue last went quiet, so many short notes do not drift). audioNotesAdd(notes, milliHz, us) appends a tone of milliHz thousandths of a hertz (AUDIO_TONE_MIN_HZ to AUDIO_TONE_MAX_HZ, or 0 for a rest) lasting us microseconds; up to AUDIO_NOTES_MAX entries may wait (AUDIO_ERR_BUSY beyond). audioNotesSetTone(notes, wave, level) sets the waveform and loudness (0..AUDIO_VOLUME_MAX, a share of the queue's volume) of the entries added from then on -- each entry plays in its own, so a tune changes them in time -- and a tone generator plays a tone at level 0 as a rest. audioNotesQueued says how many entries have not finished (and how many of them are tones), audioNotesSpace how many more fit, audioNotesClear silences the queue at once. The queue plays on its own voice (audioNotesVoice, for volume and pan) that the voice limit never drops, in the mixer or on the PC speaker's tone generator (never on the FM chip); it is silent while empty. music.lib plays QBasic's PLAY and SOUND through note queues.
audioStreamOpen(ctx, fmt, bufferMs, fill, userData, &stream) opens a stream of any rate from AUDIO_RATE_MIN (4000) to AUDIO_RATE_MAX (48000), 8 or 16 bits, mono or stereo, with a buffer of bufferMs milliseconds (0: 250). It plays at once on its own voice (audioStreamVoice), silence until samples arrive. Without fill the program writes samples with audioStreamWrite, which takes as many frames as fit and returns how many; audioStreamSpace and audioStreamQueued tell how much room and how much sound are left. With fill, the library asks for samples whenever the buffer has room: fill(stream, buf, frames, userData) writes up to frames frames in the stream's format and returns how many. Fill functions run from audioPump and the shell's idle time; a program that keeps the desktop busy every frame should call audioPump once a frame. A stream that runs dry plays silence and counts underruns. audioStreamFlush drops what the stream holds and has not played yet (a seek or a stop is heard at once; the two frames the mixer has read ahead still play). audioStreamClose stops and frees it.
+
audioStreamOpen(ctx, fmt, bufferMs, fill, userData, &stream) opens a stream of any rate from AUDIO_RATE_MIN (4000) to AUDIO_RATE_MAX (48000), 8 or 16 bits, mono or stereo, with a buffer of bufferMs milliseconds (0: 250). It plays at once on its own voice (audioStreamVoice), silence until samples arrive. Without fill the program writes samples with audioStreamWrite, which takes as many frames as fit and returns how many; audioStreamSpace and audioStreamQueued tell how much room and how much sound are left. With fill, the library asks for samples whenever the buffer has room: fill(stream, buf, frames, userData) writes up to frames frames in the stream's format and returns how many. Fill functions run from audioPump and the shell's idle time; a program that keeps the desktop busy every frame should call audioPump once a frame. Once a stream holds 100 ms of sound, a pump fills it for 15 ms at most (STREAM_FILL_BUDGET_US) and the next pump goes on, so a fill function that takes its time (a module rendered, a file read) does not hold DVX up after a start, a seek, or a stall. A stream that runs dry plays silence and counts underruns. audioStreamEnd says no more frames will come after those written (a fill function may call it with its last ones): the fill function is not asked again, and once the stream has played what it holds its voice ends as a sample's does (AUDIO_END_FINISHED to the end function), with nothing counted as an underrun; the stream stays open until audioStreamClose. Without it a stream that has played its last frame runs on in silence, counting underruns, until it is closed. audioStreamFlush drops what the stream holds and has not played yet (a seek or a stop is heard at once; the two frames the mixer has read ahead still play). audioStreamClose stops and frees it.
audioDrv.h also declares, for audio.lib and the libraries built on it (music.lib), audioDecode (a WAV or VOC file in memory as 16-bit frames), audioConvert (8- or 16-bit frames as 16-bit ones), and audioWavParse(head, headBytes, fileBytes, &fmt, &dataAt, &dataBytes) (a WAV file's format and where its frames are, from its first bytes: the frames are clamped to the file and whole; AUDIO_ERR_FORMAT for anything but a RIFF WAVE file, AUDIO_ERR_UNSUPPORTED, AUDIO_ERR_CORRUPT).
A wavetable synthesizer (a Gravis UltraSound's GF1, a Sound Blaster AWE32's or AWE64's EMU8000) plays samples from its own memory, and its voices and memory are shared by every program. audioWtGetInfo tells the chip (description, voices, free voices, memory, the largest free block, what it does itself: AUDIO_WT_ENVELOPES, AUDIO_WT_LFOS, AUDIO_WT_FILTER, AUDIO_WT_EFFECTS, AUDIO_WT_8BIT for 8-bit samples, AUDIO_WT_BANK_256K when a 16-bit sample must not cross a 256 KB boundary, AUDIO_WT_ROM for a General MIDI sample ROM; and its level, the centibels a voice at full volume is louder than full-scale digital sound, measured: 0 on a GUS, whose digital sound plays on its own voices, 76 on an AWE). audioWtMemAlloc(ctx, bytes, bits, &addr) takes memory for samples of bits bits (AUDIO_ERR_NO_ROOM when no free block is large enough), audioWtUpload(ctx, addr, frames, count, bits) copies 16-bit frames into it at bits, audioWtMemFree gives it back. audioWtAlloc(ctx) takes a voice (the least recently given back first), audioWtStart(ctx, voice, note) starts it on a note (AudioWtVoiceT: the sample's address, or AUDIO_WT_ADDR_ROM with an address in the ROM, and in frames from there the one it starts from (offset: before the loop or inside it), its loop, and its end; pitch in cents, 0 for AUDIO_WT_PITCH_RATE (44,100) frames a second; attenuation in centibels; pan; the filter, reverb and chorus; both envelopes in timecents; both LFOs; audioWtNoteInit makes a plain one, with no envelope or LFO shaping it, the filter open, no effects, centred, at full volume, for the caller to set the sample on), audioWtUpdate changes what its what bits say (AUDIO_WT_SET_PITCH, _VOLUME, _PAN, _FILTER, _EFFECTS, _LFO), audioWtRelease lets the key up, audioWtStop stops it at once, audioWtActive is false once it is silent, audioWtFree gives it back. The chip does what its flags say; the program does the rest by updating the voice as the note goes on. Closing the context gives back its voices and its memory. The voice calls may be made from a timer callback (music.lib plays SoundFont MIDI and modules that way); the memory calls may not.
+
A wavetable synthesizer (a Gravis UltraSound's GF1, a Sound Blaster AWE32's or AWE64's EMU8000) plays samples from its own memory, and its voices and memory are shared by every program. audioWtGetInfo tells the chip (description, voices, free voices, memory, the largest free block, what it does itself: AUDIO_WT_ENVELOPES, AUDIO_WT_LFOS, AUDIO_WT_FILTER, AUDIO_WT_EFFECTS, AUDIO_WT_8BIT for 8-bit samples, AUDIO_WT_BANK_256K when a 16-bit sample must not cross a 256 KB boundary, AUDIO_WT_ROM for a General MIDI sample ROM; and its level, the centibels a voice at full volume is louder than full-scale digital sound, measured: 0 on a GUS, whose digital sound plays on its own voices, 76 on an AWE). audioWtMemAlloc(ctx, bytes, bits, &addr) takes memory for samples of bits bits (AUDIO_ERR_NO_ROOM when no free block is large enough), audioWtUpload(ctx, addr, frames, count, bits) copies 16-bit frames into it at bits, audioWtMemFree gives it back. audioWtAlloc(ctx) takes a voice (the least recently given back first), audioWtStart(ctx, voice, note) starts it on a note (AUDIO_ERR_ARG for a sample in a ROM the chip does not have; AudioWtVoiceT: the sample's address, or AUDIO_WT_ADDR_ROM with an address in the ROM, and in frames from there the one it starts from (offset: before the loop or inside it), its loop, and its end; pitch in cents, 0 for AUDIO_WT_PITCH_RATE (44,100) frames a second; attenuation in centibels; pan; the filter, reverb and chorus; both envelopes in timecents; both LFOs; audioWtNoteInit makes a plain one, with no envelope or LFO shaping it, the filter open, no effects, centred, at full volume, for the caller to set the sample on), audioWtUpdate changes what its what bits say (AUDIO_WT_SET_PITCH, _VOLUME, _PAN, _FILTER, _EFFECTS, _LFO), audioWtRelease lets the key up, audioWtStop stops it at once, audioWtActive is false once it is silent, audioWtFree gives it back. The chip does what its flags say; the program does the rest by updating the voice as the note goes on. Closing the context gives back its voices and its memory. The voice calls may be made from a timer callback (music.lib plays SoundFont MIDI and modules that way); the memory calls may not.
MIDI Port
With DVX.INI [audio] midi = mpu401 the mpu401 driver opens an MPU-401 (or compatible, such as a Sound Blaster 16's) in UART mode, for an external synthesizer such as a Roland MT-32 or SC-55. The port belongs to one program at a time: audioMidiAcquire(ctx) (AUDIO_ERR_BUSY when another has it, AUDIO_ERR_NO_SYNTH without a port), audioMidiPut(ctx, byte) sends a byte and returns AUDIO_ERR_BUSY when the port cannot take one yet (send it again later; it may be called from a timer callback), audioMidiRelease gives it back (closing the context does too). audioMidiGetInfo tells whether there is a port and whether it is in use. music.lib uses the port for MIDI music.
MIDI Input
@@ -7328,7 +7497,7 @@ audioWtStart(audio, v, ¬e); // pitch 0: AUDIO_WT_PITCH_R
midiBank the OP2 instrument bank for FM MIDI (music.lib): its name in SHARED\BANKS or the user's BANKS folder (an add-on's as VENDOR\NAME), or a path from the DVX folder; not set, the bundled one (Control Panel's Sounds page picks it)
A settings screen reads and writes the keys it offers through the library, so the key names live in one place (audio.h, Settings; prefs is a copy of DVX.INI the caller loads and saves): audioSettingDriver(prefs) (AUDIO_DRIVER_AUTO when missing), audioSettingMidi(prefs) (an index in audioMidiChoices, auto when missing or unknown), audioSettingMidiIn(prefs) (an index in audioMidiInChoices, none when missing or unknown), audioSettingModules(prefs) (an index in audioModulesChoices, wavetable when missing or unknown), audioSettingVolume(prefs) (percent, clamped to 0..100, 100 when missing), and audioSettingsStore(prefs, driver, midi, modules, volume) writes all four (an empty driver stores auto, a midi index out of range stores auto, a modules index out of range wavetable); audioSettingMidiInStore(prefs, midiIn) writes midiIn (an index out of range stores none), which takes effect when the device next starts. audioMidiChoices[AUDIO_MIDI_CHOICE_COUNT] holds the midi choices in list order, each with its label for people and the name stored (a PrefsChoiceT, dvxPrefs.h); audioMidiInChoices[AUDIO_MIDI_IN_CHOICE_COUNT] the midiIn choices, audioModulesChoices[AUDIO_MODULES_CHOICE_COUNT] the modules choices (wavetable: on a wavetable synthesizer's voices when it is free and the module fits there; software: always mixed in software). audioPercentToVolume(percent) turns a percent into 0..AUDIO_VOLUME_MAX for audioSetMasterVolume. The device reads the driver and volume through the same functions when it starts. The Control Panel's Sounds page uses all of these.
Desktop Sounds
-
The library plays the sounds DVX makes for its events (dvxPlaySound in dvxApp.h): when it loads it sets DVX's sound hook to audioDeskPlay(event, wait). The file of each event is read from DVX.INI [sounds] at the first sound: the key is audioDeskKey(event) and the value a WAV or VOC file, empty or missing for none: a name alone is found in the sound folders (the user folder's SOUNDS first, then SHARED\SOUNDS: platformKindFind), anything else is a path; audioDeskReload makes the next sound read the section again (the Control Panel calls it after saving). The sounds play on a context of the library's own, so no program's end cuts one short, at full volume under the master volume. When no sound device starts, the sounds stay silent without trying again until the next reload. With wait the call returns once the sound has played (at most 5 seconds); after the exit sound further events are quiet (DVX is ending). audioDeskLabel(event) names an event for people, audioDeskPlayFile(path) plays any file the same way (to try one out). audioDeskFile(prefs, event) reads an event's file from a copy of DVX.INI ("" for none) and audioDeskSetFile(prefs, event, path) sets it; the library's own reading of [sounds] goes through audioDeskFile too. The default set, made by DVX's own sound generator at build time, is in SHARED\SOUNDS.
+
The library plays the sounds DVX makes for its events (dvxPlaySound in dvxApp.h): when it loads it sets DVX's sound hook to audioDeskPlay(event, wait). The file of each event is read from DVX.INI [sounds] at the first sound: the key is audioDeskKey(event) and the value a WAV or VOC file, empty or missing for none: a name alone is found in the sound folders (the user folder's SOUNDS first, then SHARED\SOUNDS: platformKindFind), anything else is a path; audioDeskReload makes the next sound read the section again (the Control Panel calls it after saving). Each event's file is decoded at its first sound and kept until then, so a window opening or a message box does not read the disk. The sounds play on a context of the library's own, so no program's end cuts one short, at full volume under the master volume. When no sound device starts, the sounds stay silent without trying again until the next reload. With wait the call returns once the sound has played (at most 5 seconds), running dvxWaitPump meanwhile (a BASIC program's events go on); after the exit sound further events are quiet (DVX is ending). audioDeskLabel(event) names an event for people, audioDeskPlayFile(path) plays any file the same way (to try one out). audioDeskFile(prefs, event) reads an event's file from a copy of DVX.INI ("" for none) and audioDeskSetFile(prefs, event, path) sets it; the library's own reading of [sounds] goes through audioDeskFile too. The default set, made by DVX's own sound generator at build time, is in SHARED\SOUNDS.
Key Event
--- -----
default the default beep (dvxBeep; a message box without an icon)
@@ -7356,24 +7525,33 @@ audioWtStart(audio, v, ¬e); // pitch 0: AUDIO_WT_PITCH_R
audioDriverNames(names, max) lists the short names of the sound drivers installed, the one tried first first (for a settings screen; AUDIO_DRIVERS_MAX is the most DVX's own callers ask for).
Sound drivers are tried in this order: the one DVX.INI names, then from the highest priority down (Windows Sound System, Sound Blaster, Gravis UltraSound, parallel port DAC, PC speaker, null). The first that starts is used. Synthesizer drivers are opened with the sound device, beside it: the sound device's own first (the GUS's), then of each kind (FM chip, MIDI port, wavetable) the first that finds its hardware, and they close with the device. The sound device's description and hasSynth in AudioInfoT tell whether there is a synthesizer.
Writing a Driver
-
A driver is a driver module (see dvxDrv.h in the API reference): <name>.drv in bin/libs/kpunch/audio/drivers, built by the audio Makefile from each drivers/<name>/ folder, that exports one AudioDriverT named audioDriver, written with AUDIO_DRIVER(name, AUDIO_PRIORITY_*, .open = ..., .play = ..., .close = ...). The descriptor carries DVX_ABI_VERSION; a driver built for another version is refused. audio.lib loads the drivers when the first program opens sound, keeps the one that starts (and the synthesizers that find their hardware) and unloads the rest. open detects the hardware and fills AudioDrvInfoT (description, achieved rate, bits, channels, hasDac, hasSynth, a hardware tone function) without making a sound; play starts output; close stops it and releases everything, after which the mixer is not called again. After a few seconds of silence audio.lib calls close and later open and play again on the same driver, so open must work more than once and must not make a sound (the driver's module stays loaded in between). A driver whose card records also sets AudioDrvInfoT.recSources and gives recordStart(want, source, got) and recordStop: recordStart is called with the output closed and takes the card for recording (its own DMA buffer and interrupt; the format the card records in got), its interrupt then calls audioRecPush(src, bytes) with each block recorded (whole frames; a block that does not fit is dropped and counted), and recordStop gives everything back as close does (sb.drv, wss.drv, and gus.drv use their close for both; the AD1848 codec's routines a Windows Sound System board and an UltraSound MAX share are codec/codecHw.c). While playing, the driver calls audioMixRender(buffer, frames) for each block of output from its own interrupt, or calls audioClockStart to have the timer service drive the mixer (the output is thrown away) when it only plays tones or nothing. Code and data an interrupt touches must be pinned: audioLockSpan pins a module's listed functions and data (a module's code comes before its data, so one span covers them), and the library is built without hot/cold function splitting and without loops turned into library calls. Interrupt code must not call the C library, DVX, or DOS and must not use floating point. audio.lib stops the device from a shutdown hook if DVX ends without closing it.
+
A driver is a driver module (see dvxDrv.h in the API reference): <name>.drv in bin/libs/kpunch/audio/drivers, built by the audio Makefile from each drivers/<name>/ folder, that exports one AudioDriverT named audioDriver, written with AUDIO_DRIVER(name, AUDIO_PRIORITY_*, .open = ..., .play = ..., .close = ...). The descriptor carries DVX_ABI_VERSION; a driver built for another version is refused. audio.lib loads the drivers when the first program opens sound, keeps the one that starts (and the synthesizers that find their hardware) and unloads the rest; when the last program closes sound it closes them but keeps their modules for the next device, which opens them again first (so open and close must work more than once). The drivers' names (audioDriverNames) are read once a session. open detects the hardware and fills AudioDrvInfoT (description, achieved rate, bits, channels, hasDac, hasSynth, a hardware tone function) without making a sound; play starts output; close stops it and releases everything, after which the mixer is not called again. After a few seconds of silence audio.lib calls close and later open and play again on the same driver, so open must work more than once and must not make a sound (the driver's module stays loaded in between). A driver whose card records also sets AudioDrvInfoT.recSources and gives recordStart(want, source, got) and recordStop: recordStart is called with the output closed and takes the card for recording (its own DMA buffer and interrupt; the format the card records in got), its interrupt then calls audioRecPush(src, bytes) with each block recorded (whole frames; a block that does not fit is dropped and counted), and recordStop gives everything back as close does (sb.drv, wss.drv, and gus.drv use their close for both; the AD1848 codec's routines a Windows Sound System board and an UltraSound MAX share are codec/codecHw.c). While playing, the driver calls audioMixRender(buffer, frames) for each block of output from its own interrupt, or calls audioClockStart to have the timer service drive the mixer (the output is thrown away) when it only plays tones or nothing. Code and data an interrupt touches must be pinned: audioLockSpan pins a module's listed functions and data (a module's code comes before its data, so one span covers them), and the library is built without hot/cold function splitting and without loops turned into library calls. Interrupt code must not call the C library, DVX, or DOS and must not use floating point. audio.lib stops the device from a shutdown hook if DVX ends without closing it.
A synthesizer driver exports an AudioSynthDriverT named audioSynth instead, written with AUDIO_SYNTH_DRIVER(name, priority, .kind = AUDIO_SYNTH_FM, AUDIO_SYNTH_MIDI, or AUDIO_SYNTH_WAVETABLE, .open = ..., .close = ..., then .write, .put, or the wavetable functions). open detects the hardware, fills AudioSynthDrvInfoT (description; for FM the voices and whether it is stereo; for a wavetable synthesizer its voices, memory, AUDIO_WT_* flags, and level, 0 when it is as loud as the digital sound or not known) and leaves it silent; close silences it. write(reg, value) sets an FM register (bank x AUDIO_FM_BANK_REGS + index) and must keep the index/data pair together (audioIrqOff / audioIrqRestore); put(byte) sends a MIDI byte, returning false when the port is not ready; a MIDI driver whose port receives also has get(&byte), the next byte the port has (false when none), which the MIDI input's timer calls (mpu401.drv keeps what put finds waiting for get). A wavetable driver has upload(addr, frames, count, bits) (program time; addresses as audioWtMemAlloc gives them) and start, update, release, stop, and active for its voices (numbered from 0, as audio.lib hands them out); it plays a note as AudioWtVoiceT gives it, in its own units. write, put, and the voice functions run at interrupt time too (timer callbacks), so they follow the rules above and are pinned by the driver. A sound driver whose card also has a synthesizer (the GUS) points AudioDrvInfoT.synth at its own AudioSynthDriverT, which is opened before the others.
BASIC
src/include/basic/sound.bas (DECLARE LIBRARY "audio"): PlayWave(fileName), PlayWaveLoop(fileName), PlayTone(frequency, milliseconds, volume), PlayNoise(frequency, milliseconds, volume) return a voice number (0 on failure: SoundError$ says why); StopWave(voice), StopAllSounds, SoundPlaying(voice), SoundVolume(percent), SoundInfo$ (the device, e.g. "Sound Blaster 16 (DSP 4.05) at 220h, 22050 Hz, 16-bit stereo"). Volumes are percent. Each BASIC program plays through its own context, closed when it ends.
For a settings program (the Control Panel's Sounds page): SoundEventCount, SoundEventLabel$(eventIdx), SoundEventKey$(eventIdx) (audioDeskLabel / audioDeskKey; events numbered from 0 in DvxSoundE order); SoundEventFile$(prefs, eventIdx) and SoundSetEventFile(prefs, eventIdx, fileName) (audioDeskFile / audioDeskSetFile); SoundEventsReload (audioDeskReload); SoundTestFile(fileName) (audioDeskPlayFile, a name alone found in the sound folders as the events' files are: a voice number, or 0 with SoundError$); SoundFolder$ (SHARED\SOUNDS, the sounds DVX ships; FileOpsKindDir$ and FileOpsKindList give the user's too); SoundMasterVolume and SoundSetMasterVolume(percent) (audioGetMasterVolume / audioSetMasterVolume in percent); SoundDriverCount (reads audioDriverNames, at most AUDIO_DRIVERS_MAX) and SoundDriverName$(index); SoundDriverAuto$ (AUDIO_DRIVER_AUTO); SoundMidiCount and SoundMidiLabel$(index) (audioMidiChoices); SoundMidiInCount and SoundMidiInLabel$(index) (audioMidiInChoices); SoundModulesCount and SoundModulesLabel$(index) (audioModulesChoices); SoundSettingDriver$(prefs), SoundSettingMidi(prefs), SoundSettingMidiIn(prefs), SoundSettingModules(prefs), SoundSettingVolume(prefs), SoundSettingsStore(prefs, driver, midi, modules, volume), and SoundSettingMidiInStore(prefs, midiIn) (the audioSetting functions). prefs is a prefs.bas handle (prefsBasHandle, dvxPrefs.h); a handle that is not open reads the defaults and stores nothing.
-
The rest of audio.h, in percent: PlayTriangle (audioTone with AUDIO_WAVE_TRIANGLE); SoundGetVolume (audioGetVolume); SoundCaps(field) (audioGetInfo: "dac", "synth", "tone", "running", "record" True / False; "voices" = voiceLimit, "rate", "bits", "channels", "recordsources"; any case, 0 for another field); SoundPlayEvent(eventIdx, wait) (dvxPlaySound / dvxPlaySoundWait, so the hook audioDeskPlay plays it; CONSTs SOUND_EVENT_* in DvxSoundE order). Samples: SoundLoad(fileName) (audioSampleLoad) and SoundCreate(pcm AS BINARY STRING, rate, bits, channels) (audioSampleCreate; frames = bytes / frame size, a trailing partial frame ignored) return a sound number; SoundFree (audioSampleFree), SoundLength (audioSampleInfo, in milliseconds), SoundPlay(sound, volume, pan, loopIt) (audioPlay; pan -100..100 scaled to AUDIO_PAN_LEFT..AUDIO_PAN_RIGHT). Voices: SoundSetVolume, SoundSetPan, SoundSetPitch(voice, percent) (25..400 percent of AUDIO_PITCH_NORMAL, clamped), and SoundSetFrequency(voice, hz) (the audioVoiceSet functions), SoundPause(voice) and SoundResume(voice) (audioVoicePause; a sound's or a stream's voice); the result is kept for SoundError$. Streams (push only): SoundStreamOpen(rate, bits, channels, bufferMs) (audioStreamOpen without a fill function), SoundStreamWrite(stream, pcm AS BINARY STRING) (audioStreamWrite of the whole frames; returns the bytes taken), SoundStreamSpace / SoundStreamQueued (frames), SoundStreamVoice, SoundStreamFlush (audioStreamFlush), SoundStreamClose. Recording, one per program (audioRecFile): RecordStart(fileName, rate, bits, channels, inputSource) (True / False with SoundError$; 0 for the defaults), RecordStop (audioRecClose; True when the file was all written), Recording, RecordLength (milliseconds recorded, written or waiting), RecordLevel (audioRecPeak), RecordLost, RecordRate, RecordBits, RecordChannels (audioRecFormat), with CONSTs RECORD_MIC, RECORD_LINE, RECORD_CD (AUDIO_REC_*) and SoundCaps "record" and "recordsources". FM: FmInfo$(field) (audioFmGetInfo: "description", "voices", "free", "stereo", "owned" as text), FmAlloc, FmWrite(voice, reg, value) (True / False), FmFree, FmRawAcquire, FmRawWrite(reg, value), FmRawRelease, with CONSTs FM_* for the AUDIO_FM_* values and FM_BANK_REGS (AUDIO_FM_BANK_REGS). Wavetable: WtInfo$(field) (audioWtGetInfo: "description", "voices", "free", "memory", "freememory", "level", and the flags "envelopes", "lfos", "filter", "effects", "8bit", "rom" as text), WtMemAlloc(bytes, bits) (an address, -1 for no room), WtUpload(address, pcm AS BINARY STRING, bits) (16-bit mono samples), WtMemFree, WtAlloc, WtStart(voice, address, startFrame, endFrame, loopStart, loopEnd, loopMode, bits, pitch, attenuation, pan) (a plain note: no envelope or LFO, the filter open; pitch in cents, attenuation in centibels, pan -100..100 scaled to AUDIO_WT_PAN_MAX), WtSetPitch, WtSetAttenuation, WtSetPan (audioWtUpdate of the note kept for the voice; True / False), WtRelease, WtStop, WtActive, WtFree. The MIDI port, declared in music.bas: MidiPortInfo$(field) (audioMidiGetInfo: "description", "present", "owned", "input", "inputowned"), MidiAcquire, MidiPut(byteValue) (False on AUDIO_ERR_BUSY: send it again), MidiRelease; its input: MidiInOpen (True / False with SoundError$), MidiInClose, MidiInMessage (audioMidiInRead: the message packed as status + data1 * 256 + data2 * 65536, AUDIO_MIDI_PACKED_*, or MIDI_NONE, -1), MidiInTime (the ms of the message MidiInMessage gave last), MidiInClock (audioMidiInClock), MidiInLost, with CONSTs MIDI_NOTE_OFF to MIDI_PITCH_BEND, MIDI_KIND_MASK, and MIDI_CHANNEL_MASK. CONSTs SOUND_PAN_*, SOUND_PITCH_* (percent), SOUND_RATE_* (AUDIO_RATE_*), and SOUND_STREAM_MS_* (AUDIO_STREAM_MS_*).
-
Sound and stream numbers are 1..64 per kind, valid only for the program that got them; audioClose of a BASIC context forgets its numbers (audioBasForget) as it frees the samples and streams. BINARY STRING parameters arrive as a pointer and a byte count (two C parameters; see the DECLARE LIBRARY rules in the BASIC reference).
+
The rest of audio.h, in percent: PlayTriangle (audioTone with AUDIO_WAVE_TRIANGLE); SoundGetVolume (audioGetVolume); SoundCaps(field) (audioGetInfo: "dac", "synth", "tone", "running", "record" True / False; "voices" = voiceLimit, "rate", "bits", "channels", "recordsources"; any case, 0 for another field); SoundStats(field) (audioGetStats: "renders", "frames", "underruns", "clipped", "dropped", "playing"); SoundDriver$ (AudioInfoT.driver); SoundPlayEvent(eventIdx, wait) (dvxPlaySound / dvxPlaySoundWait, so the hook audioDeskPlay plays it; CONSTs SOUND_EVENT_* in DvxSoundE order).
+
Samples: SoundLoad(fileName) (audioSampleLoad), SoundLoadData(bytes AS BINARY STRING) (audioSampleLoadMem), and SoundCreate(pcm AS BINARY STRING, rate, bits, channels) (audioSampleCreate; frames = bytes / frame size, a trailing partial frame ignored) return a sound number; SoundFree (audioSampleFree), SoundLength (audioSampleInfo, in milliseconds), SoundRate, SoundBits, and SoundChannels (audioSampleInfo's format), SoundPlay(sound, volume, pan, loopIt) (audioPlay; pan -100..100 scaled to AUDIO_PAN_LEFT..AUDIO_PAN_RIGHT). Voices: SoundSetVolume, SoundSetPan, SoundSetPitch(voice, percent) (25..400 percent of AUDIO_PITCH_NORMAL, clamped), and SoundSetFrequency(voice, hz) (the audioVoiceSet functions), SoundPause(voice) and SoundResume(voice) (audioVoicePause; a sound's or a stream's voice); the result is kept for SoundError$.
+
Ends: the program's context has audioSetEndFn set to audioBasEnded, which keeps the last BAS_ENDED_MAX (64) ended voices; SoundEnded gives the program's next one (0 for none) and SoundEndReason the AudioEndE of the one it gave last (CONSTs SOUND_END_*).
+
Note queues: NotesOpen(wave, volume) (audioNotesOpen; CONSTs SOUND_WAVE_* for AudioWaveE), NotesAdd(queue, hertz AS DOUBLE, milliseconds AS DOUBLE) (audioNotesAdd in millihertz and microseconds), NotesSetTone(queue, wave, volume) (audioNotesSetTone, the volume a percent of AUDIO_VOLUME_MAX), NotesQueued, NotesSpace, NotesVoice, NotesClear, NotesClose; numbered 1..64 like sounds.
+
Streams (push only): SoundStreamOpen(rate, bits, channels, bufferMs) (audioStreamOpen without a fill function), SoundStreamWrite(stream, pcm AS BINARY STRING) (audioStreamWrite of the whole frames; returns the bytes taken), SoundStreamSpace / SoundStreamQueued (frames), SoundStreamVoice, SoundStreamEnd (audioStreamEnd: no more will be written; the voice ends, SoundEnded tells, once what is held has played), SoundStreamFlush (audioStreamFlush), SoundStreamClose.
+
Recording, one per program (audioRecFile, or audioRecOpen into memory when fileName is ""): RecordStart(fileName, rate, bits, channels, inputSource) (True / False with SoundError$; 0 for the defaults), RecordRead$(maxBytes) AS BINARY STRING (audioRecRead of the whole frames that fit, into a buffer the next call reuses) and RecordQueued (audioRecQueued, in bytes), RecordStop (audioRecClose; True when a file was all written), Recording, RecordLength (milliseconds recorded, written or waiting), RecordLevel (audioRecPeak), RecordLost, RecordRate, RecordBits, RecordChannels (audioRecFormat), with CONSTs RECORD_MIC, RECORD_LINE, RECORD_CD (AUDIO_REC_*) and SoundCaps "record" and "recordsources".
+
FM: FmInfo$(field) (audioFmGetInfo: "description", "voices", "free", "stereo", "owned" as text), FmAlloc, FmWrite(voice, reg, value) (True / False), FmFree, FmRawAcquire, FmRawWrite(reg, value), FmRawRelease, with CONSTs FM_* for the AUDIO_FM_* values and FM_BANK_REGS (AUDIO_FM_BANK_REGS).
+
Wavetable: WtInfo$(field) (audioWtGetInfo: "description", "voices", "free", "memory", "freememory" (the largest free block), "freetotal" (freeTotal: all that is free), "level", and the flags "envelopes", "lfos", "filter", "effects", "8bit", "rom" as text), WtMemAlloc(bytes, bits) (an address, -1 for no room), WtUpload(address, pcm AS BINARY STRING, bits) (16-bit mono samples), WtMemFree, WtAlloc (the voice's note made plain: audioWtNoteInit), WtShape(voice, field, value) (an AudioWtVoiceT envelope, LFO, filter, or effects field by name -- "volattack", "modlfofreq", "filterfc", "reverb", ...; int16_t fields clamped -- kept in the voice's note, with audioWtUpdate at once on a sounding note for AUDIO_WT_SET_FILTER, _EFFECTS, and _LFO fields), WtStart(voice, address, startFrame, endFrame, loopStart, loopEnd, loopMode, bits, pitch, attenuation, pan) (the voice's note with the sample and these set; a negative address is AUDIO_WT_ADDR_ROM's, CONST WT_ADDR_ROM; pitch in cents, attenuation in centibels, pan -100..100 scaled to AUDIO_WT_PAN_MAX), WtSetPitch, WtSetAttenuation, WtSetPan (audioWtUpdate of the note kept for the voice; True / False), WtRelease, WtStop, WtActive, WtFree.
+
The MIDI port, declared in music.bas: MidiPortInfo$(field) (audioMidiGetInfo: "description", "present", "owned", "input", "inputowned"), MidiAcquire, MidiPut(byteValue) (False on AUDIO_ERR_BUSY: send it again), MidiRelease; its input: MidiInOpen (True / False with SoundError$), MidiInClose, MidiInMessage (audioMidiInRead: the message packed as status + data1 * 256 + data2 * 65536, AUDIO_MIDI_PACKED_*, or MIDI_NONE, -1), MidiInTime (the ms of the message MidiInMessage gave last), MidiInClock (audioMidiInClock), MidiInLost, with CONSTs MIDI_NOTE_OFF to MIDI_PITCH_BEND, MIDI_KIND_MASK, and MIDI_CHANNEL_MASK.
+
CONSTs SOUND_PAN_*, SOUND_PITCH_* (percent), SOUND_RATE_* (AUDIO_RATE_*), and SOUND_STREAM_MS_* (AUDIO_STREAM_MS_*).
+
Sound, stream, and note queue numbers are 1..64 per kind, valid only for the program that got them; audioClose of a BASIC context forgets its numbers and ended voices (audioBasForget) as it frees the samples, streams, and queues. BINARY STRING parameters arrive as a pointer and a byte count (two C parameters; see the DECLARE LIBRARY rules in the BASIC reference).
BASIC Development Library
-
basdev.lib is the DVX BASIC compiler and what a development tool builds on it: the build pipeline (basBuild.h), the compile-time Control.Member validator (basValidate.h), the debugger's view of a paused program (basDebug.h), and the SUB / FUNCTION scanner (basProcs.h). The DVX BASIC IDE uses it, and BASIC programs reach it through compiler.bas and debug.bas (basDevBas.c holds their entry points). It also carries the STUB and noicon resources every build inside DVX uses (BAS_BUILD_STUB_CARRIER). It needs basrt.lib, which holds the run host (basRunHost.h) the IDE, the standalone stub, and debug.bas share.
basdev.lib is the DVX BASIC compiler and what a development tool builds on it: the build pipeline (basBuild.h), the compile-time Control.Member validator (basValidate.h), the debugger's view of a paused program (basDebug.h), the SUB / FUNCTION scanner (basProcs.h), and the .frm model (frmModel.h) the IDE's designer works on. The DVX BASIC IDE uses it, and BASIC programs reach it through compiler.bas, debug.bas, and designer.bas's Frm* functions (basDevBas.c and frmModelBas.c hold their entry points). It also carries the STUB and noicon resources every build inside DVX uses (BAS_BUILD_STUB_CARRIER). It needs basrt.lib, which holds the run host (basRunHost.h) the IDE, the standalone stub, and debug.bas share.
Function Description
-------- -----------
basBuildAddSource Append one file to a BasProjectSourceT the way a project is built: a module in BEGINMODULE, a class module's code in BEGINCLASS, a UserControl's code in BEGINCLASS ... USERCONTROL (its header is kept in ctlTexts), a form's code section in BEGINFORM with its controls (the .frm text is kept in frmTexts), a lone program unwrapped. Records the file in the source map.
basBuildConcatProject Read every file an open .dbp names (modules, class modules, UserControls, forms) into a BasProjectSourceT through basBuildAddSource.
- basBuildCompile Compile source into a BasModuleT with OPTION EXPLICIT and an optional BasCtrlValidatorT. A BasCompileErrorT says why it failed: BAS_COMPILE_ERROR (line, column, message), BAS_COMPILE_NO_MEMORY, or BAS_COMPILE_NO_MODULE.
+ basBuildCompile Compile source into a BasModuleT with OPTION EXPLICIT and an optional BasCtrlValidatorT. A BasCompileErrorT says why it failed: BAS_COMPILE_ERROR (line, column, message), BAS_COMPILE_NO_MEMORY, or BAS_COMPILE_NO_MODULE. An optional unit callback runs after each statement or procedure compiled (basCompilerSetUnitFn), so a long compile can let other work go on.
basBuildErrorText A compiler message without its "Line N, Col M: " prefix.
basBuildMapLine Line of the concatenated source to file and file line.
basBuildMapFileLine File and file line to the line of the concatenated source (names compared as DOS does: any case, / and \ alike).
@@ -7385,7 +7563,7 @@ audioWtStart(audio, v, ¬e); // pitch 0: AUDIO_WT_PITCH_R
Validating Control Members (basValidate.h)
A BasValidatorT (zeroed) collects the forms, controls, and menu items of the .frm texts basValidateScanFrm parses; its onCode hook also receives each form's code section, so one parse serves both. basValidateCallbacks fills the BasCtrlValidatorT the compiler takes; pass &validator.compiler as basBuildCompile's liveCompiler so a control name resolves on the form whose code is compiled. Member names are checked against the loaded widget interfaces (WgtIfaceT) and the form runtime's own property tables. badTypes lists controls whose type no loaded widget library registers (BAS_VALIDATE_BAD_TYPE_FMT formats one). The command-line compiler does not validate: no widget library is loaded there.
Debugging (basDebug.h)
-
Every function takes the paused program's VM (or NULL when there is none).
+
Every function takes the paused program's VM (or NULL when there is none). The ones that run a snippet (basDbgEval, basDbgExec, basDbgSetVar, basDbgRunSnippet) take a BasDbgPumpT too (NULL for none): its fn(ctx) runs between the snippet's slices, so a long snippet lets other work go on (the IDE shows a frame with input held off; compiler.bas lets the other tasks run).
Function Description
-------- -----------
basDbgCurProcIdx The compiled procedure the paused VM is in (-1: module level).
@@ -7413,11 +7591,106 @@ audioWtStart(audio, v, ¬e); // pitch 0: AUDIO_WT_PITCH_R
The project (.dbp) model: PrjStateT with its file list (PrjFileT: path relative to the project's folder, kind -- PrjFileKindE: module, class module, UserControl ([Controls], .ctl), or form, in compile order -- edit buffer) and settings, prjLoad / prjSave (one table of [Project] / [Settings] keys drives load, save, prjFieldGet, and prjFieldSet; [Project] Type = Control makes a control project, prjIsControlProject), prjNew, prjAddFile, prjRemoveFile, prjRenameFile (disk and project together), prjFileIndexByPath, prjKindForPath (the build's rule, basBuildWrapForPath), prjKindWrap, prjClassName (a class module's or UserControl's class name), prjReadFileText, the fonts the program carries ([Fonts]: prjAddFont, prjRemoveFont, prjFontCount, prjFontPath; the build makes them FONTn font resources, basBuildSpecT fonts), and the source map of the last compile (prjMapLine, prjMapFileLine). The IDE keeps only its Project window and Properties dialog (ideProject.c); compiler.bas has ProjOpen, ProjNew, ProjSave, ProjClose, ProjPath$, ProjFileCount, ProjFile$, ProjFilePath$, ProjFileKind, ProjFileFind, ProjAddFile, ProjRemoveFile, ProjRenameFile, ProjFontCount, ProjFont$, ProjAddFont, ProjRemoveFont, ProjGet$, and ProjSet; CompKeywordClass and CompKeywordReserved expose the keyword table's flags.
A UserControl's header (the Begin UserControl block of a .ctl) is read and changed key by key, every other line of the text kept: prjCtlKeyAt / prjCtlKeyFind give the keys (PrjCtlKeyT: name and PrjCtlValueE kind -- Name, the control's name on the Begin line, then Width, Height, CanGetFocus, BackColor, ToolboxIcon, Description, DefaultEvent), prjCtlHeaderGet the value the control takes (its default when the header leaves the key out), prjCtlHeaderSet a malloc'd copy of the text with one key changed, added before End, or removed (an empty value), written as its kind is (a text in quotes, a color &HBBGGRR&, True or False), and prjCtlNew the text of a new UserControl (its header with Width and Height, Option Explicit, an empty UserControl_Paint written with PRJ_HANDLER_FMT and PRJ_PRIVATE, the form the IDE writes event handlers in). The IDE's File > New UserControl and Properties window use them; compiler.bas has ProjCtlNew$, ProjCtlHeader$, ProjCtlSetHeader$, and ProjCtlKeys$ (the keys and their kinds, prjCtlKeyAt order).
Running a Program (basRunHost.h, in basrt.lib)
-
basRunHostCreate makes the VM and form runtime for a module from a BasRunSpecT: App.Path, App.User (from the program's file: its folder in the user folder), App.Data (the DATA folder beside it), COMMAND$, the print, input, and DoEvents callbacks, the screen window's name, the help file, whether runtime errors show their own box, and whether it is a standalone program (the stub's own: the runtime owns the module, frees it with itself, and is ended through the shell's kill hook, shellRegisterAppKill, when the program is ended by force; the IDE and basdev end their runtimes themselves). basRunHostRegisterFrm registers a .frm text, basRunHostLoadForms loads the forms and shows the startup form, basRunHostSlice runs module code once under the step limit (an error's message and line are always set afterwards), basRunHostAlive says whether the event-loop phase goes on, basRunHostTagBreak puts BAS_RUN_BREAK_TAG on the captions while paused, and basRunHostDestroy ends it. The runtime made inside another program's code does not become the current runtime (basFormRtCurrent / basFormRtSetCurrent); each of these calls makes it current while its code runs.
+
basRunHostCreate makes the VM and form runtime for a module from a BasRunSpecT: App.Path, App.User (from the program's file: its folder in the user folder), App.Data (the DATA folder beside it), COMMAND$, the print, input, and DoEvents callbacks, the screen window's name, the help file, whether runtime errors show their own box, whether it is a standalone program (the stub's own: the runtime owns the module and frees it with itself), and whether it ends with its program (endWithApp: the IDE's runs, on a task of their own). A standalone runtime and one made with endWithApp are ended through the shell's kill hook, shellRegisterAppKill, when the program they run for is ended by force; basdev ends its runs itself. A runtime made on one task and run on another (the IDE makes it in a menu command and runs it on a work task, shellAppTaskStart) is moved with basFormRtAdopt, and basFormRtYield lets the other tasks run once without running any of the program's events. basRunHostRegisterFrm registers a .frm text, basRunHostLoadForms loads the forms and shows the startup form, basRunHostSlice runs module code once under the step limit (an error's message and line are always set afterwards), basRunHostAlive says whether the event-loop phase goes on, basRunHostTagBreak puts BAS_RUN_BREAK_TAG on the captions while paused, and basRunHostDestroy ends it. The runtime made inside another program's code does not become the current runtime (basFormRtCurrent / basFormRtSetCurrent); each of these calls makes it current while its code runs.
VM accessors a debugger host uses (vm.h): basVmSetAppPaths, basVmSetBreakpointCallback, basVmSetDebugPaused, basVmResume, basVmStop, basVmClearStepState, basVmGetCurrentLine, basVmGetErrorLine.
From BASIC
compiler.bas and debug.bas declare the entry points (see Include Libraries in the BASIC language reference). Source sets, modules, and runs are handles freed with their program. A run is polled: RunPoll runs the child for a bounded time and never waits. A pause in the child's module code returns RUN_PAUSED; a pause inside one of its event handlers (or its forms' Load code, inside RunStart) waits in place while the child pumps DVX events, so the caller's Timer and menus can step, continue, or stop it; RunPoll called from there returns at once. RunFree while the child's code is on the stack stops it at once and releases it at the next library call after that code has unwound.
CompDesignControls registers a source set's UserControls for the calling program's design forms (basCtlLoadDesign with its runtime, the current one: a child run's own when the child calls it) and holds the module -- one of the caller's own handles; another program's is refused -- until CompDesignRelease, the next CompDesignControls, or the end of that runtime (appEnded for a program, runDestroyHost for a child run), CompModuleFree meanwhile only marking it.
A form file as data, with no widgets and no windows: the form's properties, its controls (type, name, control array index, parent, the built-in size and state fields, and every other "Key = Value" pair as text), its menu items, and its code section. frmModelLoad reads .frm text (through frmParse) and frmModelWrite writes the canonical text the DVX BASIC IDE saves. The IDE's designer, properties window, menu editor, and copy and paste all work on this model and keep only their live widgets on the side (each control's userData); designer.bas binds it for BASIC programs (the Frm* functions, basdev/frmModelBas.c).
+
Header: apps/dvxbasic/basdev/frmModel.h (part of basdev.lib)
+
Types
+
Type Description
+ ---- -----------
+ FrmModelT A form: name, caption, width / height / left / top, padding, spacing, windowState, layout, helpTopic, contextMenu, centered / autoSize / resizable, controls (stb_ds array of FrmCtrlT pointers in tree order: each control directly followed by its children), menuItems (stb_ds array), props (other runtime form properties that were set: Icon, Tag, MousePointer, the Font properties ...), code (malloc'd, NULL = none), dirty (changed by an edit since it was made or loaded).
+ FrmCtrlT A control: name, typeName, parentName + parentIndex (the parent's control array index, so a child of one member of a container array belongs to that member), index (FRM_NO_INDEX: not in an array), left, top, width (MinWidth), height (MinHeight), maxWidth, maxHeight, weight, dragMode, helpTopic, visible, enabled, props (stb_ds array of FrmPropT: every other key, unquoted), userData (the caller's; the model never reads it and loads, copies, and pastes start it at NULL).
+ FrmMenuItemT A menu item: caption ("-" for a separator), name, shortcut, shortcutText, level (0: a menu-bar menu or a popup; 1: its items; 2+: submenus), id (an editor identity, see frmModelMenuReplace), checked, checkable (Checked was written), radioCheck, enabled, visible (False on a level-0 item: a popup menu).
+ FrmPropT One stored "Key = Value": name, value.
+ FrmIntPropT One built-in integer property (Left, Top, MinWidth alias Width, MinHeight alias Height, MaxWidth, MaxHeight, Weight, DragMode): name, alias, offset into FrmCtrlT, saveRule (FRM_SAVE_ALWAYS / IF_NONZERO / IF_POSITIVE).
+
Only values somebody set are stored and written: a property a control has by default (a PictureBox's BorderStyle, a TextBox's MaxLength) is not in the model and not in the file. Values are written in the canonical form for their widget interface type, or for a key the widget does not know, for their type in the runtime's common property table (STRING quoted, BOOL True / False, INT a number, FLOAT and ENUM unquoted); other keys are written as quoted strings. The limits come from the parser's: FRM_MAX_NEST_DEPTH control levels, FRM_MAX_MENU_LEVEL menu levels, and no property line longer than FRM_MAX_LINE_LEN - 1 characters (the writer fails with FRM_WRITE_LINE_TOO_LONG rather than write one the parser would cut).
+
Loading and Writing
+
Function Description
+ -------- -----------
+ frmModelNew(name) A blank form with the runtime form's defaults.
+ frmModelLoad(text, len) The model of .frm text; NULL when it is not a form (no Begin Form, frmParse refuses it, two controls share a name and index) or out of memory. A Key = Value line is the innermost open block's, also after the blocks inside it, as the runtime reads it; a child names its container by the container's final Index. Of several OptionButtons of one container saved with Value True, the last keeps it.
+ frmModelFree(m) Frees the model, its controls, and menus.
+ frmModelWrite(m, buf, bufSize) The .frm text into buf: its length, or FRM_WRITE_TOO_LARGE / FRM_WRITE_LINE_TOO_LONG (never a partial text).
+ frmModelWriteSize(m) A buffer size that always holds the text.
+ frmModelText(m, &len) The text in a new malloc'd string; NULL with len set to the FRM_WRITE_* code.
+ frmModelSetCode(m, code) Replaces the code section.
+ frmCodeSection(trailing, len, &outLen) The code section of the text after a form's End (blank lines skipped, the first code line keeps its indent).
+ frmModelFromForm(rt, form) A model of a loaded form as it is now: its properties, its controls in their containers with each property that differs from a new control of the type (compared with one made in a hidden window; the six Font properties of the form and of each control that has a font of its own), its menus; no code. What FormText$ writes.
+
Properties
+
Function Description
+ -------- -----------
+ frmModelFormProp(m, name, out, size) A form property as text (an unset design property gives "", "0", or "False"); false for run-time-only ones (Visible, DesignMode, DesignSelection, object properties).
+ frmModelSetFormProp(m, name, value) Sets one from text; Name renames the form; an empty value takes an optional one out.
+ frmCtrlPropText(ctrl, name, out, size) A control property as the control holds it (Name, Index, the built-in integers, HelpTopic, Visible / Enabled unless the widget defines them, the stored ones).
+ frmCtrlSetPropText(ctrl, key, value) Applies one the way the loader does (a VB TabIndex is dropped unless the widget defines it).
+ frmModelSetCtrlProp(m, ctrl, name, value) The same with an editor's rules: Name renames the control array, Index makes an array member, then the OptionButton group rule.
+ frmCtrlGetProp / frmCtrlSetProp / frmCtrlRemoveProp The stored properties.
+ frmIntPropAt / frmIntPropFind / frmIntPropGet / Set The built-in integer table.
+ frmIfaceProp(typeName, propName) The widget interface property of a type, NULL when none.
+
Font Properties
+
FontName, FontSize, FontBold, FontItalic, FontUnderline, and FontStrikethru (BAS_PROP_FONT*) are in both the form table and the common control table. They are one font, a DvxFontChoiceT (dvxDlg.h: family, size in pixels, FONT_STYLE_* flags), and the widget's font (wgtFont, wgtSetFont) is the only place they live: a read gives the font the object draws with (a form's is its root widget's), a write gives it that font with the one part changed. The .frm loader collects a block's Font properties and opens one font when the block's properties are read, so their order in the file does not matter. Helpers in formrt.h, for loaders and designers that hold them as .frm text:
+
Function Description
+ -------- -----------
+ basFontPropAt(idx) The Font property names in .frm order (FontName, FontSize, then the flags), NULL past the end.
+ basFontPropIs(name) True for a Font property name (any case).
+ basFontChoiceOf(font, out) A BitmapFontT as the properties name it (dvxFontFamilyOf, pixelSize, style).
+ basFontPropText(choice, name, out, size) One property of a choice as .frm text (the family, the size, True / False).
+ basFontPropSetText(choice, name, text) Sets one property of a choice from .frm text.
+
Editing Controls
+
Function Description
+ -------- -----------
+ frmModelAddCtrl(m, typeName, name, parent) Adds a control as the last child of parent (NULL: the form); name "" or NULL gives a free one (frmModelAutoName); Caption = its name. NULL when the name is taken or parent cannot hold it.
+ frmModelRemoveCtrl(m, ctrl) Removes ctrl and its subtree.
+ frmModelRenameCtrl(m, oldName, newName) Renames a whole control array; children and DataSource / MasterSource of a renamed Data control follow. Identifier rules are the caller's.
+ frmModelMakeArrayMember(m, ctrl, index) Gives a control array index; its children follow.
+ frmModelMoveCtrl(m, ctrl, before) Moves ctrl with its subtree before a sibling (NULL: after the last).
+ frmModelReparent(m, ctrl, parent) Makes ctrl the last child of another container (refuses cycles, non-containers, and too deep a nesting).
+ frmModelSibling(m, idx, dir) The sibling next to a control (+1 after its subtree, -1 before).
+ frmModelFindCtrl / CtrlPos / FindParent / CtrlDepth Lookups.
+ frmModelIsDescendant / SubtreeEnd / NormalizeOrder The tree order.
+ frmModelNameInUse / AutoName / OptionSelected Names and the OptionButton group rule.
+ frmModelCtrlText(m, ctrl, &len) ctrl and its children as .frm control blocks (a copy for the Clipboard).
+ frmModelCtrlsFromText(text, len) Copied blocks read into a scratch model.
+ frmModelInsertTree(m, src, srcRoot, parent, name, index) Moves a subtree out of the scratch model into m: the root takes name / index (NULL: its own name when free, else a free one); a child whose name is taken gets a free one, the members of one array together; children follow their renamed parents.
+
Menus
+
The frmMenu* functions work on any stb_ds array of FrmMenuItemT (an editor's working copy as well as m->menuItems) and keep the levels a tree.
+
Function Description
+ -------- -----------
+ frmMenuItemInit(mi) A blank item (enabled, visible, level 0).
+ frmMenuInsert(&items, at, level) Inserts a blank item; the level is clamped so the menus stay a tree.
+ frmMenuRemove(&items, idx) Removes an item with its subtree.
+ frmMenuMove(&items, idx, dir) Moves an item with its subtree past its previous / next sibling; its new position or FRM_NO_INDEX.
+ frmMenuIndent(items, idx, delta) Indents / outdents an item with its subtree (at most one level below the item above, not past FRM_MAX_MENU_LEVEL).
+ frmMenuSubtreeEnd / CheckLevels / StripBlank / AutoName Tree helpers; AutoName makes mnuSep1 ... or "mnu" plus the caption's letters and digits.
+ frmMenuPropText / frmMenuSetPropText Menu properties as text (Checked makes a check item).
+ frmModelMenuValidate(m, items, reason, size) The checks before items replace m's menus (levels; every name set, an identifier as the compiler takes one, not the form's or a control's, unique; every Shortcut a key): the first bad item and the reason.
+ frmModelMenuReplace(m, items) Replaces m's menus by a copy; ContextMenu bindings follow renamed items, matched by id (give every item id = position + 1 before editing).
+ frmModelSetMenuProp(m, idx, key, value) Sets an item's property; a Name rename refuses a taken name and ContextMenu bindings follow.
@@ -7846,100 +8118,6 @@ void basVmSetAppPaths(BasVmT *vm, const char *path, const char *user, const c
debugUdtDefs UDT type definitions for the debugger.
debugUdtDefCount Number of debug UDT definitions.
-
-
The .frm Model
-
A form file as data, with no widgets and no windows: the form's properties, its controls (type, name, control array index, parent, the built-in size and state fields, and every other "Key = Value" pair as text), its menu items, and its code section. frmModelLoad reads .frm text (through frmParse) and frmModelWrite writes the canonical text the DVX BASIC IDE saves. The IDE's designer, properties window, menu editor, and copy and paste all work on this model and keep only their live widgets on the side (each control's userData); designer.bas binds it for BASIC programs (the Frm* functions, formrt/frmModelBas.c).
-
Header: apps/dvxbasic/formrt/frmModel.h (part of basrt.lib)
-
Types
-
Type Description
- ---- -----------
- FrmModelT A form: name, caption, width / height / left / top, padding, spacing, windowState, layout, helpTopic, contextMenu, centered / autoSize / resizable, controls (stb_ds array of FrmCtrlT pointers in tree order: each control directly followed by its children), menuItems (stb_ds array), props (other runtime form properties that were set: Icon, Tag, MousePointer, the Font properties ...), code (malloc'd, NULL = none), dirty (changed by an edit since it was made or loaded).
- FrmCtrlT A control: name, typeName, parentName + parentIndex (the parent's control array index, so a child of one member of a container array belongs to that member), index (FRM_NO_INDEX: not in an array), left, top, width (MinWidth), height (MinHeight), maxWidth, maxHeight, weight, dragMode, helpTopic, visible, enabled, props (stb_ds array of FrmPropT: every other key, unquoted), userData (the caller's; the model never reads it and loads, copies, and pastes start it at NULL).
- FrmMenuItemT A menu item: caption ("-" for a separator), name, shortcut, shortcutText, level (0: a menu-bar menu or a popup; 1: its items; 2+: submenus), id (an editor identity, see frmModelMenuReplace), checked, checkable (Checked was written), radioCheck, enabled, visible (False on a level-0 item: a popup menu).
- FrmPropT One stored "Key = Value": name, value.
- FrmIntPropT One built-in integer property (Left, Top, MinWidth alias Width, MinHeight alias Height, MaxWidth, MaxHeight, Weight, DragMode): name, alias, offset into FrmCtrlT, saveRule (FRM_SAVE_ALWAYS / IF_NONZERO / IF_POSITIVE).
-
Only values somebody set are stored and written: a property a control has by default (a PictureBox's BorderStyle, a TextBox's MaxLength) is not in the model and not in the file. Values are written in the canonical form for their widget interface type, or for a key the widget does not know, for their type in the runtime's common property table (STRING quoted, BOOL True / False, INT a number, FLOAT and ENUM unquoted); other keys are written as quoted strings. The limits come from the parser's: FRM_MAX_NEST_DEPTH control levels, FRM_MAX_MENU_LEVEL menu levels, and no property line longer than FRM_MAX_LINE_LEN - 1 characters (the writer fails with FRM_WRITE_LINE_TOO_LONG rather than write one the parser would cut).
-
Loading and Writing
-
Function Description
- -------- -----------
- frmModelNew(name) A blank form with the runtime form's defaults.
- frmModelLoad(text, len) The model of .frm text; NULL when it is not a form (no Begin Form, frmParse refuses it, two controls share a name and index) or out of memory. A Key = Value line is the innermost open block's, also after the blocks inside it, as the runtime reads it; a child names its container by the container's final Index. Of several OptionButtons of one container saved with Value True, the last keeps it.
- frmModelFree(m) Frees the model, its controls, and menus.
- frmModelWrite(m, buf, bufSize) The .frm text into buf: its length, or FRM_WRITE_TOO_LARGE / FRM_WRITE_LINE_TOO_LONG (never a partial text).
- frmModelWriteSize(m) A buffer size that always holds the text.
- frmModelText(m, &len) The text in a new malloc'd string; NULL with len set to the FRM_WRITE_* code.
- frmModelSetCode(m, code) Replaces the code section.
- frmCodeSection(trailing, len, &outLen) The code section of the text after a form's End (blank lines skipped, the first code line keeps its indent).
- frmModelFromForm(rt, form) A model of a loaded form as it is now: its properties, its controls in their containers with each property that differs from a new control of the type (compared with one made in a hidden window; the six Font properties of the form and of each control that has a font of its own), its menus; no code. What FormText$ writes.
-
Properties
-
Function Description
- -------- -----------
- frmModelFormProp(m, name, out, size) A form property as text (an unset design property gives "", "0", or "False"); false for run-time-only ones (Visible, DesignMode, DesignSelection, object properties).
- frmModelSetFormProp(m, name, value) Sets one from text; Name renames the form; an empty value takes an optional one out.
- frmCtrlPropText(ctrl, name, out, size) A control property as the control holds it (Name, Index, the built-in integers, HelpTopic, Visible / Enabled unless the widget defines them, the stored ones).
- frmCtrlSetPropText(ctrl, key, value) Applies one the way the loader does (a VB TabIndex is dropped unless the widget defines it).
- frmModelSetCtrlProp(m, ctrl, name, value) The same with an editor's rules: Name renames the control array, Index makes an array member, then the OptionButton group rule.
- frmCtrlGetProp / frmCtrlSetProp / frmCtrlRemoveProp The stored properties.
- frmIntPropAt / frmIntPropFind / frmIntPropGet / Set The built-in integer table.
- frmIfaceProp(typeName, propName) The widget interface property of a type, NULL when none.
-
Font Properties
-
FontName, FontSize, FontBold, FontItalic, FontUnderline, and FontStrikethru (BAS_PROP_FONT*) are in both the form table and the common control table. They are one font, a DvxFontChoiceT (dvxDlg.h: family, size in pixels, FONT_STYLE_* flags), and the widget's font (wgtFont, wgtSetFont) is the only place they live: a read gives the font the object draws with (a form's is its root widget's), a write gives it that font with the one part changed. The .frm loader collects a block's Font properties and opens one font when the block's properties are read, so their order in the file does not matter. Helpers in formrt.h, for loaders and designers that hold them as .frm text:
-
Function Description
- -------- -----------
- basFontPropAt(idx) The Font property names in .frm order (FontName, FontSize, then the flags), NULL past the end.
- basFontPropIs(name) True for a Font property name (any case).
- basFontChoiceOf(font, out) A BitmapFontT as the properties name it (dvxFontFamilyOf, pixelSize, style).
- basFontPropText(choice, name, out, size) One property of a choice as .frm text (the family, the size, True / False).
- basFontPropSetText(choice, name, text) Sets one property of a choice from .frm text.
-
Editing Controls
-
Function Description
- -------- -----------
- frmModelAddCtrl(m, typeName, name, parent) Adds a control as the last child of parent (NULL: the form); name "" or NULL gives a free one (frmModelAutoName); Caption = its name. NULL when the name is taken or parent cannot hold it.
- frmModelRemoveCtrl(m, ctrl) Removes ctrl and its subtree.
- frmModelRenameCtrl(m, oldName, newName) Renames a whole control array; children and DataSource / MasterSource of a renamed Data control follow. Identifier rules are the caller's.
- frmModelMakeArrayMember(m, ctrl, index) Gives a control array index; its children follow.
- frmModelMoveCtrl(m, ctrl, before) Moves ctrl with its subtree before a sibling (NULL: after the last).
- frmModelReparent(m, ctrl, parent) Makes ctrl the last child of another container (refuses cycles, non-containers, and too deep a nesting).
- frmModelSibling(m, idx, dir) The sibling next to a control (+1 after its subtree, -1 before).
- frmModelFindCtrl / CtrlPos / FindParent / CtrlDepth Lookups.
- frmModelIsDescendant / SubtreeEnd / NormalizeOrder The tree order.
- frmModelNameInUse / AutoName / OptionSelected Names and the OptionButton group rule.
- frmModelCtrlText(m, ctrl, &len) ctrl and its children as .frm control blocks (a copy for the Clipboard).
- frmModelCtrlsFromText(text, len) Copied blocks read into a scratch model.
- frmModelInsertTree(m, src, srcRoot, parent, name, index) Moves a subtree out of the scratch model into m: the root takes name / index (NULL: its own name when free, else a free one); a child whose name is taken gets a free one, the members of one array together; children follow their renamed parents.
-
Menus
-
The frmMenu* functions work on any stb_ds array of FrmMenuItemT (an editor's working copy as well as m->menuItems) and keep the levels a tree.
-
Function Description
- -------- -----------
- frmMenuItemInit(mi) A blank item (enabled, visible, level 0).
- frmMenuInsert(&items, at, level) Inserts a blank item; the level is clamped so the menus stay a tree.
- frmMenuRemove(&items, idx) Removes an item with its subtree.
- frmMenuMove(&items, idx, dir) Moves an item with its subtree past its previous / next sibling; its new position or FRM_NO_INDEX.
- frmMenuIndent(items, idx, delta) Indents / outdents an item with its subtree (at most one level below the item above, not past FRM_MAX_MENU_LEVEL).
- frmMenuSubtreeEnd / CheckLevels / StripBlank / AutoName Tree helpers; AutoName makes mnuSep1 ... or "mnu" plus the caption's letters and digits.
- frmMenuPropText / frmMenuSetPropText Menu properties as text (Checked makes a check item).
- frmModelMenuValidate(m, items, isIdent, reason, size) The checks before items replace m's menus (levels; every name set, an identifier by isIdent, not the form's or a control's, unique; every Shortcut a key): the first bad item and the reason.
- frmModelMenuReplace(m, items) Replaces m's menus by a copy; ContextMenu bindings follow renamed items, matched by id (give every item id = position + 1 before editing).
- frmModelSetMenuProp(m, idx, key, value) Sets an item's property; a Name rename refuses a taken name and ContextMenu bindings follow.
One interface for byte-stream connections: a serial port, a modem with an AT command dialer, the secure link to the DVX host proxy (over a COM port or over the network), and Telnet. Every call except the host name lookup is non-blocking: connOpen starts the connection (dialing, TCP connect, key exchange) and connPoll moves it along, fills the receive buffer, and sends what connWrite queued. Call connPoll as often as you can (every frame). The lookup of a host name waits for the answer, but other programs keep running meanwhile; a dotted address needs no lookup.
@@ -7971,7 +8149,7 @@ if (m) {
ConnT *connCreate(void) Allocate a closed connection.
void connDestroy(c) Close and free.
void connConfigDefaults(cfg) COM1, 9600 8N1, no flow control, ATZ, ATDT.
- int32_t connOpen(c, cfg, nowMs) Start connecting (cfg is copied). CONN_OK, CONN_ERR_PARAM, CONN_ERR_PORT, or CONN_ERR_HOST (the host was not found or refused at once); connStatus explains. Later failures (refused, no answer within 30 seconds, connection lost, host closed it) end the connection with the reason in connStatus.
+ int32_t connOpen(c, cfg, nowMs) Start connecting (cfg is copied). CONN_OK, CONN_ERR_PARAM, CONN_ERR_PORT, or CONN_ERR_NO_NET (Telnet, or a secure link over the network, without the networking library); connStatus explains. A network host is looked up and connected to while connPoll runs (never waiting inside one call), so later failures (the host not found, refused, no answer within 30 seconds, connection lost, host closed it) end the connection with the reason in connStatus.
void connPoll(c, nowMs) Dial, key exchange, receive, send, timers.
int32_t connRead(c, buf, max) Take received bytes (0 when none).
int32_t connWrite(c, data, len) Queue bytes to send; returns the count accepted (0 while not open or when the 8 KB send buffer is full).
@@ -7982,7 +8160,7 @@ if (m) {
const char *connStatus(c) What is happening, or why the connection ended, in plain words.
uint32_t connStatusSerial(c) Changes whenever the status text changes.
void connSetWindowSize(c, cols, rows) The terminal size for Telnet's NAWS (sent now if the host asked for it).
- bool connTypeAvailable(type) True for every ConnTypeE value.
+ bool connTypeAvailable(type) True for every ConnTypeE value but ConnTelnetE when the networking library is not installed (an optional part of DVX; connections over the network then end at once with CONN_ERR_NO_NET). conn.lib does not import the networking library: it looks its calls up when a network connection starts.
bool connCanBreak(type) True for serial and modem.
const char *connErrorText(err) Text for a connOpen result.
Example
@@ -8018,6 +8196,7 @@ n = connRead(c, buf, sizeof(buf));
ConnState, ConnStatus$, ConnStatusSerial connState, connStatus, connStatusSerial.
ConnWrite(h, bytes$), ConnRead$(h), ConnPending(h) Bytes out and in without a Terminal: connWrite / connRead with BINARY STRING data, so every byte value passes.
ConnCanBreak(type), ConnSendBreak(h) connCanBreak, connSendBreak.
+ ConnTypeAvailable(type) connTypeAvailable: False for CONN_TELNET when Networking is not installed (a secure link over the network needs it too).
ConnClose h, ConnFree h Close (a modem hangs up); close and free the handle.
ConnAttach(h, ctrl$), ConnDetach h Show the connection on a Terminal control, and send what is typed there.
ConnSetEcho h, on; ConnSetAnswerback h, text$ Local echo (Enter as CR LF); the reply to ENQ.
@@ -8028,6 +8207,149 @@ n = connRead(c, buf, sizeof(buf));
ConnPoll with a Terminal attached does what the C Terminal's heartbeat did: it reads up to 4 KB, appends them to the capture file, answers each ENQ with the answerback (ENQ itself is not shown), stops at a ZMODEM start frame (the rest of what was read is dropped, and ConnZmodemRequest becomes True), and writes the rest to the Terminal. While a transfer started on the connection runs (or waits for XferAnswer), ConnPoll moves it along instead, nothing reaches the Terminal, and typing is dropped; a transfer on a line that is no longer open is cancelled. The receive window of a ZMODEM download on a serial or modem line without flow control is 2 KB, so a slow frame never loses data, unless the program set one with ConnSetXferWindow.
The Terminal binding goes through the form runtime (basFormRtTermBind, basFormRtTermUnbind, and basFormRtTermForget in basrt, looked up when ConnAttach first needs them): the runtime unbinds a Terminal whose form unloads, and tells the library, so a connection never writes to a freed control. Every handle belongs to the program that made it; whatever it leaves open is closed (a modem hangs up, a capture file is closed, a transfer cancelled) when it ends or is ended.
+
+
Cryptography Library
+
Cryptographic primitives for DVX programs and libraries (the secure link, the package library, the network, and the shell's random seed use them): the X25519 key exchange (RFC 7748), XChaCha20-Poly1305 authenticated encryption, Ed25519 signatures (RFC 8032), 1024-bit Diffie-Hellman key exchange (RFC 2409 Group 2), XTEA block cipher in CTR mode, SHA-256, HMAC-SHA256, and HKDF-SHA256, and a SHA-256 based random number generator. X25519, XChaCha20-Poly1305, and Ed25519 are Monocypher's (thirdparty/monocypher, BSD licence); the library keeps only the parts of it these functions use.
+
XTEA requires no lookup tables and compiles to approximately 20 instructions per round, making it suitable for 486-class hardware where AES S-box tables would thrash the 8KB cache. SHA-256 needs only 32-bit arithmetic and a 256-byte constant table.
Constant Value Description
+ -------- ----- -----------
+ SEC_DH_KEY_SIZE 128 Size of a DH public key or shared secret in bytes (1024 bits).
+ SEC_XTEA_KEY_SIZE 16 Size of an XTEA key in bytes (128 bits).
+ SEC_RNG_SEED_SIZE 32 Bytes stored in a seed file.
+ SEC_SHA256_SIZE 32 SHA-256 digest size.
+ SEC_SHA256_BLOCK_SIZE 64 SHA-256 block size.
+ SEC_HKDF_MAX_OUT 8160 Largest HKDF output (255 digests).
+ SEC_X25519_KEY_SIZE 32 An X25519 secret key, public key, or shared secret.
+ SEC_AEAD_KEY_SIZE 32 An XChaCha20-Poly1305 key.
+ SEC_AEAD_NONCE_SIZE 24 An XChaCha20-Poly1305 nonce.
+ SEC_AEAD_TAG_SIZE 16 The Poly1305 tag.
+ SEC_ED25519_SEED_SIZE 32 The random bytes an Ed25519 key pair is made from.
+ SEC_ED25519_SECRET_SIZE 64 An Ed25519 secret key (the seed, then the public key).
+ SEC_ED25519_PUBLIC_SIZE 32 An Ed25519 public key.
+ SEC_ED25519_SIG_SIZE 64 An Ed25519 signature.
+ SEC_X25519_JOB_WORDS 72 Size of a SecX25519JobT in 32-bit words.
+ SEC_X25519_PENDING 1 SecX25519Step (BASIC): more of the work to do.
+
Error Codes
+
Constant Value Description
+ -------- ----- -----------
+ SEC_SUCCESS 0 Operation succeeded.
+ SEC_ERR_PARAM -1 Invalid parameter.
+ SEC_ERR_NOT_READY -2 DH context not ready (keys not generated or secret not computed).
+ SEC_ERR_IO -3 Seed file could not be read or written.
+
RNG Functions
+
A SHA-256 hash DRBG over a 32-byte entropy pool. secRngAddEntropy hashes new bytes into the pool and never discards what is already there, so it is safe to call at any time, before or after the first output. Each output block is a hash of the pool and a counter; after every request the pool is re-hashed so earlier output cannot be recomputed from a later state.
+
The first secRngBytes call gathers hardware entropy automatically. On DOS that is 256 samples of the PIT counter, each taken after a busy wait sized by the previous sample, with the full time stamp counter on CPUs that have one (Pentium and later), else with the BIOS tick count. On a CPU without a time stamp counter the samples depend mostly on the PIT phase and time of day, which an observer can narrow down, so applications should feed user-input timings to secRngAddEntropy and keep a seed file: call secRngLoadSeed at startup and secRngSaveSeed at shutdown. Host builds read /dev/urandom.
+
Under the DVX shell this is automatic: the shell loads the seed file SECLINK.RND in the user folder at startup, feeds the timing of every key press and mouse event to secRngAddEntropy, and saves a fresh seed at shutdown. Applications only need their own calls when they run without the shell.
Function Description
+ -------- -----------
+ secRngAddEntropy Hash bytes into the pool (key timings, mouse positions, anything unpredictable).
+ secRngBytes Generate pseudorandom bytes.
+ secRngGatherEntropy Fill buf with condensed hardware entropy (one fresh gather per 32 bytes). Returns the byte count (0 if a host build cannot read its random device).
+ secRngLoadSeed Mix a seed file into the pool, then immediately rewrite it with fresh output so a seed is never used twice. SEC_SUCCESS or SEC_ERR_IO.
+ secRngSaveSeed Write SEC_RNG_SEED_SIZE bytes of fresh output for the next run. SEC_SUCCESS or SEC_ERR_IO.
+ secSecureZero Zero memory in a way the compiler cannot optimize away (for key material).
SHA-256 follows FIPS 180-4, HMAC RFC 2104, and HKDF RFC 5869 (extract and expand; a NULL or empty salt means 32 zero bytes). secHkdf returns SEC_ERR_PARAM for okmLen outside 1..SEC_HKDF_MAX_OUT. secConstTimeEqual compares without revealing where the inputs differ; use it for every MAC check.
+
Diffie-Hellman Key Exchange
+
1024-bit DH with 256-bit private exponents using the RFC 2409 Group 2 safe prime. The modular exponentiation is a Montgomery ladder with branch-free operand swaps, so its timing does not depend on the private key.
+
SecDhT *secDhCreate(void);
+int secDhGenerateKeys(SecDhT *dh);
+int secDhGetPublicKey(SecDhT *dh, uint8_t *buf, int *len);
+int secDhComputeSecret(SecDhT *dh, const uint8_t *remotePub, int len);
+int secDhGetSecret(SecDhT *dh, uint8_t *buf, int *len);
+void secDhDestroy(SecDhT *dh);
+
Function Description
+ -------- -----------
+ secDhCreate Allocate a new DH context.
+ secDhGenerateKeys Generate a private exponent and public key from the DRBG.
+ secDhGetPublicKey Copy the local public key into buf. *len receives the key size.
+ secDhComputeSecret Compute the shared secret from the remote public key. Keys outside [2, p-2] are rejected with SEC_ERR_PARAM (0, 1, and p-1 would force a trivially known secret).
+ secDhGetSecret Copy the shared secret (SEC_DH_KEY_SIZE big-endian bytes) into buf. It is input for a key derivation function such as secHkdf, never a key by itself.
+ secDhDestroy Securely zero and free the DH context.
Function Description
+ -------- -----------
+ secCipherCreate Allocate a cipher context with a 128-bit key. Returns NULL on allocation failure or NULL key. Initial counter is zero.
+ secCipherCrypt Encrypt or decrypt data in place. The 64-bit counter increments by one every 8 bytes (one XTEA block).
+ secCipherSetCounter Position the 64-bit block counter (secLink derives it from the frame sequence number).
+ secCipherDestroy Securely zero the cipher context (key and counter state) and free its memory.
+
secCipherCreate returns NULL if the key pointer is NULL or allocation fails. secCipherCrypt is a no-op when any of c/data is NULL or len <= 0.
+
X25519 Key Exchange
+
Elliptic-curve Diffie-Hellman on Curve25519 (crypto/secCurve.h). Each side makes a secret key from 32 random bytes (secRngBytes), sends its public key, and combines its secret key with the other side's public key; both get the same 32-byte secret. One call takes about a sixth of a second on a 486 (fixtures/bncurve on the emulated 486DX2-66: 165 ms), so a program that must stay responsive does it a slice at a time with a SecX25519JobT (the secure link does).
secX25519 is false when the peer's key gives an all-zero secret (one of the few keys that give everyone the same secret: a cheating peer). The secret is input for a key derivation function such as secHkdf, never a key by itself.
+
A job is the same computation in nine slices of about 20 ms each on a 486 (eight of the ladder, then the inversion): secX25519JobStart begins it (peerPublic NULL makes the public key of secretKey), secX25519JobStep does a slice and is true once there is nothing left, secX25519JobResult writes the result (false as secX25519 is, or before the job is done) and wipes the job, and secX25519JobWipe drops one. A SecX25519JobT is the caller's (no allocation); the slicing is a small DVX patch to Monocypher (thirdparty/monocypher/DVX-VERSION.md).
+
XChaCha20-Poly1305
+
Authenticated encryption: the data is encrypted and a 16-byte tag proves it, and some associated data sent in the clear, unchanged.
Both work in place. secAeadUnlock decrypts only when the tag matches; otherwise it returns false and leaves data as it was. A key and nonce pair must never seal two different messages: a counter in the nonce (the secure link uses its frame number) or a fresh random nonce (24 bytes is enough to pick at random) keeps them apart.
+
Ed25519 Signatures
+
Public-key signatures (RFC 8032, with SHA-512), as the package library uses for add-on packages.
secEd25519KeyPair makes the 64-byte secret key (the seed followed by the public key) and the public key from 32 random bytes, and wipes the seed. secSign.c holds these three and secCurve.c the rest, so a program that links the sources in (the host proxy) can leave the signatures out.
+
BASIC
+
src/include/basic/security.bas (DECLARE LIBRARY "crypto") declares the BASIC functions (crypto/secBas.c). Keys, data, and results are BINARY STRING (pointer and length in, a result buffer and length out), so every byte value passes; the result buffer is wiped before it is reused. A handle (1, 2, ...; 0 = none) is a key exchange, a cipher, or an incremental SHA-256 / HMAC; each records the program that made it and the shell's app-cleanup hook frees (and wipes) what a program leaves open.
+
BASIC C it calls
+ ----- ----------
+ SecRandom$(count), SecAddEntropy bytes secRngBytes; secRngAddEntropy.
+ SecDhCreate() secDhCreate + secDhGenerateKeys.
+ SecDhPublicKey$(h), SecDhSecret$(h) secDhGetPublicKey; secDhGetSecret ("" before SecDhComputeSecret).
+ SecDhComputeSecret(h, remotePublicKey) secDhComputeSecret: SEC_OK or a SEC_ERR_ code.
+ SecDhFree h secDhDestroy.
+ SecCipherCreate(key) secCipherCreate; 0 unless key is SEC_XTEA_KEY_SIZE bytes.
+ SecCipherCrypt$(h, bytes) secCipherCrypt on a copy.
+ SecCipherSetCounter h, counter secCipherSetCounter (a DOUBLE: exact up to 2^53).
+ SecCipherFree h secCipherDestroy.
+ SecSha256$(bytes); SecSha256Open, SecSha256Add, SecSha256End$ secSha256; secSha256Init / Update / Final (End frees the handle).
+ SecHmac$(key, bytes); SecHmacOpen, SecHmacAdd, SecHmacEnd$ secHmacSha256; secHmacInit / Update / Final.
+ SecHkdf$(salt, secret, info, length) secHkdf ("" for a length outside 1..SEC_HKDF_MAX_OUT).
+ SecEqual(a, b) secConstTimeEqual (False for different lengths).
+ SecX25519PublicKey$(secretKey) secX25519PublicKey ("" unless the key is SEC_X25519_KEY_SIZE bytes).
+ SecX25519$(secretKey, otherPublicKey) secX25519 ("" for a wrong size or a refused key).
+ SecX25519Start(secretKey, otherPublicKey) secX25519JobStart in a handle ("" otherPublicKey: the public key); 0 for wrong sizes.
+ SecX25519Step(h); SecX25519End$(h); SecX25519Free h secX25519JobStep (SEC_X25519_PENDING, then SEC_OK); secX25519JobResult (frees the handle); secX25519JobWipe.
+ SecAeadLock$(key, nonce, extra, bytes) secAeadLock on a copy: the ciphertext, then the tag.
+ SecAeadUnlock$(key, nonce, extra, sealed) secAeadUnlock on a copy ("" when the tag does not match).
+ SecEd25519KeyPair$(seed) secEd25519KeyPair: the secret key (the program's seed is not wiped).
+ SecEd25519PublicKey$(secretKey) The public key (the secret key's last SEC_ED25519_PUBLIC_SIZE bytes).
+ SecEd25519Sign$(secretKey, bytes) secEd25519Sign.
+ SecEd25519Check(signature, publicKey, bytes) secEd25519Check (False for wrong sizes).
+
The SEC_ CONSTs of security.bas are compared with security.h, secHash.h, and secCurve.h by a formrt test, which also calls every function for real.
+
Networking Library
TCP/IP for DVX programs with the standard BSD socket API: a C programmer who knows sockets writes DVX network code the usual way. IPv4 today; the types and constants for IPv6 (AF_INET6, sockaddr_in6, sockaddr_storage) are defined so code can be written for both.
@@ -8121,7 +8443,22 @@ ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
Waiting, Polling, and Cooperative Multitasking
DVX runs programs cooperatively, so a call that has to wait never stops the system: blocking connect, accept, recv, send (with a full buffer), select, poll, and name lookups wait by running the network and then letting the other tasks run (tsYield), again and again, until they can return. Other programs, and other connections, keep working meanwhile.
The network also runs from the shell's idle time, so connections progress (and data arrives) while a program is busy with other things; every socket call runs it too. A program that polls non-blocking sockets in its own loop needs nothing else.
-
Right after the network starts, DHCP needs a moment to get an address. connect, sendto, and name lookups wait for it (up to 20 seconds) instead of failing, so a program started together with DVX can connect at once.
+
Right after the network starts, DHCP needs a moment to get an address. Blocking connect and sendto calls, and name lookups, wait for it (up to 20 seconds) instead of failing, so a program started together with DVX can connect at once. A non-blocking socket does not wait.
+
A program that must not wait at all, not even inside one call, looks a name up with netLookupStart and netLookupPoll (dvxNet.h) instead of getaddrinfo: each poll only looks, and the lookup waits for DHCP's first address too, so a non-blocking connect to the address it finds goes at once. DVX's own web requests (http) and Terminal connect this way.
+
NetLookupT lk;
+uint32_t addr;
+int32_t rc;
+
+netLookupStart(&lk, "bbs.example.org");
+
+// Each frame, or from a timer:
+rc = netLookupPoll(&lk, &addr);
+
+if (rc == 0) {
+ // addr (network order): connect a non-blocking socket to it
+} else if (rc != NET_LOOKUP_BUSY) {
+ // gai_strerror(rc) says why
+}
Sockets Belong to Programs
Each socket belongs to the program that created it (accept gives the new socket to the program that accepts). When a program ends, normally or because it was ended or crashed, the shell closes every socket it still had open, and drops its unfinished name lookups.
@@ -8134,6 +8471,9 @@ ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
void netStop(void) Close every socket and stop (the packet driver is released).
int32_t netRestart(void) Stop, then start with the saved settings (after they changed).
void netPoll(void) Take received frames, run the timers, send. Called from the shell's idle time and by every socket call.
+ void netLookupStart(NetLookupT *lk, const char *name) Start looking a name (or a dotted address) up without waiting.
+ int32_t netLookupPoll(NetLookupT *lk, uint32_t *addr) NET_LOOKUP_BUSY, 0 with the address (network order), or an EAI_* code; never waits.
+ void netLookupCancel(NetLookupT *lk) Give up a lookup that has not ended.
void netGetStatus(NetStatusT *status) State (NetStoppedE, NetNoDriverE, NetConfiguringE, NetUpE), plain-words message, addresses, name servers, host name, packet driver name, interrupt and hardware address, packet and byte counters, frames lost while DVX was busy, open sockets.
const char *netStateText(int32_t state) Plain words for a state.
const char *netErrorText(int32_t err) Plain words for an errno value, network codes included.
@@ -8157,13 +8497,13 @@ ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
Driver pktDrvDos.c, pktRing.c Packet driver calls through DPMI; a real-mode callback receives frames at interrupt time into a 16-slot ring in conventional memory; the stack takes them out later.
DOS glue netDos.c Descriptors through DJGPP File System Extensions, fcntl for sockets, the shell hooks (idle poll, app cleanup), orderly shutdown.
Settings dvxNet.c DVX.INI, status, errors in plain words.
-
lwIP runs without threads (NO_SYS), configured only by port/lwipopts.h: TCP MSS 1460 with a 4-segment window, 16 TCP connections, 48 KB of stack memory, DHCP, DNS (2 servers), ARP, ICMP (ping answers), IP fragments, loopback. Nothing runs lwIP at interrupt time.
+
lwIP runs without threads (NO_SYS), configured only by port/lwipopts.h: TCP MSS 1460 with a 4-segment window, 16 TCP connections, 24 KB of stack memory for data being sent and 20 receive buffers, DHCP, DNS (2 servers), ARP, ICMP (ping answers), IP fragments, loopback. Nothing runs lwIP at interrupt time.
The receive ring holds 16 frames. A burst larger than that while DVX is busy loses frames (counted as "lost" in the status); TCP sends them again.
On the host the same sources are built into the "net" test suite: a fake packet driver passes frames through the real ring to a second copy of the stack in another process, which plays DHCP, DNS, and echo server.
Networking from BASIC
-
net.bas declares simple functions over the socket API (netBas.c): HostLookup$, HostName$, NetAddress$, NetError$, TcpConnect, TcpListen, TcpAccept, TcpSend, TcpPending, TcpRecv$, TcpConnected, TcpShutdown, TcpPeer$, TcpLocalPort, TcpSetNoDelay, TcpSetKeepAlive, TcpClose, UdpOpen, UdpSend, UdpRecv$, UdpFrom$, UdpClose. A handle is the socket's descriptor + 1, so 0 means failure. Only TcpConnect (up to 20 seconds) and TcpSend wait; everything else returns at once, so a program polls from a Timer. TcpSend, TcpRecv$, UdpSend, and UdpRecv$ pass their data as BINARY STRING (pointer and length), so every byte value goes through. See the BASIC language reference, Include Libraries.
+
net.bas declares simple functions over the socket API (netBas.c): HostLookup$, HostLookupStart / Poll / Result$ / Close, HostName$, NetAddress$, NetError$, TcpConnect, TcpConnectStart / Poll, TcpListen, TcpAccept, TcpSend, TcpSendSome, TcpPending, TcpRecv$, TcpPeek$ (recv with MSG_PEEK), TcpConnected, TcpShutdown, TcpPeer$, TcpLocalPort, TcpSetNoDelay, TcpSetKeepAlive, TcpClose, UdpOpen, UdpSend, UdpRecv$, UdpFrom$, UdpClose. A handle is the socket's descriptor + 1, so 0 means failure. Only TcpConnect (up to 20 seconds), HostLookup$, and TcpSend wait; everything else returns at once, so a program polls from a Timer. The other socket options need no binding: TcpListen sets SO_REUSEADDR and UdpOpen SO_BROADCAST, the stack takes SO_LINGER and the buffer sizes without effect (closing finishes in the background; the buffers are fixed), and no BASIC call waits on a receive, so SO_RCVTIMEO does nothing for it. TcpSend, TcpRecv$, UdpSend, and UdpRecv$ pass their data as BINARY STRING (pointer and length), so every byte value goes through. See the BASIC language reference, Include Libraries.
BASIC C it calls
----- ----------
HostName$(address) getnameinfo: the address itself, as this stack does not look names of addresses up; "" when it is not an address.
@@ -8184,6 +8524,8 @@ ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
NetConfigFree(cfg) Free the handle.
NetVectorCount / NetVector(i) The packet driver interrupt choices: 0 (find it), then NET_VECTOR_FIRST..NET_VECTOR_LAST.
NetRestart() netRestart; True for NET_OK.
+ netBasStreamRead / netBasStreamWrite A net.bas TCP handle as a byte stream that never waits, for another library's BASIC functions (comm.bas's CommOpenTcp runs the secure packet link over it): bytes read or taken (0 = none now), negative once the connection has closed or failed.
+ NetState() netGetStatus's state (NetStateE; BASIC's NET_STATE_STOPPED, _NO_DRIVER, _CONFIGURING, _UP).
NetStatus$(field) netGetStatus, one field as text: message, up, link, dhcp, address, netmask, gateway, dns1, dns2, hostname, driver, mac, vector, sockets, rxpackets, rxbytes, rxdropped, txpackets, txbytes.
Settings handles (1 to 16, 0 = none) belong to the program that made them; netCloseAppSockets frees what a program left.
@@ -8315,7 +8657,9 @@ ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
dxeCtx DxeAppContextT * Heap-allocated context (address stable across realloc).
taskCodeDepth int32_t * Per task id: SHELL_APP_CALL calls into this app still open on that task's stack.
killPending bool A force kill was deferred; shellReapApps finishes it.
- faulted bool It crashed: its force kill runs no kill hooks (shellRegisterAppKill).
+ faulted bool It crashed: its force kill runs no kill hooks (shellRegisterAppKill).
+ workTaskId uint32_t The task shellAppTaskStart started for it, while that runs; else 0.
+ workFn, workArg void (*)(void *), void * What that task runs.
App Lifecycle API
@@ -8339,7 +8683,7 @@ ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
Gracefully shut down a single app. Calls the app's shutdownFn (if present), destroys all windows belonging to the app, kills its task (if any), releases any widget classes and system-wide hotkeys the app registered and did not unregister (wgtUnregisterAppClasses, dvxUnregisterAppHotkeys), closes the DXE handle, and frees the context. The slot returns to AppStateFreeE.
Forcibly kill an app. Used by the Task Manager "End Task" or as part of crash recovery (which marks the app faulted first). The app's shutdownFn is NOT called: a force kill targets a hung or faulting app whose own code cannot be trusted to run. Instead the libraries' kill hooks (shellRegisterAppKill) end what they run for the app, unless it faulted, then the shell drops every idle and desktop-update callback the app registered, destroys all of the app's windows, kills its task (for main-loop apps), and finally closes the DXE handle.
+
Forcibly kill an app. Used by the Task Manager "End Task" or as part of crash recovery (which marks the app faulted first). The app's shutdownFn is NOT called: a force kill targets a hung or faulting app whose own code cannot be trusted to run. Instead the libraries' kill hooks (shellRegisterAppKill) end what they run for the app, unless it faulted, then the shell drops every idle and desktop-update callback the app registered, destroys all of the app's windows, kills its tasks (a main-loop app's, and a work task shellAppTaskStart started), and finally closes the DXE handle.
Cleanup order: callbacks and windows first (removes them from the compositor), then the task (frees its stack; any dispatch frames still open on it are dropped from the context's dispatch depth), then the widget classes and system-wide hotkeys the app registered (its shutdownFn never got the chance to unregister them), then the DXE handle (unmaps the code). Closing the DXE before destroying windows would cause callbacks into unmapped code.
If the app's code may still be running underneath the call -- End Task clicked in a dialog the app has open, a kill issued from the app's own task, or from inside a shell call into the app -- the unload is deferred: the app's windows are hidden and queued for destruction, the slot is marked Terminating, and the shell finishes the kill (still without shutdownFn) at the next safe point of its main loop, once that code has returned. Event-handler frames on the app's own task do not hold the kill back, since they end with the task: an app stuck in an endless event handler is killed at once (or at the next main-loop pass).
Build the full path of a file the program ships in the DATA folder beside its .app ("" when it does not fit), e.g. "APPS\ACME\MYGAME\DATA\LEVEL1.DAT". Read-only.
+
Program Cache
+
Program names, descriptions, and icons come from the programs' own files (their name, description, and icon32 resources), and reading them takes several file opens per program, tens of milliseconds each on a slow disk, every time DVX starts. The shell keeps what it read in PROGRAMS.CAC in the user folder, with each file's size and time (platformFileStamp), and answers from it while a file is unchanged: shellProgramInfo, shellItemIcon, shellItemIcons, shellItemIconFile, and shellProgramIcon all go through it, so Program Manager, File Manager, and BASIC's Shell calls start without opening any program that has not changed.
+
A file is checked against its stamp the first time it is asked about after what is installed last changed (platformInstalledChangeCount), and whenever shellProgramScan reads names (the folder is fresh in DOS's buffers then); a program that changed is read again, and a file that is gone loses its entry. A picture is kept as the image file's bytes (a DVX file's icon resource, or a picture file of up to 32 KB) and decoded for the display mode each time; bytes that turn out to be no image are noted as none.
+
The file is read on first use. A change is saved three seconds after the last one, 4 KB at a time in the shell's idle time, so no frame waits for the whole write, and what is left at shutdown is saved then; the memory goes after a minute unused. It is DVX's memory (dvxMemSetApp), whichever program's call made it. Nothing is saved when DVX cannot save (platformIsReadOnly). Deleting PROGRAMS.CAC is harmless: it is made again. A file rewritten at the same size within two seconds keeps its stamp (DOS keeps a file's time to two seconds), so a program changed that quickly shows its old name or icon until it changes again.
Random Seed File
-
When the serial library is loaded, the shell feeds keyboard and mouse event timings to its random-number generator (dvxSetInputEntropyHook) and carries generator state across runs in SECLINK.RND in the user folder: the file is read and immediately rewritten at startup, and rewritten again at shutdown. The library is found at run time; without it no seed file is written.
+
When the cryptography library is loaded, the shell feeds keyboard and mouse event timings to its random-number generator (dvxSetInputEntropyHook) and carries generator state across runs in SECLINK.RND in the user folder: the file is read and immediately rewritten at startup, and rewritten again at shutdown. The library is found at run time; without it no seed file is written.
Desktop Callbacks
-
The shell wires several AppContextT event hooks (onCtrlEsc, onF1, onTitleChange, idleCallback) during startup and exposes them to other DXEs via function-pointer extern and registration API. Desktop managers register a callback to refresh their display when apps come and go. The Task Manager DXE registers its Ctrl+Esc handler via the shellCtrlEscFn extern pointer (see Task Manager documentation); shellOpenTaskManager is the one way to open it.
+
The shell wires several AppContextT event hooks (onCtrlEsc, onF1, onTitleChange, idleCallback, busyCallback, inputHere) during startup and exposes them to other DXEs via function-pointer extern and registration API. Desktop managers register a callback to refresh their display when apps come and go. The Task Manager DXE registers its Ctrl+Esc handler via the shellCtrlEscFn extern pointer (see Task Manager documentation); shellOpenTaskManager is the one way to open it.
Shell Event Hooks
AppContextT field Handler Purpose
----------------- ------- -------
idleCallback shellIdleDispatch Runs one frame of Run Minimized (shellMinimizeIdle) and every registered idle handler (see shellRegisterIdle), then yields to cooperative tasks, when dvxUpdate has no input events and no dirty rects.
+ busyCallback shellBusyDispatch After a frame that painted: yields to cooperative tasks, and runs shellIdleDispatch when the idle handlers have not run for 20 ms, so a program that keeps the screen changing never starves the others, audio, or the network.
+ inputHere shellInputHere Input, events, and widget timers are taken on the shell's own task (or on any task once the shell's has not taken them for 250 ms), so callbacks run on the shell's task, never on another program's stack.
onCtrlEsc ctrlEscHandler shellOpenTaskManager.
onF1 f1HelpHandler Opens the focused app's helpFile/helpTopic (or the system help) in the help viewer, reusing a running viewer that sets onNewArgs.
onTitleChange titleChangeHandler Runs shellDesktopUpdate so Program Manager window lists refresh.
Register an idle handler invoked every idle frame by the shell main loop, regardless of which app is foreground. Background pollers (serial and secLink) register here so their connections keep pumping while another app is in front. Registration is idempotent on the (fn, ctx) pair. The registering module MUST unregister before it is unloaded -- app DXEs do this from their _appShutdown hook on a graceful exit. On a force kill (which does not run _appShutdown) the shell drops the app's registrations itself.
+
Register an idle handler invoked every idle frame by the shell main loop (and at least every 20 ms while frames keep painting), regardless of which app is foreground. Background pollers (serial and secLink) register here so their connections keep pumping while another app is in front. Registration is idempotent on the (fn, ctx) pair. The registering module MUST unregister before it is unloaded -- app DXEs do this from their _appShutdown hook on a graceful exit. On a force kill (which does not run _appShutdown) the shell drops the app's registrations itself.
Register a function the shell calls with an app's id when the app is ended by force (End Task, a screen saver that would not close, DVX exiting) while none of the app's code is running and its windows are still there -- before the windows go and before the cleanup hooks. A library ends what it runs for the app the way the app's own end would: the BASIC runtime (basrt) frees a standalone program's VM, forms, screen, and module this way, closing its serial ports and its forms' windows, so End Task on a BASIC program leaks nothing. Not called for an app that faulted (its state cannot be trusted), nor for one whose kill had to wait until its code returned (End Task from inside the app's own dialog: its windows went at once). Registration rules as for cleanup hooks.
Run fn(arg) on a cooperative task of its own (stackSize bytes; 0 for the default) that belongs to the running program (ctx->currentAppId), for a long piece of a callback-only program's work: the DVX BASIC IDE runs each program it starts this way. A callback-only program's code runs inside the shell's main loop, which only reaps programs that ended, carries out close and Exit requests and DOS program starts, and starts the screen saver between frames; work done in a callback holds all of that until it returns. On its own task the work leaves the main loop free. The program is the running one on the task (windows it makes are the program's), it is not reaped while the task runs (even with all its windows closed), and ending it by force (End Task) ends the task. One at a time per program. Returns the task's ID, or -1 when one runs already, no program is running, or no task can be made.
shellDesktopUpdate
void shellDesktopUpdate(void);
Notify all registered desktop callbacks that app state has changed. Called internally by the shell after loading or reaping an app. Can also be called by apps that need to trigger a desktop refresh. Each call also adds one to the change counter (shellDesktopChangeCount).
Paths are made absolute and canonical (relative names join the current directory, backslashes become forward slashes, "." and ".." are resolved), so every spelling of one file is the same database.
Locking is in-process: DOS is single user, but two connections inside DVX (two programs, or a Data control and BASIC code) can share a file. Any number may read; one may write. A write while another connection is writing, or a commit while another connection has a query open, fails with "database is locked"; retry after the other side finishes (dvxSqlFreeResult releases a query's read lock). Nothing is locked at the DOS level, so do not share a database with another program outside DVX.
Temporary files, when used, are SQLxxxxx.TMP in the directory named by TMPDIR, TEMP, or TMP (else the current directory) and are deleted when closed.
-
Randomness comes from the serial library's generator (secRngBytes) when that library is loaded, else from platformEntropyStamp mixed with the time of day.
+
Randomness comes from the cryptography library's generator (secRngBytes) when that library is loaded, else from platformEntropyStamp mixed with the time of day.
Databases written by the SQLite 3.5.6 that earlier DVX releases bundled open, read, and write unchanged. Files created now use the current file format, which SQLite 3.5.6 cannot read.
Execute one or more SQL statements that return no result rows. Suitable for DDL (CREATE TABLE, DROP TABLE, etc.) and DML (INSERT, UPDATE, DELETE).
+
Execute one or more SQL statements that return no result rows. Suitable for DDL (CREATE TABLE, DROP TABLE, etc.) and DML (INSERT, UPDATE, DELETE). Every statement in sql runs, one after another, within this one call; give the database a pump (dvxSqlSetPump) when that may take long.
Give a database a pump, or take it away (NULL; a database opens without one). While a statement runs on the database (in dvxSqlExec, dvxSqlQuery, or dvxSqlNext, whose first step of a sorted or grouped query reads every row), the pump is called with the database's handle every 1000 steps or so. It can let other work run, for example a frame of DVX's. Returning false stops the statement: the call fails, dvxSqlError says "interrupted", and what the statement changed is undone.
+
Parameter Description
+ --------- -----------
+ db Database handle.
+ pump The function to call, or NULL for none.
+
The code a pump runs may use other databases, open and close them, as always. Its calls on the database whose statement is running are refused, as are any calls made from the pump on it:
+
+
dvxSqlExec, dvxSqlQuery, and dvxSqlNext fail, and dvxSqlError says "A statement is still running on this database".
+
The field functions of that database's cursors return "" or 0.
+
dvxSqlFreeResult and dvxSqlClose are carried out when the running statement ends. The cursor or database can no longer be used from then on.
+
+
DVX BASIC gives every database a program opens a pump (see SQLOpen in the BASIC reference). The Data control's own connections have none.
Cursor Operations
@@ -9047,7 +9416,7 @@ dvxSqlExec(db, sql);
fileOpsFormatSize(bytes, out, size) "1,234,567".
fileOpsFormatDate(t, out, size) "YYYY-MM-DD HH:MM" in local time.
fileOpsParseDate(text, &t) The time fileOpsFormatDate's text stands for ("YYYY-MM-DD HH:MM", optionally ":SS"), local time; false for anything else, a date that does not exist included.
- fileOpsTreeSize(path, &bytes, &files, &folders) Total size of a file or of a folder and everything in it.
+ fileOpsTreeSize(path, &bytes, &files, &folders) Total size of a file or of a folder and everything in it (the folders inside it; a measure job run to its end). False when part of it cannot be read; the totals then cover what was read.
fileOpsMakeDir(dir, name, err, errSize) Create a folder; a reason in err on failure.
fileOpsRename(dir, oldName, newName, err, errSize) Rename within a folder; refuses a name already in use.
fileOpsValidateName(dir, name, err, errSize) Check a new name for folder dir by the rules of its disk (platformValidateFilename): on DOS long names where the drive takes them (platformLongNames), else 8.3; never a device name.
@@ -9058,7 +9427,7 @@ dvxSqlExec(db, sql);
fileOpsPathSame(a, b) Same place (platformPathSame: separators equal, case ignored, a trailing separator ignored).
fileOpsPathNormalize(path, out, size) Canonical absolute form of an existing folder (DOS: upper case, except names on a disk with long names, which keep their case).
Jobs
-
A job copies, moves, or deletes any number of files and folders (recursively):
+
A job copies, moves, deletes, or measures (FileOpsMeasureE: counts the files, folders, and bytes, changing nothing) any number of files and folders (recursively):
Each fileOpsJobStep does one bounded piece of work: read one folder, copy one FILEOPS_COPY_CHUNK of one file, or delete one item. A cooperative program yields between steps.
+
Each fileOpsJobStep does one bounded piece of work: read a few dozen entries of a folder (a big folder takes several steps), copy one FILEOPS_COPY_CHUNK of one file, or delete one item. A cooperative program yields between steps.
Step result Meaning
----------- -------
FileOpsStepBusyE More work to do.
@@ -9090,9 +9459,10 @@ fileOpsJobFree(job);
FileOpsStepErrorE Something failed; fileOpsJobMessage says why. Answer Retry, Skip, or Cancel.
FileOpsStepDoneE Finished.
FileOpsStepCancelledE Stopped by Cancel.
-
Until it is answered, the job keeps returning the same question. What the job does:
+
Until it is answered, the job keeps returning the same question. The next step tries the item again and applies the answer if it fails again; an item that goes through leaves the answer unused (it is never kept for a later question). Cancel stops at once, without trying again. What the job does:
-
Scan: each folder is read once and its files are counted, so fileOpsJobProgress (FileOpsProgressT: files and bytes done and total, percent, current path) is accurate while the transfer runs.
+
Scan: each folder is read once and its files are counted, so fileOpsJobProgress (FileOpsProgressT: files and bytes done and total, folders inside the sources, percent, current path) is accurate while the transfer runs. A folder that fails part way keeps nothing of what was read before the question.
+
Measure: only the scan, and it asks nothing: a folder it cannot read is skipped and counted in FileOpsProgressT.unread (what was read of it stays counted).
Copy: folders are created before their contents, files are copied in chunks and keep their date and attributes (plus Archive). A read-only destination is made writable before it is replaced.
Move: a top-level item whose destination does not exist yet is renamed in one go. Otherwise each file is copied and then its source removed, and the emptied source folders are removed at the end. A folder that still holds skipped items is kept.
fileOpsJobClearAttr(job, attr), called before the first step, clears those attribute bits on every file a copy or move writes: Setup passes FILEOPS_ATTR_READONLY, because every file on a CD-ROM is read-only and the installed copies must not be.
fileOpsJobRun(job, callbacks) runs a job to the end instead: progress(job, ctx) is called after every step (return false to cancel) and ask(job, kind, message, ctx) answers questions. With no callbacks, existing files are kept (No) and failing items are skipped.
BASIC
-
src/include/basic/fileops.bas declares the BASIC functions (fileOpsBas.c): whole trees at once (FileOpsCopyTree, FileOpsMoveTree, FileOpsDeleteTree), drives and disks (FileOpsDriveList$, FileOpsDriveType (the PlatformDriveTypeE of platformDriveList, CONSTs FILEOPS_DRIVE_*; -1 for no such drive), FileOpsBootDrive$ (platformBootDrive, "" when none), FileOpsFreeKb, FileOpsFreeBytes, and FileOpsTotalBytes (platformDiskSpace), FileOpsIsWritable (platformDirIsWritable), FileOpsReadOnlySystem (platformIsReadOnly), FileOpsTempDir$ (platformTempDir, "" when none)), DVX's folders (FileOpsUserDir$ (platformUserDir), FileOpsUserPath$ (platformUserPath, "" when it does not fit), FileOpsUserInTemp (platformUserInTemp), and for the kinds of DvxKindE (CONSTs FILEOPS_KIND_*) FileOpsKindDir$ (platformKindDir), FileOpsKindBrowseDir$ (platformKindBrowseDir), FileOpsKindFind$ (platformKindFind), FileOpsKindName$ (platformKindNameOf), and FileOpsKindList (platformKindReadDir as a listing, sorted by name; "" or 0 for a kind out of range)), folder listings (FileOpsList, FileOpsListCount, FileOpsListSort, FileOpsListFind, FileOpsListName$, FileOpsListIsFolder, FileOpsListSize, FileOpsListRows$, FileOpsListFree), paths and names (FileOpsPathNormalize$, FileOpsPathParent$, FileOpsPathJoin$, FileOpsPathSame, FileOpsPathIsRoot, FileOpsPathIsAbsolute (platformPathIsAbsolute), FileOpsPathIsInside, FileOpsPathSep$ (DVX_PATH_SEP), FileOpsPathNative$ (platformPathNative), FileOpsFilterMatch (platformFilterMatch), FileOpsBaseName$, FileOpsExtension$), files and folders (FileOpsMakeDir, FileOpsMakePath (platformMkdirRecursive), FileOpsRename, FileOpsError$, FileOpsTempPath$ and FileOpsReplaceFile (an atomic save: platformPathSibling + platformReplaceFile), FileOpsValidName (platformValidateFilename for a name in a folder), FileOpsValidLongName and FileOpsValidShortName (platformValidateLongName / platformValidateShortName, the rules alone), FileOpsLongNames (platformLongNames), FileOpsShortPath$ (platformShortPath, "" when it does not fit), FileOpsIsFolder, FileOpsGetAttr, FileOpsSetAttr, FileOpsModified$, FileOpsSetModified (fileOpsParseDate + fileOpsSetTime), FileOpsTreeSize with FileOpsTreeBytes / FileOpsTreeFiles / FileOpsTreeFolders, FileOpsFormatSize$), and jobs (FileOpsJobCreate, FileOpsJobAddSource, FileOpsJobClearAttr (masked to FILEOPS_ATTR_EDITABLE), FileOpsJobRunFor, FileOpsJobMessage$, FileOpsJobAnswer, FileOpsJobCancel, FileOpsJobCurrent$, FileOpsJobFilesDone, FileOpsJobFilesTotal, FileOpsJobPercent, FileOpsJobBytesDone, FileOpsJobBytesTotal (FileOpsProgressT; byte counts are DOUBLE), FileOpsJobFree). The fileops.bas CONSTs mirror fileOps.h and dvxPlat.h; the basdev test suite (caseIncludeConsts) checks each against its C value.
+
src/include/basic/fileops.bas declares the BASIC functions (fileOpsBas.c): whole trees at once (FileOpsCopyTree, FileOpsMoveTree, FileOpsDeleteTree: a job run to its end, letting DVX and the program's events go on through dvxWaitPump at the first step and every 50 ms after; when the first pump says stop the job runs on without pumps, when a later one does the program is ending and the call is cancelled), drives and disks (FileOpsDriveList$, FileOpsDriveType (the PlatformDriveTypeE of platformDriveList, CONSTs FILEOPS_DRIVE_*; -1 for no such drive), FileOpsBootDrive$ (platformBootDrive, "" when none), FileOpsFreeKb, FileOpsFreeBytes, and FileOpsTotalBytes (platformDiskSpace), FileOpsIsWritable (platformDirIsWritable), FileOpsReadOnlySystem (platformIsReadOnly), FileOpsTempDir$ (platformTempDir, "" when none)), DVX's folders (FileOpsUserDir$ (platformUserDir), FileOpsUserPath$ (platformUserPath, "" when it does not fit), FileOpsUserInTemp (platformUserInTemp), and for the kinds of DvxKindE (CONSTs FILEOPS_KIND_*) FileOpsKindDir$ (platformKindDir), FileOpsKindBrowseDir$ (platformKindBrowseDir), FileOpsKindFind$ (platformKindFind), FileOpsKindName$ (platformKindNameOf), and FileOpsKindList (platformKindReadDir as a listing, sorted by name; "" or 0 for a kind out of range)), folder listings (FileOpsList, FileOpsListCount, FileOpsListSort, FileOpsListFind, FileOpsListFolders (a new listing of a listing's folders, by name, from memory), FileOpsListName$, FileOpsListIsFolder, FileOpsListSize, FileOpsListRows$, FileOpsListFree), paths and names (FileOpsPathNormalize$, FileOpsPathParent$, FileOpsPathJoin$, FileOpsPathSame, FileOpsPathIsRoot, FileOpsPathIsAbsolute (platformPathIsAbsolute), FileOpsPathIsInside, FileOpsPathSep$ (DVX_PATH_SEP), FileOpsPathNative$ (platformPathNative), FileOpsFilterMatch (platformFilterMatch), FileOpsBaseName$, FileOpsExtension$), files and folders (FileOpsMakeDir, FileOpsMakePath (platformMkdirRecursive), FileOpsRename, FileOpsError$, FileOpsTempPath$ and FileOpsReplaceFile (an atomic save: platformPathSibling + platformReplaceFile), FileOpsValidName (platformValidateFilename for a name in a folder), FileOpsValidLongName and FileOpsValidShortName (platformValidateLongName / platformValidateShortName, the rules alone), FileOpsLongNames (platformLongNames), FileOpsShortPath$ (platformShortPath, "" when it does not fit), FileOpsIsFolder, FileOpsGetAttr, FileOpsSetAttr, FileOpsModified$, FileOpsSetModified (fileOpsParseDate + fileOpsSetTime), FileOpsTreeSize (a measure job pumping as the whole-tree calls do) with FileOpsTreeBytes / FileOpsTreeFiles / FileOpsTreeFolders, FileOpsFormatSize$), and jobs (FileOpsJobCreate (FILEOPS_COPY, FILEOPS_MOVE, FILEOPS_DELETE, or FILEOPS_MEASURE), FileOpsJobAddSource, FileOpsJobClearAttr (masked to FILEOPS_ATTR_EDITABLE), FileOpsJobRunFor, FileOpsJobMessage$, FileOpsJobAnswer, FileOpsJobCancel, FileOpsJobCurrent$, FileOpsJobFilesDone, FileOpsJobFilesTotal, FileOpsJobFoldersTotal, FileOpsJobUnread, FileOpsJobPercent, FileOpsJobBytesDone, FileOpsJobBytesTotal (FileOpsProgressT; byte counts are DOUBLE), FileOpsJobFree). The fileops.bas CONSTs mirror fileOps.h and dvxPlat.h; the basdev test suite (caseIncludeConsts) checks each against its C value.
Listings and jobs are handles (small numbers, 0 = none) into a table owned by the library; each records the app that made it, and the shell's app-cleanup hook frees what an app leaves open when it ends. FileOpsJobRunFor(job, ms) steps the job until the time is used or it needs an answer, so a BASIC Timer can run it without blocking the program. A returned string stays valid until the next call returning one (the BASIC runtime copies it at once). Single files are handled by the built-in FILECOPY, KILL, NAME, MKDIR, RMDIR, GETATTR, and SETATTR statements.
@@ -9271,7 +9641,7 @@ httpFree(h);
settingsChanged() NULL, or re-read the driver's settings (joySettingGet) after they changed.
Up to JOY_MAX_DRIVERS drivers in use get their tick called. A driver is loaded again at every probe, so it must not rely on state kept from an earlier load; set its hardware up in probe (or a constructor).
BASIC
-
src/include/basic/joystick.bas declares JoyCount, JoyName$, JoyAxes, JoyButtons, JoyX, JoyY, JoyAxis, JoyButton, and JoyHat. Joystick, axis, button, and hat numbers start at 1; each function reads through the calling program's shared handle.
+
src/include/basic/joystick.bas declares JoyCount, JoyName$, JoyAxes, JoyButtons, JoyX, JoyY, JoyAxis, JoyButton, and JoyHat. Joystick, axis, button, and hat numbers start at 1; each function reads through the calling program's shared handle. Events: JoyWatch(joy, watching) sets (or clears) that handle's joySetEventFn, which keeps the program's changes, the last BAS_EVENTS_MAX (64); JoyEvent gives the next as JoyEventKindE + JOY_BAS_EVENT_FIRST (JOY_BAS_EVENT_NONE for none; CONSTs JOY_EVENT_*), and JoyEventJoy, JoyEventIndex (both from 1), and JoyEventValue the one it gave last. joyBasForget drops a program's kept changes when it ends.
For a settings program (the Control Panel's Game Controllers page) it also declares: JoyHats, JoyAxisPresent(joy, axis), JoyCanCalibrate(joy) (JoyInfoT hatCount, axisPresent, JOY_CAP_CALIBRATE); JoyConnected(joy) (joyRead's connected); JoyRescan (joyRescanApp for the calling program: its own handles and STICK readings go first; -1 while another program has a joystick open); JoyRawAxis(joy, axis) and JoyRawButtons(joy) (joyGetRaw); JoySetCalibration(joy, axis, minV, centerV, maxV) and JoyClearCalibration(joy, axis) (joySetCalibration); JoyDeadZone / JoySetDeadZone (joyGetDeadZone / joySetDeadZone); JoyChoiceCount(which), JoyChoiceLabel$(which, index), JoyChoice(which), JoySetChoice(which, index) with JOY_CHOICE_GAMEPORT = 0 and JOY_CHOICE_FALLBACK = 1 (JoyChoiceE; joyChoices / joyChoiceGet / joyChoiceSet); JoyKeyCount and JoyKeyName$(index); JoyKbdSlotCount, JoyKbdSlot(slot), JoySetKbdSlot(slot, keyIndex) (joyKbdSlotGet / Set); JoySetting$(key, defaultValue) and JoySetSetting(key, value) ("" removes; joySettingGet / Set); JoySettingsLoad(prefs) and JoySettingsStore(prefs) with a prefs.bas handle (prefsBasHandle); JoyCalibration(joy, axis, part) (joyGetCalibration; part JOY_CAL_MIN / JOY_CAL_CENTER / JOY_CAL_MAX, joystick.h; 0 when joyGetCalibration fails); JoyIsKeyboard(joy) (JOY_CAP_KEYBOARD); JoyKeyIndex(keyName), JoyKeyCode(index), JoyKeyNameOf$(rawCode) (joyKeyIndex / joyKeyCode / joyKeyNameOf). CONSTs JOY_DEADZONE_DEFAULT / _MAX, JOY_POLL_HZ_DEFAULT / _MIN / _MAX, and JOY_KEY_POLL_HZ mirror joystick.h. Lists (choices, keys, slots) are numbered from 0; True is -1.
The BASIC runtime calls joyBasStick(n) and joyBasStrig(n) for QBasic's STICK and STRIG. Joystick A is device 0 (axes 0-1, buttons 0 and 1); joystick B is device 1, or axes 2-3 and buttons 2-3 of device 0 when it is the only device and has more than two axes. STICK(0) reads A's X and remembers A's Y and B's X and Y for STICK(1) to STICK(3) (per program, forgotten when it ends); positions are 1 to 200 ((value - JOY_AXIS_MIN) x 199 / 65535 + 1), 0 without that joystick. Even STRIG numbers use joyButtonWentDown (0 A button 1, 2 B button 1, 4 A button 2, 6 B button 2), odd ones the state joyRead reports; True is -1.
@@ -9292,7 +9662,7 @@ httpFree(h);
jsonCount(doc, node) An array's items or an object's members (0 for anything else).
jsonChild(doc, node, index) Item or member index, in order.
jsonKey(doc, node) A member's name ("" for anything else).
-
Strings are held decoded: the escapes \n, \t, \", \\, \/, \b, \f, and \r become their characters, \u escapes below 128 their character and others '?'; other bytes, UTF-8 included, pass through. Arrays and objects nest JSON_DEPTH_MAX (64) deep at most.
+
Strings are held decoded into DVX text (code page 437): the escapes \n, \t, \", \\, \/, \b, \f, \r, and \u (surrogate pairs included) and UTF-8 sequences become their characters, with '?' for a character DVX does not have; other bytes pass through. Arrays and objects nest JSON_DEPTH_MAX (64) deep at most.
char err[JSON_ERR_MAX];
JsonDocT *doc = jsonParse(text, len, err, sizeof(err));
@@ -9304,7 +9674,7 @@ for (int32_t i = 0; doc && i < jsonCount(doc, jsonFind(doc, 0, "
jsonFree(doc);
Writing
-
jsonQuote(text, out, outSize) writes text as a JSON string -- quotes, escapes, control characters as \u00XX -- and returns the length it needs (as snprintf does), so a NULL out with size 0 measures it.
+
jsonQuote(text, out, outSize) writes text as a JSON string -- quotes, escapes, control characters and characters from 128 on as \u escapes of their Unicode code points, so the result is plain ASCII -- and returns the length it needs (as snprintf does), so a NULL out with size 0 measures it.
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.
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.
dvxExclusiveEnd gives the screen back. Closing the window, the program ending, and End Task do too.
+
dvxExclusiveMirror instead shows the window's own content on the whole screen -- its widgets, or what it draws into its content buffer -- in the desktop's pixel format, scaled and centred the same way and kept up to date as the window paints; the mouse works on the content as in the window, with its cursor drawn. A BASIC form does this with FullScreen = True (and hears the changes in its FullScreen event); a BASIC SCREEN program uses FULLSCREEN.
@@ -9711,7 +10082,7 @@ End Sub
mathx.dhs Developer help, added to the system help
Makefile KIND = library
How the Library Works
-
DVX loads every library when it starts, before any widget or program, in the order the .dep files require. Every function that is not static is exported: a program, widget, or library loaded later calls it directly, and DVX connects the call when it loads that module. A library that calls another library's functions lists it in its .dep file.
+
DVX loads a library when something first needs it: a program, widget, or library that calls one of its functions (DVX loads the library just before that module), or a lookup by name (dlsym(NULL), a BASIC DECLARE LIBRARY call, dvxLibraryLoaded). Libraries with code that runs as they load (constructors) are loaded when DVX starts, so they can hook into the shell. Every function that is not static is exported: a program, widget, or library calls it directly, and DVX connects the call when it loads that module. A library that calls another library's functions lists it in its .dep file, which DVX loads first. A module that opens another module calls platformModuleOpen (dvxPlat.h), never dlopen, which dvx.exe does not export.
BASIC programs add mathx.bas to their project. DECLARE LIBRARY looks up each function by exactly the name it declares, without a trailing $ (a $ marks a function returning a string), so mathxGreet$ calls the C function mathxGreet. Keep the declarations and the C functions in step; strings are passed as const char * and returned as const char *.
A string declared AS STRING is text and stops at the first zero byte. For bytes (network data, samples, encrypted data) declare BINARY STRING: each BINARY STRING parameter becomes two C parameters at its place, the bytes (const uint8_t *, a private copy valid during the call, followed by a zero byte) and their count (int32_t); a BINARY STRING result adds one last C parameter, int32_t *outLen, where the function stores the length of the bytes it returns (the runtime copies them right after the call, so a static buffer is fine; NULL, a negative length, or a length never set give an empty string). The template's tplNameXor shows both:
// tplNameXor$(BYVAL bytes AS BINARY STRING, BYVAL key AS LONG) AS BINARY STRING
@@ -9794,7 +10165,7 @@ End Sub
CTL0, CTL1 ... Each control's Begin UserControl header text, in [Controls] order
icon24, name The first control's toolbox picture and tooltip; the second and later have "-2", "-3" ... suffixes, as for any widget file holding several widgets
author, version ... The project's metadata; helpfile its help file's name
-
The loader tells such a file from a module by its first bytes: instead of loading it, it hands it, once every library and native widget is loaded, to the BASIC run-time library's basCtlLoadFile (found with dlsym; without the library the file is skipped and logged). basCtlLoadFile registers each control in the file for every program and the loader records the file as theirs, so the toolbox finds their icon24 and name.
+
The loader tells such a file from a module by its first bytes: instead of loading it, it hands it, once every native widget is loaded, to the BASIC run-time library's basCtlLoadFile (found with dlsym; without the library the file is skipped and logged). basCtlLoadFile registers each control in the file for every program and the loader records the file as theirs, so the toolbox finds their icon24 and name.
The Bridge
Each control is one object of its BASIC class plus a border-less canvas, the UserControl surface its code draws on (the same drawing code as a BASIC PictureBox). The widget class forwards to the class's code: layout sizes the canvas, and between frames a new size runs UserControl_Resize and UserControl_Paint; the mouse, key, wheel, and focus class methods run UserControl_MouseDown and the other handlers. A control file's code runs in one VM shared by every program that uses its controls; it never yields, and a call from another task while it runs code is refused (the paint keeps the old picture, the event is dropped). A BASIC program's own controls ([Controls] in its project) are registered by the program when it starts, owned by it, and run in its own VM.
@@ -9871,7 +10242,7 @@ bin/dvxpkg info NAME.dpk # what the package says<
Give acme.KEY to the people who install your packages (Software, File > Trust a Publisher's Key) and keep acme.SEC private and backed up: anyone who has it can sign as you, and an update must be signed with the key that signed the version installed. bin/dvxpkg also signs and checks packages and lists their contents; on DVX, SYSTEM/DVXPKG.EXE does the same, and installs and removes packages. The package library and its file format are described under Package Library.
DVX's Own Packages and Optional Parts
DVX is packages too: kpunch.dvx (the loader, SYSTEM, the shell, the core libraries and widgets, and the everyday programs), kpunch.basefont (the base fonts), and the optional parts Setup's "What to install" page offers. Each part is a bundle (kind bundle: a package with no files of its own that needs its members) and each member is one module in its own package: kpunch.basic (dvxbasic, basdev), kpunch.data (dvxsql, datactrl, dbgrid), kpunch.net (dvxnet, http, pkgnet), kpunch.comm (terminal, conn, xfer; it needs kpunch.net), kpunch.media (player, recorder), kpunch.devtools (iconed, resedit, helpedit, pkgbuild, widshow), and kpunch.demos (dvxdemo, basdemo). src/packages.ini lists them. The build (src/installer/mkPackages.py) works out what each member needs from the modules its code imports, the libraries its BASIC program declares, and the manifest's uses (a module reached by name), and stops when DVX itself, or a part, would need a part it does not name. A member's files outside its folder are its paths: the DOS tools in SYSTEM go with the part they serve (BASCOMP.EXE with kpunch.basdev, DVXRES.EXE with kpunch.resedit, and DVXHLPC.EXE with kpunch.helpedit, the Help Editor); DVXPKG.EXE stays in DVX itself, to install and remove packages at the DOS prompt.
-
A module of yours that cannot work without a part's library names the library's package in PKG_NEEDS (PKG_NEEDS="kpunch.dvxnet=1.0.0"): Software then installs it only where that part is. One that can do without it does not import it: it asks dvxLibraryLoaded (BASIC: SysLibraryLoaded, system.bas) and reaches the library by name only when it is there, as Software does with pkgnet. BASIC's SQL functions do nothing without the Databases part (SQLOpen returns 0).
+
A module of yours that cannot work without a part's library names the library's package in PKG_NEEDS (PKG_NEEDS="kpunch.dvxnet=1.0.0"): Software then installs it only where that part is. One that can do without it does not import it: it asks dvxLibraryLoaded (BASIC: SysLibraryLoaded, system.bas), which loads the library when it is installed, and reaches the library by name only when it is there, as Software does with pkgnet. Not importing a library also keeps DVX from loading it before it is needed. BASIC's SQL functions do nothing without the Databases part (SQLOpen returns 0).
Publishing on the Package Server
Software's Available and Updates pages list the packages on the DVX package server. To publish one there, make an account on the server (POST /v1/account/register, then the link in the mail), give its email and password in Software's File > Package Server, and choose File > Publish a Package; Software's Published page shows each version's review. From the build machine, sign in (POST /v1/account/login gives a session token) and upload the package with it:
listItemsAdd and listItemsRemove first adopt (deep-copy) a caller's array, so they extend or shorten it without modifying it. listItemsSetExternal keeps the previously owned strings until the next adopt, clear, or free, because the new array may point into them, and never reads the caller's previous array (it may be freed already). The mutators measure in font, the widget's own (wgtFont), and return true when the widest item's width changed, i.e. when the widget's minimum width changed and it needs a relayout rather than just a repaint. listItemsGet returns "" for an index out of range.
listItemsAdd and listItemsRemove first adopt (deep-copy) a caller's array, so they extend or shorten it without modifying it. listItemsSetExternal keeps the previously owned strings until the next adopt, clear, or free, because the new array may point into them, and never reads the caller's previous array (it may be freed already). The mutators measure in font, the widget's own (wgtFont), and return true when the widest item's width changed, i.e. when the widget's minimum width changed and it needs a relayout rather than just a repaint. listItemsGet returns "" for an index out of range. listItemsSetSpec replaces the items with the pieces of a "Red|Green|Blue" spec (LIST_ITEMS_SEP; every "|" separates, so "a|" is "a" and an empty item, and "" empties the list): BASIC's SetItems.
widgetAdoptOwnedStrings(owned, src, count) builds the deep copy that adoption uses (complete before the old strings are freed, so src may alias them) and widgetFreeOwnedStrings(owned) frees every string of an owned array and empties it. ListView uses both for its row-major cell copy.
ListSelT (selection model)
Selection state shared by ListBox and ListView. cursor is the current (keyboard) item and anchor the start of Shift ranges; bits holds one byte per item in multi-select mode. Every index is a data index; functions that take a display index also take an optional display-to-data order map (NULL = identity) so a sorted view selects what the user sees as a contiguous range.
@@ -10182,6 +10554,7 @@ void listReorderPaint(DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t to
listPopupInit(p) No selection, no hover, closed.
listPopupAddItem / listPopupRemoveItem Item list edits; the selection and hover stay on the same item (-1 when it is removed).
listPopupClearItems / listPopupSetItems Replace the items; an out-of-range selection becomes -1.
+ listPopupSetItemsSpec(w, p, spec) Replace the items with a "Red|Green|Blue" spec (listItemsSetSpec); nothing is selected.
listPopupOpen(w, p) Open with the selection hovered and scrolled into view (keyboard, mouse, and accelerator all use it).
listPopupClose(w, p) WGT_METHOD_CLOSE_POPUP body.
listPopupCommit(p) Close and make the hovered item the selection; false when nothing valid is hovered.
@@ -10404,7 +10777,7 @@ int32_t listIconScrollDrag(const ListIconGridT *g, int32_t y, int32_t grab);
Music Library
Music for DVX programs on whatever sound hardware the computer has: tracker modules (MOD, S3M, XM, IT), Standard MIDI Files (type 0 and 1), WAV and VOC sound files, and live MIDI a program sends itself. Every music belongs to the program that loaded it and is stopped and freed when the program ends, however it ends; any number of programs may play music at the same time.
-
Header: music/music.h. Loaded as: bin/libs/kpunch/music/music.lib (needs audio.lib); the General MIDI instrument bank for FM is bin/libs/kpunch/music/genmidi.op2, and the General MIDI SoundFont for wavetable music is an add-on (SHARED\BANKS\KPUNCH\DVXGM.SF2).
+
Header: music/music.h. Loaded as: bin/libs/kpunch/music/music.lib (needs audio.lib); the tracker module player (libxmp-lite) is bin/libs/kpunch/music/players/xmp.drv, which music.lib loads when a module is opened and lets go a minute after the last one closes; the General MIDI instrument bank for FM is bin/libs/kpunch/music/genmidi.op2, and the General MIDI SoundFont for wavetable music is an add-on (SHARED\BANKS\KPUNCH\DVXGM.SF2).
When there is no FM chip (or it is busy), MIDI plays on DVX's own software FM synthesizer: an integer two-operator FM chip modelled on the OPL2 (the same log-sine and exponential tables, waveforms 0 to 3, feedback, the chip's envelope rates and key scaling, key scale level, tremolo, and vibrato) with 24 voices, rendering at the device's rate into a stream. It is driven with the same bank and the same register values as the chip, so music sounds alike on both. The host tests compare it with emu8950, an accurate OPL2 emulator, for several patches: pitch, loudness over time, and harmonics agree closely. Cost on a 486: roughly 1 to 2 percent of a 486DX2-66 per sounding voice at 22,050 Hz.
SoundFonts on a Wavetable Synthesizer
A wavetable synthesizer (audio.lib's gus and guswt drivers for a Gravis UltraSound, awe for the EMU8000 of a Sound Blaster AWE32 or AWE64) plays recorded instruments from its own memory. Its instruments come from a SoundFont, DVX.INI [audio] soundFont (MUSIC_INI_SOUND_FONT), named as midiBank names an OP2 bank; not set, the standard one (MUSIC_SOUND_FONT_DEFAULT, KPUNCH\DVXGM.SF2: DVX's General MIDI SoundFont add-on, made from FluidR3_GM by mksf, about 3.4 MB). SoundFont 2 files are read in full; SoundFont 1 (.SBK, the AWE32's own) as far as it is known (it has no public specification). soundFont may also name a Gravis UltraSound's ULTRASND.INI by its path, which plays that folder's .PAT patches as instruments; and where the standard SoundFont is not installed, the patches in the folder ULTRADIR names play (musicPatchSetPath), else the AWE's own bank for its sample ROM (musicRomBankPath: SFBANK\SYNTHGM.SF2 or SYNTHGM.SBK in the folder SOUND names, as Creative's drivers installed it). Control Panel's Sounds page lists all of these.
-
A song loads only the samples its notes play (the bank selects, programs, and notes are looked at first), each with the frames a chip's interpolation reads after it, into memory the synthesizer gives every program a share of; what a song loaded stays for the next while it fits, and room is made by giving up the samples used longest ago. When the samples still do not fit they are all loaded again smaller: 8-bit where the chip plays it, then at half the rate, then a quarter; at the least, the ones that do not fit are left out. Notes the bank lacks, or whose samples were left out, play on the FM chip or the software synthesizer, as do the notes beyond the synthesizer's voices. One music plays on the wavetable synthesizer at a time (another meanwhile plays on FM); its SoundFont and samples are kept 60 seconds after the last music (MUSIC_FONT_KEEP_SEC). Live MIDI loads a program's samples as it is chosen. A SoundFont made for an AWE's General MIDI ROM (ROM samples) plays from the card's ROM with nothing loaded; on any other synthesizer its notes go to FM.
+
A song loads only the samples its notes play (the bank selects, programs, and notes are looked at first), each with the frames a chip's interpolation reads after it, into memory the synthesizer gives every program a share of; what a song loaded stays for the next while it fits, and room is made by giving up the samples used longest ago. When the samples still do not fit (a size they cannot fit at, even in the whole of the synthesizer's memory, is not tried) they are all loaded again smaller: 8-bit where the chip plays it, then at half the rate, then a quarter; at the least, the ones that do not fit are left out. Notes the bank lacks, or whose samples were left out, play on the FM chip or the software synthesizer, as do the notes beyond the synthesizer's voices. One music plays on the wavetable synthesizer at a time (another meanwhile plays on FM); its samples are kept 60 seconds after the last music (MUSIC_FONT_KEEP_SEC), and the SoundFont itself (its headers, a GUS patch set's files) for the whole session, so a later song does not read them again. Live MIDI loads a program's samples once it is chosen, one at a time in the idle time after (a whole instrument at once would hold DVX up), at the best quality that fits beside the samples already loaded; until they are in, its notes play on the FM chip. A SoundFont made for an AWE's General MIDI ROM (ROM samples) plays from the card's ROM with nothing loaded; on any other synthesizer its notes go to FM.
The AWE runs the SoundFont's envelopes, LFOs, filter, and reverb and chorus sends itself; on the GUS the library runs the envelopes and LFOs (its volume and pitch changed as the note goes on), without a filter or effects.
The SoundFont Software Synthesizer
With DVX.INI [audio] midi = softwave, MIDI plays from the SoundFont in software on any device that plays sampled sound: the song's samples are read into memory at their own rates and mixed into the music's stream (linear interpolation, the envelopes and LFOs as on the GUS, no filter or effects), with the software FM synthesizer beside it for the notes the bank lacks. It needs the memory for the song's samples (a General MIDI song of ten instruments and a drum kit about 360 KB of DVXGM's) and more CPU time than FM.
A MIDI recording keeps channel messages with their times (musicMidiRecordAdd(rec, ms, status, data1, data2), ms from the recording's start; a time before the last message's counts as the last's; MUSIC_ERR_ARG for anything but a channel message) and musicMidiRecordSave(rec, path) writes them as a Standard MIDI File: type 0, one track of 1,000 ticks a quarter note at 60 quarter notes a minute, so a tick is a millisecond. A note still held at the end is let go there, so the file never leaves a note sounding. MUSIC_ERR_WRITE when the file cannot be written (nothing is left of one not all written). musicMidiRecordCount and musicMidiRecordLength (the last message's time) say what it holds; musicMidiRecordFree frees it, and a program's recordings are freed when it ends. musicLoad plays the file. The music macros (below) are written into MIDI files by the same code.
Modules
MOD (ProTracker and compatibles), S3M, XM, and IT through libxmp-lite 4.7.3 (thirdparty/libxmp-lite with DVX's patches, listed in its DVX-VERSION.md; formats only, no depackers or ProWizard). Stereo separation is 50 percent (softer than the Amiga's hard left and right). The whole module is held in memory. libxmp works out pitches with floating point, so it plays modules in the shell's idle time, never at interrupt time; on a 486SX (no maths coprocessor) that goes through floating point emulation and is slow. On a stream libxmp mixes every channel in software (linear interpolation, three multiplies a channel a frame).
-
On a wavetable synthesizer (musicModHw.c) libxmp mixes nothing: DVX's hardware voice patch has it report each of its voices as every tick starts (the sample, where it is, its rate, volume, pan, and the loop it is in), and only move them on. Those ticks are made up to 400 ms ahead in the idle time, as the synthesizer's notes, and the music timer plays them on time (500 times a second: a note starts within 2 ms of its time). A voice starts on the chip where libxmp starts or moves it (a note, a sample offset, a retrigger, another sample or loop); otherwise its pitch, volume, and pan follow libxmp's each tick, and the chip's own interpolation plays it. The samples are stored as they are (an 8-bit one 8-bit where the chip plays those, as a GUS does), else all 8-bit. The chips loop only forward, so a ping-pong loop is stored unrolled: its way back after its end, each frame where libxmp reads it (an Impulse Tracker module's way back a frame shorter, as Impulse Tracker plays it). What the chips cannot do plays near enough: a ping-pong sustain loop goes forward, a sample played backwards with no loop goes forward, and Impulse Tracker's filters are not heard; a voice the chip has none free for (a program holds the rest) is not heard. A voice at full volume plays 8 dB down, with the chip's level (AudioWtInfoT) taken off too, so a module is about as loud as libxmp mixes it. A pause stops the voices, and the play goes on from the tick that was playing; musicGetPosition is the tick being heard.
+
On a wavetable synthesizer (musicModHw.c) libxmp mixes nothing: DVX's hardware voice patch has it report each of its voices as every tick starts (the sample, where it is, its rate, volume, pan, and the loop it is in), and only move them on. Those ticks are made up to 400 ms ahead in the idle time, as the synthesizer's notes, and the music timer plays them on time (500 times a second: a note starts within 2 ms of its time). A voice starts on the chip where libxmp starts or moves it (a note, a sample offset, a retrigger, another sample or loop); otherwise its pitch, volume, and pan follow libxmp's each tick, and the chip's own interpolation plays it. The samples are stored as they are (an 8-bit one 8-bit where the chip plays those, as a GUS does), else all 8-bit; a way of storing them that cannot fit in the synthesizer's free memory is not tried (nothing is uploaded for nothing), and a module that fits in neither plays mixed in software. A big module loads giving way to DVX's other tasks between its samples (a DVX patch to libxmp, 0002-load-hook), and a file of no music kind is refused by its first bytes, without being read whole. A WAV or VOC file plays as it is read. The chips loop only forward, so a ping-pong loop is stored unrolled: its way back after its end, each frame where libxmp reads it (an Impulse Tracker module's way back a frame shorter, as Impulse Tracker plays it). What the chips cannot do plays near enough: a ping-pong sustain loop goes forward, a sample played backwards with no loop goes forward, and Impulse Tracker's filters are not heard; a voice the chip has none free for (a program holds the rest) is not heard. A voice at full volume plays 8 dB down, with the chip's level (AudioWtInfoT) taken off too, so a module is about as loud as libxmp mixes it. A pause stops the voices, and the play goes on from the tick that was playing; musicGetPosition is the tick being heard.
Measured on an emulated 486DX2-66 at 22,050 Hz, a 4-channel MOD with all four channels sounding takes about 49 percent of the CPU mixed in software and about 14 percent on a Gravis UltraSound's voices, most of that libxmp's own work each tick (about 1.3 ms of the 20).
A macro queue plays tunes written in QBasic's music macro language (the commands are listed in music.h and in the BASIC language reference: notes A-G with sharps and flats, N, O, > and <, L, P, dots, T, MN / ML / MS, MF / MB). musicMmlPlay parses the whole string first; a mistake anywhere returns MUSIC_ERR_MACRO and queues nothing. The settings last from one string to the next. musicMmlTone queues a square-wave tone of milliHz thousandths of a hertz (0: silence) for us microseconds (QBasic's SOUND). musicMmlQueued counts the notes not yet played, musicMmlStop silences the queue, musicMmlBusy says whether a caller that follows QBasic should still wait (foreground mode: until everything has played; background: while more than MUSIC_MML_BACKGROUND_MAX notes wait) and moves the queue along. A queue belongs to the program that opened it and is freed when that program ends.
-
Timing is QBasic's: a whole note is four beats of 60 / T seconds, each dot adds half again, and a note sounds for 7/8 (MN), all (ML), or 3/4 (MS) of its length. Octave 3 starts at middle C; octave 3's A is 440 Hz. The notes play as square waves on an audio.lib note queue, each to the exact sample (on the PC speaker as tones). DVX's own I command (I1 to I128) plays the notes that follow on that General MIDI instrument: each run of such notes becomes a small MIDI file that plays like any MIDI music (on the synthesizer DVX.INI [audio] midi chooses); I0 returns to tones. Runs play one after the other; when the instruments cannot play (no instrument bank, no device) the run plays as tones.
+
A macro queue plays tunes written in QBasic's music macro language (the commands are listed in music.h and in the BASIC language reference: notes A-G with sharps and flats, N, O, > and <, L, P, dots, T, MN / ML / MS, MF / MB, and QB64's V (volume) and @ (waveform: 1 square, 3 triangle, 5 noise)). musicMmlPlay parses the whole string first; a mistake anywhere returns MUSIC_ERR_MACRO and queues nothing. The settings last from one string to the next. musicMmlTone queues a square-wave tone of milliHz thousandths of a hertz (0: silence) for us microseconds (QBasic's SOUND). musicMmlQueued counts the notes not yet played, musicMmlStop silences the queue, musicMmlBusy says whether a caller that follows QBasic should still wait (foreground mode: until everything has played; background: while more than MUSIC_MML_BACKGROUND_MAX notes wait) and moves the queue along. A queue belongs to the program that opened it and is freed when that program ends.
+
Timing is QBasic's: a whole note is four beats of 60 / T seconds, each dot adds half again, and a note sounds for 7/8 (MN), all (ML), or 3/4 (MS) of its length. Octave 3 starts at middle C; octave 3's A is 440 Hz. The notes play on an audio.lib note queue, each to the exact sample in its own waveform and volume (audioNotesSetTone; on the PC speaker as square tones, V0 silent); V also scales an instrument's velocity. DVX's own I command (I1 to I128) plays the notes that follow on that General MIDI instrument: each run of such notes becomes a small MIDI file that plays like any MIDI music (on the synthesizer DVX.INI [audio] midi chooses); I0 returns to tones. Runs play one after the other; when the instruments cannot play (no instrument bank, no device) the run plays as tones.
Several Programs at Once
Every music plays through one audio context of the library's own; audio.lib mixes all streams, so modules and software MIDI of any number of programs play together. FM voices are shared with every program's tones and FM music; the MPU-401 port serves one music, and so does the wavetable synthesizer (MIDI or a module). When a program ends, its musics stop and are freed and their FM voices and the port are given back; the others go on.
BASIC
-
src/include/basic/music.bas (DECLARE LIBRARY "music"): MusicLoad(fileName) and MusicMidiOpen() return a music number (0 on failure: MusicError$ says why); MusicPlay(music, loopIt), MusicStop, MusicPause, MusicResume, MusicPlaying, MusicVolume(music, percent), MusicPosition, MusicLength, MusicSeek(music, ms), MusicFree; live MIDI MusicNoteOn(music, channel, note, velocity), MusicNoteOff, MusicProgram, MusicControl, MusicPitchBend with channels 1 to 16 (10 for drums), and MusicMidiSend(music, midiMsg) (musicMidiSend of a message packed as MidiInMessage gives it, AUDIO_MIDI_PACKED_*). MIDI recordings: MidiRecordNew (a recording number, 0 when all 8 are in use), MidiRecordAdd(recording, milliseconds, midiMsg), MidiRecordCount, MidiRecordLength, MidiRecordSave(recording, fileName) (True / False with MusicError$), MidiRecordFree. A number only works for the program that got it. MusicLoadMacro(macro) (musicLoadMacro, a music number) and MusicSetBank(music, bank) (musicSetBank, "" the standard bank, a SoundFont or an ULTRASND.INI for wavetable music; MusicError$ says when it is missing or damaged); MusicPatchSet$ and MusicRomBank$ (musicPatchSetPath, musicRomBankPath: "" when there is none). MusicGetVolume(music) (musicGetVolume in percent) and MusicInfo$(music, field) (musicGetInfo: "title", "output", "kind" = "module" / "midi" / "live" / "sound" for MusicKindE, "where" = "" / "stream" / "mpu401" / "fm" / "software" / "wave" / "softwave" for MusicOutputE, "paused" and "looping" as "-1" / "0"; any case, "" for another field). music.bas also declares the raw MIDI port calls of audio.lib (MidiPortInfo$, MidiAcquire, MidiPut, MidiRelease) and its MIDI input (MidiInOpen, MidiInClose, MidiInMessage, MidiInTime, MidiInClock, MidiInLost, and the MIDI_* CONSTs); see the sound library.
+
src/include/basic/music.bas (DECLARE LIBRARY "music"): MusicLoad(fileName), MusicLoadData(bytes AS BINARY STRING) (musicLoadMem), and MusicMidiOpen() return a music number (0 on failure: MusicError$ says why); MusicPlay(music, loopIt), MusicStop, MusicPause, MusicResume, MusicPlaying, MusicEnded (True once after the music played to its end: each number's musicSetEndFn sets a flag that MusicPlay and MusicEnded clear), MusicVolume(music, percent), MusicPosition, MusicLength, MusicSeek(music, ms), MusicFree; live MIDI MusicNoteOn(music, channel, note, velocity), MusicNoteOff, MusicProgram, MusicControl, MusicPitchBend with channels 1 to 16 (10 for drums), and MusicMidiSend(music, midiMsg) (musicMidiSend of a message packed as MidiInMessage gives it, AUDIO_MIDI_PACKED_*). MIDI recordings: MidiRecordNew (a recording number, 0 when all 8 are in use), MidiRecordAdd(recording, milliseconds, midiMsg), MidiRecordCount, MidiRecordLength, MidiRecordSave(recording, fileName) (True / False with MusicError$), MidiRecordFree. A number only works for the program that got it. MusicLoadMacro(macro) (musicLoadMacro, a music number) and MusicSetBank(music, bank) (musicSetBank, "" the standard bank, a SoundFont or an ULTRASND.INI for wavetable music; MusicError$ says when it is missing or damaged); MusicPatchSet$ and MusicRomBank$ (musicPatchSetPath, musicRomBankPath: "" when there is none). MusicGetVolume(music) (musicGetVolume in percent) and MusicInfo$(music, field) (musicGetInfo: "title", "output", "kind" = "module" / "midi" / "live" / "sound" for MusicKindE, "where" = "" / "stream" / "mpu401" / "fm" / "software" / "wave" / "softwave" for MusicOutputE, "paused" and "looping" as "-1" / "0"; any case, "" for another field). music.bas also declares the raw MIDI port calls of audio.lib (MidiPortInfo$, MidiAcquire, MidiPut, MidiRelease) and its MIDI input (MidiInOpen, MidiInClose, MidiInMessage, MidiInTime, MidiInClock, MidiInLost, and the MIDI_* CONSTs); see the sound library.
The BASIC runtime itself calls musicBasBeep, musicBasSound(hz, ticks), musicBasPlay(macro), musicBasPlayCount, and musicBasBusy for the statements BEEP, SOUND, and PLAY and the PLAY(n) function; each BASIC program has its own macro queue. QBasic's X command and "=" numbers (given with VARPTR$) are filled in by the BASIC runtime before musicBasPlay is called, so the macro language itself has neither. SOUND's duration is in clock ticks (18.2 a second) and 0 stops the queue; BEEP is 800 Hz for a quarter second.
Licences
libxmp-lite (Extended Module Player Lite), (C) 1996-2026 Claudio Matsuoka and Hipolito Carraro Jr, MIT licence:
The client of a DVX package server: what it offers (its catalogue), what it has that is newer than the packages installed (DVX itself among them), and fetching packages, with everything they need that is not installed, into a download folder; and, with an account on the server, publishing packages and listing what the account has sent. There the package library reads them (pkgOffersInFolder) and plans and installs them like any package file, so a package from the server is checked and trusted exactly as one from a disk. Software uses it for its Available and Updates pages.
Header: pkgnet/pkgNet.h
Loaded as: bin/libs/kpunch/pkgnet/pkgnet.lib (it needs the package, HTTP, and JSON libraries)
-
BASIC: pkgnet.bas (PkgNetOpen, PkgNetCatalogue, PkgNetUpdates, PkgNetFetch, PkgNetRunFor, PkgNetRows$, ...), with pkg.bas's PkgOfferFolder to plan what was fetched.
+
BASIC: pkgnet.bas (PkgNetOpen, PkgNetOpenIn for another DVX folder or download folder, PkgNetCatalogue, PkgNetUpdates, PkgNetFetch, PkgNetRunFor, PkgNetRows$, PkgNetField$ -- PKG_FIELD_SHA256 and PKG_FIELD_REPO_SIGNED give a catalogue entry's sha256 and repoSigned --, ...), with pkg.bas's PkgOfferFolderStart to read what was fetched for planning.
Add-on packages: the .DPK file format, manifests, Ed25519 keys and signatures, the packages installed in a DVX folder, the keys DVX trusts, planning what an install or removal takes, and installing and removing a package a step at a time. Software, Package Builder, the IDE's File > Make Package, DVXPKG.EXE, the build machine's dvxpkg, and Setup all use it.
Header: pkg/pkg.h (it includes the others below)
-
Loaded as: bin/libs/kpunch/pkg/pkg.lib
+
Loaded as: bin/libs/kpunch/pkg/pkg.lib, and bin/libs/kpunch/pkg/writer/writer.drv when a package is made (below)
BASIC: pkg.bas (sessions, offers, plans, install and remove jobs, building, signing, and keys; PkgFileOwner$ is pkgDbOwner).
A Package
A package is NAME.DPK: a ZIP read front to back, whose first entry is PACKAGE.INI, the manifest; then PACKAGE.SIG when the publisher signed it; then the files, named relative to the DVX folder with / between parts.
@@ -10602,7 +10975,8 @@ APPS/ACME/CHESS/CHESS.APP = 61234 <SHA-256, 64 hex digits>
pkgDpkBuildFiles(&templ, files, count, out, secret, err, size) The same from a list of PkgDpkFileT (the name in the package, the file it comes from).
pkgDpkFolderFiles(folder, prefix, &files) Every file under folder as that list, named prefix/...; pkgDpkFilesFree frees it.
pkgDpkReadInfo(path, &info, err, size) The manifest and PACKAGE.SIG, checked; pkgDpkInfoFree.
- pkgDpkCheckFiles(path, &info, err, size) Every file's size and SHA-256 against the manifest.
+ pkgDpkCheckFiles(path, &info, err, size) Every file's size and SHA-256 against the manifest, all at once.
+ pkgDpkCheckStart(path, &info, err, size) The same as a job: pkgDpkJobStep a piece at a time, pkgDpkJobPercent by bytes read (BASIC's PkgVerifyStart).
pkgDpkSign(path, &secret, detached, err, size) Sign into the package, or beside it as NAME.SIG.
pkgDpkSigner(&info, detached, keys, count) Which of keys made the signature (or the detached one).
Installed Packages and Trust (pkgDb.h, pkgTrust.h)
Each installed package has a record, SYSTEM/PACKAGES/VENDOR/NAME.INI: its manifest as installed, whether it was chosen or only needed by another, and the key that signed it. pkgDbLoad reads them all; pkgDbFind, pkgDbOwner (who has a file), pkgDbPut, and pkgDbDrop keep them. Setup writes DVX's own record (kpunch.dvx, with no files).
-
pkgKeyringLoad reads the keys DVX trusts: SYSTEM/KEYS (DVX's own) and the user's KEYS folder (publishers the user chose); pkgKeyringAdd trusts another. pkgTrustOf says how far a package is trusted (PkgTrustSystemE, PkgTrustUserE, PkgTrustNoneE). The rules the planner keeps: kpunch.* packages must be signed by a SYSTEM key; an upgrade must be signed by the key that signed the installed version (or a SYSTEM key); an unsigned package from a file is installed only after a warning.
+
pkgKeyringLoad reads the keys DVX trusts: SYSTEM/KEYS (DVX's own) and the user's KEYS folder (publishers the user chose); pkgKeyringAdd trusts another, and pkgKeyringRemove(ring, userDir, idText, err, size) stops trusting one of the user's (its files in KEYS are deleted; a SYSTEM key stays). pkgTrustOf says how far a package is trusted (PkgTrustSystemE, PkgTrustUserE, PkgTrustNoneE). The rules the planner keeps: kpunch.* packages must be signed by a SYSTEM key; an upgrade must be signed by the key that signed the installed version (or a SYSTEM key); an unsigned package from a file is installed only after a warning.
+
ZIP Files (pkgZip.h)
+
The reader goes front to back through a ZIP from a read callback (pkgZipFileRead over a FILE *), never seeking; each entry's data goes, inflated, to a write callback in steps, and its size and CRC-32 are checked at its end. pkgZipProblemText says why a step failed, as the end of a sentence. The writer makes a ZIP the reader takes: pkgZipCreate, pkgZipBeginFile or pkgZipBeginMem, pkgZipStep until each is in, pkgZipFinish (pkgZipAbort removes a half-made one). pkgZipDosTime and pkgZipDosStamp turn ZIP dates into times and back.
+
A plain ZIP job (PkgZipJobT, pkgZipJob.c) does a whole archive a step at a time, for a program that keeps drawing:
+
Function Description
+ -------- -----------
+ pkgZipJobRead(path, dir, err, size) Read a ZIP to its end, every entry checked; with dir, unpack each under it (folders made, files dated as in the ZIP; a name with a drive, a leading '/', or ".." fails it).
+ pkgZipJobCreate(path, names, sources, count, err, size) Make a ZIP of count files: names[i] in the ZIP from the file sources[i].
+ pkgZipJobStep(job, maxWork) PkgZipJobMoreE, PkgZipJobDoneE, or PkgZipJobFailedE (pkgZipJobMessage says why).
+ pkgZipJobEntries(job, &count) The entries read or written so far.
+ pkgZipJobCurrent, pkgZipJobPercent The entry it is on; how far it is (bytes of the ZIP read, or of the files added).
+ pkgZipJobFree(job) Stop and free it: a ZIP being made, or a file being unpacked, is removed.
+
pkgHexEncode and pkgHexDecode (pkgFormat.h) turn bytes into hex digits and back, as manifests, keys, and the package server write SHA-256s and key ids.
+
The Package Writer Module
+
Writing ZIPs (miniz's deflate) and making keys and signatures (Ed25519 signing) are used only when a package is made, so in DVX they are a module of their own, writer.drv in LIBS\KPUNCH\PKG\WRITER, which pkg.lib loads the first time a program makes a ZIP, a key, or a signature (pkgZipCreate, pkgKeyMake, or pkgSign, directly or through pkgKeyWrite, the .DPK and ZIP jobs, and BASIC's PkgBuildStart, PkgSignStart, PkgKeygen, PkgSignData, and PkgZipCreateStart); it stays until pkg.lib is unloaded. Nothing changes for a program: the calls keep their names (pkgWriterMod.c passes them on; pkgWriter.h has the module's descriptor). Without the module (a damaged installation) pkgZipCreate gives NULL and a key or signature made is all zeros, which nothing accepts; DVX.LOG says the writer is missing. Reading, checking, and installing packages never load it. Programs that link the library in whole (DVXPKG.EXE, the build machine's dvxpkg, the tests) compile pkgZipWrite.c and pkgSignMake.c instead of the module.
Planning (pkgPlan.h)
An offer (PkgOfferT) is a package that can be installed: pkgOfferRead reads one file, pkgOffersFor reads a file and every package in its folder (what it may need), pkgOffersInFolder a whole folder. pkgPlanInstall(db, offers, count, wanted, wantedCount, &plan) plans installing the offers wanted with everything they need, in the order they go in, each checked (the DVX and ABI it needs, files another package has, a newer version installed, the signing rules); plan.err says why when it fails, plan.bytes is what it unpacks, and plan.stale lists the add-ons an update of DVX itself would leave unable to load. pkgPlanDependents lists the installed packages that need some going (they go too); pkgPlanOrphans those only they needed.
Installing and Removing (pkgOp.h)
@@ -10639,7 +11027,7 @@ dvxpkg list [-r DVXDIR]
Printing Library
Printer ports and print jobs, text laid out on pages, pictures for dot-matrix and laser printers, the printer settings, and the common Print and Page Setup dialogs. Notepad, Image Viewer, Icon Editor, the Control Panel's Printer page, and BASIC's LPRINT all print through it; any program can.
Header: print/print.h
-
Loaded as: bin/libs/kpunch/print/print.lib
+
Loaded as: bin/libs/kpunch/print/print.lib; the PostScript printers' driver, with their text faces and vector pages, is bin/libs/kpunch/print/drivers/ps.drv, which the library loads the first time a PostScript printer is used (until then printVecBegin is false for every document).
Settings
The printer is chosen once for everything, in DVX.INI [printer] (the Control Panel's Printer page and the Page Setup dialog edit it): where output goes (PrintDestE: LPT1 to LPT3, COM1 to COM4, or a file), a serial port's speed and handshake (baud, PrintHandshakeE), the printer model (PrintModelE: text only; Epson 9-pin or 24-pin, each with a black or a color ribbon; HP LaserJet at 150 dpi; HP LaserJet III or DeskJet at 300 dpi; HP DeskJet 500C; HP DeskJet 550C and later; PostScript, black or color; Apple ImageWriter II, black or color), and the page setup (PrintPageSetupT).
RtfToText$ is the text of an RTF document, a line per paragraph (text that is not RTF comes back as it is); TextToRtf$ is plain text as RTF in the system font, a paragraph per line. A result stays valid until the next call. The RichEdit control's TextRTF, SelRTF, LoadFile, and SaveFile cover RTF in a control.
-
-
Serial Stack
-
The DVX serial/networking stack provides reliable, optionally encrypted communication over RS-232 serial ports. It is composed of four layers, each building on the one below:
-
-
rs232 -- ISR-driven UART driver with ring buffers and flow control
The security layer is invoked by secLink during the Diffie-Hellman handshake and for per-packet cipher operations. It has no runtime coupling to rs232 or packet, so it can be used standalone for cryptographic operations independent of the serial stack.
ISR-driven serial port driver supporting up to 4 simultaneous COM ports. A shared ISR drains UART FIFOs into per-port ring buffers (2048 bytes, power-of-2 for fast index wrapping). Flow control (XON/XOFF, RTS/CTS, DTR/DSR) operates within the ISR using watermark thresholds. All ISR data structures are DPMI-locked to prevent page faults. XON/XOFF bytes are queued for the transmit interrupt instead of being written over a byte still in the transmit register, and every cached register (IER, MCR) is changed with interrupts off so the ISR and the application never lose each other's updates.
-
Header: rs232/rs232.h
-
Port Constants
-
Constant Value Description
- -------- ----- -----------
- RS232_COM1 0 First serial port.
- RS232_COM2 1 Second serial port.
- RS232_COM3 2 Third serial port.
- RS232_COM4 3 Fourth serial port.
- RS232_NUM_PORTS 4 Total number of supported COM ports.
-
Handshake Modes
+
+
Secure Link Library
+
Top-level API composing all three lower layers (rs232, packet, security) into a single interface. Provides an authenticated key exchange, channel multiplexing (0-127), and per-packet encryption with integrity protection over a reliable serial link.
+
Loaded as: bin/libs/kpunch/seclink/seclink.lib (part of Communications; needs the serial and cryptography libraries). The packet transport below it is in the same library.
Type Layout Purpose
+ ---- ------ -------
+ HELLO type, version, flags, X25519 public key (32 bytes) Starts a key exchange. Flag bit 0: passphrase in use.
+ CONFIRM type, HMAC-SHA256 (32 bytes) Proves both sides derived the same keys.
+ PLAIN type, channel, payload Clear application data.
+ CIPHER type, channel, ciphertext, tag (16 bytes) Encrypted, authenticated application data.
+
Handshake packets are never delivered to the application, and application data is never taken as key material. A HELLO is kept whenever it arrives, so the side that starts first is never missed.
+
Key exchange: X25519 (RFC 7748). An X25519 takes about a sixth of a second on a 486, so each poll computes a ninth of one (about 20 ms there): first the side's public key, then the shared secret once both HELLOs are in, so the handshake never holds up other programs for long. A peer key that gives an all-zero secret fails the exchange (SECLINK_ERR_HANDSHAKE).
+
Key derivation: HKDF-SHA256 with salt = SHA-256(passphrase) (32 zero bytes without one), input = the X25519 shared secret, and info = "DVX secLink v3" followed by the lower then the higher public key. The output supplies a 32-byte sending key for each direction plus a confirmation key; the side with the lower public key sends with the "low" key. Directions never share a key.
+
Confirmation: each side sends HMAC(confirmation key, SHA-256(lower HELLO + higher HELLO) + role byte). A different passphrase, a passphrase on only one side, or a modified HELLO makes the check fail on both sides with SECLINK_ERR_AUTH. A side that has a passphrase never accepts a peer without one.
+
Encrypted frames: XChaCha20-Poly1305. Each direction numbers its encrypted frames with an implicit 64-bit sequence number that is never sent; a frame's nonce is that number, little-endian, followed by 16 zero bytes. The type and channel bytes are authenticated (associated data), the payload is encrypted, and the 16-byte Poly1305 tag follows it. Every frame is sealed on its own, so tampered, replayed, or injected frames fail the tag, are dropped and counted (secLinkGetDropped), and later frames still verify.
+
Without a passphrase the link is encrypted and integrity-protected but not authenticated: an active attacker in the middle could run two separate exchanges. Use the same passphrase on both sides to rule that out.
+
Clear frames are accepted only until the first session is established. From then on, until secLinkClose, a clear frame is delivered only on a channel marked plaintext with secLinkSetChannelPlain -- even after the session was dropped by a packet-layer reset or a failed re-key, which someone on the line can cause; so neither can turn a protected channel into a clear one. A re-key HELLO on a running session whose protocol version or passphrase flag cannot match is dropped (and counted) without ending the session.
+
Constants
Constant Value Description
-------- ----- -----------
- RS232_HANDSHAKE_NONE 0 No flow control.
- RS232_HANDSHAKE_XONXOFF 1 Software flow control (XON/XOFF characters).
- RS232_HANDSHAKE_RTSCTS 2 Hardware flow control via RTS/CTS lines.
- RS232_HANDSHAKE_DTRDSR 3 Hardware flow control via DTR/DSR lines.
-
UART Types
-
Detected automatically by probing the scratch register and FIFO capability.
-
Constant Value Description
- -------- ----- -----------
- RS232_UART_UNKNOWN 0 Detection failed.
- RS232_UART_8250 1 Original IBM PC UART. No FIFO.
- RS232_UART_16450 2 Has scratch register, no FIFO.
- RS232_UART_16550 3 Has FIFO but buggy (rare).
- RS232_UART_16550A 4 Working 16-byte FIFO. Most common in 486+ hardware.
+ SECLINK_PROTOCOL_VERSION 3 Version carried in HELLO; a mismatch fails the handshake.
+ SECLINK_FRAME_HDR_SIZE 2 Type byte plus channel byte.
+ SECLINK_MAC_SIZE 16 Poly1305 tag on encrypted frames (SEC_AEAD_TAG_SIZE).
+ SECLINK_MAX_PAYLOAD 237 Maximum payload per send (PKT_MAX_PAYLOAD minus header and tag), clear or encrypted.
+ SECLINK_MAX_CHANNEL 127 Highest valid channel number.
+ SECLINK_NUM_CHANNELS 128 Array size needed to hold one slot per channel (SECLINK_MAX_CHANNEL + 1).
+ SECLINK_CHAN_BUF_SIZE 4096 Recommended per-channel receive buffer size for callers that buffer inbound data between the receive callback and an application read.
Error Codes
-
Constant Value Description
- -------- ----- -----------
- RS232_SUCCESS 0 Operation succeeded.
- RS232_ERR_UNKNOWN -1 Unknown error.
- RS232_ERR_NOT_OPEN -2 Port is not open.
- RS232_ERR_ALREADY_OPEN -3 Port is already open.
- RS232_ERR_NO_UART -4 No UART detected at port address.
- RS232_ERR_INVALID_PORT -5 Invalid port number.
- RS232_ERR_INVALID_BASE -6 Invalid base I/O address.
- RS232_ERR_INVALID_IRQ -7 Invalid IRQ number.
- RS232_ERR_INVALID_BPS -8 Invalid baud rate.
- RS232_ERR_INVALID_DATA -9 Invalid data bits value.
- RS232_ERR_INVALID_PARITY -10 Invalid parity character.
- RS232_ERR_INVALID_STOP -11 Invalid stop bits value.
- RS232_ERR_INVALID_HANDSHAKE -12 Invalid handshake mode.
- RS232_ERR_INVALID_FIFO -13 Invalid FIFO threshold.
- RS232_ERR_NULL_PTR -14 NULL pointer argument.
- RS232_ERR_IRQ_NOT_FOUND -15 Could not detect IRQ for port.
- RS232_ERR_LOCK_MEM -16 DPMI memory lock failed.
-
rs232Open
-
Open a COM port with the specified line parameters.
Constant Value Description
+ -------- ----- -----------
+ SECLINK_SUCCESS 0 Operation succeeded.
+ SECLINK_ERR_PARAM -1 Invalid parameter (NULL pointer, bad channel, bad length).
+ SECLINK_ERR_SERIAL -2 Serial port closed or disconnected (secLinkPoll).
+ SECLINK_ERR_HANDSHAKE -4 Handshake failed: invalid public key, protocol version mismatch, reflected HELLO, a dead port or a line that died once the peer answered, or the peer reset mid-exchange.
+ SECLINK_ERR_NOT_READY -5 No session keys yet (encrypted send attempted).
+ SECLINK_ERR_SEND -6 Packet send failed, the peer stopped acknowledging, or a non-blocking send found the window full.
+ SECLINK_ERR_TIMEOUT -7 The peer did not complete the handshake within 30 seconds (secLinkHandshake; the exchange stays pending), or the line went silent before it answered at all (secLinkHandshakeStep).
+ SECLINK_ERR_AUTH -8 Passphrase mismatch, passphrase on only one side, or tampering.
+ SECLINK_PENDING 1 secLinkHandshakeStep only: the exchange is still running.
+
SecLinkRecvT
+
Receive callback type. Delivers plaintext with the channel number. encrypted is true when the frame arrived encrypted and its tag verified, false for a clear frame.
Open a secure serial link. Opens the COM port via rs232Open and creates a packet layer (default window size) with an internal receive dispatcher. The handshake must be performed separately via secLinkHandshake before encrypted data can be sent.
+
SecLinkT *secLinkOpen(int com, int32_t bps, int dataBits, char parity, int stopBits, int handshake, SecLinkRecvT callback, void *ctx);
Parameter Description
--------- -----------
- com Port index (RS232_COM1..RS232_COM4).
- bps Baud rate (e.g. 9600, 19200, 38400, 57600, 115200).
- dataBits Data bits per character (5, 6, 7, or 8).
- parity Parity mode: 'N' (none), 'O' (odd), 'E' (even), 'M' (mark), 'S' (space).
+ com COM port index (RS232_COM1..RS232_COM4).
+ bps Baud rate.
+ dataBits Data bits per character (5-8).
+ parity Parity mode ('N', 'O', 'E', 'M', 'S', or lowercase).
stopBits Stop bits (1 or 2).
- handshake Flow control mode (RS232_HANDSHAKE_*).
-
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.
-
rs232Close
-
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.
+ handshake Flow control mode (RS232_HANDSHAKE_*).
+ callback Called when a packet is received, after verification and decryption. The data pointer is valid only during the callback.
+ ctx User pointer passed to the callback.
+
Returns an opaque SecLinkT handle, or NULL on failure (allocation failure, rs232Open error, or pktOpen error). On failure all partially initialized resources are cleaned up.
+
secLinkOpenStream
+
Open a secure link over a byte stream (see pktOpenStream) instead of a COM port, e.g. a TCP connection to the host proxy's listening port. Everything else -- handshake, passphrase, channels, sending, and polling -- is the same as for a COM port link. secLinkClose does not close the stream; the caller closes it afterwards.
Close the link and free all associated resources: wipe the key material, close the packet layer and the COM port, and free the link. Safe to call with a NULL link (no-op). Safe to call from inside the receive callback: the close then happens when the outermost secLink call returns, and no further packets are delivered.
+
void secLinkClose(SecLinkT *link);
+
secLinkSetPassphrase
+
Set the shared passphrase for the next handshake. NULL or "" clears it. Only SHA-256 of the text is kept. Set it before calling secLinkHandshake; both sides must use the same passphrase, or both none. An exchange already in progress (for example one left pending by a timed-out secLinkHandshake) was started with the old setting, so it is abandoned: the next secLinkHandshake starts a fresh one.
+
int secLinkSetPassphrase(SecLinkT *link, const char *pass);
+
Returns SECLINK_SUCCESS or SECLINK_ERR_PARAM for a NULL link.
+
secLinkHandshake
+
Run the authenticated X25519 key exchange. Blocks, polling the packet layer, until both CONFIRM messages have been checked, or until 30 seconds pass.
+
int secLinkHandshake(SecLinkT *link);
+
Behavior:
+
+
A fresh X25519 key pair is made from the DRBG and our HELLO is sent. If the peer's HELLO already arrived (it started first), the shared secret follows on the next poll.
+
The X25519 secret key is wiped as soon as the session keys are derived (forward secrecy).
+
After SECLINK_ERR_TIMEOUT the exchange stays pending: it still completes during secLinkPoll if the peer shows up, and calling secLinkHandshake again resumes the wait without sending a new key.
+
On a ready link it negotiates fresh session keys. A peer that is already ready (or has an exchange pending) answers automatically from secLinkPoll.
+
Encrypted traffic under the previous keys that was sent before the new HELLO is still accepted; encrypted sends are refused (SECLINK_ERR_NOT_READY) until the new exchange completes.
+
+
Returns SECLINK_SUCCESS, SECLINK_ERR_PARAM, SECLINK_ERR_HANDSHAKE, SECLINK_ERR_TIMEOUT, or SECLINK_ERR_AUTH. When the peer confirms (or rejects) and hangs up in the same poll -- a proxy exits as soon as it sees a passphrase mismatch -- the exchange's outcome is reported, not the disconnect.
+
secLinkHandshakeStep
+
Non-blocking handshake for callers that run their own event loop.
+
int secLinkHandshakeStep(SecLinkT *link);
+
Starts an exchange on an idle link (never handshaken, or dropped by a peer reset), polls once, and returns SECLINK_SUCCESS once the link is ready, SECLINK_PENDING (1) while the exchange is still running, or the error that ended it (the same codes as secLinkHandshake). SECLINK_ERR_TIMEOUT here means the packet layer gave up on the line (PKT_LINK_TIMEOUT_MS) while the peer had not answered at all; the caller's own limit is its to keep. A failed exchange keeps returning its error; secLinkHandshake starts a new one.
+
secLinkSend
+
Send data on a channel, optionally encrypted and authenticated.
+
int secLinkSend(SecLinkT *link, const uint8_t *data, int len, uint8_t channel, bool encrypt, bool block);
Parameter Description
--------- -----------
- com Port index.
- data Buffer to receive data.
- len Maximum bytes to read.
-
Returns the number of bytes actually read (may be less than len).
-
rs232Write
-
Blocking polled write directly to UART transmit holding register. Bypasses the TX ring buffer. Polls LSR_TX_HOLD_EMPTY before writing each byte (the check and the write happen with interrupts off, so a flow-control byte from the ISR is never overwritten). Use for small, immediate writes where order matters (the packet layer uses this for frame transmission).
Returns the number of bytes actually written (may be less than len while flow control holds the line), or a negative error code (RS232_ERR_INVALID_PORT, RS232_ERR_NOT_OPEN, RS232_ERR_NULL_PTR).
-
rs232WriteBuf
-
Non-blocking write to the transmit ring buffer. The ISR drains buffered data to the UART asynchronously.
Returns the number of bytes actually queued (may be less than len if the buffer is full).
-
rs232Set
-
Change all line parameters on an open port. Internally calls rs232SetBps, rs232SetData, rs232SetParity, rs232SetStop, and rs232SetHandshake in sequence. Returns RS232_SUCCESS on success or the first negative error code encountered.
Function Returns
- -------- -------
- rs232GetBase(com) I/O base address.
- rs232GetBps(com) Current baud rate (int32_t).
- rs232GetData(com) Data bits setting (5-8).
- rs232GetParity(com) Parity character: 'n', 'o', 'e', 'm', or 's' (lowercase).
- rs232GetStop(com) Stop bits setting (1 or 2).
- rs232GetHandshake(com) Handshake mode (RS232_HANDSHAKE_*).
- rs232GetIrq(com) IRQ number.
- rs232GetUartType(com) UART type probed when the port was opened (RS232_UART_*).
-
rs232GetParity returns a lowercase character on success. Callers comparing against uppercase constants must use either both cases or tolower().
-
Status Getters
-
Function Returns
- -------- -------
- rs232GetRxBuffered(com) Bytes waiting in the receive ring buffer.
- rs232GetTxBuffered(com) Bytes waiting in the transmit ring buffer.
- rs232GetCts(com) CTS line state (0 or 1).
- rs232GetDcd(com) DCD (carrier detect) line state (0 or 1), as last reported by the modem status interrupt.
- rs232GetDsr(com) DSR line state (0 or 1).
- rs232GetDtr(com) DTR line state (0 or 1).
- rs232GetRts(com) RTS line state (0 or 1).
- rs232GetLsr(com) Line status register value as last read; RS232_LSR_OVERRUN, RS232_LSR_PARITY_ERR, RS232_LSR_FRAME_ERR, and RS232_LSR_BREAK are its error bits.
- rs232GetMcr(com) Modem control register value.
- rs232GetMsr(com) Modem status register value; RS232_MSR_RI is the ring indicator.
-
Configuration Setters
-
Function Description
- -------- -----------
- rs232SetBase(com, base) Set I/O base address (before open).
- rs232SetBps(com, bps) Change baud rate.
- rs232SetData(com, dataBits) Change data bits.
- rs232SetParity(com, parity) Change parity mode.
- rs232SetStop(com, stopBits) Change stop bits.
- 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. 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. 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. rs232Open sets 8 on a 16550A (the 8 free bytes allow 0.7 ms of interrupt latency at 115200 bps; 14 allows only 0.17 ms) and 0 on other UARTs.
-
Buffer Management
-
Function Description
- -------- -----------
- rs232ClearRxBuffer(com) Discard all data in the receive ring buffer.
- rs232ClearTxBuffer(com) Discard all data in the transmit ring buffer.
+ link Connection handle from secLinkOpen.
+ data Plaintext payload.
+ len Payload length. Must be in 1..SECLINK_MAX_PAYLOAD.
+ channel Logical channel number (0..SECLINK_MAX_CHANNEL).
+ encrypt If true, seal (encrypt and tag) before sending (requires a completed handshake).
+ block If true, wait (pumping the receive callback) until the transmit window has space. If false, return SECLINK_ERR_SEND immediately when the window is full.
+
Returns SECLINK_SUCCESS on success or a negative error code (SECLINK_ERR_PARAM, SECLINK_ERR_NOT_READY, SECLINK_ERR_SEND).
+
A frame takes its sequence number only once window space is guaranteed, and is queued without pumping in between, so frames always reach the wire in sequence order. The receive callback may itself send (blocking or not), including encrypted.
+
Once session keys exist the receiver drops clear frames unless it marked the channel plaintext with secLinkSetChannelPlain.
+
secLinkSendBuf
+
Send an arbitrarily large buffer by splitting into SECLINK_MAX_PAYLOAD chunks. Always blocks until all data is sent.
+
int secLinkSendBuf(SecLinkT *link, const uint8_t *data, int len, uint8_t channel, bool encrypt);
+
Returns SECLINK_SUCCESS or the first error encountered.
+
secLinkSetChannelPlain
+
Allow (plain = true) or refuse (plain = false, the default) clear frames on a channel once session keys exist. Before the first handshake clear frames are always accepted.
+
int secLinkSetChannelPlain(SecLinkT *link, uint8_t channel, bool plain);
+
Returns SECLINK_SUCCESS or SECLINK_ERR_PARAM for a bad link or channel.
+
secLinkPoll
+
Poll for incoming data. Drains the serial port, processes frames and retransmits, completes pending handshake steps, verifies and decrypts data frames, and forwards them to the callback.
+
int secLinkPoll(SecLinkT *link);
+
Returns the number of packets delivered by the packet layer this call, SECLINK_ERR_PARAM if link is NULL, or SECLINK_ERR_SERIAL when the port is closed or disconnected. Must be called frequently in the main loop.
+
If the remote packet layer resets, the session keys are dropped (the link is no longer ready) and a new handshake is needed. Clear frames stay refused on protected channels (see Protocol).
+
secLinkIsReady
+
Check whether the handshake is complete and the link is ready for encrypted traffic. Cleartext sends do not require the link to be ready.
+
bool secLinkIsReady(SecLinkT *link);
+
secLinkGetDropped
+
Number of received frames dropped since open: bad tag (tampered, replayed, or injected), clear frames on a protected channel, encrypted frames without session keys, and malformed frames.
+
int32_t secLinkGetDropped(SecLinkT *link);
+
Returns the count or SECLINK_ERR_PARAM when link is NULL.
+
secLinkGetPending
+
Get the number of unacknowledged packets in the transmit window. Delegates directly to pktGetPending on the underlying packet connection. Useful for non-blocking send loops to decide when to throttle.
+
int secLinkGetPending(SecLinkT *link);
+
Returns the pending count on success or SECLINK_ERR_PARAM when link is NULL.
-
+
Packet Transport
Reliable, ordered packet delivery over an RS-232 serial link. Uses HDLC-style byte-stuffed framing for frame delimiting, CRC-16-CCITT for error detection, and Go-Back-N ARQ with a configurable sliding window for retransmission.
Returns an opaque PktConnT handle, or NULL on failure (invalid port, duplicate open, or allocation failure).
pktOpenStream
-
Open a packetized connection over any byte stream instead of a COM port -- a TCP socket, for example. The stream is described by two non-blocking calls and a context pointer, copied into the connection. Any number of stream connections may be open.
+
Open a packetized connection over any byte stream instead of a COM port -- a TCP socket, for example. The stream is described by non-blocking calls and a context pointer, copied into the connection. Any number of stream connections may be open.
Member Description
------ -----------
read Up to len bytes into buf: the count, 0 when nothing has arrived, negative when the stream is gone (pktPoll then reports PKT_ERR_DISCONNECTED).
- write Bytes taken from data (0 = none right now; a frame is retried until it is all out or nothing was taken for a while), negative when the stream is gone.
- ctx Passed to both.
+ write Bytes taken from data (0 = none right now), negative when the stream is gone. Frames wait in the connection's transmit queue, and each pktPoll hands the stream what it takes; a frame that finds no room in the queue is dropped (a data frame is sent again by its retransmit timer).
+ ctx Passed to the calls.
+ pending Bytes taken but not yet on the wire (NULL: the stream does not say). Retransmit timers wait while there are any and their count keeps moving: a count that stays the same for 2 seconds (flow control holding the line) no longer holds them.
Returns the handle, or NULL for a NULL stream or call, or on allocation failure. pktClose never closes the stream.
pktClose
Close a packetized connection and release its registry slot. Does not close the underlying COM port. The caller is responsible for calling rs232Close separately when no longer needed. May be called from inside the receive callback: the connection is then freed when the outermost pktPoll returns (which reports PKT_ERR_DISCONNECTED).
Returns PKT_SUCCESS, PKT_ERR_INVALID_PARAM, PKT_ERR_DISCONNECTED, or PKT_ERR_TIMEOUT after PKT_SEND_TIMEOUT_MS without progress.
-
-
Security (DH, XTEA, SHA-256)
-
Cryptographic primitives for the serial stack: 1024-bit Diffie-Hellman key exchange (RFC 2409 Group 2), XTEA block cipher in CTR mode, SHA-256, HMAC-SHA256, and HKDF-SHA256, and a SHA-256 based random number generator.
-
XTEA requires no lookup tables and compiles to approximately 20 instructions per round, making it suitable for 486-class hardware where AES S-box tables would thrash the 8KB cache. SHA-256 needs only 32-bit arithmetic and a 256-byte constant table.
-
Headers: security/security.h, security/secHash.h
-
Constants
-
Constant Value Description
- -------- ----- -----------
- SEC_DH_KEY_SIZE 128 Size of a DH public key or shared secret in bytes (1024 bits).
- SEC_XTEA_KEY_SIZE 16 Size of an XTEA key in bytes (128 bits).
- SEC_RNG_SEED_SIZE 32 Bytes stored in a seed file.
- SEC_SHA256_SIZE 32 SHA-256 digest size.
- SEC_SHA256_BLOCK_SIZE 64 SHA-256 block size.
- SEC_HKDF_MAX_OUT 8160 Largest HKDF output (255 digests).
-
Error Codes
-
Constant Value Description
- -------- ----- -----------
- SEC_SUCCESS 0 Operation succeeded.
- SEC_ERR_PARAM -1 Invalid parameter.
- SEC_ERR_NOT_READY -2 DH context not ready (keys not generated or secret not computed).
- SEC_ERR_IO -3 Seed file could not be read or written.
-
RNG Functions
-
A SHA-256 hash DRBG over a 32-byte entropy pool. secRngAddEntropy hashes new bytes into the pool and never discards what is already there, so it is safe to call at any time, before or after the first output. Each output block is a hash of the pool and a counter; after every request the pool is re-hashed so earlier output cannot be recomputed from a later state.
-
The first secRngBytes call gathers hardware entropy automatically. On DOS that is 256 PIT samples, each taken after a busy wait sized by the previous sample, plus the full time stamp counter on CPUs that have one (Pentium and later) and the BIOS tick count. On a CPU without a time stamp counter the samples depend mostly on the PIT phase and time of day, which an observer can narrow down, so applications should feed user-input timings to secRngAddEntropy and keep a seed file: call secRngLoadSeed at startup and secRngSaveSeed at shutdown. Host builds read /dev/urandom.
-
Under the DVX shell this is automatic when the serial library is installed: the shell loads the seed file SECLINK.RND in the user folder at startup, feeds the timing of every key press and mouse event to secRngAddEntropy, and saves a fresh seed at shutdown. Applications only need their own calls when they run without the shell.
Function Description
- -------- -----------
- secRngAddEntropy Hash bytes into the pool (key timings, mouse positions, anything unpredictable).
- secRngBytes Generate pseudorandom bytes.
- secRngGatherEntropy Fill buf with condensed hardware entropy (one fresh gather per 32 bytes). Returns the byte count (0 if a host build cannot read its random device).
- secRngLoadSeed Mix a seed file into the pool, then immediately rewrite it with fresh output so a seed is never used twice. SEC_SUCCESS or SEC_ERR_IO.
- secRngSaveSeed Write SEC_RNG_SEED_SIZE bytes of fresh output for the next run. SEC_SUCCESS or SEC_ERR_IO.
- secSecureZero Zero memory in a way the compiler cannot optimize away (for key material).
SHA-256 follows FIPS 180-4, HMAC RFC 2104, and HKDF RFC 5869 (extract and expand; a NULL or empty salt means 32 zero bytes). secHkdf returns SEC_ERR_PARAM for okmLen outside 1..SEC_HKDF_MAX_OUT. secConstTimeEqual compares without revealing where the inputs differ; use it for every MAC check.
-
Diffie-Hellman Key Exchange
-
1024-bit DH with 256-bit private exponents using the RFC 2409 Group 2 safe prime. The modular exponentiation is a Montgomery ladder with branch-free operand swaps, so its timing does not depend on the private key.
-
SecDhT *secDhCreate(void);
-int secDhGenerateKeys(SecDhT *dh);
-int secDhGetPublicKey(SecDhT *dh, uint8_t *buf, int *len);
-int secDhComputeSecret(SecDhT *dh, const uint8_t *remotePub, int len);
-int secDhGetSecret(SecDhT *dh, uint8_t *buf, int *len);
-void secDhDestroy(SecDhT *dh);
-
Function Description
- -------- -----------
- secDhCreate Allocate a new DH context.
- secDhGenerateKeys Generate a private exponent and public key from the DRBG.
- secDhGetPublicKey Copy the local public key into buf. *len receives the key size.
- secDhComputeSecret Compute the shared secret from the remote public key. Keys outside [2, p-2] are rejected with SEC_ERR_PARAM (0, 1, and p-1 would force a trivially known secret).
- secDhGetSecret Copy the shared secret (SEC_DH_KEY_SIZE big-endian bytes) into buf. It is input for a key derivation function such as secHkdf, never a key by itself.
- secDhDestroy Securely zero and free the DH context.
Function Description
- -------- -----------
- secCipherCreate Allocate a cipher context with a 128-bit key. Returns NULL on allocation failure or NULL key. Initial counter is zero.
- secCipherCrypt Encrypt or decrypt data in place. The 64-bit counter increments by one every 8 bytes (one XTEA block).
- secCipherSetCounter Position the 64-bit block counter (secLink derives it from the frame sequence number).
- secCipherDestroy Securely zero the cipher context (key and counter state) and free its memory.
-
secCipherCreate returns NULL if the key pointer is NULL or allocation fails. secCipherCrypt is a no-op when any of c/data is NULL or len <= 0.
-
BASIC
-
src/include/basic/security.bas (DECLARE LIBRARY "serial") declares the BASIC functions (security/secBas.c). Keys, data, and results are BINARY STRING (pointer and length in, a result buffer and length out), so every byte value passes; the result buffer is wiped before it is reused. A handle (1, 2, ...; 0 = none) is a key exchange, a cipher, or an incremental SHA-256 / HMAC; each records the program that made it and the shell's app-cleanup hook frees (and wipes) what a program leaves open.
-
BASIC C it calls
- ----- ----------
- SecRandom$(count), SecAddEntropy bytes secRngBytes; secRngAddEntropy.
- SecDhCreate() secDhCreate + secDhGenerateKeys.
- SecDhPublicKey$(h), SecDhSecret$(h) secDhGetPublicKey; secDhGetSecret ("" before SecDhComputeSecret).
- SecDhComputeSecret(h, remotePublicKey) secDhComputeSecret: SEC_OK or a SEC_ERR_ code.
- SecDhFree h secDhDestroy.
- SecCipherCreate(key) secCipherCreate; 0 unless key is SEC_XTEA_KEY_SIZE bytes.
- SecCipherCrypt$(h, bytes) secCipherCrypt on a copy.
- SecCipherSetCounter h, counter secCipherSetCounter (a DOUBLE: exact up to 2^53).
- SecCipherFree h secCipherDestroy.
- SecSha256$(bytes); SecSha256Open, SecSha256Add, SecSha256End$ secSha256; secSha256Init / Update / Final (End frees the handle).
- SecHmac$(key, bytes); SecHmacOpen, SecHmacAdd, SecHmacEnd$ secHmacSha256; secHmacInit / Update / Final.
- SecHkdf$(salt, secret, info, length) secHkdf ("" for a length outside 1..SEC_HKDF_MAX_OUT).
- SecEqual(a, b) secConstTimeEqual (False for different lengths).
-
The SEC_ CONSTs of security.bas are compared with security.h and secHash.h by a formrt test, which also calls every function for real.
-
-
-
Secure Link
-
Top-level API composing all three lower layers (rs232, packet, security) into a single interface. Provides an authenticated key exchange, channel multiplexing (0-127), and per-packet encryption with integrity protection over a reliable serial link.
-
Header: seclink/secLink.h
-
Protocol (version 2)
-
Every packet starts with a type byte:
-
Type Layout Purpose
- ---- ------ -------
- HELLO type, version, flags, DH public key (128 bytes) Starts a key exchange. Flag bit 0: passphrase in use.
- CONFIRM type, HMAC-SHA256 (32 bytes) Proves both sides derived the same keys.
- PLAIN type, channel, payload Clear application data.
- CIPHER type, channel, ciphertext, MAC (8 bytes) Encrypted, authenticated application data.
-
Handshake packets are never delivered to the application, and application data is never taken as key material. A HELLO is kept whenever it arrives, so the side that starts first is never missed.
-
Key derivation: HKDF-SHA256 with salt = SHA-256(passphrase) (32 zero bytes without one), input = the DH shared secret, and info = "DVX secLink v2" followed by the lower then the higher public key. The output supplies an XTEA key and an HMAC key for each direction plus a confirmation key; the side with the lower public key sends with the "low" keys. Directions never share a key.
-
Confirmation: each side sends HMAC(confirmation key, SHA-256(lower HELLO + higher HELLO) + role byte). A different passphrase, a passphrase on only one side, or a modified HELLO makes the check fail on both sides with SECLINK_ERR_AUTH. A side that has a passphrase never accepts a peer without one.
-
Encrypted frames: encrypt-then-MAC. Each direction numbers its encrypted frames with an implicit 64-bit sequence number that is never sent. The MAC is the first 8 bytes of HMAC-SHA256 over type, channel, big-endian sequence number, and ciphertext, and the CTR counter for a frame starts at sequence * 32, so every frame has its own keystream range. Tampered, replayed, or injected frames fail the MAC, are dropped and counted (secLinkGetDropped); later frames still verify.
-
Without a passphrase the link is encrypted and integrity-protected but not authenticated: an active attacker in the middle could run two separate exchanges. Use the same passphrase on both sides to rule that out.
-
Clear frames are accepted only until the first session is established. From then on, until secLinkClose, a clear frame is delivered only on a channel marked plaintext with secLinkSetChannelPlain -- even after the session was dropped by a packet-layer reset or a failed re-key, which someone on the line can cause; so neither can turn a protected channel into a clear one. A re-key HELLO on a running session whose protocol version or passphrase flag cannot match is dropped (and counted) without ending the session.
-
Constants
-
Constant Value Description
- -------- ----- -----------
- SECLINK_PROTOCOL_VERSION 2 Version carried in HELLO; a mismatch fails the handshake.
- SECLINK_FRAME_HDR_SIZE 2 Type byte plus channel byte.
- SECLINK_MAC_SIZE 8 Truncated MAC on encrypted frames.
- SECLINK_MAX_PAYLOAD 245 Maximum payload per send (PKT_MAX_PAYLOAD minus header and MAC), clear or encrypted.
- SECLINK_MAX_CHANNEL 127 Highest valid channel number.
- SECLINK_NUM_CHANNELS 128 Array size needed to hold one slot per channel (SECLINK_MAX_CHANNEL + 1).
- SECLINK_CHAN_BUF_SIZE 4096 Recommended per-channel receive buffer size for callers that buffer inbound data between the receive callback and an application read.
-
Error Codes
-
Constant Value Description
- -------- ----- -----------
- SECLINK_SUCCESS 0 Operation succeeded.
- SECLINK_ERR_PARAM -1 Invalid parameter (NULL pointer, bad channel, bad length).
- SECLINK_ERR_SERIAL -2 Serial port closed or disconnected (secLinkPoll).
- SECLINK_ERR_ALLOC -3 Memory allocation failed.
- SECLINK_ERR_HANDSHAKE -4 Handshake failed: invalid public key, protocol version mismatch, reflected HELLO, dead line, or the peer reset mid-exchange.
- SECLINK_ERR_NOT_READY -5 No session keys yet (encrypted send attempted).
- SECLINK_ERR_SEND -6 Packet send failed, the peer stopped acknowledging, or a non-blocking send found the window full.
- SECLINK_ERR_TIMEOUT -7 The peer did not complete the handshake within 30 seconds (the exchange stays pending).
- SECLINK_ERR_AUTH -8 Passphrase mismatch, passphrase on only one side, or tampering.
- SECLINK_PENDING 1 secLinkHandshakeStep only: the exchange is still running.
-
SecLinkRecvT
-
Receive callback type. Delivers plaintext with the channel number. encrypted is true when the frame arrived encrypted and its MAC verified, false for a clear frame.
Open a secure serial link. Opens the COM port via rs232Open and creates a packet layer (default window size) with an internal receive dispatcher. The handshake must be performed separately via secLinkHandshake before encrypted data can be sent.
-
SecLinkT *secLinkOpen(int com, int32_t bps, int dataBits, char parity, int stopBits, int handshake, SecLinkRecvT callback, void *ctx);
-
Parameter Description
- --------- -----------
- com COM port index (RS232_COM1..RS232_COM4).
- bps Baud rate.
- dataBits Data bits per character (5-8).
- parity Parity mode ('N', 'O', 'E', 'M', 'S', or lowercase).
- stopBits Stop bits (1 or 2).
- handshake Flow control mode (RS232_HANDSHAKE_*).
- callback Called when a packet is received, after verification and decryption. The data pointer is valid only during the callback.
- ctx User pointer passed to the callback.
-
Returns an opaque SecLinkT handle, or NULL on failure (allocation failure, rs232Open error, or pktOpen error). On failure all partially initialized resources are cleaned up.
-
secLinkOpenStream
-
Open a secure link over a byte stream (see pktOpenStream) instead of a COM port, e.g. a TCP connection to the host proxy's listening port. Everything else -- handshake, passphrase, channels, sending, and polling -- is the same as for a COM port link. secLinkClose does not close the stream; the caller closes it afterwards.
Close the link and free all associated resources: securely destroy the ciphers and key material, destroy any remaining DH context, close the packet layer and the COM port, and free the link. Safe to call with a NULL link (no-op). Safe to call from inside the receive callback: the close then happens when the outermost secLink call returns, and no further packets are delivered.
-
void secLinkClose(SecLinkT *link);
-
secLinkSetPassphrase
-
Set the shared passphrase for the next handshake. NULL or "" clears it. Only SHA-256 of the text is kept. Set it before calling secLinkHandshake; both sides must use the same passphrase, or both none. An exchange already in progress (for example one left pending by a timed-out secLinkHandshake) was started with the old setting, so it is abandoned: the next secLinkHandshake starts a fresh one.
-
int secLinkSetPassphrase(SecLinkT *link, const char *pass);
-
Returns SECLINK_SUCCESS or SECLINK_ERR_PARAM for a NULL link.
-
secLinkHandshake
-
Run the authenticated DH key exchange. Blocks, polling the packet layer, until both CONFIRM messages have been checked, or until 30 seconds pass.
-
int secLinkHandshake(SecLinkT *link);
-
Behavior:
-
-
A fresh DH key pair is generated from the DRBG and our HELLO is sent. If the peer's HELLO already arrived (it started first), the keys are derived at once.
-
The DH private key is destroyed as soon as the session keys are derived (forward secrecy).
-
After SECLINK_ERR_TIMEOUT the exchange stays pending: it still completes during secLinkPoll if the peer shows up, and calling secLinkHandshake again resumes the wait without sending a new key.
-
On a ready link it negotiates fresh session keys. A peer that is already ready (or has an exchange pending) answers automatically from secLinkPoll.
-
Encrypted traffic under the previous keys that was sent before the new HELLO is still accepted; encrypted sends are refused (SECLINK_ERR_NOT_READY) until the new exchange completes.
-
-
Returns SECLINK_SUCCESS, SECLINK_ERR_PARAM, SECLINK_ERR_ALLOC, SECLINK_ERR_HANDSHAKE, SECLINK_ERR_TIMEOUT, or SECLINK_ERR_AUTH. When the peer confirms (or rejects) and hangs up in the same poll -- a proxy exits as soon as it sees a passphrase mismatch -- the exchange's outcome is reported, not the disconnect.
-
secLinkHandshakeStep
-
Non-blocking handshake for callers that run their own event loop.
-
int secLinkHandshakeStep(SecLinkT *link);
-
Starts an exchange on an idle link (never handshaken, or dropped by a peer reset), polls once, and returns SECLINK_SUCCESS once the link is ready, SECLINK_PENDING (1) while the exchange is still running, or the error that ended it (the same codes as secLinkHandshake, without SECLINK_ERR_TIMEOUT). A failed exchange keeps returning its error; secLinkHandshake starts a new one.
-
secLinkSend
-
Send data on a channel, optionally encrypted and MACed.
-
int secLinkSend(SecLinkT *link, const uint8_t *data, int len, uint8_t channel, bool encrypt, bool block);
-
Parameter Description
- --------- -----------
- link Connection handle from secLinkOpen.
- data Plaintext payload.
- len Payload length. Must be in 1..SECLINK_MAX_PAYLOAD.
- channel Logical channel number (0..SECLINK_MAX_CHANNEL).
- encrypt If true, encrypt and MAC before sending (requires a completed handshake).
- block If true, wait (pumping the receive callback) until the transmit window has space. If false, return SECLINK_ERR_SEND immediately when the window is full.
-
Returns SECLINK_SUCCESS on success or a negative error code (SECLINK_ERR_PARAM, SECLINK_ERR_NOT_READY, SECLINK_ERR_SEND).
-
A frame takes its sequence number only once window space is guaranteed, and is queued without pumping in between, so frames always reach the wire in sequence order. The receive callback may itself send (blocking or not), including encrypted.
-
Once session keys exist the receiver drops clear frames unless it marked the channel plaintext with secLinkSetChannelPlain.
-
secLinkSendBuf
-
Send an arbitrarily large buffer by splitting into SECLINK_MAX_PAYLOAD chunks. Always blocks until all data is sent.
-
int secLinkSendBuf(SecLinkT *link, const uint8_t *data, int len, uint8_t channel, bool encrypt);
-
Returns SECLINK_SUCCESS or the first error encountered.
-
secLinkSetChannelPlain
-
Allow (plain = true) or refuse (plain = false, the default) clear frames on a channel once session keys exist. Before the first handshake clear frames are always accepted.
-
int secLinkSetChannelPlain(SecLinkT *link, uint8_t channel, bool plain);
-
Returns SECLINK_SUCCESS or SECLINK_ERR_PARAM for a bad link or channel.
-
secLinkPoll
-
Poll for incoming data. Drains the serial port, processes frames and retransmits, completes pending handshake steps, verifies and decrypts data frames, and forwards them to the callback.
-
int secLinkPoll(SecLinkT *link);
-
Returns the number of packets delivered by the packet layer this call, SECLINK_ERR_PARAM if link is NULL, or SECLINK_ERR_SERIAL when the port is closed or disconnected. Must be called frequently in the main loop.
-
If the remote packet layer resets, the session keys are dropped (the link is no longer ready) and a new handshake is needed. Clear frames stay refused on protected channels (see Protocol).
-
secLinkIsReady
-
Check whether the handshake is complete and the link is ready for encrypted traffic. Cleartext sends do not require the link to be ready.
-
bool secLinkIsReady(SecLinkT *link);
-
secLinkGetDropped
-
Number of received frames dropped since open: bad MAC (tampered, replayed, or injected), clear frames on a protected channel, encrypted frames without session keys, and malformed frames.
-
int32_t secLinkGetDropped(SecLinkT *link);
-
Returns the count or SECLINK_ERR_PARAM when link is NULL.
-
secLinkGetPending
-
Get the number of unacknowledged packets in the transmit window. Delegates directly to pktGetPending on the underlying packet connection. Useful for non-blocking send loops to decide when to throttle.
-
int secLinkGetPending(SecLinkT *link);
-
Returns the pending count on success or SECLINK_ERR_PARAM when link is NULL.
-
-
+
End-to-End Example
Below is a complete secure BBS client that opens COM1, runs the authenticated key exchange, and then shuttles terminal traffic over a single encrypted channel. It demonstrates the typical serial stack usage: load the RNG seed file, open the link, set the passphrase, run the handshake, then enter a poll+send loop.
secLinkOpen internally calls rs232Open and pktOpen. secLinkClose tears them down in reverse order.
-
The DRBG gathers hardware entropy on first use. On a CPU without a time stamp counter that alone is guessable; the seed file and user-input timings (secRngAddEntropy) are what make the DH keys unpredictable.
+
The DRBG gathers hardware entropy on first use. On a CPU without a time stamp counter that alone is guessable; the seed file and user-input timings (secRngAddEntropy) are what make the X25519 keys unpredictable.
secLinkPoll must be called frequently to drain the RX ring buffer, process ACKs, and dispatch received packets to the callback.
-
For bulk transfers larger than SECLINK_MAX_PAYLOAD (245 bytes), use secLinkSendBuf which splits the data into chunks automatically.
+
For bulk transfers larger than SECLINK_MAX_PAYLOAD (237 bytes), use secLinkSendBuf which splits the data into chunks automatically.
+
+
Serial Stack
+
DVX's serial communications come in layers, in three libraries, each building on the one below:
+
+
rs232 (the serial library, serial.lib) -- ISR-driven UART driver with ring buffers and flow control
+
packet (the secure link library, seclink.lib) -- HDLC framing, CRC-16, Go-Back-N ARQ (reliable delivery)
+
security (the cryptography library, crypto.lib) -- X25519 key exchange, XChaCha20-Poly1305, SHA-256/HMAC/HKDF, SHA-256 DRBG (and Ed25519, Diffie-Hellman, and XTEA-CTR for other uses)
+
secLink (the secure link library, seclink.lib) -- authenticated handshake, channel multiplexing, per-packet encryption and MAC
+
+
The serial and cryptography libraries are part of every DVX; the secure link library comes with Communications.
The security layer is invoked by secLink during the X25519 handshake and to seal and open every encrypted packet. It has no runtime coupling to rs232 or packet, so it can be used standalone for cryptographic operations independent of the serial stack.
ISR-driven serial port driver supporting up to 4 simultaneous COM ports. A shared ISR drains UART FIFOs into per-port ring buffers (2048 bytes, power-of-2 for fast index wrapping). An interrupt the interrupt controller misses (its edge lost, seen under a flood in DOSBox, which then stops delivering received bytes) cannot stall a port: rs232Read, rs232WriteBuf, and rs232GetTxBuffered find the UART still holding an interrupt up and turn its interrupt enable off and on, which raises a fresh one, so a port that is polled at all never stays stuck. Flow control (XON/XOFF, RTS/CTS, DTR/DSR) operates within the ISR using watermark thresholds. All ISR data structures are DPMI-locked to prevent page faults. XON/XOFF bytes are queued for the transmit interrupt instead of being written over a byte still in the transmit register, and every cached register (IER, MCR) is changed with interrupts off so the ISR and the application never lose each other's updates.
+
Header: serial/rs232.h
+
Port Constants
+
Constant Value Description
+ -------- ----- -----------
+ RS232_COM1 0 First serial port.
+ RS232_COM2 1 Second serial port.
+ RS232_COM3 2 Third serial port.
+ RS232_COM4 3 Fourth serial port.
+ RS232_NUM_PORTS 4 Total number of supported COM ports.
+
Handshake Modes
+
Constant Value Description
+ -------- ----- -----------
+ RS232_HANDSHAKE_NONE 0 No flow control.
+ RS232_HANDSHAKE_XONXOFF 1 Software flow control (XON/XOFF characters).
+ RS232_HANDSHAKE_RTSCTS 2 Hardware flow control via RTS/CTS lines.
+ RS232_HANDSHAKE_DTRDSR 3 Hardware flow control via DTR/DSR lines.
+
UART Types
+
Detected automatically by probing the scratch register and FIFO capability.
+
Constant Value Description
+ -------- ----- -----------
+ RS232_UART_UNKNOWN 0 Detection failed.
+ RS232_UART_8250 1 Original IBM PC UART. No FIFO.
+ RS232_UART_16450 2 Has scratch register, no FIFO.
+ RS232_UART_16550 3 Has FIFO but buggy (rare).
+ RS232_UART_16550A 4 Working 16-byte FIFO. Most common in 486+ hardware.
+
Error Codes
+
Constant Value Description
+ -------- ----- -----------
+ RS232_SUCCESS 0 Operation succeeded.
+ RS232_ERR_UNKNOWN -1 Unknown error.
+ RS232_ERR_NOT_OPEN -2 Port is not open.
+ RS232_ERR_ALREADY_OPEN -3 Port is already open.
+ RS232_ERR_NO_UART -4 No UART detected at port address.
+ RS232_ERR_INVALID_PORT -5 Invalid port number.
+ RS232_ERR_INVALID_BASE -6 Invalid base I/O address.
+ RS232_ERR_INVALID_IRQ -7 Invalid IRQ number.
+ RS232_ERR_INVALID_BPS -8 Invalid baud rate.
+ RS232_ERR_INVALID_DATA -9 Invalid data bits value.
+ RS232_ERR_INVALID_PARITY -10 Invalid parity character.
+ RS232_ERR_INVALID_STOP -11 Invalid stop bits value.
+ RS232_ERR_INVALID_HANDSHAKE -12 Invalid handshake mode.
+ RS232_ERR_INVALID_FIFO -13 Invalid FIFO threshold.
+ RS232_ERR_NULL_PTR -14 NULL pointer argument.
+ RS232_ERR_IRQ_NOT_FOUND -15 Could not detect IRQ for port.
+ RS232_ERR_LOCK_MEM -16 DPMI memory lock failed.
+ RS232_ERR_NO_MEMORY -17 No memory for the port's buffers.
+
rs232Open
+
Open a COM port with the specified line parameters.
Parameter Description
+ --------- -----------
+ com Port index (RS232_COM1..RS232_COM4).
+ bps Baud rate (e.g. 9600, 19200, 38400, 57600, 115200).
+ dataBits Data bits per character (5, 6, 7, or 8).
+ parity Parity mode: 'N' (none), 'O' (odd), 'E' (even), 'M' (mark), 'S' (space).
+ stopBits Stop bits (1 or 2).
+ handshake Flow control mode (RS232_HANDSHAKE_*).
+
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.
+
rs232Close
+
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.
Parameter Description
+ --------- -----------
+ com Port index.
+ data Buffer to receive data.
+ len Maximum bytes to read.
+
Returns the number of bytes actually read (may be less than len).
+
rs232Write
+
Blocking polled write directly to UART transmit holding register. Bypasses the TX ring buffer. Polls LSR_TX_HOLD_EMPTY before writing each byte (the check and the write happen with interrupts off, so a flow-control byte from the ISR is never overwritten), so it takes as long as the bytes take on the wire. Use for small, immediate writes where order matters; the packet layer uses rs232WriteBuf instead, so it never waits.
Returns the number of bytes actually written (may be less than len while flow control holds the line), or a negative error code (RS232_ERR_INVALID_PORT, RS232_ERR_NOT_OPEN, RS232_ERR_NULL_PTR).
+
rs232WriteBuf
+
Non-blocking write to the transmit ring buffer. The ISR drains buffered data to the UART asynchronously, through the transmit-hold-empty interrupt.
Returns the number of bytes actually queued (may be less than len if the buffer is full).
+
rs232Set
+
Change all line parameters on an open port. Internally calls rs232SetBps, rs232SetData, rs232SetParity, rs232SetStop, and rs232SetHandshake in sequence. Returns RS232_SUCCESS on success or the first negative error code encountered.
Function Returns
+ -------- -------
+ rs232GetBase(com) I/O base address.
+ rs232GetBps(com) Current baud rate (int32_t).
+ rs232GetData(com) Data bits setting (5-8).
+ rs232GetParity(com) Parity character: 'n', 'o', 'e', 'm', or 's' (lowercase).
+ rs232GetStop(com) Stop bits setting (1 or 2).
+ rs232GetHandshake(com) Handshake mode (RS232_HANDSHAKE_*).
+ rs232GetIrq(com) IRQ number.
+ rs232GetUartType(com) UART type probed when the port was opened (RS232_UART_*).
+
rs232GetParity returns a lowercase character on success. Callers comparing against uppercase constants must use either both cases or tolower().
+
Status Getters
+
Function Returns
+ -------- -------
+ rs232GetRxBuffered(com) Bytes waiting in the receive ring buffer.
+ rs232GetTxBuffered(com) Bytes waiting in the transmit ring buffer.
+ rs232GetCts(com) CTS line state (0 or 1).
+ rs232GetDcd(com) DCD (carrier detect) line state (0 or 1), as last reported by the modem status interrupt.
+ rs232GetDsr(com) DSR line state (0 or 1).
+ rs232GetDtr(com) DTR line state (0 or 1).
+ rs232GetRts(com) RTS line state (0 or 1).
+ rs232GetLsr(com) Line status register value as last read; RS232_LSR_OVERRUN, RS232_LSR_PARITY_ERR, RS232_LSR_FRAME_ERR, and RS232_LSR_BREAK are its error bits.
+ rs232GetMcr(com) Modem control register value.
+ rs232GetMsr(com) Modem status register value; RS232_MSR_RI is the ring indicator.
+
Configuration Setters
+
Function Description
+ -------- -----------
+ rs232SetBase(com, base) Set I/O base address (before open).
+ rs232SetBps(com, bps) Change baud rate.
+ rs232SetData(com, dataBits) Change data bits.
+ rs232SetParity(com, parity) Change parity mode.
+ rs232SetStop(com, stopBits) Change stop bits.
+ 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. 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. 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. rs232Open sets 8 on a 16550A (the 8 free bytes allow 0.7 ms of interrupt latency at 115200 bps; 14 allows only 0.17 ms) and 0 on other UARTs.
+
Buffer Management
+
Function Description
+ -------- -----------
+ rs232ClearRxBuffer(com) Discard all data in the receive ring buffer.
+ rs232ClearTxBuffer(com) Discard all data in the transmit ring buffer.
+
Task Manager
System-level task manager for DVX. Displays all running applications with a six-column ListView (Name, Title, File, Type, Memory, Status) and exposes Switch To, End Task, and Run... operations. Always accessible via Ctrl+Esc regardless of which application is focused. Persists independently of the desktop app (Program Manager).
@@ -11734,6 +12031,7 @@ if (xferPoll(x, nowMs()) != XferRunningE) {
const XferProgressT *xferProgress(x) Progress and outcome.
bool xferDetectFeed(d, byte) Feed received terminal bytes; true when a ZMODEM sender's start frame (ZRQINIT) has arrived, so a terminal can start a ZMODEM receive by itself.
void xferSafeName(name, longNames, out) Reduce a sender's file name to a safe one: no path, no drive, no DOS device name (CON, PRN, COM1, and so on get a leading _). With longNames (the receiving disk takes long names, platformLongNames) a long name keeping its case, blanks, and dots, where a character a long name cannot hold becomes _ and leading blanks and trailing dots and blanks go (RECEIVED when nothing is left); otherwise an 8.3 name of portable characters in upper case. out holds XFER_NAME_MAX bytes (PLATFORM_LONG_NAME_MAX + 1).
+ int32_t xferSplitList(list, paths) Split a list of files to send, one path per line (LF or CR LF, empty lines skipped), in place into paths[XFER_MAX_FILES]; the count. ConnSendFiles and XferSendFiles use it.
uint16_t xferCrc16(data, len) CRC-16/XMODEM (polynomial 0x1021).
uint32_t xferCrc32(data, len) CRC-32 (IEEE 802.3), as ZMODEM uses it.
int32_t xferBasNew(&x) For the BASIC functions of other libraries: a new xfer.bas handle owning a new idle transfer (in x) that belongs to the running program; 0 when no handle is free. The library starts the transfer over its own transport; the program follows it with the Xfer functions (the connection library's ConnSendFiles and ConnReceiveFiles work this way).
@@ -11760,7 +12058,9 @@ if (xferPoll(x, nowMs()) != XferRunningE) {
Function Description
-------- -----------
XferSendFile(com, proto, path) Send one file over COM port com (1-4). Returns a handle, or 0.
- XferReceive(com, proto, target) Receive: XMODEM into the file target; YMODEM and ZMODEM into the folder target. Returns a handle, or 0.
+ XferSendFiles(com, proto, files, escapeCtl, resumeFiles) Send a batch, one path per line (xferSplitList), with the ZMODEM options ConnSendFiles takes. Returns a handle, or 0.
+ XferReceive(com, proto, target) Receive: XMODEM into the file target; YMODEM and ZMODEM into the folder target; a file already there is renamed. Returns a handle, or 0.
+ XferReceiveFiles(com, proto, target, exists, escapeCtl) Receive with an XFER_EXIST_ action for files already there (XFER_EXIST_ASK: XFER_ASKING and XferAnswer) and ZMODEM control escaping.
XferPoll(handle) Move the transfer along. Returns XFER_RUNNING, XFER_ASKING, XFER_DONE, XFER_FAILED, or XFER_CANCELLED.
XferStatus(handle) The same, without moving it along (XFER_IDLE for a bad handle).
XferSending(handle), XferProtocol(handle) True for a send; the protocol.
@@ -11880,13 +12180,14 @@ wgtCallMethod(list, "ListCount", NULL, 0, &count); // count.i ==
WCLASS_FIXED_FONT: it edits by columns, so its font must be fixed-pitch. Any size works (the column math uses the font's charWidth and charHeight); a proportional font set with wgtSetFont, or inherited, is replaced by the system font when it draws.
The widget renders through two paint paths: a full paint used by the normal widget pipeline, and a fast incremental repaint (wgtAnsiTermRepaint) that pushes dirty rows directly to the window content buffer for low-latency serial echo.
Header: widgets/ansiTerm.h
+
Loaded as: bin/widgets/kpunch/ansiterm/ansiterm.wgt, part of Communications. Without it there is no Terminal control and dvxAnsiTermApi() is NULL, so a program that may run without Communications checks that before it makes a terminal.
Creation
WidgetT *term = wgtAnsiTerm(parent, 80, 25);
API Functions
Function Description
-------- -----------
WidgetT *wgtAnsiTerm(parent, cols, rows) Create an ANSI terminal widget with the given column/row dimensions.
- void wgtAnsiTermWrite(w, data, len) Write raw bytes into the terminal's ANSI parser. data is a const uint8_t * buffer.
+ void wgtAnsiTermWrite(w, data, len) Write raw bytes into the terminal's ANSI parser. data is a const uint8_t * buffer. Up to 4 KB is parsed at once; the rest of a longer text waits in order and is parsed 4 KB per poll (a frame each), so a long text does not hold DVX up. Clear, a resize, and a new scrollback length parse what waits first.
void wgtAnsiTermClear(w) Clear the terminal screen and reset the cursor to the home position.
void wgtAnsiTermSetComm(w, ctx, readFn, writeFn) Attach a communication channel. readFn and writeFn are I/O callbacks; ctx is passed as their first argument.
void wgtAnsiTermSetScrollback(w, maxLines) Set the maximum number of scrollback lines.
@@ -11924,17 +12225,18 @@ wgtCallMethod(list, "ListCount", NULL, 0, &count); // count.i ==
------ -----------
Clear Clear the terminal screen.
Copy Copy the selection (copySelection); returns False when nothing is selected.
- Paste Send the clipboard to the comm write callback as Ctrl+V does (new lines as CR, 64-byte chunks).
+ Paste Send the clipboard to the comm write callback as Ctrl+V does (new lines as CR, 64-byte chunks); with no write callback, to the Send event in one piece.
Poll Process any pending bytes on the attached communication channel.
ResetColors The standard CGA colors again (setPalette NULL).
SetColor index, color: one attribute color (0-15) from a BASIC color (&HBBGGRR, converted with wgtBasicColor).
- Write Write a string into the terminal (with ANSI escape processing).
+ Write Write a string into the terminal (with ANSI escape processing). Dynamic (WGT_SIG_DYNAMIC) so every byte of the BASIC string arrives, NUL bytes included (wgtValueLen); the parser ignores a NUL as padding.
Events
-
Besides the common events, two of its own, fired through wgtFireEvent (a C host gets them in onEvent with the names ANSITERM_EVT_MUSIC and ANSITERM_EVT_BELL):
+
Besides the common events, three of its own, fired through wgtFireEvent (a C host gets them in onEvent with the names ANSITERM_EVT_MUSIC, ANSITERM_EVT_BELL, and ANSITERM_EVT_SEND):
Event Parameters Description
----- ---------- -----------
Music Macro As String An ANSI music string arrived: the BASIC PLAY text between ESC[| or ESC[N, or ESC[MF (MB, MN, ML, MS), and Ctrl+N (0x0E). For the ESC[M form the macro begins with that M command (e.g. "MFT120O4L8CDE"). Line ends are left out; a string longer than 4096 characters, an empty one, or one cut short by ESC fires nothing.
- Bell (none) A BEL character (Ctrl+G) arrived.
+ Bell (none) A BEL character (Ctrl+G) arrived.
+ Send Bytes As String Bytes for the host while no comm write callback is set (wgtAnsiTermSetComm): typed keys, a paste, and the replies to ESC[c and ESC[6n / ESC[255n. The argument is wgtValueBytes (read its length with wgtValueLen: it may hold NUL bytes). A reply waits until the byte that asked for it is parsed; keys typed while a Music, Bell, or Send handler runs go when it returns.
The widget plays nothing itself: a handler plays the macro (BASIC: PLAY Macro; C: the music library's musicMmlPlay). While a Music or Bell handler runs, the terminal takes no input: Poll leaves the bytes in the connection, and Write keeps them, in order, until the handler returns. A tune played in the foreground (MF) therefore holds back the text after it, as on a DOS terminal, while the window stays responsive. Bare ESC[M (no parameter) followed by anything but F, B, N, L, or S is still Delete Line. A handler may close the terminal's window; the input after the event is then dropped.
Keyboard Shortcuts
Within the terminal widget:
@@ -12041,6 +12343,7 @@ btn->onClick = onOkClicked;
void wgtCanvasSetMouseCallback(w, cb) Set a mouse callback. Signature: void (*cb)(WidgetT *w, int32_t cx, int32_t cy, bool drag). cx/cy are canvas-relative, drag=true on mouse-move during press.
int32_t wgtCanvasSave(w, path) Save the canvas buffer to an image file: BMP when path ends in .bmp, PNG otherwise. Returns 0 on success, -1 on error.
int32_t wgtCanvasLoad(w, path) Load an image file into the canvas (resizes buffer to match). Returns 0 on success, -1 on error.
+ int32_t wgtCanvasLoadData(w, data, len, name) Load the picture in len bytes of an image file (as dvxLoadImageFromMemory reads them) as wgtCanvasLoad loads a file; Picture then reads name (NULL: ""). Returns 0 on success, -1 when they are not a picture.
void wgtCanvasResize(w, newW, newH) Resize the canvas buffer and relayout. The old content is discarded; the new buffer is white.
void wgtCanvasSetBorderStyle(w, style) CANVAS_BORDER_NONE (0) drops the sunken frame, any other value (CANVAS_BORDER_FIXED, 1, the default) shows it. The buffer keeps its size; the widget relays out.
int32_t wgtCanvasGetBorderStyle(w) CANVAS_BORDER_FIXED or CANVAS_BORDER_NONE.
@@ -12067,14 +12370,16 @@ btn->onClick = onOkClicked;
setBorderStyle wgtCanvasSetBorderStyle
getBorderStyle wgtCanvasGetBorderStyle
getDisplay wgtCanvasGetDisplay
- drawTextScaled wgtCanvasDrawTextScaled
+ drawTextScaled wgtCanvasDrawTextScaled
+ loadData wgtCanvasLoadData
Properties
Drawing state (pen color, pen size) is managed by the API functions above. The BASIC interface registers these properties:
Property Type Access Description
-------- ---- ------ -----------
BorderStyle Enum Read/Write None (0) or FixedSingle (1, the default): wgtCanvasSetBorderStyle / wgtCanvasGetBorderStyle.
ImageHeight Integer Read-only Height of the canvas buffer in pixels (0 when there is no buffer).
- ImageWidth Integer Read-only Width of the canvas buffer in pixels (0 when there is no buffer).
+ ImageWidth Integer Read-only Width of the canvas buffer in pixels (0 when there is no buffer).
+ Picture String Read/Write The file wgtCanvasLoad loaded, or the name wgtCanvasLoadData was given ("" when none; Clear and Resize wipe it). Setting a path loads it; setting "" clears the canvas to white. In BASIC it reads as a Picture object (see LOADPICTURE).
Save(path$) Save the canvas: BMP when path$ ends in .bmp, PNG otherwise. Returns 0 on success, -1 on error (WGT_SIG_RET_INT_STR).
SetPenColor color% Set the drawing color (&HBBGGRR).
SetPenSize size% Set the pen size in pixels (1 to 255).
+ SetPictureData(bytes$[, name$]) WGT_METHOD_SET_PICTURE_DATA (WGT_SIG_DYNAMIC): wgtCanvasLoadData(bytes, length, name). Returns True when the bytes are a picture.
SetPixel x%, y%, color% Set a single pixel.
Clear Remove all items and clear the selection.
List(index%) Return the text of the item at the given index.
ListCount() Return the total number of items.
- RemoveItem index% Remove the item at the given index. The selection stays on the same item; removing the selected item leaves no selection (the text is kept).
+ RemoveItem index% Remove the item at the given index. The selection stays on the same item; removing the selected item leaves no selection (the text is kept).
+ SetItems items$ Replace all the items at once from a "|"-separated list ("Red|Green|Blue"; every "|" separates two items, "" empties the list). Nothing is selected afterwards. The text is kept.
A VB3-style Data control for database binding. Displays a visible navigation bar (|< < > >|) that connects to a SQLite database via dvxSql* functions. Reads all rows from the RecordSource query into an in-memory cache for bidirectional navigation. Fires a Reposition event each time the current row changes so bound controls can update. Supports master-detail linking between Data controls (via MasterSource / MasterField / DetailField), INSERT via AddNew, and DELETE via Delete. UPDATE and DELETE identify the row by the KeyColumn value it had when it was loaded, so editing the key column changes the key instead of writing to another row; an UPDATE writes only the changed columns and fails (the row stays unsaved) when no database row matches. A key the quoted text match cannot find (an integer in a column without a declared type, a REAL, or NULL) is matched by its text form instead, as long as that names exactly one row. Setting a field to the value it already has is not an edit. With a read-only source, pending edits are dropped when the cursor moves. Refresh saves a pending edit first; if that save fails or is cancelled the edit is discarded. With a SELECT RecordSource the control is read-only: Update and Delete do nothing, and a master-detail filter is applied to the query as a subquery.
-
The widget depends on the "dvxsql" library DXE; the navigation logic caches the full result set so there is no round-trip for MoveNext/MovePrev.
+
A VB3-style Data control for database binding. Displays a visible navigation bar (|< < > >|) that connects to a SQLite database via dvxSql* functions. Reads the RecordSource query's rows into an in-memory cache for bidirectional navigation: a Refresh reads the first 64, and the widget's polls read the rest, about 20 ms a frame, so a big record set does not hold DVX up. Fires a Reposition event each time the current row changes so bound controls can update. Supports master-detail linking between Data controls (via MasterSource / MasterField / DetailField), INSERT via AddNew, and DELETE via Delete. UPDATE and DELETE identify the row by the KeyColumn value it had when it was loaded, so editing the key column changes the key instead of writing to another row; an UPDATE writes only the changed columns and fails (the row stays unsaved) when no database row matches. A key the quoted text match cannot find (an integer in a column without a declared type, a REAL, or NULL) is matched by its text form instead, as long as that names exactly one row. Setting a field to the value it already has is not an edit. With a read-only source, pending edits are dropped when the cursor moves. Refresh saves a pending edit first; if that save fails or is cancelled the edit is discarded. With a SELECT RecordSource the control is read-only: Update and Delete do nothing, and a master-detail filter is applied to the query as a subquery.
+
The widget depends on the "dvxsql" library DXE; the navigation logic caches the result set so there is no round-trip for MoveNext/MovePrev. Until every row is read, the query stays open on a connection of its own, which holds the database's read lock: a write to that database from elsewhere (another program, or SQLExec) fails with "database is locked" until then. What needs the rows does not wait for the polls: MoveNext past the rows read and dataCtrlSetCurrentRow beyond them read on as far as they need, and MoveLast, AddNew, Update, and Delete read the rest first (the control's own writes would be locked out by its query too). The row count grows as rows arrive; a copy that runs out of memory part way frees the cache whole, as a failed Refresh does, and Reposition then fires so bound controls clear.
Header: widgets/dataCtrl.h
Creation
WidgetT *data = wgtDataCtrl(parent);
@@ -12212,20 +12519,20 @@ wgtComboBoxAddItem(cb, "Times");
void dataCtrlMoveFirst(w) Move the cursor to the first row.
void dataCtrlMovePrev(w) Move the cursor to the previous row.
void dataCtrlMoveNext(w) Move the cursor to the next row.
- void dataCtrlMoveLast(w) Move the cursor to the last row.
+ void dataCtrlMoveLast(w) Move the cursor to the last row (the record set is read to its end first).
const char *dataCtrlGetField(w, colName) Get the value of a column in the current row (case-insensitive).
void dataCtrlSetField(w, colName, value) Set the value of a column in the current row. Marks the row dirty unless the value is unchanged.
void dataCtrlUpdate(w) Flush pending changes (INSERT or UPDATE) to the database.
void dataCtrlAddNew(w) Append a blank row and move the cursor to it. The row is dirty and new until Update is called.
void dataCtrlDelete(w) Delete the current row from the cache and the database.
void dataCtrlSetMasterValue(w, val) Set the master-detail filter value. Normally managed by the form runtime.
- int32_t dataCtrlGetRowCount(w) Get the total number of cached rows.
+ int32_t dataCtrlGetRowCount(w) Get the number of rows read so far (all of them once the polls have read the rest, or after MoveLast).
int32_t dataCtrlGetColCount(w) Get the number of columns in the result set.
const char *dataCtrlGetColName(w, col) Get the name of a column by index.
const char *dataCtrlGetCellText(w, row, col) Get the text of a specific cell.
void dataCtrlSetCurrentRow(w, row) Set the current row by index. Auto-saves the previous row if dirty.
int32_t dataCtrlGetCurrentRow(w) Get the current row index (-1 if there are no rows).
- uint32_t dataCtrlGetGeneration(w) A counter that changes whenever the cached rows or cell values change (Refresh, AddNew, Delete, SetField) but not on cursor moves. Bound views use it to rebuild derived state only when needed.
+ uint32_t dataCtrlGetGeneration(w) A counter that changes whenever the cached rows or cell values change (Refresh, AddNew, Delete, SetField) but not on cursor moves, nor when more rows of the record set arrive (only the row count grows then). Bound views use it to rebuild derived state only when needed.
API Struct (wgtRegisterApi "data")
Slot Function
---- --------
@@ -12260,15 +12567,15 @@ wgtComboBoxAddItem(cb, "Times");
MasterField String Read/Write Column in the master control to read for the filter value.
DetailField String Read/Write Column in this table to filter by the master value.
Caption String Read/Write Text displayed on the navigator bar.
- BOF Boolean Read-only True when the cursor is at the first row (or no rows).
- EOF Boolean Read-only True when the cursor is past the last row (or no rows).
+ BOF Boolean Read-only True after a MovePrevious at the first row (the cursor stays there), or when there are no rows. A move onto a row clears it.
+ EOF Boolean Read-only True after a MoveNext at the last row (the cursor stays there), or when there are no rows. A move onto a row clears it.
Methods (BASIC Interface)
Method Description
------ -----------
AddNew Append a blank row and move to it.
Delete Delete the current row.
MoveFirst Move the cursor to the first row.
- MoveLast Move the cursor to the last row.
+ MoveLast Move the cursor to the last row (all rows are read first).
MoveNext Move the cursor to the next row.
MovePrevious Move the cursor to the previous row.
Refresh Save a pending edit, then re-execute the query and rebuild the cache.
@@ -12282,7 +12589,7 @@ wgtComboBoxAddItem(cb, "Times");
DbGrid
-
A database grid widget that displays all records from a Data control in a scrollable, sortable table. Columns auto-populate from the Data control's column names and can be hidden, resized (by dragging a column border), and renamed by the application. Clicking a column header sorts the display ascending/descending. The highlighted row is always the Data control's current row: selecting a row moves the Data control's cursor, and the highlight is read from the cursor whenever the grid paints. The Data control does not repaint the grid itself, so a C application that moves the cursor elsewhere (the Data control's own buttons or API) calls dbGridRefresh (or wgtInvalidatePaint on the grid) from the Data control's onChange; the form runtime does this for BASIC programs. The grid reads directly from the Data control's cached rows, so there is no separate copy of the data. Re-binding the same Data control (as the form runtime does on every Reposition) keeps the sort order and column settings while the column set is unchanged (the sort order and automatic widths are rebuilt when the Data control's rows or values changed), and a width set by dragging a column border is kept like one set with dbGridSetColumnWidth. Cell and header text is clipped to its column.
+
A database grid widget that displays all records from a Data control in a scrollable, sortable table. Columns auto-populate from the Data control's column names and can be hidden, resized (by dragging a column border), and renamed by the application. Clicking a column header sorts the display ascending/descending. The highlighted row is always the Data control's current row: selecting a row moves the Data control's cursor, and the highlight is read from the cursor whenever the grid paints. The grid's polls follow the Data control's rows: a change to them, and the rows of a record set that arrive a part at a time (sorted in as they come, in the order a full sort gives), are shown without a call. A cursor move is not: a C application that moves the cursor elsewhere (the Data control's own buttons or API) calls dbGridRefresh (or wgtInvalidatePaint on the grid) from the Data control's onChange; the form runtime does this for BASIC programs. The grid reads directly from the Data control's cached rows, so there is no separate copy of the data. Re-binding the same Data control (as the form runtime does on every Reposition) keeps the sort order and column settings while the column set is unchanged (the sort order and automatic widths are rebuilt when the Data control's rows or values changed), and a width set by dragging a column border is kept like one set with dbGridSetColumnWidth. An automatic column width fits the header and the first 200 rows. Cell and header text is clipped to its column.
The grid supports alternating row shading, vertical and horizontal scrollbars, keyboard navigation (arrows, Page Up/Down, Home, End), and double-click activation (fires onDblClick). The mouse wheel scrolls the view without moving the record cursor.
Header: widgets/dbGrid.h
Creation
@@ -12374,7 +12681,8 @@ wgtDropdownAddItem(dd, "Blue");
Clear Remove all items.
List(index%) Return the text of the item at the given index.
ListCount() Return the total number of items.
- RemoveItem index% Remove the item at the given index. The selection stays on the same item; removing the selected item leaves no selection.
+ RemoveItem index% Remove the item at the given index. The selection stays on the same item; removing the selected item leaves no selection.
+ SetItems items$ Replace all the items at once from a "|"-separated list ("Red|Green|Blue"; every "|" separates two items, "" empties the list). Nothing is selected afterwards.
WidgetT *wgtImageButton(parent, data, w, h, pitch) Create from raw pixel data (in display's native format).
WidgetT *wgtImageButtonFromFile(parent, path) Create by loading an image file.
void wgtImageButtonSetData(w, data, imgW, imgH, pitch) Replace the image with new raw pixel data.
- void wgtImageButtonLoadFile(w, path) Replace the image by loading a new file.
+ void wgtImageButtonLoadFile(w, path) Replace the image by loading a new file.
+ bool wgtImageButtonLoadData(w, data, len, name) Replace the image with the picture in len bytes of an image file (as dvxLoadImageFromMemory reads them); Picture then reads name (NULL: ""). False (the image unchanged) when they are not a picture.
Property Type Access Description
-------- ---- ------ -----------
- Picture String Read/Write Setting writes a file path (loads image). Reading returns the path of the loaded file (also set by wgtImageButtonFromFile); in BASIC the path comes as a Picture object, which stands for the path where a value is needed (see LOADPICTURE); wgtImageButtonSetData clears it. Setting "" clears the image and the path; a file that cannot be loaded leaves both unchanged.
+ Picture String Read/Write Setting writes a file path (loads image). Reading returns the path of the loaded file (also set by wgtImageButtonFromFile), or the name wgtImageButtonLoadData was given; in BASIC the path comes as a Picture object, which stands for the path where a value is needed (see LOADPICTURE); wgtImageButtonSetData clears it. Setting "" clears the image and the path; a file that cannot be loaded leaves both unchanged.
ImageWidth Integer Read-only Width of the currently loaded image in pixels.
ImageHeight Integer Read-only Height of the currently loaded image in pixels.
Methods
-
No widget-specific BASIC methods. Setting Picture loads a new image.
+
Method Description
+ ------ -----------
+ SetPictureData(bytes$[, name$]) WGT_METHOD_SET_PICTURE_DATA (WGT_SIG_DYNAMIC): wgtImageButtonLoadData(bytes, length, name). Returns True when the bytes are a picture.
+
Setting Picture loads a new image; the BASIC form runtime shows a LOADPICTUREDATA picture through SetPictureData.
WidgetT *wgtImageFromFile(parent, path) Create by loading an image file.
void wgtImageSetData(w, data, imgW, imgH, pitch) Replace the displayed image with new raw pixel data.
void wgtImageLoadFile(w, path) Replace by loading a new file. Static but exposed via the API struct.
+ bool wgtImageLoadData(w, data, len, name) Replace with the picture in len bytes of an image file (as dvxLoadImageFromMemory reads them); Picture then reads name (NULL: ""). False (the image unchanged) when they are not a picture.
void wgtImageSetTransparent(w, hasTransparency, keyColor) Set chroma-key color. Pixels matching keyColor are not drawn.
API Struct (wgtRegisterApi "image")
Slot Function
@@ -12476,16 +12790,22 @@ btn->onClick = onSaveClicked;
Property Type Access Description
-------- ---- ------ -----------
- Picture String Read/Write Setting writes a file path (loads image). Reading returns the path of the loaded file (also set by wgtImageFromFile); in BASIC the path comes as a Picture object, which stands for the path where a value is needed (see LOADPICTURE); wgtImageSetData clears it. Setting "" clears the image and the path; a file that cannot be loaded leaves both unchanged.
+ Picture String Read/Write Setting writes a file path (loads image). Reading returns the path of the loaded file (also set by wgtImageFromFile), or the name wgtImageLoadData was given; in BASIC the path comes as a Picture object, which stands for the path where a value is needed (see LOADPICTURE); wgtImageSetData clears it. Setting "" clears the image and the path; a file that cannot be loaded leaves both unchanged.
Stretch Boolean Read/Write When true, an image larger than the widget is scaled down to fit (aspect preserved, nearest neighbour); a smaller image stays at natural size. When false, shown at natural size. Changing it relayouts the widget.
ImageWidth Integer Read-only Width of the currently loaded image in pixels.
- ImageHeight Integer Read-only Height of the currently loaded image in pixels.
+ ImageHeight Integer Read-only Height of the currently loaded image in pixels.
+ MaskColor Integer Read/Write The transparent colour as a BASIC colour (&HBBGGRR), converted with wgtImageSetTransparent.
+ UseMaskColor Boolean Read/Write Whether pixels of MaskColor are left out (wgtImageSetTransparent's hasTransparency).
Methods
-
No widget-specific BASIC methods. Setting Picture loads a new image.
+
Method Description
+ ------ -----------
+ SetPictureData(bytes$[, name$]) WGT_METHOD_SET_PICTURE_DATA (WGT_SIG_DYNAMIC): wgtImageLoadData(bytes, length, name). Returns True when the bytes are a picture.
+
Setting Picture loads a new image; the BASIC form runtime shows a LOADPICTUREDATA picture through SetPictureData.
wgtListViewGetSelected(w) Get the index of the selected row (-1 if none).
wgtListViewSetSelected(w, idx) Set the selected row by index (outside the list = none) and scroll it into view.
wgtListViewSetSort(w, col, dir) Set the sort column and direction. A column outside the defined columns, or ListViewSortNoneE, clears the sort. Ignored while reorder is enabled.
+ wgtListViewGetSortCol(w) The column the list is sorted by (-1 when unsorted): the last wgtListViewSetSort or header click.
+ wgtListViewGetSortDir(w) The sort direction (ListViewSortNoneE when unsorted).
wgtListViewSetHeaderClickCallback(w, cb) Set a callback for header clicks. Signature: void (*cb)(WidgetT *w, int32_t col, ListViewSortE dir). After it, the ListView fires its own event ColumnClick (Column As Integer, 0-based) with wgtFireEvent, which reaches the widget's onEvent (BASIC: CtrlName_ColumnClick).
wgtListViewSetMultiSelect(w, multi) Enable or disable multi-selection.
wgtListViewIsItemSelected(w, idx) Check if a specific row is selected.
wgtListViewSetItemSelected(w, idx, selected) Select or deselect a specific row.
wgtListViewSelectAll(w) Select all rows.
wgtListViewClearSelection(w) Deselect all rows.
- wgtListViewSetReorderable(w, reorderable) Enable drag-to-reorder of rows.
+ wgtListViewSetReorderable(w, reorderable) Enable drag-to-reorder of rows.
+ wgtListViewAddItem(w, text) Add a row with text in its first column (the list keeps its own copy of the cells from then on).
+ wgtListViewRemoveRow(w, row) Remove a data row.
+ wgtListViewClear(w) Remove all rows.
+ wgtListViewGetCell(w, row, col) The text of a cell ("" outside the list).
+ wgtListViewSetCell(w, row, col, text) Set the text of a cell.
+ wgtListViewGetRowCount(w) The number of rows.
Property Type Access Description
-------- ---- ------ -----------
- ListIndex Integer Read/Write Index of the currently selected row.
+ ListIndex Integer Read/Write Index of the currently selected row.
+ SortColumn Integer Read/Write The sort column (-1 when unsorted). Setting it sorts by that column in the current order (ascending when unsorted); -1 unsorts.
+ SortOrder Integer Read/Write The sort order (0 none, 1 ascending, 2 descending). Setting it re-sorts the current column; an unsorted list stays unsorted.
ClearSelection Deselect all rows.
GetCell(row%, col%) Returns the text of a cell.
IsItemSelected(index%) Check if a specific row is selected.
+ ListCount() Returns the number of rows (the same as RowCount).
RemoveItem index% Remove a row by index. Rows after it keep their selection; removing the selected row leaves no selection.
RowCount() Returns the number of rows.
SelectAll Select all rows.
+ SelectedRows() Returns the selected rows' numbers separated by spaces ("" when none).
SetCell row%, col%, text$ Set the text of a cell.
SetColumns spec$ Define columns from a pipe-delimited spec string ("Name,Width|Name,Width").
SetItemSelected idx%, sel Select or deselect a specific row by index.
SetMultiSelect multi Enable or disable multi-selection.
SetReorderable reorderable Enable or disable drag-to-reorder.
+ SetRows text$ Replace all rows at once: rows separated by CHR$(10), cells by CHR$(9).
SetSort col%, dir% Set the sort column and direction (0=none, 1=ascending, 2=descending). An invalid column or direction clears the sort; ignored while reorder is enabled.
Method Description
------ -----------
+ GetSelected() The index of the selected radio button of this button's group, -1 when none (called on a group, wgtRadioGroupGetSelected).
SetSelected idx% Select the radio button at index idx% within this button's group (called on a group, within that group).
The slots are the functions above in order: create, getRtf, setRtf, getSelRtf, setSelRtf, getSelStart, setSelStart, getSelLength, setSelLength, getSelText, setSelText, getSelFontName, getSelFontSize, setSelFont, getSelStyle, setSelStyle, getSelColor, setSelColor, getSelAlign, setSelAlign, canUndo, canRedo, undo, redo, cut, copy, paste, selectAll, find, loadFile, and saveFile.
BASIC Interface
-
Properties: TextRTF and SelRTF (wgtRichEditGetRtf / SetRtf, GetSelRtf / SetSelRtf), SelStart, SelLength, SelText, SelBold, SelItalic, SelUnderline, SelStrikethru (one FONT_STYLE_* bit of SetSelStyle each), SelColor (a BASIC color; -1 for ForeColor), SelFontName and SelFontSize (wgtRichEditSetSelFont with the other part left alone), SelAlignment, and the read-only CanUndo and CanRedo. Methods: Undo, Redo, Cut, Copy, Paste, SelectAll, Find (any case), LoadFile, and SaveFile. Event: SelChange; default event Change.
+
Properties: TextRTF and SelRTF (wgtRichEditGetRtf / SetRtf, GetSelRtf / SetSelRtf), SelStart, SelLength, SelText, SelBold, SelItalic, SelUnderline, SelStrikethru (one FONT_STYLE_* bit of SetSelStyle each), SelColor (a BASIC color; -1 for ForeColor), SelFontName and SelFontSize (wgtRichEditSetSelFont with the other part left alone), SelAlignment, and the read-only CanUndo and CanRedo. Methods: Undo, Redo, Cut, Copy, Paste, SelectAll, Find (text$ and an optional matchCase, any case without it), LoadFile, and SaveFile. Event: SelChange; default event Change.
Methods Used
PAINT, CALC_MIN_SIZE (20 by 4 characters of its font, with border and scroll bar), ON_MOUSE, ON_KEY, ON_DRAG_UPDATE, ON_DRAG_END, ON_WHEEL, ON_FOCUS, ON_BLUR, POLL, QUICK_REPAINT, GET_TEXT, SET_TEXT, CLEAR_SELECTION, DESTROY. Flags: WCLASS_FOCUSABLE, WCLASS_NEEDS_POLL.
@@ -12921,17 +13255,23 @@ WidgetT *content = wgtVBox(sp);
-------- -----------
WidgetT *wgtSeparator(parent) Create a separator oriented from the parent: vertical in a horizontal container, horizontal otherwise.
WidgetT *wgtHSeparator(parent) Create a horizontal separator line.
- WidgetT *wgtVSeparator(parent) Create a vertical separator line.
+ WidgetT *wgtVSeparator(parent) Create a vertical separator line.
+ bool wgtSeparatorIsVertical(w) True for a vertical line.
+ void wgtSeparatorSetVertical(w, v) Turn the line vertical (true) or horizontal (false); the window lays out again.
API Struct (wgtRegisterApi "separator")
Slot Function
---- --------
header WGT_API_HEADER (ABI version and size, checked by wgtGetApi)
hSeparator wgtHSeparator
vSeparator wgtVSeparator
- create wgtSeparator
The BASIC Line control is registered as interface "line" with its own one-slot API (create = wgtSeparator).
-
Properties
-
No widget-specific properties.
+
Properties (BASIC Interface)
+
Property Type Access Description
+ -------- ---- ------ -----------
+ Vertical Boolean Read/Write The orientation (wgtSeparatorIsVertical / wgtSeparatorSetVertical).
Methods
No widget-specific methods.
Events
@@ -13104,8 +13444,14 @@ wgtLabel(sb, "Ready");
---- --------
header WGT_API_HEADER (ABI version and size, checked by wgtGetApi)
create wgtStatusBar
-
Properties, Methods, Events
-
StatusBar has no widget-specific properties, methods, or events. Add child widgets (labels, progress bars, etc.) to populate.
+
Properties (BASIC Interface)
+
Property Type Access Description
+ -------- ---- ------ -----------
+ Alignment Enum (Left, Center, Right) Read/Write Where the children sit along the bar when they do not fill it (the align field).
+ Padding Integer Read/Write The padding field in pixels (1 unless set; wgtGetPaddingPx / wgtSetPaddingPx).
+ Spacing Integer Read/Write The spacing field in pixels (2 unless set; wgtGetSpacingPx / wgtSetSpacingPx).
+
Methods, Events
+
StatusBar has no widget-specific methods or events. Add child widgets (labels, progress bars, etc.) to populate.
Default Event
None. (VB basName: StatusBar.)
@@ -13178,7 +13524,7 @@ WidgetT *page2 = wgtTabPage(tabs, "Advanced");
bool wgtTextAreaReplaceSelection(w, needle, repl, caseSens, fwd) If the current selection matches needle, replace it, then find the next occurrence in direction fwd. Returns true if anything was replaced or found.
void wgtTextAreaSetLineMarker(w, line, gutterRgb, backRgb) Mark 1-based line: a dot of gutterRgb (0xRRGGBB) in the line-number gutter and / or the line's background in backRgb; TEXTAREA_MARKER_NONE leaves a part out, both NONE removes the marker. Markers keep their line numbers when the text changes (the owner moves them). The DVX BASIC IDE marks breakpoints and the paused line this way.
void wgtTextAreaClearLineMarkers(w) Remove every line marker.
- void wgtTextAreaSetSyntaxMode(w, mode) Use a built-in colorizer: "bas" / "basic" (BASIC: the compiler's keyword list, date literals by the lexer's shape rule, &H / &O and exponent numbers, REM and ' comments; the IDE's code editor uses it), "dhs" / "help" (help source), or "custom" (each painted line goes to the "Colorize" event through wgtFireEvent: args LineText and Colors, a string of one digit per character, "0" = TEXT_SYNTAX_DEFAULT; the host writes the TEXT_SYNTAX_* digits back into Colors; the BASIC Colorize event); NULL or "" turns colouring off.
+ void wgtTextAreaSetSyntaxMode(w, mode) Use a built-in colorizer: "bas" / "basic" (BASIC: the compiler's keyword list, date literals by the lexer's shape rule, &H / &O and exponent numbers, REM and ' comments; the IDE's code editor uses it), "dhs" / "help" (help source), or "custom" (each painted line goes to the "Colorize" event through wgtFireEvent: args LineText and Colors, a string of one digit per character, "0" = TEXT_SYNTAX_DEFAULT; the host writes the TEXT_SYNTAX_* digits back into Colors; the BASIC Colorize event; the answers are kept with the line text, 64 lines, so a line drawn again unchanged does not fire it again, and setting the mode again drops them); NULL or "" turns colouring off.
int32_t wgtTextAreaGetCursorLine(w) Get the current cursor line number.
void wgtTextAreaSetGutterClickCallback(w, fn) Set a callback for gutter clicks (e.g. for breakpoint toggling).
int32_t wgtTextAreaGetWordAtCursor(w, buf, bufSize) Copy the word under the cursor into buf. Returns its length.
@@ -13264,6 +13610,8 @@ tmr->onChange = onTimerTick;
void wgtTimerSetInterval(w, intervalMs) Change the timer interval and restart the countdown. An interval of 0 or less never fires.
int32_t wgtTimerGetInterval(w) Returns the current interval in milliseconds.
bool wgtTimerIsRunning(w) Returns true if the timer is currently running.
+ bool wgtTimerGetRepeat(w) Returns true for a repeating timer, false for a one-shot.
+ void wgtTimerSetRepeat(w, repeat) Make the timer repeating or one-shot from its next fire on (a running countdown is kept).
void wgtTimerSetEnabled(w, enabled) Start or stop the timer based on enabled flag.
void wgtUpdateTimers(void) Global tick function. Called by the event loop to advance all active timers.
Property Type Access Description
-------- ---- ------ -----------
Enabled Boolean Read/Write Whether the timer is running. Reading returns the running state; writing starts or stops it.
- Interval Integer Read/Write Timer interval in milliseconds. Setting it restarts the countdown; 0 or less disables firing.
+ Interval Integer Read/Write Timer interval in milliseconds. Setting it restarts the countdown; 0 or less disables firing.
+ Repeat Boolean Read/Write True (the default) for a repeating timer, False for a one-shot (wgtTimerGetRepeat / wgtTimerSetRepeat).
header WGT_API_HEADER (ABI version and size, checked by wgtGetApi)
create wgtToolbar
getFace wgtToolbarGetFace
-
Properties, Methods, Events
-
Toolbar has no widget-specific properties, methods, or events. Add children (buttons, separators, etc.) to populate.
+
Properties (BASIC Interface)
+
Property Type Access Description
+ -------- ---- ------ -----------
+ Alignment Enum (Left, Center, Right) Read/Write Where the children sit along the bar when they do not fill it (the align field).
+ Padding Integer Read/Write The padding field in pixels (2 unless set; wgtGetPaddingPx / wgtSetPaddingPx).
+ Spacing Integer Read/Write The spacing field in pixels (2 unless set; wgtGetSpacingPx / wgtSetSpacingPx).
+
Methods, Events
+
Toolbar has no widget-specific methods or events. Add children (buttons, separators, etc.) to populate.
Property Type Access Description
+ -------- ---- ------ -----------
+ ListIndex Integer R/W Depth-first index of the current node, -1 when none. Setting it selects the node as a click would (Change fires).
Methods (BASIC Interface)
Method Description
------ -----------
AddChildItem parentIdx%, text$ Add a child node under the node at the given index.
AddItem text$ Add a root-level node.
Clear Remove all nodes.
+ GetItemTag(index%) Returns the text SetItemTag stored on the node ("" when none).
GetItemText(index%) Returns the text of the node at the given depth-first index.
+ HasChildren(index%) Returns True if the node has nodes under it.
IsExpanded(index%) Returns True if the node is expanded.
IsItemSelected(index%) Returns True if the node is selected.
ItemCount() Returns the total number of nodes.
+ ItemDepth(index%) Returns how many nodes the node is under (0 at the root level, -1 for no such node).
+ ItemParent(index%) Returns the index of the node's parent (-1 at the root level or for no such node).
+ RemoveItem index% Remove the node and every node under it (destroys the item widgets).
SetExpanded index%, expanded Expand or collapse the node (collapsing moves a hidden selection to the node).
SetItemSelected index%, selected Select or deselect the node (single-select: sets or clears the selection).
+ SetItemTag index%, tag$ Keep a string with the node ("" frees it).
+ SetItemText index%, text$ Change the node's text.
SetMultiSelect multi Enable or disable multi-selection.
SetReorderable reorderable Enable or disable drag-to-reorder.
Events
@@ -13408,7 +13775,9 @@ wgtButton(wrap, "Tag 3");
Properties (BASIC Interface)
Property Type Access Description
-------- ---- ------ -----------
- Alignment Enum (Left, Center, Right) Read/Write Row alignment for rows that do not fill the full width.
+ Alignment Enum (Left, Center, Right) Read/Write Row alignment for rows that do not fill the full width.
+ Padding Integer Read/Write The padding field in pixels (wgtGetPaddingPx / wgtSetPaddingPx).
+ Spacing Integer Read/Write The spacing field in pixels (wgtGetSpacingPx / wgtSetSpacingPx).
Methods
No widget-specific methods.
Events
diff --git a/dosbox-staging-overrides.conf b/dosbox-staging-overrides.conf
index 28119d67..9b401ca8 100644
--- a/dosbox-staging-overrides.conf
+++ b/dosbox-staging-overrides.conf
@@ -1,3 +1,25 @@
+# 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.
+
[sdl]
windowresolution = 1024x768
diff --git a/dosbox-x-overrides.conf b/dosbox-x-overrides.conf
index 4863b1b3..4b1eaa14 100644
--- a/dosbox-x-overrides.conf
+++ b/dosbox-x-overrides.conf
@@ -1,3 +1,25 @@
+# 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.
+
[sdl]
output = surface
windowresolution = 1024x768
diff --git a/screenshots/86box/parpm-parpm.png b/screenshots/86box/parpm-parpm.png
index a50f176d..4e6a2761 100644
--- a/screenshots/86box/parpm-parpm.png
+++ b/screenshots/86box/parpm-parpm.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:4fc2c96e65240cabcb060dbc0c92e4c59d5424afb2790448e3e66ecffe985f5a
-size 17400
+oid sha256:145f93bfbd629a4d85199335402972d315b289fb2c7e8b17148baa5726cf0c37
+size 17276
diff --git a/screenshots/86box/pkgsign-ps-plan.png b/screenshots/86box/pkgsign-ps-plan.png
new file mode 100644
index 00000000..59137296
--- /dev/null
+++ b/screenshots/86box/pkgsign-ps-plan.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:9ed7c94a9a57e3302bb292d8e12e446ad2886d76e3f2587ad4e6cbf8f353a4b8
+size 16567
diff --git a/screenshots/86box/upgdrop-ud-sw.png b/screenshots/86box/upgdrop-ud-sw.png
index 3ce54a2a..12746a3e 100644
--- a/screenshots/86box/upgdrop-ud-sw.png
+++ b/screenshots/86box/upgdrop-ud-sw.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:1aaf22316b34f748e761e710a8c5fdfab34595916975dd8a8d4c7b74ec8b21ee
-size 17154
+oid sha256:a42a5bb02f585ac5412e94c99e708097b49962ce5ac665626265a6dd72d2ad75
+size 16416
diff --git a/screenshots/dosbox/cpanel-cp-types.png b/screenshots/dosbox/cpanel-cp-types.png
index 0b7d27dd..619eb94e 100644
--- a/screenshots/dosbox/cpanel-cp-types.png
+++ b/screenshots/dosbox/cpanel-cp-types.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:2749e1d0ca5ca17f97455a0e6a4200d587f3b0961b135ce80fb37ee586325ef8
-size 24863
+oid sha256:92d8c62871629493d43a2913a148706833ed39aefc38471a0099008f0e3b1f3c
+size 24799
diff --git a/screenshots/dosbox/hang-ended.png b/screenshots/dosbox/hang-ended.png
index e8dd2143..6ac1b67c 100644
--- a/screenshots/dosbox/hang-ended.png
+++ b/screenshots/dosbox/hang-ended.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:a8e96d2fa598cf563ef80728c0201e51306b0eb9d7ab4169107d33ac62f279bb
-size 15155
+oid sha256:05c8ed764345486fd82bdd63033ef83cc9175ffe4968a30a474214b615f325db
+size 15166
diff --git a/screenshots/dosbox/hang-taskmgr.png b/screenshots/dosbox/hang-taskmgr.png
index 3aba658d..931928bc 100644
--- a/screenshots/dosbox/hang-taskmgr.png
+++ b/screenshots/dosbox/hang-taskmgr.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:45d1606cf7d9d2da3dc9f7abcccce1f1bea6b55b95fe4a27f0b0c618a8884065
-size 16019
+oid sha256:7682c8fb92700284dfd2c50ea44a5a69f15182c657358da4dc53bd8bbdcacdef
+size 16067
diff --git a/screenshots/dosbox/instadd-add-sw.png b/screenshots/dosbox/instadd-add-sw.png
index 87001408..6ab52379 100644
--- a/screenshots/dosbox/instadd-add-sw.png
+++ b/screenshots/dosbox/instadd-add-sw.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:14eba919cb53886f0496c00fb57379b238ca4754dd05bd0a13a49fd135e43599
-size 21515
+oid sha256:a95e657ccefe24dc158cdf198733228926ffd3320afd181575c41eeba7289794
+size 21479
diff --git a/screenshots/dosbox/instadd-su-what.png b/screenshots/dosbox/instadd-su-what.png
index b3f2400e..18b84e8e 100644
--- a/screenshots/dosbox/instadd-su-what.png
+++ b/screenshots/dosbox/instadd-su-what.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:d928454dc96be2d801b4dca486336ca902d461895eaddb436544f9ef2eba25a8
-size 21509
+oid sha256:b29e0eb146dba303b3bb6fc322636bd96f4eb6c2d2b4790ea0216102ced48b7f
+size 21513
diff --git a/screenshots/dosbox/menus-m-design.png b/screenshots/dosbox/menus-m-design.png
index d54d6915..3bbf9175 100644
--- a/screenshots/dosbox/menus-m-design.png
+++ b/screenshots/dosbox/menus-m-design.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:c1eeb4d3e4cb61600af378d7f36c0f96290f74fe2da8fa14066ef8e779d1459c
-size 23804
+oid sha256:72a63bc0029977479b2a9e4cb73caed8aa69c5320af08ac485386281e86452d1
+size 23941
diff --git a/screenshots/dosbox/parpm-parpm.png b/screenshots/dosbox/parpm-parpm.png
index 6b631e94..c4e4edb5 100644
--- a/screenshots/dosbox/parpm-parpm.png
+++ b/screenshots/dosbox/parpm-parpm.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:698945834aca4b2659a1f02658f2128f135459f9d28a7e10d4d19ce5c22c846d
-size 17471
+oid sha256:eb69020b4045dfac048226eebc446cc808418e0592a1f21eb4676f162bb97168
+size 17441
diff --git a/screenshots/dosbox/pkgsign-ps-plan.png b/screenshots/dosbox/pkgsign-ps-plan.png
new file mode 100644
index 00000000..59137296
--- /dev/null
+++ b/screenshots/dosbox/pkgsign-ps-plan.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:9ed7c94a9a57e3302bb292d8e12e446ad2886d76e3f2587ad4e6cbf8f353a4b8
+size 16567
diff --git a/screenshots/dosbox/player-p-wav.png b/screenshots/dosbox/player-p-wav.png
index 8c4ca0b1..eb7e09fd 100644
--- a/screenshots/dosbox/player-p-wav.png
+++ b/screenshots/dosbox/player-p-wav.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:81ab1b214f13c292b7034488d9001d405c9fa9fce3b649c412fb98bb4d5ff9da
-size 13944
+oid sha256:4155d46360ac8d7eeb050b5ede832b29c4de10595a0b0f7cf0656420a80fa20e
+size 13952
diff --git a/screenshots/dosbox/sdk-sdktbx.png b/screenshots/dosbox/sdk-sdktbx.png
index 24260f82..0782049d 100644
--- a/screenshots/dosbox/sdk-sdktbx.png
+++ b/screenshots/dosbox/sdk-sdktbx.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:fb2e99f81d2fd46ebf15c15fbd18dc40290b12aef056ce503bee8d34c72e5db8
-size 23699
+oid sha256:d05934c117be8d1d2f3d788ddb55a594af00b607a0acd42ccad013dcfe6a64e9
+size 23835
diff --git a/screenshots/dosbox/shellcmd-sc-back.png b/screenshots/dosbox/shellcmd-sc-back.png
new file mode 100644
index 00000000..488088ec
--- /dev/null
+++ b/screenshots/dosbox/shellcmd-sc-back.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:78576367ec2f8053aaad04c8e1205712207ed7fd1d4157e2da84d3c959cdc7f4
+size 17710
diff --git a/screenshots/dosbox/sql-sqlsmk.png b/screenshots/dosbox/sql-sqlsmk.png
index 91eecc55..5aab5670 100644
--- a/screenshots/dosbox/sql-sqlsmk.png
+++ b/screenshots/dosbox/sql-sqlsmk.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:6f0385872d52d227121faf4e1453cfbacae6e6be7b9568743fae5d9e53eeed3f
-size 16932
+oid sha256:b6cd37e662a61fad0598ae80f40bfb325a7e112091303436901f68f5d63c94f1
+size 17411
diff --git a/screenshots/dosbox/ucide-ucide-ds.png b/screenshots/dosbox/ucide-ucide-ds.png
index 26601f99..90670732 100644
--- a/screenshots/dosbox/ucide-ucide-ds.png
+++ b/screenshots/dosbox/ucide-ucide-ds.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:98eea34512e3f348a3041b12b6ca7ef522f9edcabe494a0f15d2a8e896cf9a1e
-size 31561
+oid sha256:75cc5f470d854edf551989cd44d1abf2e6e2b8f44dfc6d23a6890447494754d4
+size 31726
diff --git a/screenshots/dosbox/ucide-ucide-tb.png b/screenshots/dosbox/ucide-ucide-tb.png
index 646e96b4..708634f2 100644
--- a/screenshots/dosbox/ucide-ucide-tb.png
+++ b/screenshots/dosbox/ucide-ucide-tb.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:77eaddc942ed51d860f97a5701db753b7010a68fa5d74088922e1d10cc5ef039
-size 31708
+oid sha256:6641873ed5704e3ca54c5353ff07620a35ad31ae787069995d8612f7a635db2b
+size 31876
diff --git a/screenshots/dosbox/upgdrop-ud-sw.png b/screenshots/dosbox/upgdrop-ud-sw.png
index 3ce54a2a..12746a3e 100644
--- a/screenshots/dosbox/upgdrop-ud-sw.png
+++ b/screenshots/dosbox/upgdrop-ud-sw.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:1aaf22316b34f748e761e710a8c5fdfab34595916975dd8a8d4c7b74ec8b21ee
-size 17154
+oid sha256:a42a5bb02f585ac5412e94c99e708097b49962ce5ac665626265a6dd72d2ad75
+size 16416
diff --git a/server/dvxserver.lua b/server/dvxserver.lua
index a1e8ab80..68d72e9c 100644
--- a/server/dvxserver.lua
+++ b/server/dvxserver.lua
@@ -1,3 +1,25 @@
+-- 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.
+
-- dvxserver.lua -- the DVX package server: the catalogue of add-on packages that DVX's Software
-- program browses, installs, and updates from, and where publishers upload them.
--
diff --git a/server/lib/packages.lua b/server/lib/packages.lua
index 9fe28757..1d09550f 100644
--- a/server/lib/packages.lua
+++ b/server/lib/packages.lua
@@ -1,3 +1,25 @@
+-- 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.
+
-- packages.lua -- the DVX package catalogue: packages, their versions, what each needs, and the
-- review that publishes a version.
--
diff --git a/server/test/account.lua b/server/test/account.lua
index 228fec3b..c9028800 100644
--- a/server/test/account.lua
+++ b/server/test/account.lua
@@ -1,3 +1,25 @@
+-- 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.
+
-- account.lua -- an account on the service under test, made as a person makes one: registered, the
-- link in its mail followed (read from the mail queue in the database), then signed in. Loaded by
-- the suites, and run by serve.sh for the DOS tests' account:
diff --git a/server/test/run.sh b/server/test/run.sh
index fc2ceeb8..b883bc50 100755
--- a/server/test/run.sh
+++ b/server/test/run.sh
@@ -1,4 +1,26 @@
#!/bin/bash
+# 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.
+
# run.sh -- the DVX package server's tests, as singemaster's are run: fixtures made, the server
# started on a port of its own with a configuration of its own, the suites run against it, the
# server stopped. Then both services in one calog process, each answering on its own port.
diff --git a/server/test/serve.sh b/server/test/serve.sh
index 3bb43a5a..0bd847b5 100755
--- a/server/test/serve.sh
+++ b/server/test/serve.sh
@@ -1,4 +1,26 @@
#!/bin/bash
+# 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.
+
# serve.sh DATA PORT FIXTURES DVXPKG USER PASSWORD -- the package server for the DOS tests (runDos.sh starts it
# for a "# REQUIRES: PKGSERVER" script and stops it by its PID). In DATA, from nothing:
#
diff --git a/server/test/testPackages.lua b/server/test/testPackages.lua
index 2db84cf2..89fe31f5 100644
--- a/server/test/testPackages.lua
+++ b/server/test/testPackages.lua
@@ -1,3 +1,25 @@
+-- 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.
+
-- testPackages.lua -- the package server end to end: uploads checked by the package tool and
-- refused for the right reasons, review with the repository's signature (made here, as on the
-- operator's own computer), the public catalogue, downloads (resumed) that check against the
diff --git a/server/test/testReview.lua b/server/test/testReview.lua
index a090fadc..9f009a09 100644
--- a/server/test/testReview.lua
+++ b/server/test/testReview.lua
@@ -1,3 +1,25 @@
+-- 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.
+
-- testReview.lua -- the operator's review tool (tools/review.lua) against the running service: what
-- waits, a fetch and the package tool's word on it, an approval signed with the repository's key
-- that DVX would trust, a rejection, withdrawing and publishing again, and a fetched file changed
diff --git a/server/tools/review.lua b/server/tools/review.lua
index 216d0ba4..db205f72 100644
--- a/server/tools/review.lua
+++ b/server/tools/review.lua
@@ -1,3 +1,25 @@
+-- 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.
+
-- review.lua -- the operator's review of uploaded packages, run on the operator's own computer: the
-- one place the repository's secret key is kept. It lists what waits, fetches a version and shows
-- what it is (the package tool's own check of it), approves a version with the repository's
diff --git a/src/apps/kpunch/basdemo/basdemo.dhs b/src/apps/kpunch/basdemo/basdemo.dhs
index e7f39983..6d34d94d 100644
--- a/src/apps/kpunch/basdemo/basdemo.dhs
+++ b/src/apps/kpunch/basdemo/basdemo.dhs
@@ -1,24 +1,24 @@
-// 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.
+# 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.
# BASIC Demo help. Compiled to basdemo.hlp next to basdemo.app; F1 and
# Help > Contents in the BASIC Demo open it.
diff --git a/src/apps/kpunch/basdemo/shape.cls b/src/apps/kpunch/basdemo/shape.cls
index 5a07ae17..73e0c43a 100644
--- a/src/apps/kpunch/basdemo/shape.cls
+++ b/src/apps/kpunch/basdemo/shape.cls
@@ -1,6 +1,30 @@
VERSION DVX 1.00 CLASS
Attribute VB_Name = "Shape"
+
+' 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.
+
+
' A shape for the Objects demo: its kind and size, and the area that
' follows from them. Every new Shape starts as a square of size 1.
diff --git a/src/apps/kpunch/cpanel/cpSounds.c b/src/apps/kpunch/cpanel/cpSounds.c
index 41330f65..a6cab1f6 100644
--- a/src/apps/kpunch/cpanel/cpSounds.c
+++ b/src/apps/kpunch/cpanel/cpSounds.c
@@ -233,8 +233,8 @@ static int32_t sChoice[SoundCountE];
// Prototypes
// ============================================================
-static void addBankRow(WidgetT *page, const char *label, BankRowT *row, PrefsHandleT *prefs);
-static int32_t addBanks(BankRowT *row, PrefsHandleT *prefs);
+static void addBankRow(WidgetT *page, const char *label, BankRowT *row, PrefsHandleT *prefs, char **banks, const PkgDbT *db);
+static int32_t addBanks(BankRowT *row, PrefsHandleT *prefs, char **banks, const PkgDbT *db);
static int32_t addDriver(const char *name, const char *label);
static int32_t addFile(const char *path);
static void addFound(BankRowT *row, BankFindFnT *find, const char *label);
@@ -263,9 +263,10 @@ static void showVolume(void);
// A row of banks (its dropdown, and its Test when the music library is
-// there).
-static void addBankRow(WidgetT *page, const char *label, BankRowT *row, PrefsHandleT *prefs) {
- int32_t sel = addBanks(row, prefs);
+// there), from the banks folders' files (sorted) and the package records
+// (NULL: none).
+static void addBankRow(WidgetT *page, const char *label, BankRowT *row, PrefsHandleT *prefs, char **banks, const PkgDbT *db) {
+ int32_t sel = addBanks(row, prefs, banks, db);
WidgetT *box = addRow(page, label);
row->drop = wgtDropdown(box);
@@ -283,26 +284,20 @@ static void addBankRow(WidgetT *page, const char *label, BankRowT *row, PrefsHan
}
-// A row's banks: the standard one, those of its kind in the banks folders
-// (by the names DVX.INI keeps, shown by bankLabel; the standard one not
-// twice), the sound cards' own banks where the row offers them, and
-// DVX.INI's own choice when it is none of them (so OK keeps it). Returns
-// DVX.INI's choice's item.
-static int32_t addBanks(BankRowT *row, PrefsHandleT *prefs) {
- char **names = platformKindReadDir(DvxKindBanksE);
- PkgDbT db;
- bool haveDb = sPkg.load && sPkg.load(&db, "");
- int32_t sel;
-
+// A row's banks: the standard one, those of its kind among banks (the
+// banks folders' files, by the names DVX.INI keeps, shown by bankLabel;
+// the standard one not twice), the sound cards' own banks where the row
+// offers them, and DVX.INI's own choice when it is none of them (so OK
+// keeps it). Returns DVX.INI's choice's item.
+static int32_t addBanks(BankRowT *row, PrefsHandleT *prefs, char **banks, const PkgDbT *db) {
choiceAdd(&row->choices, "", CPS_BANK_LABEL);
- qsort(names, arrlen(names), sizeof(names[0]), compareNames);
- for (int32_t i = 0; i < (int32_t)arrlen(names); i++) {
- if (bankOfRow(row, names[i]) && strcasecmp(names[i], row->standard) != 0) {
+ for (int32_t i = 0; i < (int32_t)arrlen(banks); i++) {
+ if (bankOfRow(row, banks[i]) && strcasecmp(banks[i], row->standard) != 0) {
char label[DVX_MAX_PATH];
- bankLabel(names[i], row, haveDb ? &db : NULL, label, sizeof(label));
- choiceSetLabel(&row->choices, choiceAdd(&row->choices, names[i], CPS_BANK_LABEL), label);
+ bankLabel(banks[i], row, db, label, sizeof(label));
+ choiceSetLabel(&row->choices, choiceAdd(&row->choices, banks[i], CPS_BANK_LABEL), label);
}
}
@@ -311,14 +306,7 @@ static int32_t addBanks(BankRowT *row, PrefsHandleT *prefs) {
addFound(row, sMus.romBank, CPS_ROM_BANK);
}
- // Partly loaded when out of memory: freed whenever it was loaded at all
- if (sPkg.load) {
- sPkg.release(&db);
- }
-
- dvxReadDirFree(names);
- sel = choiceAdd(&row->choices, prefsGetString(prefs, AUDIO_INI_SECTION, row->key, ""), CPS_BANK_LABEL);
- return sel;
+ return choiceAdd(&row->choices, prefsGetString(prefs, AUDIO_INI_SECTION, row->key, ""), CPS_BANK_LABEL);
}
@@ -603,8 +591,22 @@ void cpSoundsBuild(AppContextT *ac, WidgetT *page, PrefsHandleT *prefs) {
wgtDropdownSetItems(sModDrop, sModLabels, AUDIO_MODULES_CHOICE_COUNT);
wgtDropdownSetSelected(sModDrop, sSnd.modules(prefs));
- addBankRow(page, "Instruments:", &sFmBanks, prefs);
- addBankRow(page, "SoundFont:", &sSoundFonts, prefs);
+ // The banks folders and the package records (they name the add-on
+ // banks), read once for both rows
+ char **banks = platformKindReadDir(DvxKindBanksE);
+ PkgDbT db;
+ bool haveDb = sPkg.load && sPkg.load(&db, "");
+
+ qsort(banks, arrlen(banks), sizeof(banks[0]), compareNames);
+ addBankRow(page, "Instruments:", &sFmBanks, prefs, banks, haveDb ? &db : NULL);
+ addBankRow(page, "SoundFont:", &sSoundFonts, prefs, banks, haveDb ? &db : NULL);
+
+ // Partly loaded when out of memory: freed whenever it was loaded at all
+ if (sPkg.load) {
+ sPkg.release(&db);
+ }
+
+ dvxReadDirFree(banks);
AudioInfoT info;
char text[CPS_TEXT_BUF];
diff --git a/src/apps/kpunch/cpanel/cpanel.dhs b/src/apps/kpunch/cpanel/cpanel.dhs
index 8f1454cf..ea689c85 100644
--- a/src/apps/kpunch/cpanel/cpanel.dhs
+++ b/src/apps/kpunch/cpanel/cpanel.dhs
@@ -1,24 +1,24 @@
-// 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.
+# 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.
# Control Panel help. Compiled to cpanel.hlp next to cpanel.app; F1 and
# Help > Contents in the Control Panel open it.
diff --git a/src/apps/kpunch/cpanel/cpanel.res b/src/apps/kpunch/cpanel/cpanel.res
index fee578c1..76bc6d56 100644
--- a/src/apps/kpunch/cpanel/cpanel.res
+++ b/src/apps/kpunch/cpanel/cpanel.res
@@ -1,3 +1,25 @@
+# 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.
+
# cpanel.res -- Resource manifest for Control Panel
icon32 icon icon32.bmp
name text "Control Panel"
diff --git a/src/apps/kpunch/dvxbasic/Makefile b/src/apps/kpunch/dvxbasic/Makefile
index 68e1b008..52851264 100644
--- a/src/apps/kpunch/dvxbasic/Makefile
+++ b/src/apps/kpunch/dvxbasic/Makefile
@@ -26,9 +26,10 @@
# basrt.lib -- BASIC runtime library (VM + values + form
# runtime + dlregsym init)
# basdev.lib -- BASIC development library (compiler, build
-# pipeline, validator, debugger; the IDE and
-# BASIC programs through compiler.bas /
-# debug.bas; standalone stub embedded)
+# pipeline, validator, debugger, the .frm
+# model; the IDE and BASIC programs through
+# compiler.bas / debug.bas / designer.bas's
+# Frm*; standalone stub embedded)
# dvxbasic.app -- BASIC IDE (UI, links against basrt.lib and
# basdev.lib)
# basstub.app -- standalone app stub (obj/dvxbasic, embedded
@@ -65,23 +66,25 @@ SYSTEMDIR = ../../../../bin/system
COMP_SRCS = compiler/lexer.c compiler/ast.c compiler/scope.c compiler/parse.c compiler/parseExpr.c compiler/parseStmt.c compiler/parseDecl.c compiler/resolve.c compiler/resolveExpr.c compiler/typecheck.c compiler/fold.c compiler/gen.c compiler/genExpr.c compiler/genStmt.c compiler/compiler.c compiler/codegen.c compiler/strip.c compiler/obfuscate.c compiler/compact.c
VM_SRCS = runtime/vm.c runtime/values.c runtime/dates.c runtime/serialize.c runtime/gfx.c runtime/screen.c
# Standalone programs that link the VM also need what libdvx provides it
-# at run time on the target (INI access through dvxPrefs).
-VM_LINK_SRCS = $(VM_SRCS) $(LIBDVX)/dvxPrefs.c
+# at run time on the target (INI access through dvxPrefs, Unicode through
+# dvxFontFmt).
+VM_LINK_SRCS = $(VM_SRCS) $(LIBDVX)/dvxPrefs.c $(LIBDVX)/dvxFontFmt.c
SUPPORT_SRCS = $(LIBDVX)/platform/dvxPlatformUtil.c $(LIBDVX)/thirdparty/stb_ds_impl.c
-RES_SRCS = $(LIBDVX)/dvxResource.c $(LIBDVX)/dvxFontFmt.c $(TOOLSDIR)/dvxResWrite.c
+RES_SRCS = $(LIBDVX)/dvxResource.c $(TOOLSDIR)/dvxResWrite.c
CLI_SRCS = basBuild.c $(COMP_SRCS) $(VM_LINK_SRCS) $(RES_SRCS) $(SUPPORT_SRCS)
# Runtime library objects (VM + values + form runtime + serialization)
-RT_OBJS = $(OBJDIR)/vm.o $(OBJDIR)/values.o $(OBJDIR)/dates.o $(OBJDIR)/gfx.o $(OBJDIR)/screen.o $(OBJDIR)/formrt.o $(OBJDIR)/formGfx.o $(OBJDIR)/formPrinter.o $(OBJDIR)/formDesign.o $(OBJDIR)/formCtl.o $(OBJDIR)/frmParser.o $(OBJDIR)/frmModel.o $(OBJDIR)/frmModelBas.o $(OBJDIR)/basNativeDos.o $(OBJDIR)/serialize.o $(OBJDIR)/basRunHost.o
+RT_OBJS = $(OBJDIR)/vm.o $(OBJDIR)/values.o $(OBJDIR)/dates.o $(OBJDIR)/gfx.o $(OBJDIR)/screen.o $(OBJDIR)/formrt.o $(OBJDIR)/formGfx.o $(OBJDIR)/formPrinter.o $(OBJDIR)/formDesign.o $(OBJDIR)/formCtl.o $(OBJDIR)/frmParser.o $(OBJDIR)/basNativeDos.o $(OBJDIR)/serialize.o $(OBJDIR)/basRunHost.o
RT_TARGETDIR = $(LIBSDIR)/kpunch/basrt
RT_TARGET = $(RT_TARGETDIR)/basrt.lib
# Compiler objects (the development library)
COMP_OBJS = $(patsubst %.c,$(OBJDIR)/%.o,$(notdir $(COMP_SRCS))) $(OBJDIR)/basBuild.o
-# Development library: the compiler plus the shared validator, debugger
-# and proc scanner and the BASIC entry points (basdev/)
-DEV_SRCS = basdev/basValidate.c basdev/basDebug.c basdev/basProcs.c basdev/basSource.c basdev/basHelpMap.c basdev/basProject.c basdev/basDevBas.c
+# Development library: the compiler plus the shared validator, debugger,
+# proc scanner, the .frm model the designer works on, and the BASIC
+# entry points (basdev/: compiler.bas, debug.bas, designer.bas's Frm*)
+DEV_SRCS = basdev/basValidate.c basdev/basDebug.c basdev/basProcs.c basdev/basSource.c basdev/basHelpMap.c basdev/basProject.c basdev/basDevBas.c basdev/frmModel.c basdev/frmModelBas.c
DEV_OBJS = $(COMP_OBJS) $(patsubst basdev/%.c,$(OBJDIR)/%.o,$(DEV_SRCS))
DEV_TARGETDIR = $(LIBSDIR)/kpunch/basdev
DEV_TARGET = $(DEV_TARGETDIR)/basdev.lib
@@ -311,7 +314,7 @@ clean: host-basrt-clean
# of the host build.
HOSTRT_DIR = $(HOST_OBJROOT)/basrt
HOSTRT_TARGET = $(HOST_OBJROOT)/basrt.a
-HOSTRT_SRCS = $(COMP_SRCS) $(VM_SRCS) formrt/formrt.c formrt/formGfx.c formrt/formPrinter.c formrt/formDesign.c formrt/formCtl.c formrt/frmParser.c formrt/frmModel.c formrt/frmModelBas.c formrt/basNativeHost.c formrt/basRunHost.c
+HOSTRT_SRCS = $(COMP_SRCS) $(VM_SRCS) formrt/formrt.c formrt/formGfx.c formrt/formPrinter.c formrt/formDesign.c formrt/formCtl.c formrt/frmParser.c basdev/frmModel.c basdev/frmModelBas.c formrt/basNativeHost.c formrt/basRunHost.c
HOSTRT_OBJS = $(patsubst %.c,$(HOSTRT_DIR)/%.o,$(notdir $(HOSTRT_SRCS)))
HOSTRT_CFLAGS = $(HOST_CFLAGS) -Wno-stringop-truncation $(DVX_INCLUDES) -I../../../widgets/kpunch -I../../../libs/kpunch/dvxshell -I../../../libs/kpunch/libtasks
@@ -335,6 +338,10 @@ $(HOSTRT_DIR)/%.o: formrt/%.c $(DVX_MAKEFILES)
@mkdir -p $(HOSTRT_DIR)
$(HOSTCC) $(HOSTRT_CFLAGS) -c -o $@ $<
+$(HOSTRT_DIR)/%.o: basdev/%.c $(DVX_MAKEFILES)
+ @mkdir -p $(HOSTRT_DIR)
+ $(HOSTCC) $(HOSTRT_CFLAGS) -c -o $@ $<
+
host-basrt-clean:
rm -rf $(HOSTRT_DIR) $(HOSTRT_TARGET)
diff --git a/src/apps/kpunch/dvxbasic/basBuild.c b/src/apps/kpunch/dvxbasic/basBuild.c
index b4776db9..85c4d3e0 100644
--- a/src/apps/kpunch/dvxbasic/basBuild.c
+++ b/src/apps/kpunch/dvxbasic/basBuild.c
@@ -34,7 +34,8 @@
// .frm texts too), compact the bytecode and re-serialize.
// 3. Debug builds serialize the debug info.
// 4. Extract the STUB resource from selfPath and write it as outPath.
-// 5. Append the resource set in canonical order:
+// 5. Gather the resource set in memory (a DvxResBatchT, written to
+// outPath once at the end) in canonical order:
// BAS_RES_NAME (always, falls back to BAS_DEFAULT_APP_NAME)
// BAS_RES_AUTHOR / PUBLISHER / VERSION / COPYRIGHT / DESCRIPTION
// BAS_RES_ICON32 (project icon, else the NOICON fallback)
@@ -47,6 +48,7 @@
// 6. Copy the help file next to outPath.
#include "basBuild.h"
+#include "../../../tools/dvxResWrite.h"
#include "basRes.h"
#include "compiler/compiler.h"
#include "compiler/compact.h"
@@ -121,11 +123,11 @@ static const char *const sWrapEnd[] = {
// Function prototypes (alphabetical)
static bool appendSource(BasProjectSourceT *ps, const char *text, int32_t len);
-static int32_t appendText(const char *path, const char *name, const char *value);
+static int32_t appendText(DvxResBatchT *res, const char *name, const char *value);
bool basBuildAddSource(BasProjectSourceT *ps, const char *path, const char *text, int32_t len, BasBuildWrapE wrap);
const char *basBuildApp(const char *outPath, const BasBuildSpecT *spec);
bool basBuildAppendFile(BasProjectSourceT *ps, const char *text, int32_t len, BasBuildWrapE wrap, const char *name, const char *ctrlList, int32_t *textLine, int32_t *textLines);
-BasModuleT *basBuildCompile(const char *source, int32_t len, bool optionExplicit, const BasCtrlValidatorT *validator, const BasCompilerT **liveCompiler, BasCompileErrorT *err);
+BasModuleT *basBuildCompile(const char *source, int32_t len, bool optionExplicit, const BasCtrlValidatorT *validator, const BasCompilerT **liveCompiler, BasCompileErrorT *err, void (*unitFn)(void *ctx), void *unitCtx);
bool basBuildConcatProject(PrefsHandleT *prefs, const char *projectDir, BasProjectSourceT *out);
const char *basBuildControl(const char *outPath, const BasBuildSpecT *spec);
const char *basBuildErrorText(const char *message);
@@ -140,11 +142,11 @@ BasBuildWrapE basBuildWrapForPath(const char *path);
static void buildLog(const BasBuildSpecT *spec, const char *fmt, ...);
static const char *copyHelpFile(const char *outPath, const BasBuildSpecT *spec);
static int32_t countLines(const char *text, int32_t len);
-static const char *emitControls(const char *outPath, const BasBuildSpecT *spec);
-static const char *emitCtlTexts(const char *outPath, const BasBuildSpecT *spec);
-static const char *emitFonts(const char *outPath, const BasBuildSpecT *spec);
-static const char *emitIcon(const char *outPath, const BasBuildSpecT *spec);
-static const char *emitResources(const char *outPath, const BasBuildSpecT *spec, const uint8_t *modData, int32_t modLen, const uint8_t *dbgData, int32_t dbgLen, uint8_t **frmData, const int32_t *frmLens, const BasObfFrmT *obfFrms, int32_t frmCount);
+static const char *emitControls(DvxResBatchT *res, const BasBuildSpecT *spec);
+static const char *emitCtlTexts(DvxResBatchT *res, const BasBuildSpecT *spec);
+static const char *emitFonts(DvxResBatchT *res, const BasBuildSpecT *spec);
+static const char *emitIcon(DvxResBatchT *res, const BasBuildSpecT *spec);
+static const char *emitResources(DvxResBatchT *res, const BasBuildSpecT *spec, const uint8_t *modData, int32_t modLen, const uint8_t *dbgData, int32_t dbgLen, uint8_t **frmData, const int32_t *frmLens, const BasObfFrmT *obfFrms, int32_t frmCount);
static void freeFrmWork(uint8_t **frmData, int32_t *frmLens, BasObfFrmT *obfFrms);
static const char *prepareModule(const BasBuildSpecT *spec, uint8_t **frmData, int32_t *frmLens, BasObfFrmT *obfFrms, int32_t frmCount, uint8_t **outMod, int32_t *outModLen, uint8_t **outDbg, int32_t *outDbgLen);
static const char *stubCarrier(const BasBuildSpecT *spec);
@@ -194,12 +196,12 @@ static bool appendSource(BasProjectSourceT *ps, const char *text, int32_t len) {
}
-static int32_t appendText(const char *path, const char *name, const char *value) {
+static int32_t appendText(DvxResBatchT *res, const char *name, const char *value) {
if (!value || !value[0]) {
return 0;
}
- return dvxResAppend(path, name, DVX_RES_TEXT, value, (uint32_t)strlen(value) + 1);
+ return dvxResBatchAdd(res, name, DVX_RES_TEXT, value, (uint32_t)strlen(value) + 1);
}
@@ -349,16 +351,31 @@ const char *basBuildApp(const char *outPath, const BasBuildSpecT *spec) {
int32_t dbgLen = 0;
const char *failure = prepareModule(spec, frmData, frmLens, obfFrms, frmCount, &modData, &modLen, &dbgData, &dbgLen);
+ // Every resource is gathered and the file written once
+ DvxResBatchT res;
+ bool batched = false;
+
if (!failure) {
failure = writeStub(outPath, spec);
}
if (!failure) {
- failure = emitResources(outPath, spec, modData, modLen, dbgData, dbgLen, frmData, frmLens, obfFrms, frmCount);
+ batched = dvxResBatchBegin(&res, outPath) == 0;
+ failure = batched ? NULL : BAS_BUILD_ERR_WRITE_RES;
}
if (!failure) {
- failure = emitFonts(outPath, spec);
+ failure = emitResources(&res, spec, modData, modLen, dbgData, dbgLen, frmData, frmLens, obfFrms, frmCount);
+ }
+
+ if (!failure) {
+ failure = emitFonts(&res, spec);
+ }
+
+ if (batched && failure) {
+ dvxResBatchFree(&res);
+ } else if (batched && dvxResBatchCommit(&res) != 0) {
+ failure = BAS_BUILD_ERR_WRITE_RES;
}
if (!failure) {
@@ -418,7 +435,7 @@ bool basBuildAppendFile(BasProjectSourceT *ps, const char *text, int32_t len, Ba
}
-BasModuleT *basBuildCompile(const char *source, int32_t len, bool optionExplicit, const BasCtrlValidatorT *validator, const BasCompilerT **liveCompiler, BasCompileErrorT *err) {
+BasModuleT *basBuildCompile(const char *source, int32_t len, bool optionExplicit, const BasCtrlValidatorT *validator, const BasCompilerT **liveCompiler, BasCompileErrorT *err, void (*unitFn)(void *ctx), void *unitCtx) {
memset(err, 0, sizeof(*err));
BasCompilerT *compiler = basCompilerCreate(source, len);
@@ -430,6 +447,7 @@ BasModuleT *basBuildCompile(const char *source, int32_t len, bool optionExplicit
}
basCompilerSetOptionExplicit(compiler, optionExplicit);
+ basCompilerSetUnitFn(compiler, unitFn, unitCtx);
if (validator) {
basCompilerSetValidator(compiler, validator);
@@ -539,16 +557,31 @@ const char *basBuildControl(const char *outPath, const BasBuildSpecT *spec) {
int32_t dbgLen = 0;
const char *failure = prepareModule(spec, NULL, NULL, NULL, 0, &modData, &modLen, &dbgData, &dbgLen);
+ // Every resource is gathered and the file written once
+ DvxResBatchT res;
+ bool batched = false;
+
if (!failure) {
failure = writeMarker(outPath);
}
- if (!failure && (dvxResAppend(outPath, BAS_RES_MODULE, DVX_RES_BINARY, modData, (uint32_t)modLen) != 0 || (dbgData && dvxResAppend(outPath, BAS_RES_DEBUG, DVX_RES_BINARY, dbgData, (uint32_t)dbgLen) != 0))) {
+ if (!failure) {
+ batched = dvxResBatchBegin(&res, outPath) == 0;
+ failure = batched ? NULL : BAS_BUILD_ERR_WRITE_RES;
+ }
+
+ if (!failure && (dvxResBatchAdd(&res, BAS_RES_MODULE, DVX_RES_BINARY, modData, (uint32_t)modLen) != 0 || (dbgData && dvxResBatchAdd(&res, BAS_RES_DEBUG, DVX_RES_BINARY, dbgData, (uint32_t)dbgLen) != 0))) {
failure = BAS_BUILD_ERR_WRITE_RES;
}
if (!failure) {
- failure = emitControls(outPath, spec);
+ failure = emitControls(&res, spec);
+ }
+
+ if (batched && failure) {
+ dvxResBatchFree(&res);
+ } else if (batched && dvxResBatchCommit(&res) != 0) {
+ failure = BAS_BUILD_ERR_WRITE_RES;
}
if (!failure) {
@@ -765,17 +798,17 @@ static int32_t countLines(const char *text, int32_t len) {
// picture (icon24, the header's ToolboxIcon, a file in the project
// folder) and its toolbox name (the header's Description, else its own
// name), the second and later with "-n" suffixes.
-static const char *emitControls(const char *outPath, const BasBuildSpecT *spec) {
+static const char *emitControls(DvxResBatchT *res, const BasBuildSpecT *spec) {
int32_t rc = 0;
- rc |= appendText(outPath, BAS_RES_AUTHOR, spec->author);
- rc |= appendText(outPath, BAS_RES_PUBLISHER, spec->publisher);
- rc |= appendText(outPath, BAS_RES_VERSION, spec->version);
- rc |= appendText(outPath, BAS_RES_COPYRIGHT, spec->copyright);
- rc |= appendText(outPath, BAS_RES_DESCRIPTION, spec->description);
+ rc |= appendText(res, BAS_RES_AUTHOR, spec->author);
+ rc |= appendText(res, BAS_RES_PUBLISHER, spec->publisher);
+ rc |= appendText(res, BAS_RES_VERSION, spec->version);
+ rc |= appendText(res, BAS_RES_COPYRIGHT, spec->copyright);
+ rc |= appendText(res, BAS_RES_DESCRIPTION, spec->description);
if (spec->helpFile && spec->helpFile[0]) {
- rc |= appendText(outPath, BAS_RES_HELPFILE, platformPathBaseName(spec->helpFile));
+ rc |= appendText(res, BAS_RES_HELPFILE, platformPathBaseName(spec->helpFile));
}
for (int32_t i = 0; rc == 0 && i < spec->ctlCount; i++) {
@@ -799,7 +832,7 @@ static const char *emitControls(const char *outPath, const BasBuildSpecT *spec)
}
snprintf(resName, sizeof(resName), (index == BAS_BUILD_CTL_FIRST) ? "%s" : BAS_CTL_RES_SUFFIX_FMT, BAS_CTL_RES_ICON, (int)index);
- rc |= dvxResAppend(outPath, resName, DVX_RES_ICON, iconData, (uint32_t)iconLen);
+ rc |= dvxResBatchAdd(res, resName, DVX_RES_ICON, iconData, (uint32_t)iconLen);
free(iconData);
}
@@ -808,19 +841,19 @@ static const char *emitControls(const char *outPath, const BasBuildSpecT *spec)
}
snprintf(resName, sizeof(resName), (index == BAS_BUILD_CTL_FIRST) ? "%s" : BAS_CTL_RES_SUFFIX_FMT, BAS_CTL_RES_NAME, (int)index);
- rc |= appendText(outPath, resName, value);
+ rc |= appendText(res, resName, value);
}
if (rc != 0) {
return BAS_BUILD_ERR_WRITE_RES;
}
- return emitCtlTexts(outPath, spec);
+ return emitCtlTexts(res, spec);
}
// The UserControls' header texts, CTL0, CTL1 ...
-static const char *emitCtlTexts(const char *outPath, const BasBuildSpecT *spec) {
+static const char *emitCtlTexts(DvxResBatchT *res, const BasBuildSpecT *spec) {
if (spec->ctlCount > BAS_MAX_CTL_RESOURCES) {
return "Too many UserControls in one project.";
}
@@ -830,7 +863,7 @@ static const char *emitCtlTexts(const char *outPath, const BasBuildSpecT *spec)
snprintf(resName, sizeof(resName), BAS_RES_CTL_FMT, (long)i);
- if (appendText(outPath, resName, spec->ctlSources[i]) != 0) {
+ if (appendText(res, resName, spec->ctlSources[i]) != 0) {
return BAS_BUILD_ERR_WRITE_RES;
}
}
@@ -841,7 +874,7 @@ static const char *emitCtlTexts(const char *outPath, const BasBuildSpecT *spec)
// The fonts the program carries as FONTn resources, each checked to be a
// DVX font first
-static const char *emitFonts(const char *outPath, const BasBuildSpecT *spec) {
+static const char *emitFonts(DvxResBatchT *res, const BasBuildSpecT *spec) {
if (spec->fontCount > BAS_BUILD_FONTS_MAX) {
snprintf(sFontError, sizeof(sFontError), "A program carries at most %d fonts.", BAS_BUILD_FONTS_MAX);
return sFontError;
@@ -877,7 +910,7 @@ static const char *emitFonts(const char *outPath, const BasBuildSpecT *spec) {
snprintf(name, sizeof(name), BAS_RES_FONT_FMT, (long)i);
- int32_t rc = dvxResAppend(outPath, name, DVX_RES_FONT, data, (uint32_t)len);
+ int32_t rc = dvxResBatchAdd(res, name, DVX_RES_FONT, data, (uint32_t)len);
free(data);
@@ -895,7 +928,7 @@ static const char *emitFonts(const char *outPath, const BasBuildSpecT *spec) {
// Embed the project icon, or the NOICON fallback of the stub carrier
// when the project names none. Returns NULL on success, else
// an error message (a named icon that cannot be read names the path).
-static const char *emitIcon(const char *outPath, const BasBuildSpecT *spec) {
+static const char *emitIcon(DvxResBatchT *res, const BasBuildSpecT *spec) {
char iconPath[DVX_MAX_PATH];
void *iconData = NULL;
int32_t iconLen = 0;
@@ -927,13 +960,13 @@ static const char *emitIcon(const char *outPath, const BasBuildSpecT *spec) {
}
}
- int32_t rc = dvxResAppend(outPath, BAS_RES_ICON32, DVX_RES_ICON, iconData, (uint32_t)iconLen);
+ int32_t rc = dvxResBatchAdd(res, BAS_RES_ICON32, DVX_RES_ICON, iconData, (uint32_t)iconLen);
free(iconData);
return (rc == 0) ? NULL : BAS_BUILD_ERR_WRITE_RES;
}
-static const char *emitResources(const char *outPath, const BasBuildSpecT *spec, const uint8_t *modData, int32_t modLen, const uint8_t *dbgData, int32_t dbgLen, uint8_t **frmData, const int32_t *frmLens, const BasObfFrmT *obfFrms, int32_t frmCount) {
+static const char *emitResources(DvxResBatchT *res, const BasBuildSpecT *spec, const uint8_t *modData, int32_t modLen, const uint8_t *dbgData, int32_t dbgLen, uint8_t **frmData, const int32_t *frmLens, const BasObfFrmT *obfFrms, int32_t frmCount) {
// Accumulate every append result so a disk-full or I/O failure on any
// resource is reported instead of being silently treated as success.
int32_t rc = 0;
@@ -941,32 +974,32 @@ static const char *emitResources(const char *outPath, const BasBuildSpecT *spec,
// Project metadata. Name is required -- fall back to a generic label
// so the compiled app always has something sensible to display.
const char *projName = (spec->projName && spec->projName[0]) ? spec->projName : BAS_DEFAULT_APP_NAME;
- rc |= appendText(outPath, BAS_RES_NAME, projName);
+ rc |= appendText(res, BAS_RES_NAME, projName);
- rc |= appendText(outPath, BAS_RES_AUTHOR, spec->author);
- rc |= appendText(outPath, BAS_RES_PUBLISHER, spec->publisher);
- rc |= appendText(outPath, BAS_RES_VERSION, spec->version);
- rc |= appendText(outPath, BAS_RES_COPYRIGHT, spec->copyright);
- rc |= appendText(outPath, BAS_RES_DESCRIPTION, spec->description);
+ rc |= appendText(res, BAS_RES_AUTHOR, spec->author);
+ rc |= appendText(res, BAS_RES_PUBLISHER, spec->publisher);
+ rc |= appendText(res, BAS_RES_VERSION, spec->version);
+ rc |= appendText(res, BAS_RES_COPYRIGHT, spec->copyright);
+ rc |= appendText(res, BAS_RES_DESCRIPTION, spec->description);
- const char *iconFailure = emitIcon(outPath, spec);
+ const char *iconFailure = emitIcon(res, spec);
if (iconFailure) {
return iconFailure;
}
if (spec->helpFile && spec->helpFile[0]) {
- rc |= appendText(outPath, BAS_RES_HELPFILE, platformPathBaseName(spec->helpFile));
+ rc |= appendText(res, BAS_RES_HELPFILE, platformPathBaseName(spec->helpFile));
}
if (spec->multiInstance) {
- rc |= appendText(outPath, BAS_RES_MULTIINSTANCE, BAS_RES_FLAG_ON);
+ rc |= appendText(res, BAS_RES_MULTIINSTANCE, BAS_RES_FLAG_ON);
}
- rc |= dvxResAppend(outPath, BAS_RES_MODULE, DVX_RES_BINARY, modData, (uint32_t)modLen);
+ rc |= dvxResBatchAdd(res, BAS_RES_MODULE, DVX_RES_BINARY, modData, (uint32_t)modLen);
if (dbgData && dbgLen > 0) {
- rc |= dvxResAppend(outPath, BAS_RES_DEBUG, DVX_RES_BINARY, dbgData, (uint32_t)dbgLen);
+ rc |= dvxResBatchAdd(res, BAS_RES_DEBUG, DVX_RES_BINARY, dbgData, (uint32_t)dbgLen);
}
// Form resources. Release builds embed the obfuscated variant when
@@ -1002,7 +1035,7 @@ static const char *emitResources(const char *outPath, const BasBuildSpecT *spec,
int32_t formLen = basFindFormEndPos((const char *)data, len);
snprintf(resName, sizeof(resName), BAS_RES_FORM_FMT, (long)outIdx);
- rc |= dvxResAppend(outPath, resName, DVX_RES_BINARY, data, (uint32_t)formLen);
+ rc |= dvxResBatchAdd(res, resName, DVX_RES_BINARY, data, (uint32_t)formLen);
outIdx++;
}
@@ -1010,7 +1043,7 @@ static const char *emitResources(const char *outPath, const BasBuildSpecT *spec,
return BAS_BUILD_ERR_WRITE_RES;
}
- return emitCtlTexts(outPath, spec);
+ return emitCtlTexts(res, spec);
}
diff --git a/src/apps/kpunch/dvxbasic/basBuild.h b/src/apps/kpunch/dvxbasic/basBuild.h
index b9c9c013..2232f2fa 100644
--- a/src/apps/kpunch/dvxbasic/basBuild.h
+++ b/src/apps/kpunch/dvxbasic/basBuild.h
@@ -191,8 +191,10 @@ typedef struct {
// optionExplicit is set and validator (may be NULL) checking
// CtrlName.Member references. *liveCompiler (may be NULL) holds the
// compiler while it runs: a validator reads its form scope from it.
-// NULL on failure, with *err telling why.
-BasModuleT *basBuildCompile(const char *source, int32_t len, bool optionExplicit, const BasCtrlValidatorT *validator, const BasCompilerT **liveCompiler, BasCompileErrorT *err);
+// unitFn(unitCtx) (NULL: none) runs after each unit compiled
+// (basCompilerSetUnitFn), for a caller that lets other work go on. NULL
+// on failure, with *err telling why.
+BasModuleT *basBuildCompile(const char *source, int32_t len, bool optionExplicit, const BasCtrlValidatorT *validator, const BasCompilerT **liveCompiler, BasCompileErrorT *err, void (*unitFn)(void *ctx), void *unitCtx);
// A compiler message without its "Line N, Col M: " prefix.
const char *basBuildErrorText(const char *message);
diff --git a/src/apps/kpunch/dvxbasic/basdev/basDebug.c b/src/apps/kpunch/dvxbasic/basdev/basDebug.c
index 8f9ba16e..ec89d604 100644
--- a/src/apps/kpunch/dvxbasic/basdev/basDebug.c
+++ b/src/apps/kpunch/dvxbasic/basdev/basDebug.c
@@ -63,12 +63,12 @@ typedef struct {
} DbgCaptureT;
// Prototypes (alphabetical)
-static bool assignSlot(BasVmT *vm, BasValueT *slot, const char *lhs, int32_t lhsLen, const char *rhs, BasPrintFnT printFn, void *ctx);
+static bool assignSlot(BasVmT *vm, BasValueT *slot, const char *lhs, int32_t lhsLen, const char *rhs, BasPrintFnT printFn, void *ctx, const BasDbgPumpT *pump);
int32_t basDbgAppendDropping(char *buf, int32_t cap, int32_t len, const char *text, int32_t textLen);
int32_t basDbgCallStack(BasVmT *vm, int32_t curLine, BasDbgRowFnT fn, void *ctx);
int32_t basDbgCurProcIdx(const BasVmT *vm);
-bool basDbgEval(BasVmT *pausedVm, const char *expr, char *out, int32_t outSize);
-bool basDbgExec(BasVmT *pausedVm, const char *line, BasPrintFnT printFn, void *ctx);
+bool basDbgEval(BasVmT *pausedVm, const char *expr, char *out, int32_t outSize, const BasDbgPumpT *pump);
+bool basDbgExec(BasVmT *pausedVm, const char *line, BasPrintFnT printFn, void *ctx, const BasDbgPumpT *pump);
const BasDebugVarT *basDbgFindVar(const BasVmT *vm, const char *name);
void basDbgFormatValue(const BasVmT *vm, const BasValueT *v, char *buf, int32_t bufSize);
const char *basDbgInstanceTypeName(const BasVmT *vm, const BasObjectT *obj);
@@ -76,14 +76,14 @@ bool basDbgIsMangledName(const char *name);
int32_t basDbgLocals(BasVmT *vm, BasDbgRowFnT fn, void *ctx);
const char *basDbgProcNameForPc(const BasVmT *vm, int32_t pc);
BasValueT *basDbgResolvePath(BasVmT *vm, const char *path, const char **endPtr);
-BasVmResultE basDbgRunSnippet(BasVmT *vm);
-bool basDbgSetVar(BasVmT *pausedVm, const char *path, const char *expr, BasPrintFnT printFn, void *ctx);
+BasVmResultE basDbgRunSnippet(BasVmT *vm, const BasDbgPumpT *pump);
+bool basDbgSetVar(BasVmT *pausedVm, const char *path, const char *expr, BasPrintFnT printFn, void *ctx, const BasDbgPumpT *pump);
BasVmT *basDbgSnippetVm(BasVmT *pausedVm, const char *src, BasModuleT **outMod, char *err, int32_t errSize, const char *captureName, int32_t *outCaptureIdx);
const char *basDbgTypeName(uint8_t dt);
BasValueT *basDbgVarSlot(BasVmT *vm, const BasDebugVarT *dv);
bool basDbgVarVisible(const BasVmT *vm, const BasDebugVarT *dv, int32_t curProcIdx);
static void capturePrint(void *ctx, const char *text, bool newline);
-static bool evalRhs(BasVmT *vm, const char *rhs, uint8_t targetType, BasValueT *outVal, BasPrintFnT printFn, void *ctx);
+static bool evalRhs(BasVmT *vm, const char *rhs, uint8_t targetType, BasValueT *outVal, BasPrintFnT printFn, void *ctx, const BasDbgPumpT *pump);
static int32_t findMemberVarIdx(const BasVmT *vm, const BasObjectT *obj, const char *name);
static int32_t findUdtFieldIdx(const BasModuleT *mod, int32_t typeId, const char *fieldName);
static bool isPaused(const BasVmT *vm);
@@ -94,12 +94,12 @@ static const char *parseIndexList(const char *p, int32_t *indices, int32_t *out
static void printError(BasPrintFnT printFn, void *ctx, const char *msg);
static int32_t procIndexForPc(const BasVmT *vm, int32_t pc);
static void reportSnippetResult(BasVmT *vm, BasVmResultE result, BasPrintFnT printFn, void *ctx);
-static bool tryAssign(BasVmT *vm, const char *line, BasPrintFnT printFn, void *ctx, bool *assigned);
+static bool tryAssign(BasVmT *vm, const char *line, BasPrintFnT printFn, void *ctx, bool *assigned, const BasDbgPumpT *pump);
// Evaluate rhs as a value of slot's type and store it there. lhs (lhsLen
// bytes) names the variable in the confirmation.
-static bool assignSlot(BasVmT *vm, BasValueT *slot, const char *lhs, int32_t lhsLen, const char *rhs, BasPrintFnT printFn, void *ctx) {
+static bool assignSlot(BasVmT *vm, BasValueT *slot, const char *lhs, int32_t lhsLen, const char *rhs, BasPrintFnT printFn, void *ctx, const BasDbgPumpT *pump) {
// Only scalars can be assigned; the RHS is evaluated as an expression
bool scalar = slot->type == BAS_TYPE_INTEGER || slot->type == BAS_TYPE_LONG || slot->type == BAS_TYPE_SINGLE || slot->type == BAS_TYPE_DOUBLE || slot->type == BAS_TYPE_STRING || slot->type == BAS_TYPE_BOOLEAN || slot->type == BAS_TYPE_DATE || slot->type == BAS_TYPE_BYTE || slot->type == BAS_TYPE_CURRENCY;
@@ -110,7 +110,7 @@ static bool assignSlot(BasVmT *vm, BasValueT *slot, const char *lhs, int32_t lhs
BasValueT newVal;
- if (!evalRhs(vm, rhs, slot->type, &newVal, printFn, ctx)) {
+ if (!evalRhs(vm, rhs, slot->type, &newVal, printFn, ctx, pump)) {
return false;
}
@@ -197,7 +197,7 @@ int32_t basDbgCurProcIdx(const BasVmT *vm) {
}
-bool basDbgEval(BasVmT *pausedVm, const char *expr, char *out, int32_t outSize) {
+bool basDbgEval(BasVmT *pausedVm, const char *expr, char *out, int32_t outSize, const BasDbgPumpT *pump) {
const char *end = NULL;
BasValueT *slot = isPaused(pausedVm) ? basDbgResolvePath(pausedVm, dvxSkipWs(expr), &end) : NULL;
@@ -231,7 +231,7 @@ bool basDbgEval(BasVmT *pausedVm, const char *expr, char *out, int32_t outSize)
cap.len = 0;
out[0] = '\0';
basVmSetPrintCallback(tvm, capturePrint, &cap);
- basDbgRunSnippet(tvm);
+ basDbgRunSnippet(tvm, pump);
// Strip the trailing newline
if (cap.len > 0 && out[cap.len - 1] == '\n') {
@@ -244,10 +244,10 @@ bool basDbgEval(BasVmT *pausedVm, const char *expr, char *out, int32_t outSize)
}
-bool basDbgExec(BasVmT *pausedVm, const char *line, BasPrintFnT printFn, void *ctx) {
+bool basDbgExec(BasVmT *pausedVm, const char *line, BasPrintFnT printFn, void *ctx, const BasDbgPumpT *pump) {
bool assigned = false;
- if (!line || *line == '\0' || tryAssign(pausedVm, line, printFn, ctx, &assigned)) {
+ if (!line || *line == '\0' || tryAssign(pausedVm, line, printFn, ctx, &assigned, pump)) {
return assigned;
}
@@ -270,7 +270,7 @@ bool basDbgExec(BasVmT *pausedVm, const char *line, BasPrintFnT printFn, void *c
}
basVmSetPrintCallback(vm, printFn, ctx);
- reportSnippetResult(vm, basDbgRunSnippet(vm), printFn, ctx);
+ reportSnippetResult(vm, basDbgRunSnippet(vm, pump), printFn, ctx);
basVmDestroy(vm);
basModuleFree(mod);
return false;
@@ -298,12 +298,13 @@ const BasDebugVarT *basDbgFindVar(const BasVmT *vm, const char *name) {
void basDbgFormatValue(const BasVmT *vm, const BasValueT *v, char *buf, int32_t bufSize) {
switch (v->type) {
+ // Numbers as PRINT and CStr show them
case BAS_TYPE_INTEGER:
case BAS_TYPE_BYTE:
- snprintf(buf, bufSize, "%d", (int)v->intVal);
- break;
-
- case BAS_TYPE_CURRENCY: {
+ case BAS_TYPE_CURRENCY:
+ case BAS_TYPE_LONG:
+ case BAS_TYPE_SINGLE:
+ case BAS_TYPE_DOUBLE: {
BasStringT *text = basValFormatString(*v);
snprintf(buf, bufSize, "%s", text->data);
@@ -311,18 +312,6 @@ void basDbgFormatValue(const BasVmT *vm, const BasValueT *v, char *buf, int32_t
break;
}
- case BAS_TYPE_LONG:
- snprintf(buf, bufSize, "%ld", (long)v->longVal);
- break;
-
- case BAS_TYPE_SINGLE:
- snprintf(buf, bufSize, "%.6g", (double)v->sngVal);
- break;
-
- case BAS_TYPE_DOUBLE:
- snprintf(buf, bufSize, "%.10g", v->dblVal);
- break;
-
case BAS_TYPE_BOOLEAN:
snprintf(buf, bufSize, "%s", v->boolVal ? "True" : "False");
break;
@@ -584,12 +573,16 @@ BasValueT *basDbgResolvePath(BasVmT *vm, const char *path, const char **endPtr)
}
-BasVmResultE basDbgRunSnippet(BasVmT *vm) {
+BasVmResultE basDbgRunSnippet(BasVmT *vm, const BasDbgPumpT *pump) {
BasVmResultE result = BAS_VM_STEP_LIMIT;
basVmSetStepLimit(vm, DBG_SNIPPET_SLICE);
for (int32_t slice = 0; slice < DBG_SNIPPET_SLICES && result == BAS_VM_STEP_LIMIT; slice++) {
+ if (slice > 0 && pump && pump->fn) {
+ pump->fn(pump->ctx);
+ }
+
result = basVmRun(vm);
}
@@ -597,7 +590,7 @@ BasVmResultE basDbgRunSnippet(BasVmT *vm) {
}
-bool basDbgSetVar(BasVmT *pausedVm, const char *path, const char *expr, BasPrintFnT printFn, void *ctx) {
+bool basDbgSetVar(BasVmT *pausedVm, const char *path, const char *expr, BasPrintFnT printFn, void *ctx, const BasDbgPumpT *pump) {
const char *start = dvxSkipWs(path);
const char *end = NULL;
BasValueT *slot = isPaused(pausedVm) ? basDbgResolvePath(pausedVm, start, &end) : NULL;
@@ -607,7 +600,7 @@ bool basDbgSetVar(BasVmT *pausedVm, const char *path, const char *expr, BasPrint
return false;
}
- return assignSlot(pausedVm, slot, start, (int32_t)(end - start), expr, printFn, ctx);
+ return assignSlot(pausedVm, slot, start, (int32_t)(end - start), expr, printFn, ctx, pump);
}
@@ -811,7 +804,7 @@ static void capturePrint(void *ctx, const char *text, bool newline) {
// Evaluate the right-hand side of an assignment as a BASIC expression
// (it may use the paused program's variables) converted to targetType.
// Reports an error to printFn and returns false.
-static bool evalRhs(BasVmT *vm, const char *rhs, uint8_t targetType, BasValueT *outVal, BasPrintFnT printFn, void *ctx) {
+static bool evalRhs(BasVmT *vm, const char *rhs, uint8_t targetType, BasValueT *outVal, BasPrintFnT printFn, void *ctx, const BasDbgPumpT *pump) {
char src[DBG_RHS_SRC_LEN];
snprintf(src, sizeof(src), "%s %s AS %s: %s = %s", DBG_KW_DIM, DBG_RHS_VAR, basDbgTypeName(targetType), DBG_RHS_VAR, rhs);
@@ -826,7 +819,7 @@ static bool evalRhs(BasVmT *vm, const char *rhs, uint8_t targetType, BasValueT *
return false;
}
- BasVmResultE result = basDbgRunSnippet(svm);
+ BasVmResultE result = basDbgRunSnippet(svm, pump);
bool ok = (result == BAS_VM_HALTED || result == BAS_VM_OK) && idx >= 0 && idx < BAS_VM_MAX_GLOBALS;
if (ok) {
@@ -1002,7 +995,7 @@ static void reportSnippetResult(BasVmT *vm, BasVmResultE result, BasPrintFnT pri
// "var = expr" (LET optional) when paused: assigns to the program's
// variable. Returns true when line was handled as an assignment (*assigned
// tells whether it succeeded); false lets the caller run it as a snippet.
-static bool tryAssign(BasVmT *vm, const char *line, BasPrintFnT printFn, void *ctx, bool *assigned) {
+static bool tryAssign(BasVmT *vm, const char *line, BasPrintFnT printFn, void *ctx, bool *assigned, const BasDbgPumpT *pump) {
if (!isPaused(vm)) {
return false;
}
@@ -1034,6 +1027,6 @@ static bool tryAssign(BasVmT *vm, const char *line, BasPrintFnT printFn, void *c
return false;
}
- *assigned = assignSlot(vm, slot, p, (int32_t)(afterLhs - p), rhs, printFn, ctx);
+ *assigned = assignSlot(vm, slot, p, (int32_t)(afterLhs - p), rhs, printFn, ctx, pump);
return true;
}
diff --git a/src/apps/kpunch/dvxbasic/basdev/basDebug.h b/src/apps/kpunch/dvxbasic/basdev/basDebug.h
index 652b0ffd..ebb5047d 100644
--- a/src/apps/kpunch/dvxbasic/basdev/basDebug.h
+++ b/src/apps/kpunch/dvxbasic/basdev/basDebug.h
@@ -75,18 +75,26 @@ int32_t basDbgCallStack(BasVmT *vm, int32_t curLine, BasDbgRowFnT fn, void *ctx)
// The compiled procedure the paused vm is in, or -1 at module level.
int32_t basDbgCurProcIdx(const BasVmT *vm);
+// What a snippet calls between its slices (basDbgRunSnippet), so a long
+// one lets other work go on: fn(ctx). The calls below take one (NULL:
+// none).
+typedef struct {
+ void (*fn)(void *ctx);
+ void *ctx;
+} BasDbgPumpT;
+
// A watch: a variable path is read directly, anything else is evaluated
// as an expression in the paused program (pausedVm NULL: fails). out
// receives the value text; false (out undefined) when it cannot be
// evaluated.
-bool basDbgEval(BasVmT *pausedVm, const char *expr, char *out, int32_t outSize);
+bool basDbgEval(BasVmT *pausedVm, const char *expr, char *out, int32_t outSize, const BasDbgPumpT *pump);
// An Immediate-window line: "var = expr" (LET optional) assigns to the
// paused program's variable; a line starting with PRINT, DIM or LET runs
// as it is; anything else is printed (PRINT expr). Everything it shows
// -- the output, "name = value" after an assignment, "Error: ..." -- goes
// to printFn. True when a variable of the program was changed.
-bool basDbgExec(BasVmT *pausedVm, const char *line, BasPrintFnT printFn, void *ctx);
+bool basDbgExec(BasVmT *pausedVm, const char *line, BasPrintFnT printFn, void *ctx, const BasDbgPumpT *pump);
// The variable called name that is visible where vm is paused, or NULL.
const BasDebugVarT *basDbgFindVar(const BasVmT *vm, const char *name);
@@ -117,13 +125,14 @@ const char *basDbgProcNameForPc(const BasVmT *vm, int32_t pc);
BasValueT *basDbgResolvePath(BasVmT *vm, const char *path, const char **endPtr);
// Run a snippet VM (basDbgSnippetVm) in bounded slices, so an endless
-// loop cannot hang the host. BAS_VM_STEP_LIMIT means it was abandoned.
-BasVmResultE basDbgRunSnippet(BasVmT *vm);
+// loop cannot hang the host, with pump between them. BAS_VM_STEP_LIMIT
+// means it was abandoned.
+BasVmResultE basDbgRunSnippet(BasVmT *vm, const BasDbgPumpT *pump);
// Set variable path (see basDbgResolvePath) of the paused program to the
// value of expr, converted to its type (scalars only). printFn receives
// "path = value" or why it failed. True when it was set.
-bool basDbgSetVar(BasVmT *pausedVm, const char *path, const char *expr, BasPrintFnT printFn, void *ctx);
+bool basDbgSetVar(BasVmT *pausedVm, const char *path, const char *expr, BasPrintFnT printFn, void *ctx, const BasDbgPumpT *pump);
// Compile src as a snippet and return a VM ready to run it; with a
// paused VM, its visible variables are the snippet's globals, holding
diff --git a/src/apps/kpunch/dvxbasic/basdev/basDevBas.c b/src/apps/kpunch/dvxbasic/basdev/basDevBas.c
index b72c2d5b..2ffedb38 100644
--- a/src/apps/kpunch/dvxbasic/basdev/basDevBas.c
+++ b/src/apps/kpunch/dvxbasic/basdev/basDevBas.c
@@ -74,18 +74,19 @@
DVX_ABI_STAMP(basdev);
-#define SHELL_CLEANUP_SYM "shellRegisterAppCleanup"
-#define DEV_OUTPUT_MAX 32768 // PRINT text kept for RunOutput$ (oldest lines dropped)
-#define DEV_MESSAGE_LEN (BAS_DBG_VALUE_LEN * 2)
-#define DEV_INPUT_PROMPT "Enter value:"
-#define DEV_KIND_SUB "Sub"
-#define DEV_KIND_FUNCTION "Function"
-#define DEV_LINE_SEPS " ,\t\r\n" // between DbgSetBreakpoints line numbers
-#define DEV_DECIMAL 10
-#define DEV_KIND_AUTO (-1) // CompSourceAdd: the kind follows the file name
-#define DEV_NO_INDEX (-1) // no such keyword / file
-#define DEV_TOKEN_ROW_MAX (BAS_MAX_TOKEN_LEN + DEV_MESSAGE_LEN) // one CompTokens$ row
-#define DEV_DESIGN_BUSY "The design UserControls' code is running; try again when it is done."
+#define SHELL_CLEANUP_SYM "shellRegisterAppCleanup"
+#define DEV_OUTPUT_MAX 32768 // PRINT text kept for RunOutput$ (oldest lines dropped)
+#define DEV_MESSAGE_LEN (BAS_DBG_VALUE_LEN * 2)
+#define DEV_INPUT_PROMPT "Enter value:"
+#define DEV_KIND_SUB "Sub"
+#define DEV_KIND_FUNCTION "Function"
+#define DEV_LINE_SEPS " ,\t\r\n" // between DbgSetBreakpoints line numbers
+#define DEV_DECIMAL 10
+#define DEV_KIND_AUTO (-1) // CompSourceAdd: the kind follows the file name
+#define DEV_NO_INDEX (-1) // no such keyword / file
+#define DEV_TOKEN_ROW_MAX (BAS_MAX_TOKEN_LEN + DEV_MESSAGE_LEN) // one CompTokens$ row
+#define DEV_YIELD_MS 50 // a compile or a debugger snippet lets the other tasks run this often (devYield)
+#define DEV_DESIGN_BUSY "The design UserControls' code is running; try again when it is done."
// The states RunState reports (debug.bas RUN_*)
#define RUN_READY 0
@@ -176,15 +177,16 @@ typedef void (*DevCleanupRegFnT)(DevCleanupFnT fn);
// Module state
// ============================================================
-static DevHandleT *sHandles = NULL; // stb_ds; handle = index + 1
-static bool sCleanupHooked = false;
-static BasTextT sText; // the last string result
-static char *sCompError = NULL; // stb_ds: why the last Comp* call failed
-static int32_t sCompErrorLine = 0; // concatenated line of the last compile error
-static int32_t sCompErrorCol = 0;
-static int32_t sCompFileLine = 0; // the same line in its file
-static char sCompErrorFile[DVX_MAX_PATH];
-static char sDbgMessage[DEV_MESSAGE_LEN]; // the last Dbg* / Run* message
+static DevHandleT *sHandles = NULL; // stb_ds; handle = index + 1
+static bool sCleanupHooked = false;
+static BasTextT sText; // the last string result
+static char *sCompError = NULL; // stb_ds: why the last Comp* call failed
+static int32_t sCompErrorLine = 0; // concatenated line of the last compile error
+static int32_t sCompErrorCol = 0;
+static int32_t sCompFileLine = 0; // the same line in its file
+static char sCompErrorFile[DVX_MAX_PATH];
+static PlatformTicksT sYieldAt = 0; // when devYield last let the other tasks run
+static char sDbgMessage[DEV_MESSAGE_LEN]; // the last Dbg* / Run* message
// ============================================================
// Prototypes
@@ -253,6 +255,7 @@ static bool devInput(void *ctx, const char *prompt, char *buf, int32_t bu
static void devOnBreak(void *ctx, int32_t line);
static void devPrint(void *ctx, const char *text, bool newline);
static const char *devTitle(const DevSourceT *s);
+static void devYield(void *ctx);
static int32_t handleNew(DevKindE kind, void *obj);
static void *handleOf(int32_t handle, DevKindE kind);
static void handleRelease(int32_t idx);
@@ -303,6 +306,9 @@ static void sweepDeferred(void);
static void tokenRow(void *ctx, BasSrcKindE kind, int32_t line, int32_t col, int32_t pos, int32_t len);
static void withEventsRow(void *ctx, const char *name, const char *className);
+// The debugger's snippets let the other tasks run (devYield)
+static const BasDbgPumpT sDevPump = { devYield, NULL };
+
// Shell app-cleanup hook: free what a program that ended left open (its
// code has unwound, so nothing of it is on the stack). Its runtime let
@@ -465,7 +471,7 @@ int32_t CompCompile(int32_t src, int32_t optionExplicit, int32_t validate) {
}
BasCompileErrorT err;
- BasModuleT *mod = basBuildCompile(s->ps.source, s->ps.sourceLen, optionExplicit || s->ps.optionExplicit, validate ? &cb : NULL, &v.compiler, &err);
+ BasModuleT *mod = basBuildCompile(s->ps.source, s->ps.sourceLen, optionExplicit || s->ps.optionExplicit, validate ? &cb : NULL, &v.compiler, &err, devYield, NULL);
basValidateFree(&v);
@@ -907,7 +913,7 @@ const char *DbgEval(int32_t run, const char *expr) {
return "";
}
- return basTextSet(&sText, basDbgEval(r->rt->vm, expr, out, sizeof(out)) ? out : BAS_DBG_EVAL_ERROR);
+ return basTextSet(&sText, basDbgEval(r->rt->vm, expr, out, sizeof(out), &sDevPump) ? out : BAS_DBG_EVAL_ERROR);
}
@@ -920,7 +926,7 @@ const char *DbgExec(int32_t run, const char *line) {
cap.cap = sizeof(out);
cap.len = 0;
out[0] = '\0';
- basDbgExec(r ? r->rt->vm : NULL, line ? line : "", collectPrint, &cap);
+ basDbgExec(r ? r->rt->vm : NULL, line ? line : "", collectPrint, &cap, &sDevPump);
return basTextSet(&sText, out);
}
@@ -1033,7 +1039,7 @@ int32_t DbgSetVar(int32_t run, const char *path, const char *expr) {
cap.len = 0;
sDbgMessage[0] = '\0';
- bool ok = basDbgSetVar(r ? r->rt->vm : NULL, path ? path : "", expr ? expr : "", collectPrint, &cap);
+ bool ok = basDbgSetVar(r ? r->rt->vm : NULL, path ? path : "", expr ? expr : "", collectPrint, &cap, &sDevPump);
dvxTrimRight(sDbgMessage);
return ok ? BAS_TRUE : BAS_FALSE;
@@ -1111,12 +1117,12 @@ static bool devDoEvents(void *ctx) {
return false;
}
- if (run->parentRt && run->parentRt->eventDepth == 0 && !basFormRtKeepsRunning(run->parentRt)) {
+ if (run->parentRt && !basFormRtCodeOnStack(run->parentRt) && !basFormRtKeepsRunning(run->parentRt)) {
return false;
}
run->busy++;
- bool ok = dvxUpdate(ac);
+ bool ok = basFormRtUpdate(run->rt);
run->busy--;
return ok && !run->stopRequested;
@@ -1171,6 +1177,19 @@ static const char *devTitle(const DevSourceT *s) {
}
+// basBuildCompile's unit callback in CompCompile and the debugger's
+// snippet pump: every DEV_YIELD_MS the other tasks run (the program's own
+// events wait, so its sources and its paused run stay as they are).
+static void devYield(void *ctx) {
+ (void)ctx;
+
+ if (platformClock() - sYieldAt >= PLATFORM_MS_TO_TICKS(DEV_YIELD_MS)) {
+ basFormRtYield();
+ sYieldAt = platformClock();
+ }
+}
+
+
static int32_t handleNew(DevKindE kind, void *obj) {
if (!sCleanupHooked) {
DevCleanupRegFnT reg = (DevCleanupRegFnT)dlsym(NULL, DVX_SYM(SHELL_CLEANUP_SYM));
@@ -1789,7 +1808,7 @@ void RunFree(int32_t run) {
// Whether the child's code is on the stack (a handler of it, or a call
// of this library into it).
static bool runOnStack(const DevRunT *run) {
- return run->busy > 0 || (run->rt && run->rt->eventDepth > 0);
+ return run->busy > 0 || (run->rt && basFormRtCodeOnStack(run->rt));
}
diff --git a/src/apps/kpunch/dvxbasic/basdev/basHelpMap.c b/src/apps/kpunch/dvxbasic/basdev/basHelpMap.c
index a826eaf1..5fcef13d 100644
--- a/src/apps/kpunch/dvxbasic/basdev/basHelpMap.c
+++ b/src/apps/kpunch/dvxbasic/basdev/basHelpMap.c
@@ -161,8 +161,11 @@ static const BasHelpMapEntryT sHelpMap[] = {
// String functions
{"Asc", "lang.func.string"},
+ {"AscW", "lang.func.string"},
{"Chr", "lang.func.string"},
{"Chr$", "lang.func.string"},
+ {"ChrW", "lang.func.string"},
+ {"ChrW$", "lang.func.string"},
{"Environ", "lang.func.string"},
{"Environ$", "lang.func.string"},
{"Format", "lang.func.string"},
@@ -224,6 +227,7 @@ static const BasHelpMapEntryT sHelpMap[] = {
{"InputBox", "lang.func.misc"},
{"InputBox$", "lang.func.misc"},
{"LoadPicture", "lang.func.misc"},
+ {"LoadPictureData", "lang.func.misc"},
{"MsgBox", "lang.func.misc"},
// SQL functions
@@ -232,12 +236,18 @@ static const BasHelpMapEntryT sHelpMap[] = {
{"SQLEof", "lang.sql"},
{"SQLError", "lang.sql"},
{"SQLError$", "lang.sql"},
+ {"SQLEscape", "lang.sql"},
+ {"SQLEscape$", "lang.sql"},
{"SQLExec", "lang.sql"},
{"SQLField", "lang.sql"},
{"SQLField$", "lang.sql"},
{"SQLFieldCount", "lang.sql"},
{"SQLFieldDbl", "lang.sql"},
{"SQLFieldInt", "lang.sql"},
+ {"SQLFieldName", "lang.sql"},
+ {"SQLFieldName$", "lang.sql"},
+ {"SQLFieldText", "lang.sql"},
+ {"SQLFieldText$", "lang.sql"},
{"SQLFreeResult", "lang.sql"},
{"SQLNext", "lang.sql"},
{"SQLOpen", "lang.sql"},
diff --git a/src/apps/kpunch/dvxbasic/basdev/basdev.dhs b/src/apps/kpunch/dvxbasic/basdev/basdev.dhs
index ea9c0bf8..66e210b7 100644
--- a/src/apps/kpunch/dvxbasic/basdev/basdev.dhs
+++ b/src/apps/kpunch/dvxbasic/basdev/basdev.dhs
@@ -46,9 +46,9 @@
.h1 BASIC Development Library
-basdev.lib is the DVX BASIC compiler and what a development tool builds on it: the build pipeline (basBuild.h), the compile-time Control.Member validator (basValidate.h), the debugger's view of a paused program (basDebug.h), and the SUB / FUNCTION scanner (basProcs.h). The DVX BASIC IDE uses it, and BASIC programs reach it through compiler.bas and debug.bas (basDevBas.c holds their entry points). It also carries the STUB and noicon resources every build inside DVX uses (BAS_BUILD_STUB_CARRIER). It needs basrt.lib, which holds the run host (basRunHost.h) the IDE, the standalone stub, and debug.bas share.
+basdev.lib is the DVX BASIC compiler and what a development tool builds on it: the build pipeline (basBuild.h), the compile-time Control.Member validator (basValidate.h), the debugger's view of a paused program (basDebug.h), the SUB / FUNCTION scanner (basProcs.h), and the .frm model (frmModel.h) the IDE's designer works on. The DVX BASIC IDE uses it, and BASIC programs reach it through compiler.bas, debug.bas, and designer.bas's Frm* functions (basDevBas.c and frmModelBas.c hold their entry points). It also carries the STUB and noicon resources every build inside DVX uses (BAS_BUILD_STUB_CARRIER). It needs basrt.lib, which holds the run host (basRunHost.h) the IDE, the standalone stub, and debug.bas share.
-Headers: apps/dvxbasic/basBuild.h, apps/dvxbasic/basdev/basValidate.h, apps/dvxbasic/basdev/basDebug.h, apps/dvxbasic/basdev/basProcs.h, apps/dvxbasic/basdev/basSource.h, apps/dvxbasic/basdev/basHelpMap.h, apps/dvxbasic/basdev/basProject.h, apps/dvxbasic/formrt/basRunHost.h
+Headers: apps/dvxbasic/basBuild.h, apps/dvxbasic/basdev/basValidate.h, apps/dvxbasic/basdev/basDebug.h, apps/dvxbasic/basdev/basProcs.h, apps/dvxbasic/basdev/basSource.h, apps/dvxbasic/basdev/basHelpMap.h, apps/dvxbasic/basdev/basProject.h, apps/dvxbasic/basdev/frmModel.h, apps/dvxbasic/formrt/basRunHost.h
.h2 Compiling (basBuild.h)
@@ -57,7 +57,7 @@ Headers: apps/dvxbasic/basBuild.h, apps/dvxbasic/basdev/basValidate.h, apps/dvxb
-------- -----------
basBuildAddSource Append one file to a BasProjectSourceT the way a project is built: a module in BEGINMODULE, a class module's code in BEGINCLASS, a UserControl's code in BEGINCLASS ... USERCONTROL (its header is kept in ctlTexts), a form's code section in BEGINFORM with its controls (the .frm text is kept in frmTexts), a lone program unwrapped. Records the file in the source map.
basBuildConcatProject Read every file an open .dbp names (modules, class modules, UserControls, forms) into a BasProjectSourceT through basBuildAddSource.
- basBuildCompile Compile source into a BasModuleT with OPTION EXPLICIT and an optional BasCtrlValidatorT. A BasCompileErrorT says why it failed: BAS_COMPILE_ERROR (line, column, message), BAS_COMPILE_NO_MEMORY, or BAS_COMPILE_NO_MODULE.
+ basBuildCompile Compile source into a BasModuleT with OPTION EXPLICIT and an optional BasCtrlValidatorT. A BasCompileErrorT says why it failed: BAS_COMPILE_ERROR (line, column, message), BAS_COMPILE_NO_MEMORY, or BAS_COMPILE_NO_MODULE. An optional unit callback runs after each statement or procedure compiled (basCompilerSetUnitFn), so a long compile can let other work go on.
basBuildErrorText A compiler message without its "Line N, Col M: " prefix.
basBuildMapLine Line of the concatenated source to file and file line.
basBuildMapFileLine File and file line to the line of the concatenated source (names compared as DOS does: any case, / and \ alike).
@@ -74,7 +74,7 @@ A BasValidatorT (zeroed) collects the forms, controls, and menu items of the .fr
.h2 Debugging (basDebug.h)
-Every function takes the paused program's VM (or NULL when there is none).
+Every function takes the paused program's VM (or NULL when there is none). The ones that run a snippet (basDbgEval, basDbgExec, basDbgSetVar, basDbgRunSnippet) take a BasDbgPumpT too (NULL for none): its fn(ctx) runs between the snippet's slices, so a long snippet lets other work go on (the IDE shows a frame with input held off; compiler.bas lets the other tasks run).
.table
Function Description
@@ -116,7 +116,7 @@ A UserControl's header (the Begin UserControl block of a .ctl) is read and chang
.h2 Running a Program (basRunHost.h, in basrt.lib)
-basRunHostCreate makes the VM and form runtime for a module from a BasRunSpecT: App.Path, App.User (from the program's file: its folder in the user folder), App.Data (the DATA folder beside it), COMMAND$, the print, input, and DoEvents callbacks, the screen window's name, the help file, whether runtime errors show their own box, and whether it is a standalone program (the stub's own: the runtime owns the module, frees it with itself, and is ended through the shell's kill hook, shellRegisterAppKill, when the program is ended by force; the IDE and basdev end their runtimes themselves). basRunHostRegisterFrm registers a .frm text, basRunHostLoadForms loads the forms and shows the startup form, basRunHostSlice runs module code once under the step limit (an error's message and line are always set afterwards), basRunHostAlive says whether the event-loop phase goes on, basRunHostTagBreak puts BAS_RUN_BREAK_TAG on the captions while paused, and basRunHostDestroy ends it. The runtime made inside another program's code does not become the current runtime (basFormRtCurrent / basFormRtSetCurrent); each of these calls makes it current while its code runs.
+basRunHostCreate makes the VM and form runtime for a module from a BasRunSpecT: App.Path, App.User (from the program's file: its folder in the user folder), App.Data (the DATA folder beside it), COMMAND$, the print, input, and DoEvents callbacks, the screen window's name, the help file, whether runtime errors show their own box, whether it is a standalone program (the stub's own: the runtime owns the module and frees it with itself), and whether it ends with its program (endWithApp: the IDE's runs, on a task of their own). A standalone runtime and one made with endWithApp are ended through the shell's kill hook, shellRegisterAppKill, when the program they run for is ended by force; basdev ends its runs itself. A runtime made on one task and run on another (the IDE makes it in a menu command and runs it on a work task, shellAppTaskStart) is moved with basFormRtAdopt, and basFormRtYield lets the other tasks run once without running any of the program's events. basRunHostRegisterFrm registers a .frm text, basRunHostLoadForms loads the forms and shows the startup form, basRunHostSlice runs module code once under the step limit (an error's message and line are always set afterwards), basRunHostAlive says whether the event-loop phase goes on, basRunHostTagBreak puts BAS_RUN_BREAK_TAG on the captions while paused, and basRunHostDestroy ends it. The runtime made inside another program's code does not become the current runtime (basFormRtCurrent / basFormRtSetCurrent); each of these calls makes it current while its code runs.
VM accessors a debugger host uses (vm.h): basVmSetAppPaths, basVmSetBreakpointCallback, basVmSetDebugPaused, basVmResume, basVmStop, basVmClearStepState, basVmGetCurrentLine, basVmGetErrorLine.
@@ -125,3 +125,144 @@ VM accessors a debugger host uses (vm.h): basVmSetAppPaths, basVmSetBreakpointCa
compiler.bas and debug.bas declare the entry points (see Include Libraries in the BASIC language reference). Source sets, modules, and runs are handles freed with their program. A run is polled: RunPoll runs the child for a bounded time and never waits. A pause in the child's module code returns RUN_PAUSED; a pause inside one of its event handlers (or its forms' Load code, inside RunStart) waits in place while the child pumps DVX events, so the caller's Timer and menus can step, continue, or stop it; RunPoll called from there returns at once. RunFree while the child's code is on the stack stops it at once and releases it at the next library call after that code has unwound.
CompDesignControls registers a source set's UserControls for the calling program's design forms (basCtlLoadDesign with its runtime, the current one: a child run's own when the child calls it) and holds the module -- one of the caller's own handles; another program's is refused -- until CompDesignRelease, the next CompDesignControls, or the end of that runtime (appEnded for a program, runDestroyHost for a child run), CompModuleFree meanwhile only marking it.
+
+.link lib.basdev.frmmodel The .frm Model (frmModel.h)
+
+.topic lib.basdev.frmmodel
+.title The .frm Model
+.toc 1 The .frm Model
+.index FrmModelT
+.index FrmCtrlT
+.index FrmMenuItemT
+.index frmModelLoad
+.index frmModelWrite
+.index frmModelText
+.index frmModelFromForm
+.index basFontChoiceOf
+.index basFontPropText
+.index Font properties (C)
+
+.h1 The .frm Model
+
+A form file as data, with no widgets and no windows: the form's properties, its controls (type, name, control array index, parent, the built-in size and state fields, and every other "Key = Value" pair as text), its menu items, and its code section. frmModelLoad reads .frm text (through frmParse) and frmModelWrite writes the canonical text the DVX BASIC IDE saves. The IDE's designer, properties window, menu editor, and copy and paste all work on this model and keep only their live widgets on the side (each control's userData); designer.bas binds it for BASIC programs (the Frm* functions, basdev/frmModelBas.c).
+
+Header: apps/dvxbasic/basdev/frmModel.h (part of basdev.lib)
+
+.h2 Types
+
+.table
+ Type Description
+ ---- -----------
+ FrmModelT A form: name, caption, width / height / left / top, padding, spacing, windowState, layout, helpTopic, contextMenu, centered / autoSize / resizable, controls (stb_ds array of FrmCtrlT pointers in tree order: each control directly followed by its children), menuItems (stb_ds array), props (other runtime form properties that were set: Icon, Tag, MousePointer, the Font properties ...), code (malloc'd, NULL = none), dirty (changed by an edit since it was made or loaded).
+ FrmCtrlT A control: name, typeName, parentName + parentIndex (the parent's control array index, so a child of one member of a container array belongs to that member), index (FRM_NO_INDEX: not in an array), left, top, width (MinWidth), height (MinHeight), maxWidth, maxHeight, weight, dragMode, helpTopic, visible, enabled, props (stb_ds array of FrmPropT: every other key, unquoted), userData (the caller's; the model never reads it and loads, copies, and pastes start it at NULL).
+ FrmMenuItemT A menu item: caption ("-" for a separator), name, shortcut, shortcutText, level (0: a menu-bar menu or a popup; 1: its items; 2+: submenus), id (an editor identity, see frmModelMenuReplace), checked, checkable (Checked was written), radioCheck, enabled, visible (False on a level-0 item: a popup menu).
+ FrmPropT One stored "Key = Value": name, value.
+ FrmIntPropT One built-in integer property (Left, Top, MinWidth alias Width, MinHeight alias Height, MaxWidth, MaxHeight, Weight, DragMode): name, alias, offset into FrmCtrlT, saveRule (FRM_SAVE_ALWAYS / IF_NONZERO / IF_POSITIVE).
+.endtable
+
+Only values somebody set are stored and written: a property a control has by default (a PictureBox's BorderStyle, a TextBox's MaxLength) is not in the model and not in the file. Values are written in the canonical form for their widget interface type, or for a key the widget does not know, for their type in the runtime's common property table (STRING quoted, BOOL True / False, INT a number, FLOAT and ENUM unquoted); other keys are written as quoted strings. The limits come from the parser's: FRM_MAX_NEST_DEPTH control levels, FRM_MAX_MENU_LEVEL menu levels, and no property line longer than FRM_MAX_LINE_LEN - 1 characters (the writer fails with FRM_WRITE_LINE_TOO_LONG rather than write one the parser would cut).
+
+.h2 Loading and Writing
+
+.table
+ Function Description
+ -------- -----------
+ frmModelNew(name) A blank form with the runtime form's defaults.
+ frmModelLoad(text, len) The model of .frm text; NULL when it is not a form (no Begin Form, frmParse refuses it, two controls share a name and index) or out of memory. A Key = Value line is the innermost open block's, also after the blocks inside it, as the runtime reads it; a child names its container by the container's final Index. Of several OptionButtons of one container saved with Value True, the last keeps it.
+ frmModelFree(m) Frees the model, its controls, and menus.
+ frmModelWrite(m, buf, bufSize) The .frm text into buf: its length, or FRM_WRITE_TOO_LARGE / FRM_WRITE_LINE_TOO_LONG (never a partial text).
+ frmModelWriteSize(m) A buffer size that always holds the text.
+ frmModelText(m, &len) The text in a new malloc'd string; NULL with len set to the FRM_WRITE_* code.
+ frmModelSetCode(m, code) Replaces the code section.
+ frmCodeSection(trailing, len, &outLen) The code section of the text after a form's End (blank lines skipped, the first code line keeps its indent).
+ frmModelFromForm(rt, form) A model of a loaded form as it is now: its properties, its controls in their containers with each property that differs from a new control of the type (compared with one made in a hidden window; the six Font properties of the form and of each control that has a font of its own), its menus; no code. What FormText$ writes.
+.endtable
+
+.h2 Properties
+
+.table
+ Function Description
+ -------- -----------
+ frmModelFormProp(m, name, out, size) A form property as text (an unset design property gives "", "0", or "False"); false for run-time-only ones (Visible, DesignMode, DesignSelection, object properties).
+ frmModelSetFormProp(m, name, value) Sets one from text; Name renames the form; an empty value takes an optional one out.
+ frmCtrlPropText(ctrl, name, out, size) A control property as the control holds it (Name, Index, the built-in integers, HelpTopic, Visible / Enabled unless the widget defines them, the stored ones).
+ frmCtrlSetPropText(ctrl, key, value) Applies one the way the loader does (a VB TabIndex is dropped unless the widget defines it).
+ frmModelSetCtrlProp(m, ctrl, name, value) The same with an editor's rules: Name renames the control array, Index makes an array member, then the OptionButton group rule.
+ frmCtrlGetProp / frmCtrlSetProp / frmCtrlRemoveProp The stored properties.
+ frmIntPropAt / frmIntPropFind / frmIntPropGet / Set The built-in integer table.
+ frmIfaceProp(typeName, propName) The widget interface property of a type, NULL when none.
+.endtable
+
+.h2 Font Properties
+
+FontName, FontSize, FontBold, FontItalic, FontUnderline, and FontStrikethru (BAS_PROP_FONT*) are in both the form table and the common control table. They are one font, a DvxFontChoiceT (dvxDlg.h: family, size in pixels, FONT_STYLE_* flags), and the widget's font (wgtFont, wgtSetFont) is the only place they live: a read gives the font the object draws with (a form's is its root widget's), a write gives it that font with the one part changed. The .frm loader collects a block's Font properties and opens one font when the block's properties are read, so their order in the file does not matter. Helpers in formrt.h, for loaders and designers that hold them as .frm text:
+
+.table
+ Function Description
+ -------- -----------
+ basFontPropAt(idx) The Font property names in .frm order (FontName, FontSize, then the flags), NULL past the end.
+ basFontPropIs(name) True for a Font property name (any case).
+ basFontChoiceOf(font, out) A BitmapFontT as the properties name it (dvxFontFamilyOf, pixelSize, style).
+ basFontPropText(choice, name, out, size) One property of a choice as .frm text (the family, the size, True / False).
+ basFontPropSetText(choice, name, text) Sets one property of a choice from .frm text.
+.endtable
+
+.h2 Editing Controls
+
+.table
+ Function Description
+ -------- -----------
+ frmModelAddCtrl(m, typeName, name, parent) Adds a control as the last child of parent (NULL: the form); name "" or NULL gives a free one (frmModelAutoName); Caption = its name. NULL when the name is taken or parent cannot hold it.
+ frmModelRemoveCtrl(m, ctrl) Removes ctrl and its subtree.
+ frmModelRenameCtrl(m, oldName, newName) Renames a whole control array; children and DataSource / MasterSource of a renamed Data control follow. Identifier rules are the caller's.
+ frmModelMakeArrayMember(m, ctrl, index) Gives a control array index; its children follow.
+ frmModelMoveCtrl(m, ctrl, before) Moves ctrl with its subtree before a sibling (NULL: after the last).
+ frmModelReparent(m, ctrl, parent) Makes ctrl the last child of another container (refuses cycles, non-containers, and too deep a nesting).
+ frmModelSibling(m, idx, dir) The sibling next to a control (+1 after its subtree, -1 before).
+ frmModelFindCtrl / CtrlPos / FindParent / CtrlDepth Lookups.
+ frmModelIsDescendant / SubtreeEnd / NormalizeOrder The tree order.
+ frmModelNameInUse / AutoName / OptionSelected Names and the OptionButton group rule.
+ frmModelCtrlText(m, ctrl, &len) ctrl and its children as .frm control blocks (a copy for the Clipboard).
+ frmModelCtrlsFromText(text, len) Copied blocks read into a scratch model.
+ frmModelInsertTree(m, src, srcRoot, parent, name, index) Moves a subtree out of the scratch model into m: the root takes name / index (NULL: its own name when free, else a free one); a child whose name is taken gets a free one, the members of one array together; children follow their renamed parents.
+.endtable
+
+.h2 Menus
+
+The frmMenu* functions work on any stb_ds array of FrmMenuItemT (an editor's working copy as well as m->menuItems) and keep the levels a tree.
+
+.table
+ Function Description
+ -------- -----------
+ frmMenuItemInit(mi) A blank item (enabled, visible, level 0).
+ frmMenuInsert(&items, at, level) Inserts a blank item; the level is clamped so the menus stay a tree.
+ frmMenuRemove(&items, idx) Removes an item with its subtree.
+ frmMenuMove(&items, idx, dir) Moves an item with its subtree past its previous / next sibling; its new position or FRM_NO_INDEX.
+ frmMenuIndent(items, idx, delta) Indents / outdents an item with its subtree (at most one level below the item above, not past FRM_MAX_MENU_LEVEL).
+ frmMenuSubtreeEnd / CheckLevels / StripBlank / AutoName Tree helpers; AutoName makes mnuSep1 ... or "mnu" plus the caption's letters and digits.
+ frmMenuPropText / frmMenuSetPropText Menu properties as text (Checked makes a check item).
+ frmModelMenuValidate(m, items, reason, size) The checks before items replace m's menus (levels; every name set, an identifier as the compiler takes one, not the form's or a control's, unique; every Shortcut a key): the first bad item and the reason.
+ frmModelMenuReplace(m, items) Replaces m's menus by a copy; ContextMenu bindings follow renamed items, matched by id (give every item id = position + 1 before editing).
+ frmModelSetMenuProp(m, idx, key, value) Sets an item's property; a Name rename refuses a taken name and ContextMenu bindings follow.
+.endtable
+
+.code
+// Load a form, rename a control, add a label inside a frame, save.
+FrmModelT *m = frmModelLoad(text, len);
+
+if (m) {
+ int32_t at = frmModelFindCtrl(m, "Frame1", FRM_NO_INDEX);
+ FrmCtrlT *frame = (at >= 0) ? m->controls[at] : NULL;
+ FrmCtrlT *lbl = frmModelAddCtrl(m, "Label", "", frame);
+ int32_t outLen = 0;
+
+ frmModelRenameCtrl(m, "Command1", "cmdOk");
+ frmModelSetCtrlProp(m, lbl, "Caption", "Name:");
+
+ char *out = frmModelText(m, &outLen);
+
+ // write out[0 .. outLen) to the file
+ free(out);
+ frmModelFree(m);
+}
+.endcode
diff --git a/src/apps/kpunch/dvxbasic/formrt/frmModel.c b/src/apps/kpunch/dvxbasic/basdev/frmModel.c
similarity index 95%
rename from src/apps/kpunch/dvxbasic/formrt/frmModel.c
rename to src/apps/kpunch/dvxbasic/basdev/frmModel.c
index 5706390d..8998b16e 100644
--- a/src/apps/kpunch/dvxbasic/formrt/frmModel.c
+++ b/src/apps/kpunch/dvxbasic/basdev/frmModel.c
@@ -31,11 +31,11 @@
// whether it came from a fresh placement, a loaded file or an edit.
#include "frmModel.h"
+#include "../compiler/lexer.h"
#include "dvxApp.h"
#include "stb_ds_wrap.h"
#include
-#include
#include
#include
#include
@@ -77,14 +77,6 @@ static const FrmIntPropT sIntProps[] = {
#define FRM_INT_PROP_COUNT DVX_ARRAY_LEN(sIntProps)
-// Form properties of the runtime's table that are run-time state, never
-// part of a .frm (besides the object-typed ones).
-static const char *const sRuntimeOnlyFormProps[] = {
- BAS_PROP_VISIBLE,
- BAS_PROP_DESIGNMODE,
- BAS_PROP_DESIGNSELECTION
-};
-
// ============================================================
// .frm loading context (used by the loadOn* callbacks)
// ============================================================
@@ -123,9 +115,8 @@ typedef struct {
static void buildPad(int32_t indent, char *pad);
static bool canTakeSubtree(const FrmModelT *m, const FrmCtrlT *parent, int32_t height);
static bool ctrlsAmbiguous(const FrmModelT *m);
-static int32_t emitClampedV(char *buf, int32_t bufSize, int32_t pos, const char *fmt, va_list args);
static int32_t emitControlHead(const FrmCtrlT *ctrl, char *buf, int32_t bufSize, int32_t pos, const char *pad);
-static int32_t emitIndented(char *buf, int32_t bufSize, int32_t pos, int32_t indent, const char *fmt, ...);
+static int32_t emitEnd(char *buf, int32_t bufSize, int32_t pos, int32_t indent);
static int32_t emitPropLine(char *buf, int32_t bufSize, int32_t pos, const char *pad, const char *name, uint8_t type, const char *value);
static const BasPropDescT *formExtraProp(const char *name);
static bool formFieldText(const FrmModelT *m, const char *name, char *out, int32_t outSize);
@@ -139,7 +130,6 @@ bool frmCtrlPropText(const FrmCtrlT *ctrl, const char *nam
bool frmCtrlRemoveProp(FrmCtrlT *ctrl, const char *name);
void frmCtrlSetProp(FrmCtrlT *ctrl, const char *name, const char *value);
void frmCtrlSetPropText(FrmCtrlT *ctrl, const char *key, const char *value);
-int32_t frmEmitClamped(char *buf, int32_t bufSize, int32_t pos, const char *fmt, ...);
const WgtPropDescT *frmIfaceProp(const char *typeName, const char *propName);
const FrmIntPropT *frmIntPropAt(int32_t idx);
const FrmIntPropT *frmIntPropFind(const char *name);
@@ -171,7 +161,7 @@ bool frmModelIsDescendant(const FrmModelT *m, const FrmCtr
FrmModelT *frmModelLoad(const char *text, int32_t len);
void frmModelMakeArrayMember(FrmModelT *m, FrmCtrlT *ctrl, int32_t index);
bool frmModelMenuReplace(FrmModelT *m, const FrmMenuItemT *items);
-int32_t frmModelMenuValidate(const FrmModelT *m, const FrmMenuItemT *items, FrmIdentFnT isIdent, char *reason, int32_t reasonSize);
+int32_t frmModelMenuValidate(const FrmModelT *m, const FrmMenuItemT *items, char *reason, int32_t reasonSize);
bool frmModelMoveCtrl(FrmModelT *m, FrmCtrlT *ctrl, FrmCtrlT *before);
bool frmModelNameInUse(const FrmModelT *m, const char *name, const char *exceptName, bool checkMenus);
FrmModelT *frmModelNew(const char *name);
@@ -260,36 +250,15 @@ static bool ctrlsAmbiguous(const FrmModelT *m) {
}
-static int32_t emitClampedV(char *buf, int32_t bufSize, int32_t pos, const char *fmt, va_list args) {
- if (!buf || pos < 0 || pos >= bufSize) {
- return pos;
- }
-
- int32_t avail = bufSize - pos;
- int32_t written = (int32_t)vsnprintf(buf + pos, (size_t)avail, fmt, args);
-
- if (written < 0) {
- return pos;
- }
-
- // Cut: vsnprintf wrote avail - 1 characters and the NUL
- if (written >= avail) {
- return bufSize - 1;
- }
-
- return pos + written;
-}
-
-
// "Begin Type Name" and every property line of one control, without the
// closing End so the writer can nest the children first. Interface-typed
// keys and the runtime's common properties are written by their type;
// only other keys go out as quoted strings.
static int32_t emitControlHead(const FrmCtrlT *ctrl, char *buf, int32_t bufSize, int32_t pos, const char *pad) {
- pos = frmEmitClamped(buf, bufSize, pos, "%sBegin %s %s\n", pad, ctrl->typeName, ctrl->name);
+ pos = dvxStrAppendfCut(buf, bufSize, pos, "%sBegin %s %s\n", pad, ctrl->typeName, ctrl->name);
if (ctrl->index >= 0) {
- pos = frmEmitClamped(buf, bufSize, pos, "%s %s = %" PRId32 "\n", pad, BAS_PROP_INDEX, ctrl->index);
+ pos = dvxStrAppendfCut(buf, bufSize, pos, "%s %s = %" PRId32 "\n", pad, BAS_PROP_INDEX, ctrl->index);
}
const char *caption = frmCtrlGetProp(ctrl, BAS_PROP_CAPTION);
@@ -315,7 +284,7 @@ static int32_t emitControlHead(const FrmCtrlT *ctrl, char *buf, int32_t bufSize,
}
if (emit) {
- pos = frmEmitClamped(buf, bufSize, pos, "%s %s = %" PRId32 "\n", pad, ip->name, val);
+ pos = dvxStrAppendfCut(buf, bufSize, pos, "%s %s = %" PRId32 "\n", pad, ip->name, val);
}
}
@@ -360,17 +329,12 @@ static int32_t emitControlHead(const FrmCtrlT *ctrl, char *buf, int32_t bufSize,
}
-// One formatted line indented to an indent-level block.
-static int32_t emitIndented(char *buf, int32_t bufSize, int32_t pos, int32_t indent, const char *fmt, ...) {
- char pad[FRM_PAD_BUF];
- va_list args;
+// The End line closing an indent-level block.
+static int32_t emitEnd(char *buf, int32_t bufSize, int32_t pos, int32_t indent) {
+ char pad[FRM_PAD_BUF];
buildPad(indent, pad);
- pos = frmEmitClamped(buf, bufSize, pos, "%s", pad);
- va_start(args, fmt);
- pos = emitClampedV(buf, bufSize, pos, fmt, args);
- va_end(args);
- return frmEmitClamped(buf, bufSize, pos, "\n");
+ return dvxStrAppendfCut(buf, bufSize, pos, "%s" FRM_KW_END "\n", pad);
}
@@ -383,17 +347,17 @@ static int32_t emitPropLine(char *buf, int32_t bufSize, int32_t pos, const char
switch (type) {
case WGT_IFACE_STRING:
- end = frmEmitClamped(buf, bufSize, pos, "%s %s = \"%s\"\n", pad, name, value);
+ end = dvxStrAppendfCut(buf, bufSize, pos, "%s %s = \"%s\"\n", pad, name, value);
break;
case WGT_IFACE_BOOL:
- end = frmEmitClamped(buf, bufSize, pos, "%s %s = %s\n", pad, name, frmParseBool(value) ? FRM_TEXT_TRUE : FRM_TEXT_FALSE);
+ end = dvxStrAppendfCut(buf, bufSize, pos, "%s %s = %s\n", pad, name, frmParseBool(value) ? FRM_TEXT_TRUE : FRM_TEXT_FALSE);
break;
case WGT_IFACE_INT:
- end = frmEmitClamped(buf, bufSize, pos, "%s %s = %d\n", pad, name, atoi(value));
+ end = dvxStrAppendfCut(buf, bufSize, pos, "%s %s = %d\n", pad, name, atoi(value));
break;
default:
// FLOAT and ENUM: decimal text or the value name, unquoted
- end = frmEmitClamped(buf, bufSize, pos, "%s %s = %s\n", pad, name, value);
+ end = dvxStrAppendfCut(buf, bufSize, pos, "%s %s = %s\n", pad, name, value);
break;
}
@@ -412,16 +376,10 @@ static const BasPropDescT *formExtraProp(const char *name) {
const BasPropDescT *pd = basFormRtFindFormProp(name);
char dummy[FRM_INT_TEXT_MAX];
- if (!pd || !pd->writable || pd->type == BAS_PROP_OBJECT || formFieldText(NULL, pd->name, dummy, sizeof(dummy))) {
+ if (!pd || !pd->writable || pd->type == BAS_PROP_OBJECT || basFormRtFormPropRuntimeOnly(pd->name) || formFieldText(NULL, pd->name, dummy, sizeof(dummy))) {
return NULL;
}
- for (int32_t i = 0; i < DVX_ARRAY_LEN(sRuntimeOnlyFormProps); i++) {
- if (strcasecmp(pd->name, sRuntimeOnlyFormProps[i]) == 0) {
- return NULL;
- }
- }
-
return pd;
}
@@ -691,16 +649,6 @@ void frmCtrlSetPropText(FrmCtrlT *ctrl, const char *key, const char *value) {
}
-int32_t frmEmitClamped(char *buf, int32_t bufSize, int32_t pos, const char *fmt, ...) {
- va_list args;
-
- va_start(args, fmt);
- pos = emitClampedV(buf, bufSize, pos, fmt, args);
- va_end(args);
- return pos;
-}
-
-
const WgtPropDescT *frmIfaceProp(const char *typeName, const char *propName) {
return wgtIfaceFindProp(basCtlIface(typeName), propName);
}
@@ -1478,7 +1426,7 @@ bool frmModelMenuReplace(FrmModelT *m, const FrmMenuItemT *items) {
}
-int32_t frmModelMenuValidate(const FrmModelT *m, const FrmMenuItemT *items, FrmIdentFnT isIdent, char *reason, int32_t reasonSize) {
+int32_t frmModelMenuValidate(const FrmModelT *m, const FrmMenuItemT *items, char *reason, int32_t reasonSize) {
int32_t count = (int32_t)arrlen(items);
int32_t bad = frmMenuCheckLevels(items);
@@ -1497,7 +1445,7 @@ int32_t frmModelMenuValidate(const FrmModelT *m, const FrmMenuItemT *items, FrmI
return i;
}
- if (isIdent && !isIdent(name)) {
+ if (!basIsValidIdent(name)) {
snprintf(reason, reasonSize, "Menu name must be an identifier (letters, digits, underscores): %s", name);
return i;
}
@@ -1902,9 +1850,9 @@ int32_t frmModelWrite(const FrmModelT *m, char *buf, int32_t bufSize) {
return FRM_WRITE_TOO_LARGE;
}
- int32_t pos = frmEmitClamped(buf, bufSize, 0, FRM_VERSION_LINE);
+ int32_t pos = dvxStrAppendfCut(buf, bufSize, 0, FRM_VERSION_LINE);
- pos = frmEmitClamped(buf, bufSize, pos, FRM_KW_BEGIN " " FRM_KW_FORM " %s\n", m->name);
+ pos = dvxStrAppendfCut(buf, bufSize, pos, FRM_KW_BEGIN " " FRM_KW_FORM " %s\n", m->name);
// Form header in the runtime table's order. Name comes from the
// Begin line; Left / Top are implied by Centered and Width / Height
@@ -1964,13 +1912,13 @@ int32_t frmModelWrite(const FrmModelT *m, char *buf, int32_t bufSize) {
pos = writeControls(m, parents, buf, bufSize, pos, NULL, 1);
arrfree(parents);
- pos = frmEmitClamped(buf, bufSize, pos, FRM_KW_END "\n");
+ pos = dvxStrAppendfCut(buf, bufSize, pos, FRM_KW_END "\n");
if (pos < 0) {
return pos;
}
- // frmEmitClamped parks pos at bufSize - 1 once the buffer is full, so
+ // dvxStrAppendfCut parks pos at bufSize - 1 once the buffer is full, so
// reaching it means the form text was cut short
if (pos >= bufSize - 1) {
return FRM_WRITE_TOO_LARGE;
@@ -2099,12 +2047,15 @@ static void loadOnFormEnd(void *userData, const char *trailingSrc, int32_t trail
return;
}
- ctx->m->code = (char *)malloc((size_t)codeLen + 1);
+ char *code = (char *)malloc((size_t)codeLen + 1);
- if (ctx->m->code) {
- memcpy(ctx->m->code, codeStart, codeLen);
- ctx->m->code[codeLen] = '\0';
+ if (code) {
+ memcpy(code, codeStart, codeLen);
+ code[codeLen] = '\0';
}
+
+ free(ctx->m->code);
+ ctx->m->code = code;
}
@@ -2360,7 +2311,7 @@ static int32_t writeCtrlTree(const FrmModelT *m, FrmCtrlT **parents, const FrmCt
pos = writeControls(m, parents, buf, bufSize, pos, ctrl, indent + 1);
}
- return frmEmitClamped(buf, bufSize, pos, "%s" FRM_KW_END "\n", pad);
+ return dvxStrAppendfCut(buf, bufSize, pos, "%s" FRM_KW_END "\n", pad);
}
@@ -2378,11 +2329,11 @@ static int32_t writeMenus(const FrmModelT *m, char *buf, int32_t bufSize, int32_
// Close blocks back to this item's level
while (curLevel > mi->level) {
curLevel--;
- pos = emitIndented(buf, bufSize, pos, curLevel + 1, FRM_KW_END);
+ pos = emitEnd(buf, bufSize, pos, curLevel + 1);
}
buildPad(mi->level + 1, pad);
- pos = frmEmitClamped(buf, bufSize, pos, "%s" FRM_KW_BEGIN " " FRM_KW_MENU " %s\n", pad, mi->name);
+ pos = dvxStrAppendfCut(buf, bufSize, pos, "%s" FRM_KW_BEGIN " " FRM_KW_MENU " %s\n", pad, mi->name);
pos = emitPropLine(buf, bufSize, pos, pad, BAS_PROP_CAPTION, WGT_IFACE_STRING, mi->caption);
if (mi->checked || mi->checkable) {
@@ -2415,7 +2366,7 @@ static int32_t writeMenus(const FrmModelT *m, char *buf, int32_t bufSize, int32_
while (curLevel > 0) {
curLevel--;
- pos = emitIndented(buf, bufSize, pos, curLevel + 1, FRM_KW_END);
+ pos = emitEnd(buf, bufSize, pos, curLevel + 1);
}
return pos;
diff --git a/src/apps/kpunch/dvxbasic/formrt/frmModel.h b/src/apps/kpunch/dvxbasic/basdev/frmModel.h
similarity index 97%
rename from src/apps/kpunch/dvxbasic/formrt/frmModel.h
rename to src/apps/kpunch/dvxbasic/basdev/frmModel.h
index 4c65319f..8db45830 100644
--- a/src/apps/kpunch/dvxbasic/formrt/frmModel.h
+++ b/src/apps/kpunch/dvxbasic/basdev/frmModel.h
@@ -20,7 +20,7 @@
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
// IN THE SOFTWARE.
-// frmModel.h -- the .frm document model (BASIC run-time library)
+// frmModel.h -- the .frm document model (BASIC development library)
//
// A form file as data, with no widgets and no windows: the form's
// properties, its controls (type, name, control array index, parent,
@@ -44,8 +44,8 @@
#ifndef DVXBASIC_FRMMODEL_H
#define DVXBASIC_FRMMODEL_H
-#include "formrt.h"
-#include "frmParser.h"
+#include "../formrt/formrt.h"
+#include "../formrt/frmParser.h"
#include
#include
@@ -179,10 +179,6 @@ typedef struct FrmModelT {
bool dirty; // changed by an edit operation since it was made or loaded
} FrmModelT;
-// Identifier rule for names (the compiler's basIsValidIdent): the model
-// lives in the run-time library, which has no compiler.
-typedef bool (*FrmIdentFnT)(const char *name);
-
// ============================================================
// Text
// ============================================================
@@ -193,10 +189,6 @@ typedef bool (*FrmIdentFnT)(const char *name);
// start and length, or NULL when there is no code.
const char *frmCodeSection(const char *trailingSrc, int32_t trailingLen, int32_t *outLen);
-// Clamped formatted append into a fixed buffer: returns pos unchanged once
-// it has reached bufSize, else the new position, capped at bufSize - 1
-// when the text was cut.
-int32_t frmEmitClamped(char *buf, int32_t bufSize, int32_t pos, const char *fmt, ...);
// ============================================================
// Model lifecycle and .frm text
@@ -346,7 +338,7 @@ void frmModelRemoveCtrl(FrmModelT *m, FrmCtrlT *ctrl);
// newName: their children follow, and DataSource / MasterSource that
// named a renamed Data control too. False (nothing changed) when there
// is no such control or newName is in use. Identifier rules are the
-// caller's (FrmIdentFnT).
+// caller's (basIsValidIdent).
bool frmModelRenameCtrl(FrmModelT *m, const char *oldName, const char *newName);
// Give ctrl control array index index (FRM_NO_INDEX: no array); its
@@ -457,10 +449,10 @@ bool frmMenuPropText(const FrmMenuItemT *mi, const char *key, char *out, int32_t
bool frmMenuSetPropText(FrmMenuItemT *mi, const char *key, const char *value);
// Check a menu array before it replaces m's: the levels, and every name
-// is set, an identifier (isIdent; NULL skips this), not the form's or a
+// is set, an identifier (the compiler's basIsValidIdent), not the form's or a
// control's, not used twice; every Shortcut is a key. Returns the first
// bad item with the reason in reason, FRM_NO_INDEX when all are good.
-int32_t frmModelMenuValidate(const FrmModelT *m, const FrmMenuItemT *items, FrmIdentFnT isIdent, char *reason, int32_t reasonSize);
+int32_t frmModelMenuValidate(const FrmModelT *m, const FrmMenuItemT *items, char *reason, int32_t reasonSize);
// Replace m's menu items by a copy of items. ContextMenu bindings of the
// form and its controls follow renamed items, matched by id (see
diff --git a/src/apps/kpunch/dvxbasic/formrt/frmModelBas.c b/src/apps/kpunch/dvxbasic/basdev/frmModelBas.c
similarity index 93%
rename from src/apps/kpunch/dvxbasic/formrt/frmModelBas.c
rename to src/apps/kpunch/dvxbasic/basdev/frmModelBas.c
index 538e99b4..93b36930 100644
--- a/src/apps/kpunch/dvxbasic/formrt/frmModelBas.c
+++ b/src/apps/kpunch/dvxbasic/basdev/frmModelBas.c
@@ -24,7 +24,7 @@
// Frm*, FrmCtl*, FrmMenu*, FormText$) and the model of a live form
// (frmModelFromForm)
//
-// Called through DECLARE LIBRARY "basrt", so the signatures use only
+// Called through DECLARE LIBRARY "basdev", so the signatures use only
// BASIC-marshalable types: strings and LONG in, LONG or STRING out, True
// (-1) / False (0). A model is a handle (1, 2, ...; 0 means none) owned
// by the program that opened it and freed when that program ends.
@@ -39,7 +39,6 @@
#include "stb_ds_wrap.h"
#include "dvxBas.h"
-#include
#include
#include
#include
@@ -50,16 +49,6 @@
#define FRM_VALUE_MAX FRM_MAX_TEXT
#define FRM_SCRATCH_SIZE 64 // the hidden window new controls are made in to read their defaults
#define FRM_SCRATCH_TITLE ""
-#define SHELL_CLEANUP_SYM "shellRegisterAppCleanup"
-
-typedef void (*FrmCleanupFnT)(int32_t appId);
-typedef void (*FrmCleanupRegFnT)(FrmCleanupFnT fn);
-
-// One open model and the program it belongs to (m NULL: free slot)
-typedef struct {
- FrmModelT *m;
- int32_t appId;
-} BasFrmHandleT;
// Building the model of a live form
typedef struct {
@@ -73,15 +62,12 @@ typedef struct {
// Module state
// ============================================================
-static BasFrmHandleT *sModels = NULL; // stb_ds; handle = index + 1
-static BasTextT sText; // the last string result
-static bool sCleanupHooked = false;
+static BasTextT sText; // the last string result
// ============================================================
// Prototypes
// ============================================================
-static void appEnded(int32_t appId);
static BasControlT *ctrlForWidget(const BasFormT *form, const WidgetT *w);
static FrmCtrlT *ctrlOf(int32_t handle, int32_t i, FrmModelT **outModel);
const char *FormText(const char *formName);
@@ -129,18 +115,12 @@ static void liveMenu(FrmLiveT *live, const MenuT *menu, int32_t level)
static void liveMenuAdd(FrmLiveT *live, const char *name, const char *label, int32_t level);
static const char *liveMenuName(const BasFormT *form, const MenuT *menu);
static void liveMenus(FrmLiveT *live);
+static void modelFree(void *obj);
static FrmModelT *modelOf(int32_t handle);
static void valueText(BasValueT v, uint8_t type, char *out, int32_t outSize);
+static BasHandlesT sModels = BAS_HANDLES(modelFree, 0); // the open models
-// Shell app-cleanup hook: free the models a program that ended left open.
-static void appEnded(int32_t appId) {
- for (int32_t i = 0; i < (int32_t)arrlen(sModels); i++) {
- if (sModels[i].m && sModels[i].appId == appId) {
- FrmFree(i + 1);
- }
- }
-}
static BasControlT *ctrlForWidget(const BasFormT *form, const WidgetT *w) {
@@ -369,10 +349,7 @@ const char *FrmCtlType(int32_t handle, int32_t i) {
void FrmFree(int32_t handle) {
- if (modelOf(handle)) {
- frmModelFree(sModels[handle - 1].m);
- sModels[handle - 1].m = NULL;
- }
+ basHandleFree(&sModels, handle);
}
@@ -571,43 +548,16 @@ const char *FrmText(int32_t handle) {
}
-// A handle for m (freed when the calling program ends), 0 when m is NULL.
+// A handle for m (freed when the calling program ends), 0 when m is NULL
+// (or, m freed, when out of memory).
static int32_t handleNew(FrmModelT *m) {
- if (!m) {
- return BAS_NO_HANDLE;
+ int32_t handle = basHandleAdd(&sModels, m);
+
+ if (handle == BAS_NO_HANDLE) {
+ frmModelFree(m);
}
- if (!sCleanupHooked) {
- FrmCleanupRegFnT reg = (FrmCleanupRegFnT)dlsym(NULL, DVX_SYM(SHELL_CLEANUP_SYM));
-
- sCleanupHooked = true;
-
- if (reg) {
- reg(appEnded);
- }
- }
-
- int32_t idx = 0;
-
- while (idx < (int32_t)arrlen(sModels) && sModels[idx].m) {
- idx++;
- }
-
- if (idx == (int32_t)arrlen(sModels)) {
- BasFrmHandleT blank;
-
- memset(&blank, 0, sizeof(blank));
- arrput(sModels, blank);
-
- if (idx == (int32_t)arrlen(sModels)) {
- frmModelFree(m);
- return BAS_NO_HANDLE;
- }
- }
-
- sModels[idx].m = m;
- sModels[idx].appId = dvxMemAppIdPtr ? *dvxMemAppIdPtr : 0;
- return idx + 1;
+ return handle;
}
@@ -691,6 +641,11 @@ static void liveDefaults(FrmLiveT *live, BasControlT *ctrl, FrmCtrlT *fc) {
continue;
}
+ // A LoadPictureData picture has no file for the .frm to name
+ if (strcasecmp(p->name, BAS_PROP_PICTURE) == 0 && basPictureIsData(val)) {
+ val[0] = '\0';
+ }
+
if (!wgtIfaceGetPropText(iface, dflt, p, old, sizeof(old)) || strcmp(val, old) != 0) {
frmCtrlSetProp(fc, p->name, val);
}
@@ -762,9 +717,10 @@ static void liveForm(FrmLiveT *live) {
continue;
}
- // Icon reads as a Picture; the .frm holds its file name
+ // Icon reads as a Picture; the .frm holds its file name (none for a
+ // LoadPictureData picture)
if (strcasecmp(pd->name, BAS_PROP_ICON) == 0) {
- frmModelSetFormProp(live->m, pd->name, form->iconPath);
+ frmModelSetFormProp(live->m, pd->name, basPictureIsData(form->iconPath) ? "" : form->iconPath);
continue;
}
@@ -890,8 +846,13 @@ static void liveMenus(FrmLiveT *live) {
}
+static void modelFree(void *obj) {
+ frmModelFree((FrmModelT *)obj);
+}
+
+
static FrmModelT *modelOf(int32_t handle) {
- return (handle >= 1 && handle <= (int32_t)arrlen(sModels)) ? sModels[handle - 1].m : NULL;
+ return (FrmModelT *)basHandleGet(&sModels, handle);
}
diff --git a/src/apps/kpunch/dvxbasic/basrt.dhs b/src/apps/kpunch/dvxbasic/basrt.dhs
index b0449212..7ebe8ccd 100644
--- a/src/apps/kpunch/dvxbasic/basrt.dhs
+++ b/src/apps/kpunch/dvxbasic/basrt.dhs
@@ -39,7 +39,6 @@ Headers: apps/dvxbasic/runtime/vm.h, apps/dvxbasic/runtime/values.h
.link lib.basrt.values Value System (values.h)
.link lib.basrt.vm Virtual Machine (vm.h)
-.link lib.basrt.frmmodel The .frm Model (frmModel.h)
.topic lib.basrt.values
.title Value System
@@ -675,142 +674,3 @@ typedef struct {
debugUdtDefs UDT type definitions for the debugger.
debugUdtDefCount Number of debug UDT definitions.
.endtable
-
-.topic lib.basrt.frmmodel
-.title The .frm Model
-.toc 1 The .frm Model
-.index FrmModelT
-.index FrmCtrlT
-.index FrmMenuItemT
-.index frmModelLoad
-.index frmModelWrite
-.index frmModelText
-.index frmModelFromForm
-.index basFontChoiceOf
-.index basFontPropText
-.index Font properties (C)
-
-.h1 The .frm Model
-
-A form file as data, with no widgets and no windows: the form's properties, its controls (type, name, control array index, parent, the built-in size and state fields, and every other "Key = Value" pair as text), its menu items, and its code section. frmModelLoad reads .frm text (through frmParse) and frmModelWrite writes the canonical text the DVX BASIC IDE saves. The IDE's designer, properties window, menu editor, and copy and paste all work on this model and keep only their live widgets on the side (each control's userData); designer.bas binds it for BASIC programs (the Frm* functions, formrt/frmModelBas.c).
-
-Header: apps/dvxbasic/formrt/frmModel.h (part of basrt.lib)
-
-.h2 Types
-
-.table
- Type Description
- ---- -----------
- FrmModelT A form: name, caption, width / height / left / top, padding, spacing, windowState, layout, helpTopic, contextMenu, centered / autoSize / resizable, controls (stb_ds array of FrmCtrlT pointers in tree order: each control directly followed by its children), menuItems (stb_ds array), props (other runtime form properties that were set: Icon, Tag, MousePointer, the Font properties ...), code (malloc'd, NULL = none), dirty (changed by an edit since it was made or loaded).
- FrmCtrlT A control: name, typeName, parentName + parentIndex (the parent's control array index, so a child of one member of a container array belongs to that member), index (FRM_NO_INDEX: not in an array), left, top, width (MinWidth), height (MinHeight), maxWidth, maxHeight, weight, dragMode, helpTopic, visible, enabled, props (stb_ds array of FrmPropT: every other key, unquoted), userData (the caller's; the model never reads it and loads, copies, and pastes start it at NULL).
- FrmMenuItemT A menu item: caption ("-" for a separator), name, shortcut, shortcutText, level (0: a menu-bar menu or a popup; 1: its items; 2+: submenus), id (an editor identity, see frmModelMenuReplace), checked, checkable (Checked was written), radioCheck, enabled, visible (False on a level-0 item: a popup menu).
- FrmPropT One stored "Key = Value": name, value.
- FrmIntPropT One built-in integer property (Left, Top, MinWidth alias Width, MinHeight alias Height, MaxWidth, MaxHeight, Weight, DragMode): name, alias, offset into FrmCtrlT, saveRule (FRM_SAVE_ALWAYS / IF_NONZERO / IF_POSITIVE).
-.endtable
-
-Only values somebody set are stored and written: a property a control has by default (a PictureBox's BorderStyle, a TextBox's MaxLength) is not in the model and not in the file. Values are written in the canonical form for their widget interface type, or for a key the widget does not know, for their type in the runtime's common property table (STRING quoted, BOOL True / False, INT a number, FLOAT and ENUM unquoted); other keys are written as quoted strings. The limits come from the parser's: FRM_MAX_NEST_DEPTH control levels, FRM_MAX_MENU_LEVEL menu levels, and no property line longer than FRM_MAX_LINE_LEN - 1 characters (the writer fails with FRM_WRITE_LINE_TOO_LONG rather than write one the parser would cut).
-
-.h2 Loading and Writing
-
-.table
- Function Description
- -------- -----------
- frmModelNew(name) A blank form with the runtime form's defaults.
- frmModelLoad(text, len) The model of .frm text; NULL when it is not a form (no Begin Form, frmParse refuses it, two controls share a name and index) or out of memory. A Key = Value line is the innermost open block's, also after the blocks inside it, as the runtime reads it; a child names its container by the container's final Index. Of several OptionButtons of one container saved with Value True, the last keeps it.
- frmModelFree(m) Frees the model, its controls, and menus.
- frmModelWrite(m, buf, bufSize) The .frm text into buf: its length, or FRM_WRITE_TOO_LARGE / FRM_WRITE_LINE_TOO_LONG (never a partial text).
- frmModelWriteSize(m) A buffer size that always holds the text.
- frmModelText(m, &len) The text in a new malloc'd string; NULL with len set to the FRM_WRITE_* code.
- frmModelSetCode(m, code) Replaces the code section.
- frmCodeSection(trailing, len, &outLen) The code section of the text after a form's End (blank lines skipped, the first code line keeps its indent).
- frmModelFromForm(rt, form) A model of a loaded form as it is now: its properties, its controls in their containers with each property that differs from a new control of the type (compared with one made in a hidden window; the six Font properties of the form and of each control that has a font of its own), its menus; no code. What FormText$ writes.
-.endtable
-
-.h2 Properties
-
-.table
- Function Description
- -------- -----------
- frmModelFormProp(m, name, out, size) A form property as text (an unset design property gives "", "0", or "False"); false for run-time-only ones (Visible, DesignMode, DesignSelection, object properties).
- frmModelSetFormProp(m, name, value) Sets one from text; Name renames the form; an empty value takes an optional one out.
- frmCtrlPropText(ctrl, name, out, size) A control property as the control holds it (Name, Index, the built-in integers, HelpTopic, Visible / Enabled unless the widget defines them, the stored ones).
- frmCtrlSetPropText(ctrl, key, value) Applies one the way the loader does (a VB TabIndex is dropped unless the widget defines it).
- frmModelSetCtrlProp(m, ctrl, name, value) The same with an editor's rules: Name renames the control array, Index makes an array member, then the OptionButton group rule.
- frmCtrlGetProp / frmCtrlSetProp / frmCtrlRemoveProp The stored properties.
- frmIntPropAt / frmIntPropFind / frmIntPropGet / Set The built-in integer table.
- frmIfaceProp(typeName, propName) The widget interface property of a type, NULL when none.
-.endtable
-
-.h2 Font Properties
-
-FontName, FontSize, FontBold, FontItalic, FontUnderline, and FontStrikethru (BAS_PROP_FONT*) are in both the form table and the common control table. They are one font, a DvxFontChoiceT (dvxDlg.h: family, size in pixels, FONT_STYLE_* flags), and the widget's font (wgtFont, wgtSetFont) is the only place they live: a read gives the font the object draws with (a form's is its root widget's), a write gives it that font with the one part changed. The .frm loader collects a block's Font properties and opens one font when the block's properties are read, so their order in the file does not matter. Helpers in formrt.h, for loaders and designers that hold them as .frm text:
-
-.table
- Function Description
- -------- -----------
- basFontPropAt(idx) The Font property names in .frm order (FontName, FontSize, then the flags), NULL past the end.
- basFontPropIs(name) True for a Font property name (any case).
- basFontChoiceOf(font, out) A BitmapFontT as the properties name it (dvxFontFamilyOf, pixelSize, style).
- basFontPropText(choice, name, out, size) One property of a choice as .frm text (the family, the size, True / False).
- basFontPropSetText(choice, name, text) Sets one property of a choice from .frm text.
-.endtable
-
-.h2 Editing Controls
-
-.table
- Function Description
- -------- -----------
- frmModelAddCtrl(m, typeName, name, parent) Adds a control as the last child of parent (NULL: the form); name "" or NULL gives a free one (frmModelAutoName); Caption = its name. NULL when the name is taken or parent cannot hold it.
- frmModelRemoveCtrl(m, ctrl) Removes ctrl and its subtree.
- frmModelRenameCtrl(m, oldName, newName) Renames a whole control array; children and DataSource / MasterSource of a renamed Data control follow. Identifier rules are the caller's.
- frmModelMakeArrayMember(m, ctrl, index) Gives a control array index; its children follow.
- frmModelMoveCtrl(m, ctrl, before) Moves ctrl with its subtree before a sibling (NULL: after the last).
- frmModelReparent(m, ctrl, parent) Makes ctrl the last child of another container (refuses cycles, non-containers, and too deep a nesting).
- frmModelSibling(m, idx, dir) The sibling next to a control (+1 after its subtree, -1 before).
- frmModelFindCtrl / CtrlPos / FindParent / CtrlDepth Lookups.
- frmModelIsDescendant / SubtreeEnd / NormalizeOrder The tree order.
- frmModelNameInUse / AutoName / OptionSelected Names and the OptionButton group rule.
- frmModelCtrlText(m, ctrl, &len) ctrl and its children as .frm control blocks (a copy for the Clipboard).
- frmModelCtrlsFromText(text, len) Copied blocks read into a scratch model.
- frmModelInsertTree(m, src, srcRoot, parent, name, index) Moves a subtree out of the scratch model into m: the root takes name / index (NULL: its own name when free, else a free one); a child whose name is taken gets a free one, the members of one array together; children follow their renamed parents.
-.endtable
-
-.h2 Menus
-
-The frmMenu* functions work on any stb_ds array of FrmMenuItemT (an editor's working copy as well as m->menuItems) and keep the levels a tree.
-
-.table
- Function Description
- -------- -----------
- frmMenuItemInit(mi) A blank item (enabled, visible, level 0).
- frmMenuInsert(&items, at, level) Inserts a blank item; the level is clamped so the menus stay a tree.
- frmMenuRemove(&items, idx) Removes an item with its subtree.
- frmMenuMove(&items, idx, dir) Moves an item with its subtree past its previous / next sibling; its new position or FRM_NO_INDEX.
- frmMenuIndent(items, idx, delta) Indents / outdents an item with its subtree (at most one level below the item above, not past FRM_MAX_MENU_LEVEL).
- frmMenuSubtreeEnd / CheckLevels / StripBlank / AutoName Tree helpers; AutoName makes mnuSep1 ... or "mnu" plus the caption's letters and digits.
- frmMenuPropText / frmMenuSetPropText Menu properties as text (Checked makes a check item).
- frmModelMenuValidate(m, items, isIdent, reason, size) The checks before items replace m's menus (levels; every name set, an identifier by isIdent, not the form's or a control's, unique; every Shortcut a key): the first bad item and the reason.
- frmModelMenuReplace(m, items) Replaces m's menus by a copy; ContextMenu bindings follow renamed items, matched by id (give every item id = position + 1 before editing).
- frmModelSetMenuProp(m, idx, key, value) Sets an item's property; a Name rename refuses a taken name and ContextMenu bindings follow.
-.endtable
-
-.code
-// Load a form, rename a control, add a label inside a frame, save.
-FrmModelT *m = frmModelLoad(text, len);
-
-if (m) {
- int32_t at = frmModelFindCtrl(m, "Frame1", FRM_NO_INDEX);
- FrmCtrlT *frame = (at >= 0) ? m->controls[at] : NULL;
- FrmCtrlT *lbl = frmModelAddCtrl(m, "Label", "", frame);
- int32_t outLen = 0;
-
- frmModelRenameCtrl(m, "Command1", "cmdOk");
- frmModelSetCtrlProp(m, lbl, "Caption", "Name:");
-
- char *out = frmModelText(m, &outLen);
-
- // write out[0 .. outLen) to the file
- free(out);
- frmModelFree(m);
-}
-.endcode
diff --git a/src/apps/kpunch/dvxbasic/compiler/basEvents.h b/src/apps/kpunch/dvxbasic/compiler/basEvents.h
index 1809eee0..840d3e7c 100644
--- a/src/apps/kpunch/dvxbasic/compiler/basEvents.h
+++ b/src/apps/kpunch/dvxbasic/compiler/basEvents.h
@@ -93,6 +93,7 @@ typedef enum {
#define BAS_EVT_DSGNPROPCHG "DesignPropertyChanged"
#define BAS_EVT_LINKCLICK "LinkClick"
#define BAS_EVT_NEWARGS "NewArgs"
+#define BAS_EVT_FULLSCREEN "FullScreen"
#define BAS_EVENT_LIST(X) \
X(BAS_EVT_LOAD, BAS_EVT_SCOPE_FORM, "") \
@@ -125,7 +126,8 @@ typedef enum {
X(BAS_EVT_DESIGNKEY, BAS_EVT_SCOPE_FORM, "KeyCode As Integer, Shift As Integer") \
X(BAS_EVT_DSGNPROPCHG, BAS_EVT_SCOPE_FORM, "CtrlName As String, PropertyName As String") \
X(BAS_EVT_LINKCLICK, BAS_EVT_SCOPE_WIDGET, NULL) \
- X(BAS_EVT_NEWARGS, BAS_EVT_SCOPE_FORM, "")
+ X(BAS_EVT_NEWARGS, BAS_EVT_SCOPE_FORM, "") \
+ X(BAS_EVT_FULLSCREEN, BAS_EVT_SCOPE_FORM, "State As Integer")
// A UserControl (a widget class written in BASIC, a .ctl file): the name
// of its own object in its code (UserControl.Refresh), the word after the
diff --git a/src/apps/kpunch/dvxbasic/compiler/codegen.c b/src/apps/kpunch/dvxbasic/compiler/codegen.c
index 62f0c139..b6aed6f5 100644
--- a/src/apps/kpunch/dvxbasic/compiler/codegen.c
+++ b/src/apps/kpunch/dvxbasic/compiler/codegen.c
@@ -322,14 +322,7 @@ void basPatch32(BasCodeGenT *cg, int32_t pos, int32_t val) {
// FNV-1a over the constant's bytes (case-sensitive: constants are exact).
static uint32_t constantHash(const char *text, int32_t len) {
- uint32_t h = BAS_FNV1A_OFFSET;
-
- for (int32_t i = 0; i < len; i++) {
- h ^= (uint32_t)(uint8_t)text[i];
- h *= BAS_FNV1A_PRIME;
- }
-
- return h;
+ return dvxFnvBytes(DVX_FNV_BASIS, text, len);
}
diff --git a/src/apps/kpunch/dvxbasic/compiler/compiler.c b/src/apps/kpunch/dvxbasic/compiler/compiler.c
index 949e54be..f0683ff3 100644
--- a/src/apps/kpunch/dvxbasic/compiler/compiler.c
+++ b/src/apps/kpunch/dvxbasic/compiler/compiler.c
@@ -248,6 +248,11 @@ static const PredefConstT sPredefConsts[] = {
{ "dvxArrowLeft", ScrollArrowLeftE },
{ "dvxArrowRight", ScrollArrowRightE },
+ // A form's FullScreen event's State
+ { "dvxFullScreenEnded", BAS_FULLSCREEN_ENDED },
+ { "dvxFullScreenActive", BAS_FULLSCREEN_ACTIVE },
+ { "dvxFullScreenSuspended", BAS_FULLSCREEN_SUSPENDED },
+
// ScrollBarsNeeded's answer
{ "vbSBNone", BAS_SB_NONE },
{ "vbHorizontal", BAS_SB_HORIZONTAL },
@@ -301,6 +306,7 @@ int32_t basCompilerErrorLine(const BasCompilerT *c);
const char *basCompilerFormScope(const BasCompilerT *c);
int32_t basCompilerGlobalSlot(const BasCompilerT *c, const char *name);
void basCompilerSetOptionExplicit(BasCompilerT *c, bool on);
+void basCompilerSetUnitFn(BasCompilerT *c, void (*fn)(void *ctx), void *ctx);
void basCompilerSetValidator(BasCompilerT *c, const BasCtrlValidatorT *v);
void basErrorAt(BasCompilerT *c, int32_t line, int32_t col, const char *fmt, ...);
void basErrorNode(BasCompilerT *c, const BasNodeT *n, const char *fmt, ...);
@@ -441,6 +447,10 @@ bool basCompile(BasCompilerT *c) {
arrsetlen(c->symChanges, 0);
basResolveUnitEnd(c, unit);
basArenaRelease(&c->unitArena, mark);
+
+ if (c->unitFn) {
+ c->unitFn(c->unitCtx);
+ }
}
if (!c->hasError) {
@@ -609,6 +619,12 @@ void basCompilerSetOptionExplicit(BasCompilerT *c, bool on) {
}
+void basCompilerSetUnitFn(BasCompilerT *c, void (*fn)(void *ctx), void *ctx) {
+ c->unitFn = fn;
+ c->unitCtx = ctx;
+}
+
+
void basCompilerSetValidator(BasCompilerT *c, const BasCtrlValidatorT *v) {
c->validator = v;
}
@@ -734,7 +750,7 @@ static bool buildClasses(const BasCompilerT *c, BasModuleT *mod) {
m->typedParams |= basConvertsArg(pi->paramTypes[p], pi->paramFlags[p]) ? (1u << p) : 0u;
}
- if (pi->paramCount > 0 && !joinParamNames(pi, &m->paramNames)) {
+ if (!joinParamNames(pi, &m->paramNames)) {
basModuleFree(mod);
return false;
}
@@ -1194,11 +1210,17 @@ static bool globalStartsTyped(const BasSymT *s) {
// A procedure's parameter names, comma-separated, into a new string
-// (*out): what a late-bound call with named arguments reads. False when
-// memory runs out.
+// (*out; NULL for a procedure without parameters): what a late-bound
+// call with named arguments reads. False when memory runs out.
static bool joinParamNames(const BasProcInfoT *pi, char **out) {
size_t len = 0;
+ *out = NULL;
+
+ if (pi->paramCount <= 0) {
+ return true;
+ }
+
for (int32_t p = 0; p < pi->paramCount; p++) {
len += strlen(pi->paramNames[p]) + 1;
}
diff --git a/src/apps/kpunch/dvxbasic/compiler/compiler.h b/src/apps/kpunch/dvxbasic/compiler/compiler.h
index 1b4e1063..20fd5294 100644
--- a/src/apps/kpunch/dvxbasic/compiler/compiler.h
+++ b/src/apps/kpunch/dvxbasic/compiler/compiler.h
@@ -118,6 +118,11 @@ int32_t basCompilerGlobalSlot(const BasCompilerT *c, const char *name);
// OPTION EXPLICIT for the whole program (the project setting).
void basCompilerSetOptionExplicit(BasCompilerT *c, bool on);
+// fn(ctx) is called after each unit (a statement or procedure) basCompile
+// compiles, so a caller can let other work go on through a long compile;
+// NULL calls nothing.
+void basCompilerSetUnitFn(BasCompilerT *c, void (*fn)(void *ctx), void *ctx);
+
// Attaches the validator (borrowed: it must outlive basCompile).
void basCompilerSetValidator(BasCompilerT *c, const BasCtrlValidatorT *v);
diff --git a/src/apps/kpunch/dvxbasic/compiler/front.h b/src/apps/kpunch/dvxbasic/compiler/front.h
index 37adccfd..e1d349ae 100644
--- a/src/apps/kpunch/dvxbasic/compiler/front.h
+++ b/src/apps/kpunch/dvxbasic/compiler/front.h
@@ -314,6 +314,9 @@ struct BasCompilerS {
BasVmT *constVm; // evaluates constant expressions (made on first use)
const BasCtrlValidatorT *validator;
+
+ void (*unitFn)(void *ctx); // basCompilerSetUnitFn: after each unit
+ void *unitCtx;
};
// ============================================================
diff --git a/src/apps/kpunch/dvxbasic/compiler/opcodes.h b/src/apps/kpunch/dvxbasic/compiler/opcodes.h
index 58108ecf..ad5a936d 100644
--- a/src/apps/kpunch/dvxbasic/compiler/opcodes.h
+++ b/src/apps/kpunch/dvxbasic/compiler/opcodes.h
@@ -33,6 +33,8 @@
#include
#include
+#include "dvxEndian.h"
+
// ============================================================
// Variable scope tags
// Emitted in bytecode (e.g. OP_FOR scopeTag byte) and consumed
@@ -523,99 +525,103 @@ typedef enum {
// OP_BUILTIN function numbers. Part of the bytecode ABI -- append only.
typedef enum {
- BAS_FN_REPLACE = 0, // REPLACE$(s, find, repl [, start [, count [, compare]]])
- BAS_FN_SPLIT = 1, // SPLIT(s [, delim [, limit [, compare]]]) -> STRING array
- BAS_FN_JOIN = 2, // JOIN$(array [, delim])
- BAS_FN_INSTRREV = 3, // INSTRREV(s, find [, start [, compare]])
- BAS_FN_STRCOMP = 4, // STRCOMP(a, b [, compare])
- BAS_FN_STRREVERSE = 5, // STRREVERSE$(s)
- BAS_FN_FILTER = 6, // FILTER(array, match [, include [, compare]]) -> STRING array
- BAS_FN_LSET = 7, // LSET target = s: (current target, s) -> s left-justified in LEN(target)
- BAS_FN_RSET = 8, // RSET target = s: (current target, s) -> s right-justified in LEN(target)
- BAS_FN_IIF = 9, // IIF(cond, a, b)
- BAS_FN_CHOOSE = 10, // CHOOSE(index, a [, b ...])
- BAS_FN_NOW = 11, // NOW
- BAS_FN_DATE = 12, // DATE
- BAS_FN_TIME = 13, // TIME
- BAS_FN_DATESERIAL = 14, // DATESERIAL(y, m, d)
- BAS_FN_TIMESERIAL = 15, // TIMESERIAL(h, m, s)
- BAS_FN_DATEVALUE = 16, // DATEVALUE(d)
- BAS_FN_TIMEVALUE = 17, // TIMEVALUE(d)
- BAS_FN_YEAR = 18, // YEAR(d)
- BAS_FN_MONTH = 19, // MONTH(d)
- BAS_FN_DAY = 20, // DAY(d)
- BAS_FN_WEEKDAY = 21, // WEEKDAY(d [, firstDayOfWeek])
- BAS_FN_HOUR = 22, // HOUR(d)
- BAS_FN_MINUTE = 23, // MINUTE(d)
- BAS_FN_SECOND = 24, // SECOND(d)
- BAS_FN_DATEADD = 25, // DATEADD(interval, n, d)
- BAS_FN_DATEDIFF = 26, // DATEDIFF(interval, d1, d2 [, firstDayOfWeek [, firstWeekOfYear]])
- BAS_FN_DATEPART = 27, // DATEPART(interval, d [, firstDayOfWeek [, firstWeekOfYear]])
- BAS_FN_ISDATE = 28, // ISDATE(v)
- BAS_FN_MONTHNAME = 29, // MONTHNAME(m [, abbrev])
- BAS_FN_WEEKDAYNAME = 30, // WEEKDAYNAME(w [, abbrev [, firstDayOfWeek]])
- BAS_FN_PARAMARRAY = 31, // (elementType [| BAS_PARAMARRAY_BASE_ONE], v0, v1, ...) -> 0-based (1-based) array of the values: a ParamArray argument, ARRAY(...)
+ BAS_FN_REPLACE = 0, // REPLACE$(s, find, repl [, start [, count [, compare]]])
+ BAS_FN_SPLIT = 1, // SPLIT(s [, delim [, limit [, compare]]]) -> STRING array
+ BAS_FN_JOIN = 2, // JOIN$(array [, delim])
+ BAS_FN_INSTRREV = 3, // INSTRREV(s, find [, start [, compare]])
+ BAS_FN_STRCOMP = 4, // STRCOMP(a, b [, compare])
+ BAS_FN_STRREVERSE = 5, // STRREVERSE$(s)
+ BAS_FN_FILTER = 6, // FILTER(array, match [, include [, compare]]) -> STRING array
+ BAS_FN_LSET = 7, // LSET target = s: (current target, s) -> s left-justified in LEN(target)
+ BAS_FN_RSET = 8, // RSET target = s: (current target, s) -> s right-justified in LEN(target)
+ BAS_FN_IIF = 9, // IIF(cond, a, b)
+ BAS_FN_CHOOSE = 10, // CHOOSE(index, a [, b ...])
+ BAS_FN_NOW = 11, // NOW
+ BAS_FN_DATE = 12, // DATE
+ BAS_FN_TIME = 13, // TIME
+ BAS_FN_DATESERIAL = 14, // DATESERIAL(y, m, d)
+ BAS_FN_TIMESERIAL = 15, // TIMESERIAL(h, m, s)
+ BAS_FN_DATEVALUE = 16, // DATEVALUE(d)
+ BAS_FN_TIMEVALUE = 17, // TIMEVALUE(d)
+ BAS_FN_YEAR = 18, // YEAR(d)
+ BAS_FN_MONTH = 19, // MONTH(d)
+ BAS_FN_DAY = 20, // DAY(d)
+ BAS_FN_WEEKDAY = 21, // WEEKDAY(d [, firstDayOfWeek])
+ BAS_FN_HOUR = 22, // HOUR(d)
+ BAS_FN_MINUTE = 23, // MINUTE(d)
+ BAS_FN_SECOND = 24, // SECOND(d)
+ BAS_FN_DATEADD = 25, // DATEADD(interval, n, d)
+ BAS_FN_DATEDIFF = 26, // DATEDIFF(interval, d1, d2 [, firstDayOfWeek [, firstWeekOfYear]])
+ BAS_FN_DATEPART = 27, // DATEPART(interval, d [, firstDayOfWeek [, firstWeekOfYear]])
+ BAS_FN_ISDATE = 28, // ISDATE(v)
+ BAS_FN_MONTHNAME = 29, // MONTHNAME(m [, abbrev])
+ BAS_FN_WEEKDAYNAME = 30, // WEEKDAYNAME(w [, abbrev [, firstDayOfWeek]])
+ BAS_FN_PARAMARRAY = 31, // (elementType [| BAS_PARAMARRAY_BASE_ONE], v0, v1, ...) -> 0-based (1-based) array of the values: a ParamArray argument, ARRAY(...)
// The program screen (runtime/screen.c): statements return 0, which
// the compiler pops. Omitted arguments are pushed as 0 and flagged in
// a BAS_GIVEN_* / BAS_GFX_* mask pushed last.
- BAS_FN_CLS = 32, // CLS [n]: (n, -1 when omitted)
- BAS_FN_LOCATE = 33, // LOCATE [row] [, [col] [, cursor]]: (row, col, cursor, given)
- BAS_FN_COLOR = 34, // COLOR [fore] [, back]: (fore, back, given)
- BAS_FN_SCREEN = 35, // SCREEN mode [, [colorswitch] [, [apage] [, vpage]]]: (mode, colorswitch, apage, vpage, given)
- BAS_FN_WIDTH = 36, // WIDTH [cols] [, rows]: (cols, rows, given)
- BAS_FN_PSET = 37, // PSET / PRESET [STEP](x, y) [, color]: (x, y, color, flags)
- BAS_FN_LINE = 38, // LINE [[STEP](x1, y1)]-[STEP](x2, y2) [, color] [, B|BF] [, style]: (x1, y1, x2, y2, color, style, flags)
- BAS_FN_CIRCLE = 39, // CIRCLE [STEP](x, y), r [, color [, start [, end [, aspect]]]]: (x, y, r, color, start, end, aspect, flags)
- BAS_FN_PAINT = 40, // PAINT [STEP](x, y) [, paint | tile$ [, border [, background$]]]: (x, y, paint, border, background, flags)
- BAS_FN_DRAW = 41, // DRAW macro$
- BAS_FN_PALETTE = 42, // PALETTE [attr, color]: (attr, color, given)
- BAS_FN_PALETTE_USING = 43, // PALETTE USING array[(first)]: (array, first, given)
- BAS_FN_VIEW = 44, // VIEW [[SCREEN] (x1, y1)-(x2, y2) [, [fill] [, border]]]: (x1, y1, x2, y2, fill, border, flags)
- BAS_FN_WINDOW = 45, // WINDOW [[SCREEN] (x1, y1)-(x2, y2)]: (x1, y1, x2, y2, flags)
- BAS_FN_FULLSCREEN = 46, // FULLSCREEN [ON | OFF | expr]: (on)
- BAS_FN_INKEY = 47, // INKEY$ -> "" / one character / CHR$(0) + scan code
- BAS_FN_CSRLIN = 48, // CSRLIN -> cursor row
- BAS_FN_POS = 49, // POS(n) -> cursor column
- BAS_FN_POINT = 50, // POINT(x, y) -> colour (-1 outside the view); POINT(n) -> the last point
- BAS_FN_SCREENFN = 51, // SCREEN(row, col [, colorflag]) -> character code or colour
- BAS_FN_LINE_INPUT = 52, // LINE INPUT [prompt$;] v$: (prompt) -> the line typed
+ BAS_FN_CLS = 32, // CLS [n]: (n, -1 when omitted)
+ BAS_FN_LOCATE = 33, // LOCATE [row] [, [col] [, cursor]]: (row, col, cursor, given)
+ BAS_FN_COLOR = 34, // COLOR [fore] [, back]: (fore, back, given)
+ BAS_FN_SCREEN = 35, // SCREEN mode [, [colorswitch] [, [apage] [, vpage]]]: (mode, colorswitch, apage, vpage, given)
+ BAS_FN_WIDTH = 36, // WIDTH [cols] [, rows]: (cols, rows, given)
+ BAS_FN_PSET = 37, // PSET / PRESET [STEP](x, y) [, color]: (x, y, color, flags)
+ BAS_FN_LINE = 38, // LINE [[STEP](x1, y1)]-[STEP](x2, y2) [, color] [, B|BF] [, style]: (x1, y1, x2, y2, color, style, flags)
+ BAS_FN_CIRCLE = 39, // CIRCLE [STEP](x, y), r [, color [, start [, end [, aspect]]]]: (x, y, r, color, start, end, aspect, flags)
+ BAS_FN_PAINT = 40, // PAINT [STEP](x, y) [, paint | tile$ [, border [, background$]]]: (x, y, paint, border, background, flags)
+ BAS_FN_DRAW = 41, // DRAW macro$
+ BAS_FN_PALETTE = 42, // PALETTE [attr, color]: (attr, color, given)
+ BAS_FN_PALETTE_USING = 43, // PALETTE USING array[(first)]: (array, first, given)
+ BAS_FN_VIEW = 44, // VIEW [[SCREEN] (x1, y1)-(x2, y2) [, [fill] [, border]]]: (x1, y1, x2, y2, fill, border, flags)
+ BAS_FN_WINDOW = 45, // WINDOW [[SCREEN] (x1, y1)-(x2, y2)]: (x1, y1, x2, y2, flags)
+ BAS_FN_FULLSCREEN = 46, // FULLSCREEN [ON | OFF | expr]: (on)
+ BAS_FN_INKEY = 47, // INKEY$ -> "" / one character / CHR$(0) + scan code
+ BAS_FN_CSRLIN = 48, // CSRLIN -> cursor row
+ BAS_FN_POS = 49, // POS(n) -> cursor column
+ BAS_FN_POINT = 50, // POINT(x, y) -> colour (-1 outside the view); POINT(n) -> the last point
+ BAS_FN_SCREENFN = 51, // SCREEN(row, col [, colorflag]) -> character code or colour
+ BAS_FN_LINE_INPUT = 52, // LINE INPUT [prompt$;] v$: (prompt) -> the line typed
// Variants and objects
- BAS_FN_VARTYPE = 53, // VARTYPE(v) -> vbEmpty .. vbArray + element type
- BAS_FN_TYPENAME = 54, // TYPENAME(v) -> "Integer", "Nothing", class name ...
- BAS_FN_ISEMPTY = 55, // ISEMPTY(v)
- BAS_FN_ISNULL = 56, // ISNULL(v)
- BAS_FN_ISNUMERIC = 57, // ISNUMERIC(v)
- BAS_FN_ISOBJECT = 58, // ISOBJECT(v)
- BAS_FN_ISARRAY = 59, // ISARRAY(v)
- BAS_FN_CVAR = 60, // CVAR(v) -> v as it is
- BAS_FN_TYPEOF = 61, // TYPEOF obj IS name: (obj, name) -> the object is of that type
- BAS_FN_EACH_COUNT = 62, // FOR EACH (compiler-generated): (group) -> items in the array or Collection
- BAS_FN_EACH_ITEM = 63, // FOR EACH (compiler-generated): (group, i) -> item i (0-based)
- BAS_FN_ISMISSING = 64, // ISMISSING(v)
- BAS_FN_LSET_TYPE = 65, // LSET rec1 = rec2 (compiler-generated): (rec1, rec2, rec1 layout, rec2 layout) -> rec1 holding rec2's bytes
- BAS_FN_PCOPY = 66, // PCOPY source, destination (screen pages)
- BAS_FN_VIEW_PRINT = 67, // VIEW PRINT [top TO bottom]: (top, bottom, given)
- BAS_FN_PMAP = 68, // PMAP(coordinate, fn) -> world <-> physical
- BAS_FN_VARPTR_STR = 69, // VARPTR$(v) -> v packed for DRAW's X and "=" (BAS_VARPTR_*)
- BAS_FN_LOADPICTURE = 70, // LOADPICTURE([file]) -> a Picture object for picture properties (DragIcon, Picture); no file: an empty picture
- BAS_FN_RECORD_ASSIGN = 71, // rec = value (compiler-generated): (reference to the target, TYPE value) -> Empty; the value's fields are copied into the record the target holds (VB)
- BAS_FN_CLOSE = 72, // CLOSE [#n [, #n ...]] with no channel or several, RESET: (channels...) -> 0; none closes every channel
- BAS_FN_FILE_PRINT = 73, // PRINT #n, ... (compiler-generated): (n) -> 0 sends the statement's PRINT output to channel n; () -> 0 ends it
- BAS_FN_FILE_WIDTH = 74, // WIDTH #n, w / WIDTH "device", w / WIDTH LPRINT w: (n | device$ | BAS_WIDTH_LPRINT, w) -> 0
- BAS_FN_PEN = 75, // PEN(n) -> the light pen's (the mouse's) state on the program screen (BAS_PEN_*)
- BAS_FN_PEN_SET = 76, // PEN ON | OFF | STOP: (BAS_PEN_ON / OFF / STOP) -> 0
+ BAS_FN_VARTYPE = 53, // VARTYPE(v) -> vbEmpty .. vbArray + element type
+ BAS_FN_TYPENAME = 54, // TYPENAME(v) -> "Integer", "Nothing", class name ...
+ BAS_FN_ISEMPTY = 55, // ISEMPTY(v)
+ BAS_FN_ISNULL = 56, // ISNULL(v)
+ BAS_FN_ISNUMERIC = 57, // ISNUMERIC(v)
+ BAS_FN_ISOBJECT = 58, // ISOBJECT(v)
+ BAS_FN_ISARRAY = 59, // ISARRAY(v)
+ BAS_FN_CVAR = 60, // CVAR(v) -> v as it is
+ BAS_FN_TYPEOF = 61, // TYPEOF obj IS name: (obj, name) -> the object is of that type
+ BAS_FN_EACH_COUNT = 62, // FOR EACH (compiler-generated): (group) -> items in the array or Collection
+ BAS_FN_EACH_ITEM = 63, // FOR EACH (compiler-generated): (group, i) -> item i (0-based)
+ BAS_FN_ISMISSING = 64, // ISMISSING(v)
+ BAS_FN_LSET_TYPE = 65, // LSET rec1 = rec2 (compiler-generated): (rec1, rec2, rec1 layout, rec2 layout) -> rec1 holding rec2's bytes
+ BAS_FN_PCOPY = 66, // PCOPY source, destination (screen pages)
+ BAS_FN_VIEW_PRINT = 67, // VIEW PRINT [top TO bottom]: (top, bottom, given)
+ BAS_FN_PMAP = 68, // PMAP(coordinate, fn) -> world <-> physical
+ BAS_FN_VARPTR_STR = 69, // VARPTR$(v) -> v packed for DRAW's X and "=" (BAS_VARPTR_*)
+ BAS_FN_LOADPICTURE = 70, // LOADPICTURE([file]) -> a Picture object for picture properties (DragIcon, Picture); no file: an empty picture
+ BAS_FN_RECORD_ASSIGN = 71, // rec = value (compiler-generated): (reference to the target, TYPE value) -> Empty; the value's fields are copied into the record the target holds (VB)
+ BAS_FN_CLOSE = 72, // CLOSE [#n [, #n ...]] with no channel or several, RESET: (channels...) -> 0; none closes every channel
+ BAS_FN_FILE_PRINT = 73, // PRINT #n, ... (compiler-generated): (n) -> 0 sends the statement's PRINT output to channel n; () -> 0 ends it
+ BAS_FN_FILE_WIDTH = 74, // WIDTH #n, w / WIDTH "device", w / WIDTH LPRINT w: (n | device$ | BAS_WIDTH_LPRINT, w) -> 0
+ BAS_FN_PEN = 75, // PEN(n) -> the light pen's (the mouse's) state on the program screen (BAS_PEN_*)
+ BAS_FN_PEN_SET = 76, // PEN ON | OFF | STOP: (BAS_PEN_ON / OFF / STOP) -> 0
// The Err object and Error$
- BAS_FN_ERR_DESCRIPTION = 77, // Err.Description -> the current error's text ("" when none)
- BAS_FN_ERR_SOURCE = 78, // Err.Source -> the source Err.Raise gave ("" otherwise)
- BAS_FN_ERR_SET = 79, // Err.Clear: () -> 0 clears the error; Err.Number = n: (n) -> 0 sets it (its text the number's)
- BAS_FN_ERR_RAISE = 80, // Err.Raise number [, source [, description]] -> raises the error
- BAS_FN_ERROR_TEXT = 81, // ERROR$([n]) / ERROR([n]) -> the text of error n (none: the current error's)
- BAS_FN_LIKE = 82, // a LIKE pattern: (a, pattern) -> a matches the pattern (Option Compare decides case)
- BAS_FN_FILEATTR = 83, // FILEATTR(n, attribute) -> the open mode of file n (BAS_FILEATTR_MODE) or its system handle (BAS_FILEATTR_HANDLE)
- BAS_FN_QBCOLOR = 84, // QBCOLOR(n) -> QBasic colour n (0..15) as an RGB value
- BAS_FN_NEW_FORM = 85, // New FormName (compiler-generated): (name) -> a new copy of the form (its own window, controls and form-level variables)
- BAS_FN_CALLBYNAME = 86, // CALLBYNAME(object, name, callType [, args...]) -> the member's value or result (Empty for vbLet / vbSet)
- BAS_FN_LOADFORMTEXT = 87 // LOADFORMTEXT(frmText) -> a form loaded from .frm text
+ BAS_FN_ERR_DESCRIPTION = 77, // Err.Description -> the current error's text ("" when none)
+ BAS_FN_ERR_SOURCE = 78, // Err.Source -> the source Err.Raise gave ("" otherwise)
+ BAS_FN_ERR_SET = 79, // Err.Clear: () -> 0 clears the error; Err.Number = n: (n) -> 0 sets it (its text the number's)
+ BAS_FN_ERR_RAISE = 80, // Err.Raise number [, source [, description]] -> raises the error
+ BAS_FN_ERROR_TEXT = 81, // ERROR$([n]) / ERROR([n]) -> the text of error n (none: the current error's)
+ BAS_FN_LIKE = 82, // a LIKE pattern: (a, pattern) -> a matches the pattern (Option Compare decides case)
+ BAS_FN_FILEATTR = 83, // FILEATTR(n, attribute) -> the open mode of file n (BAS_FILEATTR_MODE) or its system handle (BAS_FILEATTR_HANDLE)
+ BAS_FN_QBCOLOR = 84, // QBCOLOR(n) -> QBasic colour n (0..15) as an RGB value
+ BAS_FN_NEW_FORM = 85, // New FormName (compiler-generated): (name) -> a new copy of the form (its own window, controls and form-level variables)
+ BAS_FN_CALLBYNAME = 86, // CALLBYNAME(object, name, callType [, args...]) -> the member's value or result (Empty for vbLet / vbSet)
+ BAS_FN_LOADFORMTEXT = 87, // LOADFORMTEXT(frmText) -> a form loaded from .frm text
+ BAS_FN_CHRW = 88, // CHRW$(code) -> the DVX (code page 437) character for a Unicode code point ("?" when it has none)
+ BAS_FN_ASCW = 89, // ASCW(s) -> the Unicode code point of s's first character
+ BAS_FN_LOADPICTUREDATA = 90, // LOADPICTUREDATA(bytes) -> a Picture object for the bytes of a picture file
+ BAS_FN_FULLSCREEN_STATE = 91 // FULLSCREEN (a function) -> True while the program screen has (or keeps, behind the desktop) the whole screen
} BasBuiltinFnE;
// CallByName's call types (VB's vbMethod, vbGet, vbLet, vbSet) and its
@@ -698,14 +704,25 @@ typedef enum {
#define BAS_VT_ARRAY 8192
// The program-screen functions are numbered BAS_FN_CLS through
-// BAS_FN_LINE_INPUT and BAS_FN_PCOPY through BAS_FN_PMAP (the VM hands
-// them to runtime/screen.c; see basBuiltinIsScreen).
+// BAS_FN_LINE_INPUT, BAS_FN_PCOPY through BAS_FN_PMAP, BAS_FN_PEN
+// through BAS_FN_PEN_SET, and BAS_FN_FULLSCREEN_STATE (the VM hands them
+// to runtime/screen.c; see basBuiltinIsScreen).
#define BAS_FN_SCREEN_FIRST BAS_FN_CLS
#define BAS_FN_SCREEN_LAST BAS_FN_LINE_INPUT
#define BAS_FN_SCREEN2_FIRST BAS_FN_PCOPY
#define BAS_FN_SCREEN2_LAST BAS_FN_PMAP
#define BAS_FN_SCREEN3_FIRST BAS_FN_PEN
#define BAS_FN_SCREEN3_LAST BAS_FN_PEN_SET
+#define BAS_FN_SCREEN4_FIRST BAS_FN_FULLSCREEN_STATE
+#define BAS_FN_SCREEN4_LAST BAS_FN_FULLSCREEN_STATE
+
+// A form's FullScreen event's State (dvxFullScreen* in BASIC): the whole
+// screen is the form's, the desktop is back while another window has the
+// focus (the form gets it again with the focus), or it is over (no video
+// mode could be set again).
+#define BAS_FULLSCREEN_ENDED 0
+#define BAS_FULLSCREEN_ACTIVE 1
+#define BAS_FULLSCREEN_SUSPENDED 2
// PEN(n) (QBasic's light pen; the mouse on the program screen) and the
// PEN statement's argument.
@@ -935,12 +952,13 @@ static inline bool basBuiltinArity(uint8_t fn, int32_t *minArgs, int32_t *maxArg
switch (fn) {
case BAS_FN_NOW: case BAS_FN_DATE: case BAS_FN_TIME:
case BAS_FN_INKEY: case BAS_FN_CSRLIN: case BAS_FN_ERR_DESCRIPTION:
- case BAS_FN_ERR_SOURCE:
+ case BAS_FN_ERR_SOURCE: case BAS_FN_FULLSCREEN_STATE:
*minArgs = 0;
*maxArgs = 0;
return true;
case BAS_FN_QBCOLOR: case BAS_FN_NEW_FORM: case BAS_FN_LOADFORMTEXT:
+ case BAS_FN_CHRW: case BAS_FN_ASCW: case BAS_FN_LOADPICTUREDATA:
case BAS_FN_STRREVERSE: case BAS_FN_DATEVALUE: case BAS_FN_TIMEVALUE:
case BAS_FN_YEAR: case BAS_FN_MONTH: case BAS_FN_DAY:
case BAS_FN_HOUR: case BAS_FN_MINUTE: case BAS_FN_SECOND:
@@ -1126,7 +1144,7 @@ static inline bool basBuiltinArity(uint8_t fn, int32_t *minArgs, int32_t *maxArg
// True for the program-screen functions (runtime/screen.c).
static inline bool basBuiltinIsScreen(uint8_t fn) {
- return (fn >= BAS_FN_SCREEN_FIRST && fn <= BAS_FN_SCREEN_LAST) || (fn >= BAS_FN_SCREEN2_FIRST && fn <= BAS_FN_SCREEN2_LAST) || (fn >= BAS_FN_SCREEN3_FIRST && fn <= BAS_FN_SCREEN3_LAST);
+ return (fn >= BAS_FN_SCREEN_FIRST && fn <= BAS_FN_SCREEN_LAST) || (fn >= BAS_FN_SCREEN2_FIRST && fn <= BAS_FN_SCREEN2_LAST) || (fn >= BAS_FN_SCREEN3_FIRST && fn <= BAS_FN_SCREEN3_LAST) || (fn >= BAS_FN_SCREEN4_FIRST && fn <= BAS_FN_SCREEN4_LAST);
}
@@ -1369,41 +1387,36 @@ static inline int32_t basOpcodeOperandSize(uint8_t op) {
// ============================================================
//
// Bytecode is always little-endian regardless of the host; the compiler,
-// the compactor and the serializer all go through these.
+// the compactor and the serializer all go through these (signed and
+// unsigned forms of dvxEndian.h's loads and stores).
static inline int16_t basReadI16LE(const uint8_t *p) {
- return (int16_t)basReadU16LE(p);
+ return (int16_t)dvxGetLe16(p);
}
static inline int32_t basReadI32LE(const uint8_t *p) {
- return (int32_t)((uint32_t)p[0] | ((uint32_t)p[1] << 8) | ((uint32_t)p[2] << 16) | ((uint32_t)p[3] << 24));
+ return (int32_t)dvxGetLe32(p);
}
static inline uint16_t basReadU16LE(const uint8_t *p) {
- return (uint16_t)((uint16_t)p[0] | ((uint16_t)p[1] << 8));
+ return dvxGetLe16(p);
}
static inline void basWriteI16LE(uint8_t *p, int16_t v) {
- basWriteU16LE(p, (uint16_t)v);
+ dvxPutLe16(p, (uint16_t)v);
}
static inline void basWriteI32LE(uint8_t *p, int32_t v) {
- uint32_t u = (uint32_t)v;
-
- p[0] = (uint8_t)(u & 0xFF);
- p[1] = (uint8_t)((u >> 8) & 0xFF);
- p[2] = (uint8_t)((u >> 16) & 0xFF);
- p[3] = (uint8_t)((u >> 24) & 0xFF);
+ dvxPutLe32(p, (uint32_t)v);
}
static inline void basWriteU16LE(uint8_t *p, uint16_t v) {
- p[0] = (uint8_t)(v & 0xFF);
- p[1] = (uint8_t)((v >> 8) & 0xFF);
+ dvxPutLe16(p, v);
}
diff --git a/src/apps/kpunch/dvxbasic/compiler/resolveExpr.c b/src/apps/kpunch/dvxbasic/compiler/resolveExpr.c
index ef7e9a91..677dc161 100644
--- a/src/apps/kpunch/dvxbasic/compiler/resolveExpr.c
+++ b/src/apps/kpunch/dvxbasic/compiler/resolveExpr.c
@@ -98,7 +98,9 @@ static const char *const sArgsWeekdayName[] = { "Weekday", "Abbreviate", "FirstD
static const BasBuiltinT sBuiltins[] = {
// String functions
{"ASC", OP_STR_ASC, 0, 1, 1, BAS_NODE_TYPE_UNKNOWN, BI_PURE, 0, NULL},
+ {"ASCW", OP_BUILTIN, BAS_FN_ASCW, 0, 0, BAS_TYPE_LONG, BI_PURE, 0, NULL},
{"CHR$", OP_STR_CHR, 0, 1, 1, BAS_TYPE_STRING, BI_PURE, 0, NULL},
+ {"CHRW$", OP_BUILTIN, BAS_FN_CHRW, 0, 0, BAS_TYPE_STRING, BI_PURE, 0, NULL},
{"COMMAND$", OP_COMMAND, 0, 0, 1, BAS_TYPE_STRING, 0, 0, NULL},
{"DATE$", OP_DATE_STR, 0, 0, 0, BAS_TYPE_STRING, 0, 0, NULL},
{"ENVIRON$", OP_ENVIRON, 0, 1, 1, BAS_TYPE_STRING, 0, 0, NULL},
@@ -123,6 +125,7 @@ static const BasBuiltinT sBuiltins[] = {
// VB's Variant forms of the string functions: Null text gives Null
{"CHR", OP_STR_CHR, 0, 1, 1, BAS_TYPE_STRING, BI_PURE, 0, NULL},
+ {"CHRW", OP_BUILTIN, BAS_FN_CHRW, 0, 0, BAS_TYPE_STRING, BI_PURE, 0, NULL},
{"HEX", OP_STR_HEX, 0, 1, 1, BAS_NODE_TYPE_UNKNOWN, BI_PURE, BAS_NULL_ARG_1, NULL},
{"LCASE", OP_STR_LCASE, 0, 1, 1, BAS_NODE_TYPE_UNKNOWN, BI_PURE, BAS_NULL_ARG_1, NULL},
{"LEFT", OP_STR_LEFT, 0, 2, 2, BAS_NODE_TYPE_UNKNOWN, BI_PURE, BAS_NULL_ARG_1, sArgsLeft},
@@ -180,6 +183,7 @@ static const BasBuiltinT sBuiltins[] = {
// The program screen
{"CSRLIN", OP_BUILTIN, BAS_FN_CSRLIN, 0, 0, BAS_NODE_TYPE_UNKNOWN, 0, 0, NULL},
+ {"FULLSCREEN", OP_BUILTIN, BAS_FN_FULLSCREEN_STATE, 0, 0, BAS_NODE_TYPE_UNKNOWN, 0, 0, NULL},
{"INKEY$", OP_BUILTIN, BAS_FN_INKEY, 0, 0, BAS_TYPE_STRING, 0, 0, NULL},
{"PEN", OP_BUILTIN, BAS_FN_PEN, 0, 0, BAS_NODE_TYPE_UNKNOWN, 0, 0, NULL},
{"PMAP", OP_BUILTIN, BAS_FN_PMAP, 0, 0, BAS_NODE_TYPE_UNKNOWN, 0, 0, NULL},
@@ -245,6 +249,7 @@ static const BasBuiltinT sBuiltins[] = {
{"ISOBJECT", OP_BUILTIN, BAS_FN_ISOBJECT, 0, 0, BAS_NODE_TYPE_UNKNOWN, 0, 0, NULL},
{"LOADFORMTEXT", OP_BUILTIN, BAS_FN_LOADFORMTEXT, 0, 0, BAS_NODE_TYPE_UNKNOWN, 0, 0, NULL},
{"LOADPICTURE", OP_BUILTIN, BAS_FN_LOADPICTURE, 0, 0, BAS_NODE_TYPE_UNKNOWN, 0, 0, NULL},
+ {"LOADPICTUREDATA", OP_BUILTIN, BAS_FN_LOADPICTUREDATA, 0, 0, BAS_NODE_TYPE_UNKNOWN, 0, 0, NULL},
{"TYPENAME", OP_BUILTIN, BAS_FN_TYPENAME, 0, 0, BAS_NODE_TYPE_UNKNOWN, 0, 0, NULL},
{"VARTYPE", OP_BUILTIN, BAS_FN_VARTYPE, 0, 0, BAS_NODE_TYPE_UNKNOWN, 0, 0, NULL}
};
diff --git a/src/apps/kpunch/dvxbasic/compiler/scope.c b/src/apps/kpunch/dvxbasic/compiler/scope.c
index 03d683ca..b397554a 100644
--- a/src/apps/kpunch/dvxbasic/compiler/scope.c
+++ b/src/apps/kpunch/dvxbasic/compiler/scope.c
@@ -103,11 +103,10 @@ BasSymT *basScopeFindKind(const BasScopeTabT *tab, const char *name, uint8_t kin
uint32_t basScopeHash(const char *name) {
- uint32_t h = BAS_FNV1A_OFFSET;
+ uint32_t h = DVX_FNV_BASIS;
for (; *name != '\0'; name++) {
- h ^= (uint32_t)(uint8_t)basAsciiUpper(*name);
- h *= BAS_FNV1A_PRIME;
+ h = dvxFnvByte(h, (uint8_t)basAsciiUpper(*name));
}
return h;
diff --git a/src/apps/kpunch/dvxbasic/ctrlover.dhs b/src/apps/kpunch/dvxbasic/ctrlover.dhs
index 187b187d..7042a78b 100644
--- a/src/apps/kpunch/dvxbasic/ctrlover.dhs
+++ b/src/apps/kpunch/dvxbasic/ctrlover.dhs
@@ -92,7 +92,7 @@ Every control in DVX BASIC inherits a set of common properties, events, and meth
ToolTipText String R/W Tooltip text shown after the mouse hovers over the control. Empty string or unset = no tooltip.
HelpTopic String R/W Help topic opened by F1 while the control has the focus. Empty = the form's HelpTopic. F1 uses the project's help file.
DragMode Integer R/W vbManual (0, the default): the program starts a drag with the Drag method. vbAutomatic (1): pressing the left mouse button on the control starts a drag. See Drag and Drop below.
- DragIcon Picture R/W Picture shown under the mouse while the control is dragged: a picture file name (BMP, PNG, JPEG, or GIF) or a Picture object from LOADPICTURE (Set works too). Reading it gives a Picture object, which stands for the file name where a value is needed. Empty = a dotted outline the size of the control.
+ DragIcon Picture R/W Picture shown under the mouse while the control is dragged: a picture file name (BMP, PNG, JPEG, or GIF) or a Picture object from LOADPICTURE or LOADPICTUREDATA (Set works too). Reading it gives a Picture object, which stands for the file name where a value is needed. Empty = a dotted outline the size of the control.
Parent Form R The form the control is on (Text1.Parent.Caption).
Tag String R/W Any text the program wants to keep with the control (DVX does nothing with it), as in Visual Basic. Also in the .frm file.
MousePointer Integer R/W The mouse pointer shown over the control (and the controls inside it that have none of their own): vbDefault (0, the control's own pointer), vbArrow (1), vbCrosshair (2), vbSizeNESW (6), vbSizeNS (7), vbSizeNWSE (8), vbSizeWE (9), vbHourglass (11), or vbNoDrop (12). A form's MousePointer is used where no control has one, and Screen.MousePointer over every form of the program. Change it in MouseMove to show different pointers over parts of a control. Also in the .frm file; another value is an error.
@@ -169,7 +169,7 @@ Drop targets only react: nothing is moved or copied for you. Use Source in DragD
Note that vbCancel is the MsgBox Cancel value (2), which as a Drag action means vbEndDrag; cancel a drag with Drag 0.
-DragIcon takes a file name or a picture loaded with LOADPICTURE: Set Label1.DragIcon = LoadPicture("drag.bmp"). A missing file or one that is not a picture raises its error at LOADPICTURE; a file name that cannot be read when the drag starts shows the outline instead.
+DragIcon takes a file name or a picture loaded with LOADPICTURE or LOADPICTUREDATA: Set Label1.DragIcon = LoadPicture("drag.bmp"). A missing file or one that is not a picture raises its error at LOADPICTURE; a file name that cannot be read when the drag starts shows the outline instead.
.code
' Label1 has DragMode = 1 (vbAutomatic) in the form designer.
@@ -335,7 +335,7 @@ PaintPicture's opcode says how each copied pixel is combined with the pixel alre
vbTransparentCopy &H2000000 Every pixel of the picture but the key colour: a colour given after the opcode, else the colour of the picture's top-left pixel (DVX only)
.endtable
-vbGrayCopy and vbTransparentCopy copy the picture at its own size (width and height are not used): Picture1.PaintPicture "ship.bmp", x, y, 32, 32, 0, 0, 32, 32, vbTransparentCopy, vbMagenta draws a sprite.
+vbGrayCopy and vbTransparentCopy copy the picture at its own size (width and height are not used): Picture1.PaintPicture "ship.bmp", x, y, 32, 32, 0, 0, 32, 32, vbTransparentCopy, vbMagenta draws a sprite. A picture file (or a LOADPICTURE picture) drawn again and again, a sprite in a game loop say, is read from the disk only the first time while the program runs and the file does not change (about a megabyte of pictures is kept that way).
Any of the 256 raster operations of Windows can be given by its number; the bits work on the pixel values, so with fewer than 32 bits a pixel the results follow the display's colour format.
diff --git a/src/apps/kpunch/dvxbasic/dvxbasic.res b/src/apps/kpunch/dvxbasic/dvxbasic.res
index 891bc801..d38cc048 100644
--- a/src/apps/kpunch/dvxbasic/dvxbasic.res
+++ b/src/apps/kpunch/dvxbasic/dvxbasic.res
@@ -1,3 +1,25 @@
+# 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.
+
# dvxbasic.res -- Resource manifest for DVX BASIC
icon32 icon icon32.bmp
name text "DVX BASIC"
diff --git a/src/apps/kpunch/dvxbasic/form.dhs b/src/apps/kpunch/dvxbasic/form.dhs
index ed669675..d4f5d7c5 100644
--- a/src/apps/kpunch/dvxbasic/form.dhs
+++ b/src/apps/kpunch/dvxbasic/form.dhs
@@ -37,6 +37,11 @@
.index vbModal
.index New (forms)
.index WindowState
+.index MaxWidth (form)
+.index MaxHeight (form)
+.index GameInput
+.index FullScreen (form)
+.index dvxFullScreenActive
.index vbMinimized
.index NewArgs
.index COMMAND$ (new arguments)
@@ -88,12 +93,16 @@ The Form is the top-level container representing a DVX window. It is declared in
WindowState Integer 0 vbNormal (0), vbMinimized (1, shown only as an icon at the bottom of the screen), or vbMaximized (2, filling the screen). Set on a hidden form (or in the .frm file), it takes effect when the form is shown; reading it then gives the state the form will have.
Controls Collection (none) The form's controls (read-only), in the order they were made: Me.Controls.Count, Me.Controls(1) (the first), Me.Controls("Text1"), For Each c In Me.Controls. Elements of a control array are in it too but have no name key. Each use makes a new Collection; changing it does not change the form.
DesignMode Boolean False True puts the form in design mode, for a form designer: see Designing Forms below. Not in .frm files.
- Icon Picture (DVX's) The picture shown for the window while it is minimized: a picture file name (BMP, PNG, JPEG, or GIF) or a picture from LOADPICTURE (Set works too); "" puts DVX's back. Reading it gives a Picture object. Also in the .frm file. A file that is not a picture is an error.
+ Icon Picture (DVX's) The picture shown for the window while it is minimized: a picture file name (BMP, PNG, JPEG, or GIF) or a picture from LOADPICTURE or LOADPICTUREDATA (Set works too); "" puts DVX's back. Reading it gives a Picture object. Also in the .frm file. A file that is not a picture is an error.
Tag String "" Any text the program wants to keep with the form, as for controls.
MousePointer Integer 0 The mouse pointer over the form where its controls have none (see MousePointer under Common Properties).
FontName String "System" The form's font: the font of every control on it that has none of its own, and of the form's Print and TextWidth. FontSize, FontBold, FontItalic, FontUnderline, and FontStrikethru go with it, as for controls (see Fonts under Common Properties). Also in the .frm file.
ActiveControl Control (none) The control of the form that has the keyboard focus (read-only), Nothing when none of them has it: Me.ActiveControl.SelText for an Edit menu's Copy. Screen.ActiveControl is the focused control of the whole program. Not in .frm files.
DesignSelection String "" The name of the control (Text1, or Text1(2) in a control array) whose selection handles are drawn while the form is in design mode; "" for none. Not in .frm files.
+ MaxWidth Integer 0 The widest the user can make the window, by dragging or maximizing; 0 is the screen's width. A window wider than a new MaxWidth shrinks to it. Also in the .frm file.
+ MaxHeight Integer 0 The tallest the window can be made, as MaxWidth; 0 is the screen's height.
+ GameInput Boolean False True for a game: while the form has the keyboard, every key goes to it -- none to F1 (help), F10 or Alt (the menu bar), menu shortcuts, or DVX's shortcut keys. Only Alt+Tab, Ctrl+Esc, and Alt+F4 still reach DVX. Also in the .frm file.
+ FullScreen Boolean False True gives the form the whole screen: a video mode as large as the form's inside (its controls and drawing, without the title bar), the form shown on it at the largest whole multiple of its size that fits, centred on black, kept up to date as it changes. The mouse works on it as in the window, and every key goes to it as with GameInput. A hidden form is shown first. When the user switches to another program (Alt+Tab, Ctrl+Esc), or the program shows a message box or another form, the desktop comes back; the form gets the whole screen again with the focus. Reads False when no video mode could be set (the form stays in its window). False gives the desktop back. Run time only; see the FullScreen event.
.endtable
Maximizing a window, or restoring a maximized one, changes its size, so Resize fires. Minimizing does not change the size and fires no Resize. A minimized window keeps its maximized state: set WindowState = vbMaximized to bring it back filling the screen, vbNormal to bring it back at its normal size.
@@ -118,6 +127,7 @@ frmTerminal.WindowState = vbMinimized ' starts as an icon
DragDrop Source As Control, X As Integer, Y As Integer Fires when a dragged control is dropped on an empty part of the form. X and Y are where, in the form.
DragOver Source As Control, X As Integer, Y As Integer, State As Integer Fires while a dragged control moves over an empty part of the form. State is vbEnter, vbLeave, or vbOver.
NewArgs (none) Fires when the running program is handed new arguments: COMMAND$ has changed (see New Arguments below).
+ FullScreen State As Integer With FullScreen True: dvxFullScreenActive (1) when the form has the whole screen (draw anything that must look new then), dvxFullScreenSuspended (2) when the desktop came back while another window has the focus, dvxFullScreenEnded (0) when the form's video mode could not be set again (FullScreen is False now).
DesignMouse Action As Integer, Button As Integer, X As Integer, Y As Integer, CtrlName As String, Handle As Integer In design mode: the mouse was pressed, moved, released, or double-clicked in the form. See Designing Forms below.
DesignKey KeyCode As Integer, Shift As Integer In design mode: a key was pressed in the form (KeyCode and Shift as in KeyDown).
DesignPropertyChanged CtrlName As String, PropertyName As String In design mode: the code of a UserControl on the form called PropertyChanged. See Designing Forms below.
diff --git a/src/apps/kpunch/dvxbasic/formrt/basRunHost.c b/src/apps/kpunch/dvxbasic/formrt/basRunHost.c
index a4b0f811..2432ede8 100644
--- a/src/apps/kpunch/dvxbasic/formrt/basRunHost.c
+++ b/src/apps/kpunch/dvxbasic/formrt/basRunHost.c
@@ -40,7 +40,12 @@
#define RUN_HOST_ERR_FORMRT "Out of memory creating the form runtime."
#define RUN_HOST_ERR_OTHER "Runtime error"
-// A standalone program's runtime, its module, and the program it runs for
+// A program's slice also ends at the next BIOS clock tick (about 55 ms)
+// after it began, however few its steps (basVmSetSliceClock)
+#define RUN_HOST_SLICE_TICKS 1
+
+// A runtime ended with its program by force (standalone or endWithApp),
+// its module when it owns it (standalone), and the program it runs for
typedef struct {
BasFormRtT *rt;
BasModuleT *module;
@@ -68,11 +73,13 @@ bool basRunHostAlive(const BasFormRtT *rt) {
// The shell's kill hook: a program ended by force has its runtimes ended
// as its own end would (its windows are still there, none of its code
-// runs).
+// runs), except that what it drew for the printer is thrown away, not
+// printed (that would hold the kill for seconds).
static void basRunHostAppKilled(int32_t appId) {
for (ptrdiff_t i = arrlen(sHosts) - 1; i >= 0; i--) {
if (i < arrlen(sHosts) && sHosts[i].appId == appId) {
dvxLog("BASIC: '%s' ended by force; its runtime freed", sHosts[i].rt->title);
+ basFormRtPrinterKill(sHosts[i].rt);
basRunHostDestroy(sHosts[i].rt);
}
}
@@ -116,6 +123,9 @@ BasFormRtT *basRunHostCreate(AppContextT *ctx, BasModuleT *module, const BasRunS
basVmSetDoEventsCallback(vm, spec->doEventsFn, spec->doEventsCtx);
}
+ basVmSetSliceClock(vm, platformBiosTicks(), RUN_HOST_SLICE_TICKS);
+ basVmSetMicrosClock(vm, platformMicros);
+
// DECLARE LIBRARY functions
BasExternCallbacksT extCb;
@@ -142,8 +152,8 @@ BasFormRtT *basRunHostCreate(AppContextT *ctx, BasModuleT *module, const BasRunS
snprintf(rt->helpFile, sizeof(rt->helpFile), "%s", spec->helpFile);
}
- if (spec->standalone) {
- HostRecT rec = { rt, module, ctx->currentAppId };
+ if (spec->standalone || spec->endWithApp) {
+ HostRecT rec = { rt, spec->standalone ? module : NULL, ctx->currentAppId };
arrput(sHosts, rec);
diff --git a/src/apps/kpunch/dvxbasic/formrt/basRunHost.h b/src/apps/kpunch/dvxbasic/formrt/basRunHost.h
index e7bcac7d..0596f719 100644
--- a/src/apps/kpunch/dvxbasic/formrt/basRunHost.h
+++ b/src/apps/kpunch/dvxbasic/formrt/basRunHost.h
@@ -39,6 +39,9 @@
#include
#include
+// A program's task stack: the standalone stub's, and the IDE's runs.
+#define BAS_RUN_STACK_SIZE 65536
+
// What a program is run with. Any pointer may be NULL (none / default).
typedef struct {
const char *appDir; // App.Path; App.Data is its DATA folder (the files it ships)
@@ -54,6 +57,7 @@ typedef struct {
void *doEventsCtx;
bool suppressErrorDialog; // the host reports runtime errors itself
bool standalone; // the .app's own program (basstub): see basRunHostCreate
+ bool endWithApp; // ended with the program it runs for when that is ended by force (the IDE's runs on a task of their own); standalone ones are
} BasRunSpecT;
// A form's caption saved while " [break]" is appended to it.
@@ -71,10 +75,10 @@ bool basRunHostAlive(const BasFormRtT *rt);
// A VM running module (borrowed: it must outlive the runtime) and its
// form runtime, set up from spec. A standalone program's runtime
-// (spec->standalone, once it is made) owns the module, freed with it, and
-// is ended as the program's own end would when the program is ended by
-// force (the shell's kill hook); the IDE and basdev end theirs
-// themselves. Returns the runtime (rt->vm is the
+// (spec->standalone, once it is made) owns the module, freed with it. It
+// and one made with spec->endWithApp are ended as the program's own end
+// would when the program they run for (ctx->currentAppId) is ended by
+// force (the shell's kill hook); basdev ends its runs itself. Returns the runtime (rt->vm is the
// VM), or NULL with *outError set when memory runs out. A module the VM
// refuses (too many globals) leaves rt->vm->module NULL; running it then
// fails with the reason in basVmGetError.
diff --git a/src/apps/kpunch/dvxbasic/formrt/formCtl.c b/src/apps/kpunch/dvxbasic/formrt/formCtl.c
index 02f32d5d..bfae120d 100644
--- a/src/apps/kpunch/dvxbasic/formrt/formCtl.c
+++ b/src/apps/kpunch/dvxbasic/formrt/formCtl.c
@@ -61,9 +61,11 @@
// Running code: a program's own controls run in its VM with its rules (an
// untrapped error ends the program). An installed control file has one VM
// shared by every program that uses its controls: it has no DoEvents host
-// (its code never yields), a call from another task while it runs code is
-// refused (the event is dropped, a paint keeps the old picture; logged
-// once), and an untrapped error is shown and the control carries on.
+// (its code never yields, so a call that runs too long is stopped and its
+// waits are refused: UC_RUN_LIMIT_SLICES), a call from another task while
+// it runs code is refused (the event is dropped, a paint keeps the old
+// picture; logged once), and an untrapped error is shown and the control
+// carries on.
//
// Lifetime: a widget destroyed while its code runs (a host handler that
// unloads the form) stays allocated, cut off from its widgets, until the
@@ -79,7 +81,6 @@
#include "dvxPlat.h"
#include "thirdparty/stb_ds_wrap.h"
-#include
#include
#include
#include
@@ -100,6 +101,7 @@
// The mouse event handlers' four arguments, a key handler's two.
#define UC_MOUSE_ARGS 4
#define UC_KEY_ARGS 2
+#define UC_EVENT_ARGS_MAX UC_MOUSE_ARGS
// WGT_METHOD_ON_MOUSE_EVENT gives a pressed or released button as its
// ordinal (1 left, 2 right, 3 middle); BASIC handlers get VB's button
@@ -130,6 +132,12 @@ static const int32_t sButtonBits[UC_MOUSE_ORDINALS] = { 0, MOUSE_LEFT, MOUSE_RIG
#define UC_PROGRAM_KEY_EXTRA 12
#define UC_TYPE_KEY_MAX (BAS_MAX_IDENT + UC_PROGRAM_KEY_EXTRA)
+// An installed control file's code has no event pump, so nothing else runs
+// while it does: a call into it (or its module code) that runs more than
+// this many slices (about two seconds) is stopped with error 18, and its
+// waits (SLEEP ...) are refused (basVmSetRunLimit).
+#define UC_RUN_LIMIT_SLICES 40
+
// An installed control file: its module, and the runtime and VM its code
// runs in (made with its first control). A designer's project controls
// (basCtlLoadDesign) are one too.
@@ -172,6 +180,14 @@ typedef struct {
bool retired; // unregistered (its program ended); freed with its last control
} UcClassT;
+// An event of a control whose program runs on another task, waiting for
+// that program's own (ucDefer, basUcPump), its arguments copied.
+typedef struct {
+ int32_t evt;
+ int32_t argc;
+ BasValueT args[UC_EVENT_ARGS_MAX];
+} UcPendingT;
+
// One control (widget).
typedef struct {
UcClassT *cls;
@@ -192,6 +208,7 @@ typedef struct {
bool inPaint;
bool dead; // its widget is gone; freed when depth is 0
bool orphan; // its program ended first: no object, no code, only the widget left
+ UcPendingT *pending; // stb_ds: events from other tasks, oldest first (basUcPump)
} UcInstT;
static UcClassT **sClasses = NULL; // stb_ds
@@ -199,20 +216,20 @@ static uint32_t sProgramKeys = 0; // serials of program-owned ty
static UcInstT **sInsts = NULL; // stb_ds
static UcFileT **sFiles = NULL; // stb_ds
static BasUcPropChangedFnT sPropChanged = NULL;
-static uint32_t (*sTaskIdFn)(void) = NULL;
-static bool sTaskIdResolved = false;
// Function prototypes (alphabetical)
int32_t basCtlLoadDesign(const BasFormRtT *rt, BasModuleT *module, const char *const *headers, int32_t count, const char *path);
bool basCtlLoadFile(const char *path);
bool basCtlUnloadDesign(const BasFormRtT *rt);
bool basFormRtRegisterCtl(BasFormRtT *rt, const char *text, int32_t len);
+bool basUcBusy(void);
bool basUcHeaderParse(const char *text, int32_t len, BasUcHeaderT *out);
bool basUcIconPath(const WgtIfaceT *iface, char *out, int32_t outSize);
const BasUcHeaderT *basUcIfaceHeader(const WgtIfaceT *iface);
bool basUcIfaceVisible(const BasFormRtT *rt, const WgtIfaceT *iface);
bool basUcIsControlType(const char *typeName);
const WgtIfaceT *basUcOwnIface(const BasFormRtT *rt, const char *typeName);
+void basUcPump(void);
BasControlT *basUcResolveSurface(const BasFormRtT *rt, void *ref);
const char *basUcResolveType(const BasFormRtT *rt, const char *typeName);
void basUcRuntimeEnd(BasFormRtT *rt);
@@ -227,6 +244,7 @@ static bool ucCallEvent(UcInstT *inst, int32_t evt, const BasValueT *
static const BasClassInfoT *ucClassInfo(const UcClassT *cls);
static UcClassT *ucClassOf(const WgtIfaceT *iface);
static WidgetT *ucCreate(WidgetT *parent, const WgtIfaceT *iface);
+static bool ucDefer(UcInstT *inst, int32_t evt, const BasValueT *args, int32_t argc);
static void ucDestroy(WidgetT *w);
static void ucDropObject(UcInstT *inst);
static bool ucDynCall(WidgetT *w, int32_t methodIdx, const WgtValueT *args, int32_t argc, WgtValueT *ret);
@@ -238,6 +256,7 @@ static void ucEventHost(void *ctx, const BasObjectT *obj, int32_t eve
static bool ucFileReady(UcFileT *file, WidgetT *parent);
static UcInstT *ucFindInst(const BasObjectT *obj);
static void ucFocus(WidgetT *w);
+static bool ucForeign(const UcInstT *inst);
static void ucFreeClass(UcClassT *cls);
static void ucFreeInst(UcInstT *inst);
static void ucHold(UcInstT *inst);
@@ -256,7 +275,9 @@ static const char *ucNameTaken(const BasFormRtT *rt, const UcFileT *file, co
static UcClassT *ucOwnClass(const BasFormRtT *rt, const char *name);
static const BasFormRtT *ucOwner(const UcClassT *cls);
static void ucPaintNow(UcInstT *inst);
+static void ucPendingFree(UcPendingT *ev);
static void ucPoll(WidgetT *w, WindowT *win);
+static void ucPollNow(UcInstT *inst);
static bool ucPrivate(const UcClassT *cls);
static bool ucPrivateIface(const WgtIfaceT *iface);
static UcClassT *ucRegister(BasFormRtT *rt, UcFileT *file, const BasModuleT *module, const BasUcHeaderT *header);
@@ -264,7 +285,6 @@ static bool ucRelease(UcInstT *inst);
static void ucRetireClass(UcClassT *cls);
static BasFormRtT *ucRt(const UcClassT *cls);
static UcInstT *ucSurfaceInst(const BasFormRtT *rt, const void *ref);
-static uint32_t ucTaskId(void);
static WgtValueT ucToWgt(UcInstT *inst, BasValueT v, uint8_t want);
static bool ucUserMode(const UcInstT *inst);
static bool ucWheel(WidgetT *w, int32_t delta);
@@ -429,6 +449,19 @@ bool basFormRtRegisterCtl(BasFormRtT *rt, const char *text, int32_t len) {
}
+// A control has events from another task waiting for its program's turn
+// (basFormRtBusy).
+bool basUcBusy(void) {
+ for (int32_t i = 0; i < (int32_t)arrlen(sInsts); i++) {
+ if (arrlen(sInsts[i]->pending) > 0) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+
bool basUcHeaderParse(const char *text, int32_t len, BasUcHeaderT *out) {
char value[FRM_MAX_LINE_LEN];
@@ -520,6 +553,46 @@ const WgtIfaceT *basUcOwnIface(const BasFormRtT *rt, const char *typeName) {
}
+// The program's turn on its own task (basFormRtUpdate): the events its
+// controls got on other tasks run, oldest first, then their Resize and
+// Paint. Every control taken is held, so one a handler destroys stays
+// readable until all have run.
+void basUcPump(void) {
+ UcInstT **todo = NULL;
+
+ for (int32_t i = 0; i < (int32_t)arrlen(sInsts); i++) {
+ const BasFormRtT *rt = ucRt(sInsts[i]->cls);
+
+ if (!sInsts[i]->dead && !sInsts[i]->orphan && rt && rt->ownTask == basFormRtTaskId()) {
+ ucHold(sInsts[i]);
+ arrput(todo, sInsts[i]);
+ }
+ }
+
+ for (int32_t i = 0; i < (int32_t)arrlen(todo); i++) {
+ UcInstT *inst = todo[i];
+
+ while (arrlen(inst->pending) > 0 && !inst->dead) {
+ UcPendingT ev = inst->pending[0];
+
+ arrdel(inst->pending, 0);
+ ucCallEvent(inst, ev.evt, ev.args, ev.argc);
+ ucPendingFree(&ev);
+ }
+
+ if (!inst->dead) {
+ ucPollNow(inst);
+ }
+ }
+
+ for (int32_t i = 0; i < (int32_t)arrlen(todo); i++) {
+ ucRelease(todo[i]);
+ }
+
+ arrfree(todo);
+}
+
+
BasControlT *basUcResolveSurface(const BasFormRtT *rt, void *ref) {
UcInstT *inst = ucSurfaceInst(rt, ref);
@@ -835,7 +908,16 @@ static bool ucCallEvent(UcInstT *inst, int32_t evt, const BasValueT *args, int32
int32_t proc = ucClassInfo(inst->cls)->ucProcs[evt];
BasFormRtT *prev = NULL;
- if (proc < 0 || inst->dead || !inst->obj || !ucEnter(inst, &prev)) {
+ if (proc < 0 || inst->dead || !inst->obj) {
+ return false;
+ }
+
+ // Its program's code runs only on its program's task
+ if (ucForeign(inst)) {
+ return ucDefer(inst, evt, args, argc);
+ }
+
+ if (!ucEnter(inst, &prev)) {
return false;
}
@@ -943,6 +1025,38 @@ static WidgetT *ucCreate(WidgetT *parent, const WgtIfaceT *iface) {
}
+// An event of a control whose program runs on another task waits for
+// that program's turn (basUcPump); a newer MouseMove replaces one still
+// waiting. True: it will run.
+static bool ucDefer(UcInstT *inst, int32_t evt, const BasValueT *args, int32_t argc) {
+ UcPendingT *ev = NULL;
+
+ for (int32_t i = 0; evt == BAS_UC_EVT_MOUSEMOVE && i < (int32_t)arrlen(inst->pending); i++) {
+ if (inst->pending[i].evt == evt) {
+ ev = &inst->pending[i];
+ ucPendingFree(ev);
+ }
+ }
+
+ if (!ev) {
+ UcPendingT fresh;
+
+ memset(&fresh, 0, sizeof(fresh));
+ arrput(inst->pending, fresh);
+ ev = &arrlast(inst->pending);
+ }
+
+ ev->evt = evt;
+ ev->argc = DVX_MIN(argc, UC_EVENT_ARGS_MAX);
+
+ for (int32_t i = 0; i < ev->argc; i++) {
+ ev->args[i] = basValCopy(args[i]);
+ }
+
+ return true;
+}
+
+
// The widget is being destroyed (its canvas already is): UserControl_
// Terminate, then the object goes. While code of it runs, the control
// stays allocated, cut off from the widgets, until the calls return. The
@@ -1084,7 +1198,7 @@ static bool ucEnter(UcInstT *inst, BasFormRtT **outPrev) {
}
if (file) {
- uint32_t task = ucTaskId();
+ uint32_t task = basFormRtTaskId();
if (file->depth > 0 && file->task != task) {
if (!file->refusedLogged) {
@@ -1230,6 +1344,8 @@ static bool ucFileReady(UcFileT *file, WidgetT *parent) {
}
basVmSetEventHost(file->rt->vm, ucEventHost, NULL);
+ basVmSetRunLimit(file->rt->vm, UC_RUN_LIMIT_SLICES);
+ file->rt->ownTask = BAS_RT_ANY_TASK;
// A designer's controls never run the module-level code: it is the
// program's start
@@ -1278,6 +1394,16 @@ static void ucFocus(WidgetT *w) {
}
+// The control's program runs on another task: its code runs on that
+// task alone (two tasks in one VM would interleave their calls). An
+// installed file's runs on any (ucEnter refuses a second while it runs).
+static bool ucForeign(const UcInstT *inst) {
+ const BasFormRtT *rt = ucRt(inst->cls);
+
+ return rt && rt->ownTask != BAS_RT_ANY_TASK && rt->ownTask != basFormRtTaskId();
+}
+
+
// A class's memory (out of the registry, its controls gone).
static void ucFreeClass(UcClassT *cls) {
for (int32_t i = 0; i < (int32_t)arrlen(cls->texts); i++) {
@@ -1319,6 +1445,11 @@ static void ucFreeInst(UcInstT *inst) {
basStringUnref(inst->hold);
}
+ for (int32_t i = 0; i < (int32_t)arrlen(inst->pending); i++) {
+ ucPendingFree(&inst->pending[i]);
+ }
+
+ arrfree(inst->pending);
basFormRtDrawFree(&inst->surface);
UcClassT *cls = inst->cls;
@@ -1647,21 +1778,42 @@ static void ucPaintNow(UcInstT *inst) {
}
-// Between frames: a size the layout gave the control makes a canvas of
-// that size (cleared to its BackColor), then Resize and Paint; a Refresh
-// that came during Paint paints again.
+// A waiting event's copied arguments go.
+static void ucPendingFree(UcPendingT *ev) {
+ for (int32_t i = 0; i < ev->argc; i++) {
+ basValRelease(&ev->args[i]);
+ }
+
+ ev->argc = 0;
+}
+
+
+// The widget poll between frames (ucPollNow).
+// cppcheck-suppress constParameterCallback ; a widget class method: WidgetClassT fixes the signature
static void ucPoll(WidgetT *w, WindowT *win) {
UcInstT *inst = ucInstOf(w);
(void)win;
- if (!inst) {
+ // On another task than its program's, its program's turn does it
+ // (basUcPump)
+ if (!inst || ucForeign(inst)) {
return;
}
ucHold(inst);
+ ucPollNow(inst);
+ ucRelease(inst);
+}
- if (inst->sizeDirty && w->w > 0 && w->h > 0) {
+
+// ucPoll's work (the caller holds inst): a new size makes a canvas of
+// that size, then Resize and Paint; a Refresh that came during Paint
+// paints again.
+static void ucPollNow(UcInstT *inst) {
+ WidgetT *w = inst->self;
+
+ if (inst->sizeDirty && w && w->w > 0 && w->h > 0) {
inst->sizeDirty = false;
inst->lastW = w->w;
inst->lastH = w->h;
@@ -1674,8 +1826,6 @@ static void ucPoll(WidgetT *w, WindowT *win) {
if (inst->paintPending && !inst->dead) {
ucPaintNow(inst);
}
-
- ucRelease(inst);
}
@@ -1831,18 +1981,6 @@ static UcInstT *ucSurfaceInst(const BasFormRtT *rt, const void *ref) {
}
-// The running task (the task switcher's, found at run time; 0 without
-// one).
-static uint32_t ucTaskId(void) {
- if (!sTaskIdResolved) {
- sTaskIdResolved = true;
- sTaskIdFn = (uint32_t (*)(void))dlsym(NULL, DVX_SYM("tsCurrentId"));
- }
-
- return sTaskIdFn ? sTaskIdFn() : 0;
-}
-
-
// A BASIC value as the widget value the host gets: its text held by the
// control until the next value it gives.
static WgtValueT ucToWgt(UcInstT *inst, BasValueT v, uint8_t want) {
diff --git a/src/apps/kpunch/dvxbasic/formrt/formDesign.c b/src/apps/kpunch/dvxbasic/formrt/formDesign.c
index fda082f3..ff4d4dd3 100644
--- a/src/apps/kpunch/dvxbasic/formrt/formDesign.c
+++ b/src/apps/kpunch/dvxbasic/formrt/formDesign.c
@@ -967,8 +967,9 @@ const char *CtlTypeEventParams(const char *typeName, const char *eventName) {
// designer.bas: CTL_FLAG_CONTAINER (holds controls) and CTL_FLAG_DESIGNABLE
-// (can be created with CreateControl, so placed in a designer); -1 for an
-// unknown type.
+// (can be created with CreateControl, so placed in a designer: every type
+// can, an Image or ImageButton with an empty picture); -1 for an unknown
+// type.
int32_t CtlTypeFlags(const char *typeName) {
const WgtIfaceT *iface = basCtlIface(typeName);
@@ -976,7 +977,7 @@ int32_t CtlTypeFlags(const char *typeName) {
return BAS_CTL_TYPE_UNKNOWN;
}
- return (iface->isContainer ? BAS_CTL_FLAG_CONTAINER : 0) | ((iface->createSig != WGT_CREATE_PARENT_DATA) ? BAS_CTL_FLAG_DESIGNABLE : 0);
+ return (iface->isContainer ? BAS_CTL_FLAG_CONTAINER : 0) | BAS_CTL_FLAG_DESIGNABLE;
}
diff --git a/src/apps/kpunch/dvxbasic/formrt/formGfx.c b/src/apps/kpunch/dvxbasic/formrt/formGfx.c
index f1512b27..02d0792b 100644
--- a/src/apps/kpunch/dvxbasic/formrt/formGfx.c
+++ b/src/apps/kpunch/dvxbasic/formrt/formGfx.c
@@ -67,6 +67,8 @@
#include
#include
#include
+#include
+#include
#define SCREEN_SCALE_MAX 3 // largest whole-number window scale
#define SCREEN_PIXEL_SINGLE 1 // a framebuffer pixel one window pixel across / down (square)
@@ -117,6 +119,13 @@
#define DRAW_ACCEL_ENABLED_ARG 1 // PrintAccel text [, enabled]
#define DRAW_ACCEL_MAX_ARGS 2
#define DRAW_PAINT_KEY_ARG 10 // PaintPicture ... , vbTransparentCopy, key colour
+#define DRAW_NO_COLUMN (-1) // PaintPicture: a destination column with no source pixel
+
+// PaintPicture's pictures kept decoded for the runtime (drawPictureLoad):
+// at most this many bytes, and a file changed this recently is decoded
+// again (FAT times are 2 seconds apart)
+#define DRAW_PICS_KEEP_BYTES (1024 * 1024)
+#define DRAW_PICS_TIME_SEC 2
// The scale units (BAS_SCALE_*) in twips: 15 to a pixel, as VB on a
// standard (96 DPI) display, whatever the real display is; characters are
@@ -299,7 +308,9 @@ static bool drawIsSurface(const BasControlT *ctrl);
static bool drawMeasure(BasDrawT *d, const BitmapFontT *font, DrawMethodE method, const BasValueT *args, int32_t argc, BasValueT *result);
static void drawPaintKeyed(BasFormRtT *rt, BasDrawT *d, WidgetT *src, const uint8_t *pixels, int32_t pitch, uint32_t opcode, const BasValueT *args, int32_t argc, int32_t sx, int32_t sy, int32_t sw, int32_t sh, int32_t dx, int32_t dy);
static void drawPaintPicture(BasFormRtT *rt, BasControlT *ctrl, BasDrawT *d, BasValueT *args, int32_t argc);
-static bool drawPictureOf(BasFormRtT *rt, BasFormT *form, BasValueT v, WidgetT **src, uint8_t **pixels, int32_t *w, int32_t *h, int32_t *pitch, const BasGfxT **scale);
+static void drawPaintRows(BasFormRtT *rt, BasDrawT *d, WidgetT *src, const uint8_t *pixels, int32_t pitch, int32_t srcW, int32_t srcH, uint32_t opcode, int32_t sx, int32_t sy, int32_t sw, int32_t sh, int32_t dx, int32_t dy, int32_t dw, int32_t dh);
+static uint8_t *drawPictureLoad(BasFormRtT *rt, const char *path, const BasPictureT *pic, int32_t *w, int32_t *h, int32_t *pitch, bool *owned);
+static bool drawPictureOf(BasFormRtT *rt, BasFormT *form, BasValueT v, WidgetT **src, uint8_t **pixels, int32_t *w, int32_t *h, int32_t *pitch, const BasGfxT **scale, bool *owned);
static int32_t drawPixelSpan(const BasGfxT *g, double length, bool across);
static void drawPrint(BasDrawT *d, const BitmapFontT *font, BasValueT value, int32_t mode);
static uint32_t drawRasterOp(uint32_t rop, uint32_t pattern, uint32_t source, uint32_t dest);
@@ -520,6 +531,16 @@ BasDrawT *basFormRtDrawNew(AppContextT *ctx, const BasGfxSurfaceT *surf, double
}
+void basFormRtDrawPicsFree(BasFormRtT *rt) {
+ for (int32_t i = 0; i < (int32_t)arrlen(rt->drawPics); i++) {
+ dvxFreeImage(rt->drawPics[i].pixels);
+ }
+
+ arrfree(rt->drawPics);
+ rt->drawPicsBytes = 0;
+}
+
+
// Columns and lines count charW x charH pixel cells from the left edge,
// CurrentX / CurrentY kept in the scale; text moves CurrentX by the width
// emit drew it at.
@@ -530,24 +551,27 @@ uint8_t *basFormRtDrawPictureRgb(BasFormRtT *rt, BasFormT *form, BasValueT pic,
uint8_t *pixels = NULL;
WidgetT *src = NULL;
int32_t pitch = 0;
- int32_t bpp = rt->ctx->display.format.bytesPerPixel;
+ bool owned = false;
+ DisplayT canvas;
- if (!drawPictureOf(rt, form, pic, &src, &pixels, w, h, &pitch, scale)) {
+ if (!drawPictureOf(rt, form, pic, &src, &pixels, w, h, &pitch, scale, &owned)) {
return NULL;
}
- uint8_t *rgb = (uint8_t *)malloc((size_t)*w * (size_t)*h * DRAW_RGB_BYTES);
+ // A PictureBox's (or form's) pixels are its canvas's, in the display's
+ // format either way
+ const uint8_t *buf = pixels;
- for (int32_t y = 0; rgb && y < *h; y++) {
- for (int32_t x = 0; x < *w; x++) {
- uint8_t *p = rgb + ((size_t)y * (size_t)*w + (size_t)x) * DRAW_RGB_BYTES;
- uint32_t pixel = pixels ? readPixel(pixels + y * pitch + x * bpp, bpp) : wgtCanvasGetPixel(src, x, y);
-
- unpackColor(&rt->ctx->display, pixel, &p[0], &p[1], &p[2]);
- }
+ if (!buf && wgtCanvasGetDisplay(src, &canvas)) {
+ buf = canvas.backBuf;
+ pitch = canvas.pitch;
}
- dvxFreeImage(pixels);
+ uint8_t *rgb = buf ? dvxBufferToRgb(&rt->ctx->display, buf, *w, *h, pitch) : NULL;
+
+ if (owned) {
+ dvxFreeImage(pixels);
+ }
if (!rgb) {
basFormRtRuntimeError(rt, "Out of memory", "There is not enough memory for the picture.");
@@ -1117,34 +1141,37 @@ static void drawPaintKeyed(BasFormRtT *rt, BasDrawT *d, WidgetT *src, const uint
// destination (nearest pixel), each pixel combined with the one there and
// the FillColor pattern by the raster operation opcode (vbSrcCopy when
// left out; vbSrcAnd, vbSrcPaint, vbSrcInvert ... -- any of Windows' 256).
+// A plain copy at the source's size is one rectangle copy; otherwise each
+// destination column's source column is worked out once and a row is
+// built and copied whole, the canvas buffers read directly.
static void drawPaintPicture(BasFormRtT *rt, BasControlT *ctrl, BasDrawT *d, BasValueT *args, int32_t argc) {
uint8_t *pixels = NULL;
WidgetT *src = NULL;
const BasGfxT *srcScale = NULL;
+ bool owned = false;
int32_t srcW = 0;
int32_t srcH = 0;
int32_t pitch = 0;
- int32_t bpp = rt->ctx->display.format.bytesPerPixel;
if (argc < DRAW_PAINT_MIN_ARGS) {
basFormRtRuntimeError(rt, "PaintPicture needs a picture, x, and y", "Usage: PaintPicture picture, x, y [, width, height [, srcX, srcY, srcWidth, srcHeight [, opcode]]]");
return;
}
- if (!drawPictureOf(rt, ctrl->form, args[0], &src, &pixels, &srcW, &srcH, &pitch, &srcScale)) {
+ if (!drawPictureOf(rt, ctrl->form, args[0], &src, &pixels, &srcW, &srcH, &pitch, &srcScale, &owned)) {
return;
}
// The destination in this object's scale, the source rectangle in the
// source PictureBox's (an image file's in pixels)
- int32_t sx = 0;
- int32_t sy = 0;
- int32_t sw = srcW;
- int32_t sh = srcH;
- int32_t dx = 0;
- int32_t dy = 0;
- int32_t dw = 0;
- int32_t dh = 0;
+ int32_t sx = 0;
+ int32_t sy = 0;
+ int32_t sw = srcW;
+ int32_t sh = srcH;
+ int32_t dx = 0;
+ int32_t dy = 0;
+ int32_t dw = 0;
+ int32_t dh = 0;
if (argc >= DRAW_PAINT_SRC_ARGS) {
basFormRtDrawToPixels(srcScale, basValToNumber(args[5]), basValToNumber(args[6]), basValToNumber(args[7]), basValToNumber(args[8]), &sx, &sy, &sw, &sh);
@@ -1162,62 +1189,195 @@ static void drawPaintPicture(BasFormRtT *rt, BasControlT *ctrl, BasDrawT *d, Bas
// DVX's copies: greyed, or colour-keyed (at the source's size)
if (opcode == BAS_ROP_GRAYCOPY || opcode == BAS_ROP_TRANSPCOPY) {
drawPaintKeyed(rt, d, src, pixels, pitch, opcode, args, argc, sx, sy, sw, sh, dx, dy);
+ } else {
+ drawPaintRows(rt, d, src, pixels, pitch, srcW, srcH, opcode, sx, sy, sw, sh, dx, dy, dw, dh);
+ }
+
+ if (owned) {
dvxFreeImage(pixels);
+ }
+}
+
+
+// drawPaintPicture's raster-op copy onto the canvas, its source the image
+// pixels or else the PictureBox src's canvas.
+static void drawPaintRows(BasFormRtT *rt, BasDrawT *d, WidgetT *src, const uint8_t *pixels, int32_t pitch, int32_t srcW, int32_t srcH, uint32_t opcode, int32_t sx, int32_t sy, int32_t sw, int32_t sh, int32_t dx, int32_t dy, int32_t dw, int32_t dh) {
+ const BlitOpsT *ops = &rt->ctx->blitOps;
+ int32_t bpp = ops->bytesPerPixel;
+ DisplayT dst;
+ DisplayT srcDisp;
+
+ if (!pixels) {
+ if (!src || !wgtCanvasGetDisplay(src, &srcDisp)) {
+ return;
+ }
+
+ pixels = srcDisp.backBuf;
+ pitch = srcDisp.pitch;
+ }
+
+ if (sw <= 0 || sh <= 0 || dw <= 0 || dh <= 0 || !wgtCanvasGetDisplay(d->canvas, &dst)) {
+ return;
+ }
+
+ uint32_t rop = (opcode >> BAS_ROP_TABLE_SHIFT) & BAS_ROP_TABLE_MASK;
+ bool copy = rop == (BAS_ROP_SRCCOPY >> BAS_ROP_TABLE_SHIFT);
+
+ // A plain copy at the source's size: one rectangle copy (rectCopy clips
+ // to the canvas), the source rectangle kept inside the source
+ if (copy && dw == sw && dh == sh) {
+ int32_t cutX = DVX_MAX(0, -sx);
+ int32_t cutY = DVX_MAX(0, -sy);
+ int32_t w = DVX_MIN(sw - cutX, srcW - (sx + cutX));
+ int32_t h = DVX_MIN(sh - cutY, srcH - (sy + cutY));
+
+ if (w > 0 && h > 0) {
+ rectCopy(&dst, ops, dx + cutX, dy + cutY, pixels, pitch, sx + cutX, sy + cutY, w, h);
+ wgtInvalidatePaint(d->canvas);
+ }
+
return;
}
- uint32_t rop = (opcode >> BAS_ROP_TABLE_SHIFT) & BAS_ROP_TABLE_MASK;
- bool copy = rop == (BAS_ROP_SRCCOPY >> BAS_ROP_TABLE_SHIFT);
uint32_t pattern = surfToPixel(d, d->gfx.fillColor);
uint32_t mask = (bpp >= (int32_t)sizeof(uint32_t)) ? UINT32_MAX : ((1u << (bpp * DRAW_BITS_PER_BYTE)) - 1);
+ int32_t *cols = (int32_t *)malloc((size_t)dw * sizeof(int32_t));
+ uint8_t *row = (uint8_t *)malloc((size_t)dw * (size_t)bpp);
- for (int32_t y = 0; y < dh && sw > 0 && sh > 0; y++) {
+ if (!cols || !row) {
+ free(cols);
+ free(row);
+ return;
+ }
+
+ for (int32_t x = 0; x < dw; x++) {
+ int32_t rx = sx + (int32_t)((int64_t)x * sw / dw);
+
+ cols[x] = (rx >= 0 && rx < srcW) ? rx : DRAW_NO_COLUMN;
+ }
+
+ for (int32_t y = 0; y < dh; y++) {
int32_t py = dy + y;
int32_t ry = sy + (int32_t)((int64_t)y * sh / dh);
- if (py < 0 || py >= d->gfx.surf.height || ry < 0 || ry >= srcH) {
+ if (py < 0 || py >= dst.height || ry < 0 || ry >= srcH) {
continue;
}
+ const uint8_t *srcRow = pixels + (size_t)ry * (size_t)pitch;
+ const uint8_t *dstRow = dst.backBuf + (size_t)py * (size_t)dst.pitch;
+
for (int32_t x = 0; x < dw; x++) {
- int32_t px = dx + x;
- int32_t rx = sx + (int32_t)((int64_t)x * sw / dw);
+ int32_t px = dx + x;
+ bool inside = px >= 0 && px < dst.width;
+ uint32_t under = (inside && (!copy || cols[x] == DRAW_NO_COLUMN)) ? readPixel(dstRow + px * bpp, bpp) : 0;
+ uint32_t pixel = under;
- if (px < 0 || px >= d->gfx.surf.width || rx < 0 || rx >= srcW) {
- continue;
+ if (cols[x] != DRAW_NO_COLUMN) {
+ pixel = readPixel(srcRow + cols[x] * bpp, bpp);
+ pixel = copy ? pixel : drawRasterOp(rop, pattern, pixel, under) & mask;
}
- uint32_t pixel = pixels ? readPixel(pixels + ry * pitch + rx * bpp, bpp) : wgtCanvasGetPixel(src, rx, ry);
-
- if (!copy) {
- pixel = drawRasterOp(rop, pattern, pixel, wgtCanvasGetPixel(d->canvas, px, py)) & mask;
- }
-
- surfSpan(d, px, px, py, pixel);
+ memcpy(row + x * bpp, &pixel, (size_t)bpp);
}
+
+ rectCopy(&dst, ops, dx, py, row, dw * bpp, 0, 0, dw, 1);
}
- dvxFreeImage(pixels);
+ free(cols);
+ free(row);
+ wgtInvalidatePaint(d->canvas);
+}
+
+
+// The pixels of a picture file (path) or of a LoadPictureData picture
+// (pic) in the display format, as dvxLoadImage gives them: those kept
+// from an earlier call while the file is unchanged (its size and time,
+// and not changed in the last DRAW_PICS_TIME_SEC; a picture's bytes never
+// change) and the display's pixel size the same, else decoded and, while
+// there is room (DRAW_PICS_KEEP_BYTES for the runtime), kept: a picture
+// drawn every frame is decoded once. *owned: the caller frees them (not
+// kept).
+static uint8_t *drawPictureLoad(BasFormRtT *rt, const char *path, const BasPictureT *pic, int32_t *w, int32_t *h, int32_t *pitch, bool *owned) {
+ struct stat st;
+ int32_t bpp = rt->ctx->display.format.bytesPerPixel;
+ bool stable = true;
+
+ memset(&st, 0, sizeof(st));
+
+ if (!pic) {
+ stable = stat(path, &st) == 0 && (int64_t)time(NULL) - (int64_t)st.st_mtime > DRAW_PICS_TIME_SEC;
+ }
+
+ *owned = true;
+
+ for (int32_t i = 0; i < (int32_t)arrlen(rt->drawPics); i++) {
+ BasDrawPicT *p = &rt->drawPics[i];
+
+ if (p->picture != pic || (!pic && !platformPathSame(p->path, path))) {
+ continue;
+ }
+
+ if (stable && p->size == (int64_t)st.st_size && p->time == (int64_t)st.st_mtime && p->bpp == bpp) {
+ *w = p->w;
+ *h = p->h;
+ *pitch = p->pitch;
+ *owned = false;
+ return p->pixels;
+ }
+
+ rt->drawPicsBytes -= p->pitch * p->h;
+ dvxFreeImage(p->pixels);
+ arrdel(rt->drawPics, i);
+ break;
+ }
+
+ uint8_t *pixels = pic ? dvxLoadImageFromMemory(rt->ctx, pic->data, pic->dataLen, w, h, pitch) : dvxLoadImage(rt->ctx, path, w, h, pitch);
+ int32_t bytes = pixels ? *pitch * *h : 0;
+
+ if (!pixels || !stable || rt->drawPicsBytes + bytes > DRAW_PICS_KEEP_BYTES) {
+ return pixels;
+ }
+
+ BasDrawPicT kept;
+
+ memset(&kept, 0, sizeof(kept));
+ kept.pixels = pixels;
+ kept.picture = pic;
+ kept.size = (int64_t)st.st_size;
+ kept.time = (int64_t)st.st_mtime;
+ kept.w = *w;
+ kept.h = *h;
+ kept.pitch = *pitch;
+ kept.bpp = bpp;
+ snprintf(kept.path, sizeof(kept.path), "%s", pic ? "" : path);
+ arrput(rt->drawPics, kept);
+ rt->drawPicsBytes += bytes;
+ *owned = false;
+ return pixels;
}
// PaintPicture's picture: a PictureBox (a Control variable, or its name,
// looked up from form first), else an image file (named, or a LOADPICTURE
-// Picture object) loaded into *pixels in the display format (free with
-// dvxFreeImage). *scale is the PictureBox's scale (NULL: pixels). False,
-// a runtime error raised, when there is none.
-static bool drawPictureOf(BasFormRtT *rt, BasFormT *form, BasValueT v, WidgetT **src, uint8_t **pixels, int32_t *w, int32_t *h, int32_t *pitch, const BasGfxT **scale) {
- const char *path = basFormRtPicturePath(rt, v);
- BasStringT *name = path ? basStringNew(path, (int32_t)strlen(path)) : (v.type == BAS_TYPE_OBJECT ? NULL : basValFormatString(v));
- void *ref = name ? basFormRtFindCtrl(rt, form, name->data) : v.objVal;
- BasControlT *pic = basFormRtResolveCtrl(rt, ref);
+// Picture object) or a LOADPICTUREDATA Picture object as *pixels in the
+// display format (drawPictureLoad; *owned: free them with dvxFreeImage).
+// *scale is the PictureBox's scale (NULL: pixels). False, a runtime error
+// raised, when there is none.
+static bool drawPictureOf(BasFormRtT *rt, BasFormT *form, BasValueT v, WidgetT **src, uint8_t **pixels, int32_t *w, int32_t *h, int32_t *pitch, const BasGfxT **scale, bool *owned) {
+ const BasPictureT *bytes = basFormRtPictureData(rt, v);
+ const char *path = basFormRtPicturePath(rt, v);
+ BasStringT *name = path ? basStringNew(path, (int32_t)strlen(path)) : (v.type == BAS_TYPE_OBJECT ? NULL : basValFormatString(v));
+ void *ref = name ? basFormRtFindCtrl(rt, form, name->data) : (bytes ? NULL : v.objVal);
+ BasControlT *pic = basFormRtResolveCtrl(rt, ref);
*pixels = NULL;
+ *owned = false;
*src = pic && drawIsSurface(pic) && (!pic->form || pic != &pic->form->formCtrl) ? pic->widget : NULL;
*scale = *src && pic->draw ? &pic->draw->gfx : NULL;
- if (!*src && name) {
- *pixels = dvxLoadImage(rt->ctx, name->data, w, h, pitch);
+ if (!*src && (name || bytes)) {
+ *pixels = drawPictureLoad(rt, name ? name->data : NULL, bytes, w, h, pitch, owned);
}
if (name) {
@@ -1225,7 +1385,7 @@ static bool drawPictureOf(BasFormRtT *rt, BasFormT *form, BasValueT v, WidgetT *
}
if (!*pixels && (!*src || !canvasSize(*src, w, h))) {
- basFormRtRuntimeError(rt, "PaintPicture has no picture", "The picture must be a PictureBox (its name, or a Control variable holding it), a BMP, PNG, JPEG, or GIF image file, a program, widget or library file (its icon), or a picture from LoadPicture.");
+ basFormRtRuntimeError(rt, "PaintPicture has no picture", "The picture must be a PictureBox (its name, or a Control variable holding it), a BMP, PNG, JPEG, or GIF image file, a program, widget or library file (its icon), or a picture from LoadPicture or LoadPictureData.");
return false;
}
diff --git a/src/apps/kpunch/dvxbasic/formrt/formPrinter.c b/src/apps/kpunch/dvxbasic/formrt/formPrinter.c
index 0248db9b..849201ac 100644
--- a/src/apps/kpunch/dvxbasic/formrt/formPrinter.c
+++ b/src/apps/kpunch/dvxbasic/formrt/formPrinter.c
@@ -300,6 +300,7 @@ static bool sApiTried;
void basFormRtPrinterEnd(BasFormRtT *rt);
BasValueT basFormRtPrinterGetProp(BasFormRtT *rt, const char *name);
bool basFormRtPrinterIs(const void *ref);
+void basFormRtPrinterKill(BasFormRtT *rt);
BasValueT basFormRtPrinterMethod(BasFormRtT *rt, const char *name, BasValueT *args, int32_t argc);
void *basFormRtPrinterRef(void);
void basFormRtPrinterSetProp(BasFormRtT *rt, const char *name, BasValueT value);
@@ -309,6 +310,7 @@ static void prEndDoc(BasFormRtT *rt, BasPrinterT *p);
static int32_t prFind(const char *const *names, int32_t count, const char *name);
static int32_t prFont(BasPrinterT *p);
static void prFontsClose(BasPrinterT *p);
+static void prFree(BasFormRtT *rt);
static BasPrinterT *prGet(BasFormRtT *rt);
static void prHome(BasPrinterT *p);
static void prKillDoc(BasPrinterT *p);
@@ -323,7 +325,7 @@ static void prPicture(BasFormRtT *rt, BasPrinterT *p, BasValueT *args, in
static void prPictureBand(const PrOpT *op, const PrBandT *b);
static bool prPrintPage(BasFormRtT *rt, BasPrinterT *p);
static void prShape(BasFormRtT *rt, BasPrinterT *p, PrOpKindE kind, BasValueT *args, int32_t argc);
-static void prShapeAgain(const BasPrinterT *p, const PrOpT *op, const BasGfxSurfaceT *surf);
+static void prShapeAgain(const BasPrinterT *p, const PrOpT *op, const BasGfxSurfaceT *surf, int32_t bandY0, int32_t bandY1);
static int32_t prShapeDo(BasGfxT *g, PrOpKindE kind, const BasValueT *args);
static int32_t prText(void *ctx, const char *text, int32_t len, int32_t x, int32_t y);
static double prTwipsX(const BasPrinterT *p);
@@ -347,9 +349,7 @@ void basFormRtPrinterEnd(BasFormRtT *rt) {
}
prEndDoc(rt, p);
- basFormRtDrawDelete(p->draw);
- free(p);
- rt->printer = NULL;
+ prFree(rt);
}
@@ -426,6 +426,18 @@ bool basFormRtPrinterIs(const void *ref) {
}
+void basFormRtPrinterKill(BasFormRtT *rt) {
+ BasPrinterT *p = rt->printer;
+
+ if (!p) {
+ return;
+ }
+
+ prKillDoc(p);
+ prFree(rt);
+}
+
+
BasValueT basFormRtPrinterMethod(BasFormRtT *rt, const char *name, BasValueT *args, int32_t argc) {
BasValueT result = basValInteger(0);
BasPrinterT *p = prGet(rt);
@@ -742,6 +754,14 @@ static void prFontsClose(BasPrinterT *p) {
// The runtime's Printer, made on first use; NULL (a runtime error raised)
// when printing is not installed.
+// The program's Printer state gone (its document printed or abandoned).
+static void prFree(BasFormRtT *rt) {
+ basFormRtDrawDelete(rt->printer->draw);
+ free(rt->printer);
+ rt->printer = NULL;
+}
+
+
static BasPrinterT *prGet(BasFormRtT *rt) {
const PrApiT *api = prApi();
@@ -916,7 +936,7 @@ static bool prPageBand(void *ctx, uint8_t *rgb, int32_t width, int32_t y0, int32
} else if (op->kind == PrOpPictureE) {
prPictureBand(op, &b);
} else {
- prShapeAgain(p, op, &surf);
+ prShapeAgain(p, op, &surf, y0, last);
}
}
@@ -1021,14 +1041,11 @@ static void prPictureBand(const PrOpT *op, const PrBandT *b) {
int32_t x1 = op->u.picture.dx + op->u.picture.dw < b->width ? op->u.picture.dx + op->u.picture.dw : b->width;
for (int32_t y = top; y <= bottom; y++) {
- int32_t ry = op->u.picture.sy + (int32_t)((int64_t)(y - op->u.picture.dy) * op->u.picture.sh / op->u.picture.dh);
- uint8_t *dst = b->rgb + (int64_t)(y - b->y0) * b->width * PR_RGB_BYTES;
+ int32_t ry = op->u.picture.sy + (int32_t)((int64_t)(y - op->u.picture.dy) * op->u.picture.sh / op->u.picture.dh);
+ uint8_t *dst = b->rgb + ((int64_t)(y - b->y0) * b->width + x0) * PR_RGB_BYTES;
+ const uint8_t *src = op->u.picture.rgb + ((int64_t)ry * op->u.picture.w + op->u.picture.sx) * PR_RGB_BYTES;
- for (int32_t x = x0; x < x1; x++) {
- int32_t rx = op->u.picture.sx + (int32_t)((int64_t)(x - op->u.picture.dx) * op->u.picture.sw / op->u.picture.dw);
-
- memcpy(dst + (int64_t)x * PR_RGB_BYTES, op->u.picture.rgb + ((int64_t)ry * op->u.picture.w + rx) * PR_RGB_BYTES, PR_RGB_BYTES);
- }
+ drawScaleRow(dst, src, PR_RGB_BYTES, x0 - op->u.picture.dx, x1 - x0, op->u.picture.dw, op->u.picture.sw, false, 0);
}
}
@@ -1114,14 +1131,18 @@ static void prShape(BasFormRtT *rt, BasPrinterT *p, PrOpKindE kind, BasValueT *a
}
-// A kept shape done again on surf, in the drawing state it was done in.
-static void prShapeAgain(const BasPrinterT *p, const PrOpT *op, const BasGfxSurfaceT *surf) {
+// A kept shape done again on surf, in the drawing state it was done in,
+// only its rows bandY0..bandY1 (a band's: the fills and pen dots of the
+// rest are not worked out at all).
+static void prShapeAgain(const BasPrinterT *p, const PrOpT *op, const BasGfxSurfaceT *surf, int32_t bandY0, int32_t bandY1) {
BasGfxT g = p->states[op->u.shape.state];
BasValueT args[BAS_GFX_CIRCLE_ARGS];
- g.surf = *surf;
- g.lastX = op->u.shape.lastX;
- g.lastY = op->u.shape.lastY;
+ g.surf = *surf;
+ g.lastX = op->u.shape.lastX;
+ g.lastY = op->u.shape.lastY;
+ g.bandY0 = bandY0;
+ g.bandY1 = bandY1;
for (int32_t a = 0; a < op->u.shape.argc; a++) {
args[a] = basValDouble(op->u.shape.args[a]);
@@ -1238,7 +1259,7 @@ static bool prVecPage(BasPrinterT *p) {
} else if (op->kind == PrOpPictureE) {
api->vecImage(p->doc, op->u.picture.rgb, op->u.picture.w, op->u.picture.h, op->u.picture.sx, op->u.picture.sy, op->u.picture.sw, op->u.picture.sh, op->u.picture.dx, op->u.picture.dy, op->u.picture.dw, op->u.picture.dh);
} else {
- prShapeAgain(p, op, &surf);
+ prShapeAgain(p, op, &surf, 0, surf.height - 1);
}
}
diff --git a/src/apps/kpunch/dvxbasic/formrt/formrt.c b/src/apps/kpunch/dvxbasic/formrt/formrt.c
index 409d3546..e35cf0f1 100644
--- a/src/apps/kpunch/dvxbasic/formrt/formrt.c
+++ b/src/apps/kpunch/dvxbasic/formrt/formrt.c
@@ -34,7 +34,8 @@
#include "../runtime/gfx.h"
#include "../runtime/serialize.h"
#include "../../../../libs/kpunch/serial/rs232/rs232.h"
-#include "../../../../libs/kpunch/serial/seclink/secLink.h"
+#include "../../../../libs/kpunch/seclink/secLink.h"
+#include "../../../../libs/kpunch/dvxnet/dvxNet.h"
#include "../../../../libs/kpunch/game/game.h"
#include "../../../../libs/kpunch/joystick/joystick.h"
#include "../../../../libs/kpunch/music/music.h"
@@ -46,6 +47,7 @@
#include "dvxWgtP.h"
#include "dvxWm.h"
#include "shellApp.h"
+#include "shellRun.h"
// Widget headers below publish inline accessor macros over
// dvx*Api() -> wgtGetApi(...) lookups, so they're lightweight
// compile-time type-safety over a runtime-dynamic dispatch -- no
@@ -58,7 +60,7 @@
#include "dataCtrl/dataCtrl.h"
#include "dbGrid/dbGrid.h"
#include "frmParser.h"
-#include "../../../../tools/hlpcCompile.h"
+#include "../../../../tools/hlpcModule.h"
#include "thirdparty/stb_ds_wrap.h"
#include
@@ -97,6 +99,8 @@ DVX_ABI_STAMP(basrt);
#define BAS_ERR_SUMMARY "Runtime error" // when the error names none
#define BAS_ERR_UNKNOWN "Unknown error"
#define BAS_SQL_MISSING "Databases are not installed: add the Databases part of DVX with Setup (Upgrade or repair)." // SQLError$ without SQLite
+#define BAS_SQL_ESC_GROWTH 2 // SQLEscape$: a quote becomes two
+#define FORM_NO_MAX 0 // MaxWidth / MaxHeight: no limit but the screen
// dlsym name buffer: DVX_SYM_PREFIX + the longest BASIC identifier.
#define BAS_MANGLED_NAME_LEN (BAS_MAX_IDENT + 2)
@@ -134,6 +138,8 @@ DVX_ABI_STAMP(basrt);
#define BAS_FORM_TYPE_NAME "Form" // TypeName / TypeOf of a form
#define BAS_WINSTATE_NONE (-1) // BasFormT.frmWindowState: no WindowState waits for Show
#define BAS_PICTURE_TYPE_NAME "Picture" // TypeName of a LoadPicture object
+#define BAS_PICTURE_NAME_MAX 16 // a LoadPictureData picture's name: the mark, its place, and a NUL
+#define BAS_PICTURE_NAME_RADIX 10 // its place is decimal
#define BAS_PIC_PROP_HEIGHT "Height" // Picture object properties (read-only)
#define BAS_PIC_PROP_TYPE "Type"
#define BAS_PIC_PROP_WIDTH "Width"
@@ -153,19 +159,57 @@ DVX_ABI_STAMP(basrt);
#define BAS_DRAG_OVER_ARGS 4
#define BAS_DRAG_STATE_NONE (-1)
-// ResGetText result cap (static return buffer).
-#define BAS_RES_TEXT_LEN 1024
-
-// HelpCompile's messages (HelpCompileMessages$)
+// HelpCompile's messages (HelpCompileMessages$), and how often a compile
+// lets the program's events and DVX go on (helpCompileTick)
#define HELP_MESSAGE_SEP '\n'
#define HELP_COMPILER_MISSING "The help compiler is not available, or no input or output file was given."
+#define HELP_PUMP_MS 50
-// Module-level form runtime pointer for onFormClose callback
-static BasFormRtT *sFormRt = NULL;
+// The runtime whose code runs, one per task (curRt): a program's code
+// that yields (DoEvents, a wait, a modal dialog) finds its own runtime
+// current when it resumes, whatever another task's program set meanwhile.
+typedef struct {
+ uint32_t task;
+ BasFormRtT *rt;
+} TaskRtT;
+
+static TaskRtT *sTaskRts = NULL; // stb_ds
// How many programs' code calls are on the stack (handlers, sliced
// module code): a runtime created inside one does not become current.
static int32_t sCodeDepth = 0;
+// The task switcher's tsCurrentId and tsYield, found at run time
+// (basFormRtTaskId, basFormRtYield).
+static uint32_t (*sTaskIdFn)(void) = NULL;
+static bool sTaskIdResolved = false;
+static void (*sYieldFn)(void) = NULL;
+static bool sYieldResolved = false;
+
+// Events waiting in every runtime's queue (basFormRtBusy), and every
+// runtime (stb_ds, oldest first): a pump runs the waiting events of all
+// those of its task (a program's own, and one it runs: basdev's child).
+static int32_t sQueuedTotal = 0;
+static BasFormRtT **sRuntimes = NULL;
+
+// Events of these kinds report a state, so a newer one waiting for the
+// program's turn replaces an older one of the same control instead of
+// queueing behind it (a timer that fired twice while the program was
+// busy runs once, as in VB).
+static const char *const sQueueLatest[] = {
+ BAS_EVT_MOUSEMOVE,
+ BAS_EVT_PAINT,
+ BAS_EVT_RESIZE,
+ BAS_EVT_TIMER
+};
+
+// A form's life: these run where they are fired, as part of what fires
+// them (an unload must not wait for the program's turn).
+static const char *const sQueueNever[] = {
+ BAS_EVT_LOAD,
+ BAS_EVT_QUERYUNLOAD,
+ BAS_EVT_UNLOAD
+};
+
// True only while frmParse is running inside basFormRtLoadFrm. The
// parser calls basFormRtLoadForm via onFormBegin to create the bare form
// before it starts firing onCtrlBegin callbacks; while this is set that
@@ -189,6 +233,9 @@ static bool sNewFormCopy = false;
// default number.
static bool sLastInputBoxCancelled = false;
+// The BIOS tick of the last pump a long SQL statement made (sqlPump)
+static uint32_t sSqlPumpTick = 0;
+
// The font the last DlgFont call ended on, which DlgFontName$ /
// DlgFontSize / DlgFontStyle give: the one chosen, or after Cancel the
// one it started at.
@@ -259,25 +306,25 @@ static bool sDragCleanupHooked = false;
// ============================================================
typedef struct {
- int (*open)(int, int32_t, int, char, int, int);
- int (*close)(int);
- int (*read)(int, char *, int);
- int (*write)(int, const char *, int);
- int (*writeBuf)(int, const char *, int);
- int (*getRxBuffered)(int);
- int (*clearRxBuffer)(int);
- int (*getUartType)(int);
- int (*getBase)(int);
- 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);
+ __typeof__(rs232Open) *open;
+ __typeof__(rs232Close) *close;
+ __typeof__(rs232Read) *read;
+ __typeof__(rs232Write) *write;
+ __typeof__(rs232WriteBuf) *writeBuf;
+ __typeof__(rs232GetRxBuffered) *getRxBuffered;
+ __typeof__(rs232ClearRxBuffer) *clearRxBuffer;
+ __typeof__(rs232GetUartType) *getUartType;
+ __typeof__(rs232GetBase) *getBase;
+ __typeof__(rs232GetIrq) *getIrq;
+ __typeof__(rs232SetBase) *setBase;
+ __typeof__(rs232SetIrq) *setIrq;
+ __typeof__(rs232GetCts) *getCts;
+ __typeof__(rs232GetDcd) *getDcd;
+ __typeof__(rs232GetDsr) *getDsr;
+ __typeof__(rs232GetDtr) *getDtr;
+ __typeof__(rs232GetRts) *getRts;
+ __typeof__(rs232SetDtr) *setDtr;
+ __typeof__(rs232SetRts) *setRts;
__typeof__(rs232ClearTxBuffer) *clearTxBuffer;
__typeof__(rs232GetBps) *getBps;
__typeof__(rs232GetData) *getData;
@@ -291,6 +338,11 @@ typedef struct {
__typeof__(rs232SetBreak) *setBreak;
} SerApiT;
+// A modem line's reader and setter (rs232GetCts / rs232SetDtr and their
+// kin: serLineGet, serLineSet)
+typedef __typeof__(rs232GetCts) SerLineGetFnT;
+typedef __typeof__(rs232SetDtr) SerLineSetFnT;
+
static SerApiT sSerApi;
static bool sSerApiResolved = false;
@@ -301,11 +353,12 @@ static bool sSerApiResolved = false;
#define SER_LINE_ERRORS (RS232_LSR_OVERRUN | RS232_LSR_PARITY_ERR | RS232_LSR_FRAME_ERR | RS232_LSR_BREAK)
typedef struct {
- bool opened; // SerOpen succeeded and SerClose has not run
- bool attached;
- WidgetT *term;
- int32_t com; // 0-based COM port index
- int32_t lastError; // RS232_ result of the last SerOpen / SerSet
+ bool opened; // SerOpen succeeded and SerClose has not run
+ bool attached;
+ WidgetT *term;
+ int32_t com; // 0-based COM port index
+ int32_t lastError; // RS232_ result of the last SerOpen / SerSet
+ BasFormRtT *owner; // the program that opened or attached it (basFormRtSerialShutdown)
} SerAttachT;
static SerAttachT sSerAttach[RS232_NUM_PORTS];
@@ -322,18 +375,20 @@ static TermBindT *sTermBinds = NULL; // stb_ds
// SecLink function pointers (resolved lazily)
typedef struct {
- void *(*open)(int, int32_t, int, char, int, int, void (*)(void *, const uint8_t *, int, uint8_t, bool), void *);
- void (*close)(void *);
- int (*handshake)(void *);
- bool (*isReady)(void *);
- int (*poll)(void *);
- int (*send)(void *, const uint8_t *, int, uint8_t, bool, bool);
- int (*sendBuf)(void *, const uint8_t *, int, uint8_t, bool);
- int (*getPending)(void *);
- int (*setPassphrase)(void *, const char *);
- int (*setChannelPlain)(void *, uint8_t, bool);
- int (*handshakeStep)(void *);
- int32_t (*getDropped)(void *);
+ __typeof__(secLinkOpen) *open;
+ __typeof__(secLinkClose) *close;
+ __typeof__(secLinkHandshakeStart) *handshakeStart;
+ __typeof__(secLinkIsReady) *isReady;
+ __typeof__(secLinkPoll) *poll;
+ __typeof__(secLinkSend) *send;
+ __typeof__(secLinkGetPending) *getPending;
+ __typeof__(secLinkSetPassphrase) *setPassphrase;
+ __typeof__(secLinkSetChannelPlain) *setChannelPlain;
+ __typeof__(secLinkHandshakeStep) *handshakeStep;
+ __typeof__(secLinkGetDropped) *getDropped;
+ __typeof__(secLinkOpenStream) *openStream;
+ __typeof__(netBasStreamRead) *tcpRead; // CommOpenTcp (NULL without networking)
+ __typeof__(netBasStreamWrite) *tcpWrite;
} CommSecLinkApiT;
static CommSecLinkApiT sCommApi;
@@ -348,9 +403,9 @@ typedef struct {
static PrintApiT sPrintApi;
-// The music, joystick and game libraries' functions behind BEEP, SOUND,
-// PLAY, STICK, STRIG and FRAMEWAIT (deviceBind; a group stays NULL when
-// its library is not loaded)
+// The music, joystick, game, and shell libraries' functions behind BEEP,
+// SOUND, PLAY, STICK, STRIG, FRAMEWAIT, and SHELL (deviceBind; a group
+// stays NULL when its library is not loaded)
typedef struct {
__typeof__(musicBasBeep) *beep;
__typeof__(musicBasBusy) *busy;
@@ -361,6 +416,9 @@ typedef struct {
__typeof__(joyBasStrig) *strig;
__typeof__(gameFrameInit) *frameInit;
__typeof__(gameFrameNext) *frameNext;
+ __typeof__(shellRunDosFor) *dosFor;
+ __typeof__(shellRunDosDone) *dosDone;
+ __typeof__(shellSplitCommand) *dosSplit;
} DeviceApiT;
static DeviceApiT sDeviceApi;
@@ -392,7 +450,10 @@ typedef struct {
int32_t termChannel; // channel for terminal I/O
bool termEncrypt; // encrypt terminal traffic
bool stepping; // CommHandshakeStep exchange running
+ bool waiting; // CommHandshake or CommSend running (the program's events with it)
int32_t lastError; // SECLINK_ result of the last call
+ int32_t tcp; // CommOpenTcp's net.bas handle (0 on a COM port)
+ BasFormRtT *owner; // the program that opened it (basFormRtSerialShutdown)
} CommSlotT;
// Dynamic array of heap-allocated comm slots (stb_ds), allocated on
@@ -401,14 +462,8 @@ typedef struct {
// keeping handles stable.
static CommSlotT **sCommSlots = NULL;
-// hlpcCompile function pointer (resolved lazily); typed from the
-// hlpcCompile.h prototype so the signature has one source of truth.
-typedef __typeof__(hlpcCompile) *HlpcCompileFnT;
-typedef __typeof__(hlpcSetMessageFn) *HlpcSetMessageFnT;
-
-static HlpcCompileFnT sHlpcCompile = NULL;
-static HlpcSetMessageFnT sHlpcSetMessage = NULL;
-static bool sHlpcResolved = false;
+static bool sHelpCompiling = false; // a HelpCompile runs (the compiler does one at a time)
+static PlatformTicksT sHelpPumpAt = 0; // when helpCompileTick last let DVX go on
static char *sHelpMessages = NULL; // stb_ds: the last HelpCompile's messages, one line each
// shellOpenHelp function pointer (resolved lazily); typed from the
@@ -430,13 +485,6 @@ static ShellIdleFnT sShellUnregisterIdle = NULL;
static bool sShellIdleResolved = false;
-// Maximum number of simultaneously open resource handles
-#define RES_MAX_HANDLES 8
-
-static DvxResHandleT *sResHandles[RES_MAX_HANDLES];
-
-// The last ResGetData result, valid until the next call
-static uint8_t *sResData = NULL;
// ============================================================
@@ -528,6 +576,10 @@ typedef enum {
FORM_PROP_FONTUNDERLINE,
FORM_PROP_FONTSTRIKETHRU,
FORM_PROP_ACTIVECONTROL,
+ FORM_PROP_MAXWIDTH,
+ FORM_PROP_MAXHEIGHT,
+ FORM_PROP_GAMEINPUT,
+ FORM_PROP_FULLSCREEN,
FORM_PROP_COUNT
} BasFormPropE;
@@ -561,6 +613,18 @@ static const BasPropDescT sFormProps[FORM_PROP_COUNT] = {
[FORM_PROP_FONTUNDERLINE] = { BAS_PROP_FONTUNDERLINE, WGT_IFACE_BOOL, true, true },
[FORM_PROP_FONTSTRIKETHRU] = { BAS_PROP_FONTSTRIKETHRU, WGT_IFACE_BOOL, true, true },
[FORM_PROP_ACTIVECONTROL] = { BAS_PROP_ACTIVECONTROL, BAS_PROP_OBJECT, true, false },
+ [FORM_PROP_MAXWIDTH] = { BAS_PROP_MAXWIDTH, WGT_IFACE_INT, true, true },
+ [FORM_PROP_MAXHEIGHT] = { BAS_PROP_MAXHEIGHT, WGT_IFACE_INT, true, true },
+ [FORM_PROP_GAMEINPUT] = { BAS_PROP_GAMEINPUT, WGT_IFACE_BOOL, true, true },
+ [FORM_PROP_FULLSCREEN] = { BAS_PROP_FULLSCREEN, WGT_IFACE_BOOL, true, true },
+};
+
+// The form properties that are run-time state (basFormRtFormPropRuntimeOnly)
+static const char *const sRuntimeOnlyFormProps[] = {
+ BAS_PROP_VISIBLE,
+ BAS_PROP_DESIGNMODE,
+ BAS_PROP_DESIGNSELECTION,
+ BAS_PROP_FULLSCREEN
};
// Common control property ids (every non-menu control, consulted after
@@ -782,11 +846,14 @@ const char *basFontPropAt(int32_t idx);
bool basFontPropIs(const char *name);
bool basFontPropSetText(DvxFontChoiceT *choice, const char *name, const char *text);
bool basFontPropText(const DvxFontChoiceT *choice, const char *name, char *out, int32_t outSize);
+void basFormRtAdopt(BasFormRtT *rt);
void basFormRtBindVm(BasFormRtT *rt);
+bool basFormRtBusy(void);
BasValueT basFormRtCallMethod(void *ctx, void *ctrlRef, const char *methodName, BasValueT *args, int32_t argc);
BasFormRtT *basFormRtCodeEnter(BasFormRtT *rt);
void basFormRtCodeFailed(BasFormRtT *rt);
void basFormRtCodeLeave(BasFormRtT *rt, BasFormRtT *prev);
+bool basFormRtCodeOnStack(const BasFormRtT *rt);
int32_t basFormRtCommonMethodId(const char *name);
const char *basFormRtCommonMethodName(int32_t id);
const BasPropDescT *basFormRtCommonProps(int32_t *count);
@@ -810,6 +877,7 @@ bool basFormRtFireEventArgs(BasFormRtT *rt, BasFormT *form, const char *ctrlName
static bool basFormRtFireEventWithCancel(BasFormRtT *rt, BasFormT *form, const char *ctrlName, const char *eventName);
const char *basFormRtFormMethodAt(int32_t idx);
int32_t basFormRtFormMethodCount(void);
+bool basFormRtFormPropRuntimeOnly(const char *name);
const BasPropDescT *basFormRtFormProps(int32_t *count);
BasValueT basFormRtFromWgt(const WgtValueT *v);
BasValueT basFormRtGetProp(void *ctx, void *ctrlRef, const char *propName);
@@ -823,22 +891,25 @@ void *basFormRtLoadForm(void *ctx, const char *formName);
void *basFormRtLoadFormText(void *ctx, const char *text);
BasFormT *basFormRtLoadFrm(BasFormRtT *rt, const char *source, int32_t sourceLen);
void *basFormRtLoadPicture(void *ctx, const char *path, int32_t *outErr);
+void *basFormRtLoadPictureData(void *ctx, const char *data, int32_t len, int32_t *outErr);
bool basFormRtMenuLabel(const char *caption, const char *shortcut, const char *shortcutText, char *out, int32_t outSize, int32_t *outKey, int32_t *outMods);
const BasPropDescT *basFormRtMenuProps(int32_t *count);
int32_t basFormRtMsgBox(void *ctx, const char *message, int32_t flags, const char *title);
void basFormRtNewArgs(BasFormRtT *rt, const char *args);
void *basFormRtNewForm(void *ctx, const char *formName);
+BasPictureT *basFormRtPictureData(const BasFormRtT *rt, BasValueT v);
const char *basFormRtPicturePath(const BasFormRtT *rt, BasValueT v);
void basFormRtRegisterFrm(BasFormRtT *rt, const char *formName, const char *source, int32_t sourceLen);
void basFormRtRemoveCtrl(void *ctx, void *formRef, const char *ctrlName);
BasControlT *basFormRtResolveCtrl(BasFormRtT *rt, void *ref);
void basFormRtRunSimple(BasFormRtT *rt);
void basFormRtRuntimeError(BasFormRtT *rt, const char *summary, const char *detailFmt, ...);
-void basFormRtSerialShutdown(void);
+void basFormRtSerialShutdown(const BasFormRtT *rt);
BasFormRtT *basFormRtSetCurrent(BasFormRtT *rt);
void basFormRtSetEvent(void *ctx, void *ctrlRef, const char *eventName, const char *handlerName);
void basFormRtSetProp(void *ctx, void *ctrlRef, const char *propName, BasValueT value);
void basFormRtShowForm(void *ctx, void *formRef, bool modal);
+uint32_t basFormRtTaskId(void);
static void basFormRtTeardownForm(BasFormRtT *rt, BasFormT *form);
WidgetT *basFormRtTermBind(const char *ctrlName, void *ctx, BasTermWriteFnT writeFn, BasTermUnbindFnT onUnbind);
void basFormRtTermForget(void *ctx);
@@ -847,17 +918,21 @@ void basFormRtTestReset(void);
WgtValueT basFormRtToWgt(BasValueT v, uint8_t want, BasStringT **hold);
const char *basFormRtTypeName(void *ctx, void *objRef);
void basFormRtUnloadForm(void *ctx, void *formRef);
+bool basFormRtUpdate(BasFormRtT *rt);
bool basFormRtWantsNewArgs(BasFormRtT *rt);
+void basFormRtYield(void);
static BasValueT basFromWgt(const WgtValueT *v);
static BasValueT callCommonMethod(BasFormRtT *rt, BasControlT *ctrl, const char *methodName, BasValueT *args, int32_t argc);
static bool callEventProc(BasFormRtT *rt, BasFormT *form, const BasProcEntryT *proc, const BasValueT *args, int32_t argCount, BasValueT *outArgs, int32_t outArgCount);
static BasValueT callIfaceMethod(BasFormRtT *rt, BasControlT *ctrl, const WgtMethodDescT *m, BasValueT *args, int32_t argc);
static BasValueT clipboardMethod(BasFormRtT *rt, const char *name, BasValueT *args, int32_t argc);
+static int32_t commAddSlot(CommSlotT *slot);
void CommAttach(int32_t handle, const char *termCtrlName, int32_t channel, int32_t encrypt);
void CommClose(int32_t handle);
void CommDetach(int32_t handle);
int32_t CommDropped(int32_t handle);
static CommSlotT *commGetSlot(int32_t handle);
+static bool commGoOn(BasFormRtT *rt, int32_t handle, PlatformTicksT since, uint32_t limitMs);
int32_t CommHandshake(int32_t handle, const char *passphrase);
int32_t CommHandshakeStep(int32_t handle, const char *passphrase);
static void commIdlePoll(void *ctx);
@@ -866,6 +941,7 @@ int32_t CommLastError(int32_t handle);
static int32_t commNote(CommSlotT *slot, int32_t rc);
static void commOnRecv(void *ctx, const uint8_t *data, int len, uint8_t channel, bool encrypted);
int32_t CommOpen(int32_t com, int32_t baud, int32_t dataBits, const char *parity, int32_t stopBits, int32_t handshake);
+int32_t CommOpenTcp(int32_t tcp);
int32_t CommPending(int32_t handle);
int32_t CommPoll(int32_t handle);
const uint8_t *CommRecv(int32_t handle, int32_t channel, int32_t *outLen);
@@ -873,11 +949,16 @@ int32_t CommRecvEncrypted(int32_t handle, int32_t channel);
static bool commResolveApi(void);
int32_t CommSend(int32_t handle, const uint8_t *data, int32_t len, int32_t channel, int32_t encrypt);
int32_t CommSetPlain(int32_t handle, int32_t channel, int32_t plain);
+static int32_t commTcpRead(void *ctx, uint8_t *buf, int32_t len);
+static int32_t commTcpWrite(void *ctx, const uint8_t *data, int32_t len);
static int32_t commTermRead(void *ctx, uint8_t *buf, int32_t maxLen);
static int32_t commTermWrite(void *ctx, const uint8_t *data, int32_t len);
static BasValueT contentBoxProp(BasFormRtT *rt, BasFormT *form, const char *propName, const BasValueT *value);
static const char *ctrlDisplayName(const BasControlT *ctrl);
static BasControlT *ctrlFromWidget(WidgetT *w, BasFormRtT **outRt);
+static BasFormRtT *curRt(void);
+static void curRtForget(const BasFormRtT *rt);
+static BasFormRtT *curRtSet(BasFormRtT *rt);
static const char *defaultPropOf(BasFormRtT *rt, void *ref);
static void destroyFormNow(BasFormRtT *rt, BasFormT *form);
static void detachFormForUnload(BasFormRtT *rt, BasFormT *form);
@@ -887,6 +968,9 @@ static bool deviceFrameBusy(void *ctx);
static void deviceFrameStart(void *ctx, int32_t hz);
static bool devicePlay(void *ctx, const char *macro);
static int32_t devicePlayCount(void *ctx);
+static bool deviceShellBusy(void *ctx);
+static int32_t deviceShellResult(void *ctx);
+static bool deviceShellStart(void *ctx, const char *command);
static void deviceSound(void *ctx, double hz, double ticks);
static bool deviceSoundBusy(void *ctx);
static int32_t deviceStick(void *ctx, int32_t n);
@@ -922,20 +1006,22 @@ static void dragHookForms(BasFormRtT *rt, bool hook);
static void dragOnKey(WindowT *win, int32_t key, int32_t mod);
static void dragOnMouse(WindowT *win, int32_t x, int32_t y, int32_t buttons);
static void dragOverUpdate(void);
-static bool dragShowIcon(AppContextT *ctx, const char *path);
+static bool dragShowIcon(const BasFormRtT *rt, AppContextT *ctx, const char *path);
static BasControlT *dragTargetAt(BasFormRtT *rt);
static const BasProcEntryT *eventHandler(BasFormRtT *rt, const BasControlT *obj, const char *ctrlName, const char *eventName, bool *outOverridden);
+static bool eventProcFits(const BasProcEntryT *proc, int32_t argCount);
+static bool eventWanted(BasFormRtT *rt, const BasControlT *obj, const char *ctrlName, const char *eventName, int32_t argCount);
static const char *fileDialog(const char *title, const char *filter, int32_t flags, const char *folder);
static BasControlT *findButtonWith(BasFormT *form, const char *propName);
static BasControlT *findCtrlByName(const char *name);
static BasControlT *findCtrlByWidget(BasFormT *form, const WidgetT *w);
static BasControlT *findCtrlInForm(BasFormT *form, const char *name);
-static WidgetT *findCtrlWidgetByName(const char *name);
static BasFormT *findFormByWindow(const BasFormRtT *rt, const WindowT *win);
static BasControlT *findMenuProxyInForm(BasFormT *form, const char *name);
static BasControlT *findNamedObjectInForm(BasFormT *form, const char *name);
static BasFrmPopupMenuT *findPopupMenu(BasFormT *form, const char *name);
static const BasPropDescT *findPropDesc(const BasPropDescT *table, int32_t count, const char *name);
+static WidgetT *findTermWidget(const char *name);
static void fireCtrlEvent(BasFormRtT *rt, BasControlT *ctrl, const char *eventName, const BasValueT *args, int32_t argCount);
static void fireCtrlEventOut(BasFormRtT *rt, BasControlT *ctrl, const char *eventName, const BasValueT *args, int32_t argCount, BasValueT *outArgs, int32_t outArgCount);
static bool fireEventArgsOut(BasFormRtT *rt, BasFormT *form, BasControlT *obj, const char *ctrlName, const char *eventName, const BasValueT *args, int32_t argCount, BasValueT *outArgs, int32_t outArgCount);
@@ -946,7 +1032,9 @@ static const BasFontPropT *fontPropFind(const char *name);
static BasValueT fontPropGet(const WidgetT *w, const char *name);
static bool fontPropSet(WidgetT *w, const char *name, BasValueT value);
static void formatPropNames(const BasPropDescT *table, int32_t count, bool forWrite, char *buf, int32_t bufSize);
+static void formClose(BasFormRtT *rt, BasFormT *form);
static BasControlT *formEventObject(BasFormT *form, const char *ctrlName);
+static void formFullChanged(WindowT *win, ExclusiveStateE state, void *userData);
static bool formMethod(BasFormRtT *rt, BasFormT *frm, const char *name, BasValueT *args, int32_t argc);
static BasFormT *formOfWindow(const WindowT *win, BasFormRtT **outRt);
static void formRtCommIdleSync(void);
@@ -973,6 +1061,7 @@ static BasValueT getIfaceProp(const WgtIfaceT *iface, WidgetT *w, const char *pr
int32_t HelpCompile(const char *inputFile, const char *outputFile);
static void helpCompileMessage(void *ctx, const char *line);
const char *HelpCompileMessages(void);
+static bool helpCompileTick(void *ctx);
void HelpView(const char *hlpFile);
static void helpViewOpen(const char *hlpFile, const char *topicId);
void HelpViewTopic(const char *hlpFile, const char *topicId);
@@ -1013,10 +1102,14 @@ static void onWidgetScroll(WidgetT *w, int32_t delta);
static bool onWidgetValidate(WidgetT *w);
static int32_t parseFileFilters(const char *filter, FileFilterT **outFilters, char *buf, int32_t bufSize);
static BasPictureT *pictureAdd(BasFormRtT *rt, const char *path, int32_t width, int32_t height);
+static BasPictureT *pictureByDataName(const BasFormRtT *rt, const char *path);
+static void pictureDataName(const BasFormRtT *rt, const BasPictureT *pic, char *out, int32_t outSize);
static char *pictureFileRead(const char *path, int32_t *outLen);
static BasPictureT *pictureFind(const BasFormRtT *rt, const void *ref);
static BasPictureT *pictureFor(BasFormRtT *rt, const char *path);
static BasValueT pictureProp(BasFormRtT *rt, const BasPictureT *pic, const char *propName);
+static void pictureShowData(BasFormRtT *rt, BasControlT *ctrl, const char *propName, const BasPictureT *pic);
+static bool pictureTakesData(const BasControlT *ctrl, const char *propName);
static bool pointerCursor(int32_t pointer, int32_t *outCursor);
static void popupMenuAdd(BasFormT *form, const char *name, MenuT *menu, bool ownsMenu);
static const char *popupMenuNameFor(const BasFormT *form, const MenuT *menu);
@@ -1027,18 +1120,15 @@ static bool printerWrite(void *ctx, void *job, const char *data, int32_t len);
int32_t PrintForm(const char *formName);
int32_t PrintFormDialog(const char *formName);
static int32_t printFormWindow(const char *formName, bool askFirst);
+static void queueArgsFree(BasQueuedEventT *ev);
+static void queueDrain(BasFormRtT *rt);
+static bool queueEvent(BasFormRtT *rt, BasFormT *form, BasControlT *obj, const char *ctrlName, const char *eventName, const BasValueT *args, int32_t argCount, BasQueuedKindE kind);
+static bool queueForeign(const BasFormRtT *rt, const char *eventName);
+static void queueFree(BasFormRtT *rt);
+static bool queueLive(BasFormRtT *rt, const BasQueuedEventT *ev);
static void refreshDetailControls(BasFormT *form, BasControlT *masterCtrl);
static void removeCtrlTree(BasFormRtT *rt, BasFormT *form, BasControlT *ctrl);
-int32_t ResAddData(const char *path, const char *name, int32_t type, const uint8_t *data, int32_t len);
-int32_t ResAddFile(const char *path, const char *name, int32_t type, const char *srcFile);
-int32_t ResAddText(const char *path, const char *name, const char *text);
-void ResClose(int32_t handle);
-int32_t ResCount(int32_t handle);
-int32_t ResExtract(const char *path, const char *name, const char *outFile);
-const uint8_t *ResGetData(const char *path, const char *name, int32_t *outLen);
-const char *ResGetText(const char *path, const char *name);
static void resizeIfaceBuffer(BasControlT *ctrl);
-const char *ResName(int32_t handle, int32_t index);
static BasControlT *resolveCtrlArg(BasFormRtT *rt, BasFormT *form, BasValueT arg);
static BasControlT *resolveCtrlRef(BasFormRtT *rt, void *ref);
static BasControlT *resolveCtrlRefIn(BasFormT **forms, void *ref);
@@ -1046,15 +1136,11 @@ static BasFormT *resolveFormRef(BasFormRtT *rt, void *ref);
static MenuItemT *resolveMenuItem(BasFormT *form, int32_t menuId, bool *fromBar);
static BasFormT *resolveOwningForm(BasFormRtT *rt, BasFormT *eventForm, const BasProcEntryT *proc);
static bool resolveShellIdle(void);
-int32_t ResOpen(const char *path);
-static void *resReadNamed(const char *path, const char *name, uint32_t *outSize);
-int32_t ResRemove(const char *path, const char *name);
-int32_t ResSize(int32_t handle, int32_t index);
-int32_t ResType(int32_t handle, int32_t index);
static bool rtCallHandler(BasFormRtT *rt, BasFormT *eventForm, const BasProcEntryT *proc, int32_t codeAddr, const BasValueT *args, int32_t argCount, BasValueT *outArgs, int32_t outArgCount);
static void rtEventEnter(BasFormRtT *rt);
static void rtEventLeave(BasFormRtT *rt);
static const BasProcEntryT *rtFindProc(BasFormRtT *rt, const char *name);
+static bool rtPump(BasFormRtT *rt);
static BasVmResultE runSliced(BasFormRtT *rt);
static void runtimeErrorEnd(BasFormRtT *rt, const char *summary, const char *details);
static BasValueT screenMethod(BasFormRtT *rt, const char *name, BasValueT *args, int32_t argc);
@@ -1083,8 +1169,8 @@ int32_t SerGetUart(int32_t com);
static void serIdlePoll(void *ctx);
int32_t SerLastError(int32_t com);
int32_t SerLineErrors(int32_t com);
-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);
+static int32_t serLineGet(SerLineGetFnT *const *get, int32_t com);
+static void serLineSet(SerLineSetFnT *const *set, int32_t com, int32_t on);
static int32_t serNote(int32_t com, int32_t rc);
int32_t SerOpen(int32_t com, int32_t baud, int32_t dataBits, const char *parity, int32_t stopBits, int32_t handshake);
static char serParity(const char *parity);
@@ -1103,7 +1189,9 @@ static int32_t serValueGet(int32_t (*const *get)(int32_t), int32_t com);
int32_t SerWrite(int32_t com, const uint8_t *data, int32_t len);
static bool setCommonProp(BasControlT *ctrl, const char *propName, BasValueT value);
static void setControlString(char *dst, int32_t dstSize, BasValueT value);
+static void setFormFullScreen(BasFormRtT *rt, BasFormT *frm, bool on);
static void setFormIcon(BasFormRtT *rt, BasFormT *frm, BasValueT value);
+static void setFormMax(BasFormRtT *rt, WindowT *win, bool across, int32_t limit);
static void setFormProp(BasFormRtT *rt, BasFormT *frm, const char *propName, BasValueT value);
static void setHeapString(char **dst, BasValueT value);
static bool setIfaceProp(BasFormRtT *rt, const WgtIfaceT *iface, WidgetT *w, const char *propName, BasValueT value);
@@ -1121,6 +1209,7 @@ static const BasSqlCallbacksT *sqlApi(void);
void SQLClose(int32_t db);
int32_t SQLEof(int32_t rs);
const char *SQLError(int32_t db);
+const char *SQLEscape(const char *text);
int32_t SQLExec(int32_t db, const char *sql);
const char *SQLField(int32_t rs, const char *colName);
int32_t SQLFieldCount(int32_t rs);
@@ -1131,6 +1220,7 @@ const char *SQLFieldText(int32_t rs, int32_t col);
void SQLFreeResult(int32_t rs);
int32_t SQLNext(int32_t rs);
int32_t SQLOpen(const char *path);
+static bool sqlPump(int32_t db);
int32_t SQLQuery(int32_t db, const char *sql);
static void sqlResolveApi(BasSqlCallbacksT *sql);
static int32_t termBindIndex(const void *ctx);
@@ -1138,6 +1228,7 @@ static void unloadFormNow(BasFormRtT *rt, BasFormT *form);
static void updateBoundControls(BasFormT *form, BasControlT *dataCtrl);
static void upperCopy(char *dst, int32_t dstSize, const char *src);
static void vmErrorEnd(BasFormRtT *rt);
+static bool waitPump(void);
static WgtValueT wgtFromBas(BasValueT v, uint8_t want, BasStringT **hold);
static bool widgetHasAncestor(const WidgetT *w, const WidgetT *ancestor);
static bool windowLive(const AppContextT *ctx, const WindowT *win, int32_t winId);
@@ -1326,7 +1417,7 @@ BasValueT basExternCall(void *ctx, void *funcPtr, const char *libName, const cha
memset(&result, 0, sizeof(result));
if (oom) {
- basFormRtRuntimeError(sFormRt, "Out of memory", "Out of memory marshalling arguments for external function '%s'.", funcName ? funcName : "?");
+ basFormRtRuntimeError(curRt(), "Out of memory", "Out of memory marshalling arguments for external function '%s'.", funcName ? funcName : "?");
} else {
BasNativeRetE ret = BasNativeRetIntE;
@@ -1442,32 +1533,40 @@ bool basFontPropText(const DvxFontChoiceT *choice, const char *name, char *out,
}
+void basFormRtAdopt(BasFormRtT *rt) {
+ curRtForget(rt);
+ rt->ownTask = basFormRtTaskId();
+ curRtSet(rt);
+}
+
+
void basFormRtBindVm(BasFormRtT *rt) {
BasUiCallbacksT ui;
memset(&ui, 0, sizeof(ui));
- ui.getProp = basFormRtGetProp;
- ui.setProp = basFormRtSetProp;
- ui.callMethod = basFormRtCallMethod;
- ui.createCtrl = basFormRtCreateCtrl;
- ui.findCtrl = basFormRtFindCtrl;
- ui.findCtrlIdx = basFormRtFindCtrlIdx;
- ui.loadForm = basFormRtLoadForm;
- ui.unloadForm = basFormRtUnloadForm;
- ui.showForm = basFormRtShowForm;
- ui.hideForm = basFormRtHideForm;
- ui.msgBox = basFormRtMsgBox;
- ui.inputBox = basFormRtInputBox;
- ui.createForm = basFormRtCreateForm;
- ui.newForm = basFormRtNewForm;
- ui.createCtrlEx = basFormRtCreateCtrlEx;
- ui.setEvent = basFormRtSetEvent;
- ui.removeCtrl = basFormRtRemoveCtrl;
- ui.typeName = basFormRtTypeName;
- ui.loadPicture = basFormRtLoadPicture;
- ui.loadFormText = basFormRtLoadFormText;
- ui.defaultProp = basFormRtDefaultProp;
- ui.ctx = rt;
+ ui.getProp = basFormRtGetProp;
+ ui.setProp = basFormRtSetProp;
+ ui.callMethod = basFormRtCallMethod;
+ ui.createCtrl = basFormRtCreateCtrl;
+ ui.findCtrl = basFormRtFindCtrl;
+ ui.findCtrlIdx = basFormRtFindCtrlIdx;
+ ui.loadForm = basFormRtLoadForm;
+ ui.unloadForm = basFormRtUnloadForm;
+ ui.showForm = basFormRtShowForm;
+ ui.hideForm = basFormRtHideForm;
+ ui.msgBox = basFormRtMsgBox;
+ ui.inputBox = basFormRtInputBox;
+ ui.createForm = basFormRtCreateForm;
+ ui.newForm = basFormRtNewForm;
+ ui.createCtrlEx = basFormRtCreateCtrlEx;
+ ui.setEvent = basFormRtSetEvent;
+ ui.removeCtrl = basFormRtRemoveCtrl;
+ ui.typeName = basFormRtTypeName;
+ ui.loadPicture = basFormRtLoadPicture;
+ ui.loadPictureData = basFormRtLoadPictureData;
+ ui.loadFormText = basFormRtLoadFormText;
+ ui.defaultProp = basFormRtDefaultProp;
+ ui.ctx = rt;
basVmSetUiCallbacks(rt->vm, &ui);
printerBind(rt);
@@ -1477,6 +1576,17 @@ void basFormRtBindVm(BasFormRtT *rt) {
}
+bool basFormRtBusy(void) {
+ for (int32_t i = 0; i < (int32_t)arrlen(sRuntimes); i++) {
+ if (sRuntimes[i]->formsLoading) {
+ return true;
+ }
+ }
+
+ return sCodeDepth > 0 || sQueuedTotal > 0 || basUcBusy();
+}
+
+
BasValueT basFormRtCallMethod(void *ctx, void *ctrlRef, const char *methodName, BasValueT *args, int32_t argc) {
BasFormRtT *rt = (BasFormRtT *)ctx;
size_t len = strlen(methodName);
@@ -1573,9 +1683,8 @@ BasValueT basFormRtCallMethod(void *ctx, void *ctrlRef, const char *methodName,
BasFormRtT *basFormRtCodeEnter(BasFormRtT *rt) {
- BasFormRtT *prev = sFormRt;
+ BasFormRtT *prev = curRtSet(rt);
- sFormRt = rt;
sCodeDepth++;
rtEventEnter(rt);
return prev;
@@ -1591,11 +1700,17 @@ void basFormRtCodeFailed(BasFormRtT *rt) {
void basFormRtCodeLeave(BasFormRtT *rt, BasFormRtT *prev) {
sCodeDepth--;
- sFormRt = prev;
+ curRtSet(prev);
rtEventLeave(rt);
}
+// eventDepth counts the queue's bracket too (BasFormRtT.queueHeld).
+bool basFormRtCodeOnStack(const BasFormRtT *rt) {
+ return rt->eventDepth > (rt->queueHeld ? 1 : 0);
+}
+
+
int32_t basFormRtCommonMethodId(const char *name) {
for (int32_t i = 0; i < BAS_CM_COUNT; i++) {
if (strcasecmp(sCommonMethodNames[i], name) == 0) {
@@ -1625,12 +1740,15 @@ BasFormRtT *basFormRtCreate(AppContextT *ctx, BasVmT *vm, BasModuleT *module) {
return NULL;
}
- rt->ctx = ctx;
- rt->vm = vm;
- rt->module = module;
+ rt->ctx = ctx;
+ rt->vm = vm;
+ rt->module = module;
+ rt->ownTask = basFormRtTaskId();
+ arrput(sRuntimes, rt);
+ dvxSetWaitPump(waitPump);
if (sCodeDepth == 0) {
- sFormRt = rt;
+ curRtSet(rt);
}
basFormRtBindVm(rt);
@@ -1855,15 +1973,16 @@ WindowT *basFormRtCreateFormWindow(AppContextT *ctx, const char *title, const ch
}
-// BasUiCallbacksT.defaultProp: a Picture's default value is its file name
-// (BAS_PIC_PROP_DEFAULT), a control's its VB default property
-// (sDefaultProps) when it has that property; forms, menus and the
-// Clipboard have none.
+// The runtime the library-wide externs act for now (NULL: none).
BasFormRtT *basFormRtCurrent(void) {
- return sFormRt;
+ return curRt();
}
+// BasUiCallbacksT.defaultProp: a Picture's default value is its file name
+// (BAS_PIC_PROP_DEFAULT), a control's its VB default property
+// (sDefaultProps) when it has that property; forms, menus, and the
+// Clipboard have none.
const char *basFormRtDefaultProp(void *ctx, void *objRef) {
BasFormRtT *rt = (BasFormRtT *)ctx;
@@ -1894,8 +2013,13 @@ void basFormRtDestroy(BasFormRtT *rt) {
return;
}
- if (sFormRt == rt) {
- sFormRt = NULL;
+ curRtForget(rt);
+
+ for (int32_t i = 0; i < (int32_t)arrlen(sRuntimes); i++) {
+ if (sRuntimes[i] == rt) {
+ arrdel(sRuntimes, i);
+ break;
+ }
}
if (sDrag.rt == rt) {
@@ -1912,10 +2036,10 @@ void basFormRtDestroy(BasFormRtT *rt) {
dvxSetBusy(rt->ctx, false);
}
- // Close serial/secLink and drop the idle pollers before tearing down the
- // forms whose terminal widgets they reference. Also invoked from the
- // stub's _appShutdown hook so a force-killed app gets the same cleanup.
- basFormRtSerialShutdown();
+ // Close this program's serial ports and secure links (and drop the
+ // idle pollers when nothing is left) before tearing down the forms
+ // whose terminal widgets they reference.
+ basFormRtSerialShutdown(rt);
for (int32_t i = 0; i < (int32_t)arrlen(rt->forms); i++) {
destroyFormNow(rt, rt->forms[i]);
@@ -1929,6 +2053,8 @@ void basFormRtDestroy(BasFormRtT *rt) {
// reaches basFormRtDestroy; there the shell's per-app window scan
// reclaims the windows before dlclose. destroyFormNow (not
// unloadFormNow) so nothing re-touches vm fields on a dying runtime.
+ queueFree(rt);
+
while (arrlen(rt->pendingCtrlFree) > 0) {
BasControlT *ctrl = arrpop(rt->pendingCtrlFree);
@@ -1956,10 +2082,12 @@ void basFormRtDestroy(BasFormRtT *rt) {
arrfree(rt->frmCache);
for (int32_t i = 0; i < (int32_t)arrlen(rt->pictures); i++) {
+ free(rt->pictures[i]->data);
free(rt->pictures[i]);
}
arrfree(rt->pictures);
+ basFormRtDrawPicsFree(rt);
free(rt->pendingArgs);
free(rt);
}
@@ -1971,7 +2099,7 @@ void basFormRtEventLoop(BasFormRtT *rt) {
}
while (rt->ctx->running && basFormRtKeepsRunning(rt)) {
- if (!dvxUpdate(rt->ctx)) {
+ if (!basFormRtUpdate(rt)) {
break;
}
@@ -2109,6 +2237,17 @@ int32_t basFormRtFormMethodCount(void) {
}
+bool basFormRtFormPropRuntimeOnly(const char *name) {
+ for (int32_t i = 0; i < DVX_ARRAY_LEN(sRuntimeOnlyFormProps); i++) {
+ if (strcasecmp(name, sRuntimeOnlyFormProps[i]) == 0) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+
const BasPropDescT *basFormRtFormProps(int32_t *count) {
*count = FORM_PROP_COUNT;
return sFormProps;
@@ -2417,9 +2556,13 @@ void basFormRtLoadAllForms(BasFormRtT *rt, const char *startupFormName) {
return;
}
- // Load .frm text cache entries (skip if already loaded)
+ // Load .frm text cache entries (skip if already loaded), the other
+ // tasks running between them
+ rt->formsLoading = true;
+
for (int32_t i = 0; i < rt->frmCacheCount; i++) {
basFormRtLoadForm(rt, rt->frmCache[i].formName);
+ basFormRtYield();
}
// Show the startup form (named or first)
@@ -2439,6 +2582,8 @@ void basFormRtLoadAllForms(BasFormRtT *rt, const char *startupFormName) {
basFormRtShowForm(rt, startup, false);
}
+
+ rt->formsLoading = false;
}
@@ -2877,7 +3022,7 @@ void *basFormRtLoadPicture(void *ctx, const char *path, int32_t *outErr) {
BasFormRtT *rt = (BasFormRtT *)ctx;
for (int32_t i = 0; i < (int32_t)arrlen(rt->pictures); i++) {
- if (platformPathSame(rt->pictures[i]->path, path)) {
+ if (!rt->pictures[i]->data && platformPathSame(rt->pictures[i]->path, path)) {
return rt->pictures[i];
}
}
@@ -2895,19 +3040,15 @@ void *basFormRtLoadPicture(void *ctx, const char *path, int32_t *outErr) {
return NULL;
}
- int32_t pitch = 0;
- bool hasKey = false;
- uint32_t key = 0;
- uint8_t *pixels = dvxLoadImageAlpha(rt->ctx, (const uint8_t *)data, len, &w, &h, &pitch, &hasKey, &key);
+ // Its header: the control showing it decodes it
+ bool picture = dvxImageInfoFromMemory((const uint8_t *)data, len, &w, &h);
free(data);
- if (!pixels) {
+ if (!picture) {
*outErr = BAS_ERR_INVALID_PICTURE;
return NULL;
}
-
- dvxFreeImage(pixels);
}
BasPictureT *pic = pictureAdd(rt, path, w, h);
@@ -2920,6 +3061,49 @@ void *basFormRtLoadPicture(void *ctx, const char *path, int32_t *outErr) {
}
+// Each picture made from bytes keeps them for the runtime's life, as the
+// file pictures keep their names: the same bytes give the same object.
+void *basFormRtLoadPictureData(void *ctx, const char *data, int32_t len, int32_t *outErr) {
+ BasFormRtT *rt = (BasFormRtT *)ctx;
+
+ if (len <= 0) {
+ return basFormRtLoadPicture(ctx, "", outErr);
+ }
+
+ for (int32_t i = 0; i < (int32_t)arrlen(rt->pictures); i++) {
+ BasPictureT *pic = rt->pictures[i];
+
+ if (pic->data && pic->dataLen == len && memcmp(pic->data, data, (size_t)len) == 0) {
+ return pic;
+ }
+ }
+
+ int32_t w = 0;
+ int32_t h = 0;
+
+ if (!dvxImageInfoFromMemory((const uint8_t *)data, len, &w, &h)) {
+ *outErr = BAS_ERR_INVALID_PICTURE;
+ return NULL;
+ }
+
+ // The NUL after the bytes lets them go to a widget as one WgtValueT
+ uint8_t *copy = (uint8_t *)malloc((size_t)len + 1);
+ BasPictureT *pic = copy ? pictureAdd(rt, "", w, h) : NULL;
+
+ if (!pic) {
+ free(copy);
+ *outErr = BAS_ERR_OUT_OF_MEMORY;
+ return NULL;
+ }
+
+ memcpy(copy, data, (size_t)len);
+ copy[len] = '\0';
+ pic->data = copy;
+ pic->dataLen = len;
+ return pic;
+}
+
+
bool basFormRtMenuLabel(const char *caption, const char *shortcut, const char *shortcutText, char *out, int32_t outSize, int32_t *outKey, int32_t *outMods) {
char keyText[BAS_MENU_KEY_TEXT_MAX];
bool known = true;
@@ -2987,10 +3171,18 @@ void *basFormRtNewForm(void *ctx, const char *formName) {
}
+BasPictureT *basFormRtPictureData(const BasFormRtT *rt, BasValueT v) {
+ BasPictureT *pic = (v.type == BAS_TYPE_OBJECT) ? pictureFind(rt, v.objVal) : NULL;
+
+ return (pic && pic->data) ? pic : NULL;
+}
+
+
+// NULL for a picture from bytes (LoadPictureData): it has no file
const char *basFormRtPicturePath(const BasFormRtT *rt, BasValueT v) {
const BasPictureT *pic = (v.type == BAS_TYPE_OBJECT) ? pictureFind(rt, v.objVal) : NULL;
- return pic ? pic->path : NULL;
+ return (pic && !pic->data) ? pic->path : NULL;
}
@@ -3112,44 +3304,39 @@ void basFormRtRuntimeError(BasFormRtT *rt, const char *summary, const char *deta
}
-// Release all serial/secLink resources and unregister both idle pollers from
-// the shell. Safe to call repeatedly. The pollers live in this shared
-// runtime DXE but iterate per-app slots/terminals, so they MUST be dropped
-// when a BASIC app goes away -- on normal exit via basFormRtDestroy, and on
-// force-kill (where basFormRtDestroy never runs) via the stub's _appShutdown.
-// NOTE: closes every open connection/port, which is correct for the single
-// foreground BASIC app; concurrent BASIC instances sharing these module-scope
-// slots is a separate, pre-existing limitation.
-void basFormRtSerialShutdown(void) {
+// Close the connections and serial ports the program rt opened (NULL:
+// every one) and drop the idle pollers once nothing is left for them.
+// Safe to call repeatedly. The slots and ports are shared by every BASIC
+// program in this runtime library, so a program's end (basFormRtDestroy,
+// which a force kill reaches through basRunHost's kill hook) leaves the
+// other programs' links and ports open.
+void basFormRtSerialShutdown(const BasFormRtT *rt) {
for (int32_t i = 0; i < (int32_t)arrlen(sCommSlots); i++) {
- if (sCommSlots[i]) {
+ if (sCommSlots[i] && (!rt || sCommSlots[i]->owner == rt)) {
CommClose(i + 1);
}
}
- arrfree(sCommSlots);
- sCommSlots = NULL;
+ if (!rt) {
+ arrfree(sCommSlots);
+ sCommSlots = NULL;
+ }
if (sSerApiResolved && sSerApi.close) {
for (int32_t i = 0; i < RS232_NUM_PORTS; i++) {
- if (sSerAttach[i].opened || sSerAttach[i].attached) {
+ if ((sSerAttach[i].opened || sSerAttach[i].attached) && (!rt || sSerAttach[i].owner == rt)) {
SerClose(i + 1);
}
}
}
- if (sShellUnregisterIdle) {
- sShellUnregisterIdle(commIdlePoll, NULL);
- sShellUnregisterIdle(serIdlePoll, NULL);
- }
+ formRtCommIdleSync();
+ formRtSerIdleSync();
}
BasFormRtT *basFormRtSetCurrent(BasFormRtT *rt) {
- BasFormRtT *prev = sFormRt;
-
- sFormRt = rt;
- return prev;
+ return curRtSet(rt);
}
@@ -3293,7 +3480,7 @@ void basFormRtShowForm(void *ctx, void *formRef, bool modal) {
bool running = true;
while (running && !form->detached && form->window->visible && !rt->terminated) {
- running = dvxUpdate(rt->ctx);
+ running = basFormRtUpdate(rt);
}
if (rt->ctx->modalWindow == NULL || rt->ctx->modalWindow == form->window) {
@@ -3305,12 +3492,22 @@ void basFormRtShowForm(void *ctx, void *formRef, bool modal) {
}
+// The task running now (tsCurrentId; 0 without the task switcher).
+uint32_t basFormRtTaskId(void) {
+ if (!sTaskIdResolved) {
+ sTaskIdResolved = true;
+ sTaskIdFn = (uint32_t (*)(void))dlsym(NULL, DVX_SYM("tsCurrentId"));
+ }
+
+ return sTaskIdFn ? sTaskIdFn() : 0;
+}
+
+
// Free ALL per-form heap resources: form variables, controls (with
// tooltips), menuIdMap proxies + array, and popup menus. Does NOT
// destroy the window, does NOT remove the form from rt->forms, and does
-// NOT free the BasFormT itself -- callers differ on those. Single
-// source of truth for the three formerly-diverged teardown sites
-// (basFormRtDestroy, basFormRtUnloadForm, onFormClose).
+// NOT free the BasFormT itself: destroyFormNow, its caller, destroys the
+// window next and frees the form.
static void basFormRtTeardownForm(BasFormRtT *rt, BasFormT *form) {
rt->refCacheKey = NULL;
basFormRtDesignForget(form);
@@ -3335,8 +3532,16 @@ static void basFormRtTeardownForm(BasFormRtT *rt, BasFormT *form) {
form->formVarCount = 0;
}
+ // The window goes after the controls: its widgets (a timer's tick, a
+ // UserControl's Terminate code that gives way) must find none
for (int32_t i = 0; i < (int32_t)arrlen(form->controls); i++) {
- freeControl(form->controls[i]);
+ BasControlT *ctrl = form->controls[i];
+
+ if (ctrl->widget && ctrl->widget->userData == ctrl) {
+ ctrl->widget->userData = NULL;
+ }
+
+ freeControl(ctrl);
}
arrfree(form->controls);
@@ -3384,27 +3589,27 @@ static void basFormRtTeardownForm(BasFormRtT *rt, BasFormT *form) {
// Bind a library's byte stream to the Terminal control ctrlName (see
// formrt.h). NULL when the running program has no Terminal of that name.
WidgetT *basFormRtTermBind(const char *ctrlName, void *ctx, BasTermWriteFnT writeFn, BasTermUnbindFnT onUnbind) {
- BasControlT *ctrl = findCtrlByName(ctrlName);
+ WidgetT *term = findTermWidget(ctrlName);
- if (!ctrl || !ctrl->widget || strcasecmp(ctrl->typeName, BAS_TERMINAL_TYPE) != 0) {
+ if (!term) {
return NULL;
}
// One binding per terminal and per stream, as SerAttach / CommAttach
basFormRtTermUnbind(ctx);
- if (formRtDetachTermsForWidget(ctrl->widget)) {
+ if (formRtDetachTermsForWidget(term)) {
formRtSerIdleSync();
}
TermBindT bind;
- bind.term = ctrl->widget;
+ bind.term = term;
bind.ctx = ctx;
bind.onUnbind = onUnbind;
arrput(sTermBinds, bind);
- wgtAnsiTermSetComm(ctrl->widget, ctx, NULL, writeFn);
- return ctrl->widget;
+ wgtAnsiTermSetComm(term, ctx, NULL, writeFn);
+ return term;
}
@@ -3435,17 +3640,8 @@ void basFormRtTermUnbind(void *ctx) {
// Serial/comm state goes through the normal shutdown path first so open
// ports and idle pollers are released rather than leaked.
void basFormRtTestReset(void) {
- basFormRtSerialShutdown();
+ basFormRtSerialShutdown(NULL);
- for (int32_t i = 0; i < RES_MAX_HANDLES; i++) {
- if (sResHandles[i]) {
- dvxResClose(sResHandles[i]);
- sResHandles[i] = NULL;
- }
- }
-
- free(sResData);
- sResData = NULL;
memset(sSerAttach, 0, sizeof(sSerAttach));
memset(&sSerApi, 0, sizeof(sSerApi));
arrfree(sTermBinds);
@@ -3456,16 +3652,21 @@ void basFormRtTestReset(void) {
sCommApiResolved = false;
arrfree(sHelpMessages);
sHelpMessages = NULL;
- sHlpcCompile = NULL;
- sHlpcSetMessage = NULL;
- sHlpcResolved = false;
+ sHelpCompiling = false;
sShellOpenHelp = NULL;
sShellOpenHelpResolved = false;
sShellRegisterIdle = NULL;
sShellUnregisterIdle = NULL;
sShellIdleResolved = false;
- sFormRt = NULL;
+ arrfree(sTaskRts);
sCodeDepth = 0;
+ sTaskIdFn = NULL;
+ sTaskIdResolved = false;
+ sYieldFn = NULL;
+ sYieldResolved = false;
+ sQueuedTotal = 0;
+ arrfree(sRuntimes);
+ sRuntimes = NULL;
sLoadingFrm = false;
sLastInputBoxCancelled = false;
memset(&sDrag, 0, sizeof(sDrag));
@@ -3552,6 +3753,22 @@ void basFormRtUnloadForm(void *ctx, void *formRef) {
}
+// The waiting events of every runtime of this task run (rt's, and those
+// of programs it runs), then a frame.
+bool basFormRtUpdate(BasFormRtT *rt) {
+ uint32_t task = basFormRtTaskId();
+
+ for (int32_t i = 0; i < (int32_t)arrlen(sRuntimes); i++) {
+ if (sRuntimes[i]->ownTask == task) {
+ queueDrain(sRuntimes[i]);
+ }
+ }
+
+ basUcPump();
+ return dvxUpdate(rt->ctx);
+}
+
+
bool basFormRtWantsNewArgs(BasFormRtT *rt) {
for (int32_t i = 0; rt && i < (int32_t)arrlen(rt->forms); i++) {
BasFormT *form = rt->forms[i];
@@ -3566,13 +3783,30 @@ bool basFormRtWantsNewArgs(BasFormRtT *rt) {
}
+void basFormRtYield(void) {
+ if (!sYieldResolved) {
+ sYieldResolved = true;
+ sYieldFn = (void (*)(void))dlsym(NULL, DVX_SYM("tsYield"));
+ }
+
+ if (sYieldFn) {
+ sYieldFn();
+ }
+}
+
+
// A widget value as a BASIC value: INT / ENUM Long, BOOL Boolean, FLOAT
// Single (the BASIC type of WGT_IFACE_FLOAT), DOUBLE Double, STRING a new
-// string.
+// string of its bytes (NUL bytes included: wgtValueLen).
static BasValueT basFromWgt(const WgtValueT *v) {
switch (v->type) {
- case WGT_IFACE_STRING:
- return basValStringFromC(v->s ? v->s : "");
+ case WGT_IFACE_STRING: {
+ BasValueT out = zeroValue();
+
+ out.type = BAS_TYPE_STRING;
+ out.strVal = basStringNew(v->s ? v->s : "", wgtValueLen(v));
+ return out;
+ }
case WGT_IFACE_INT:
case WGT_IFACE_ENUM:
@@ -4122,7 +4356,7 @@ static BasValueT callCommonMethod(BasFormRtT *rt, BasControlT *ctrl, const char
// are skipped. outArgs, when given, are what the handler's leading
// parameters refer to (ByRef event parameters such as Cancel).
static bool callEventProc(BasFormRtT *rt, BasFormT *form, const BasProcEntryT *proc, const BasValueT *args, int32_t argCount, BasValueT *outArgs, int32_t outArgCount) {
- if (!proc || proc->isFunction) {
+ if (!eventProcFits(proc, argCount)) {
return false;
}
@@ -4130,10 +4364,6 @@ static bool callEventProc(BasFormRtT *rt, BasFormT *form, const BasProcEntryT *p
return rtCallHandler(rt, form, proc, proc->codeAddr, NULL, 0, NULL, 0);
}
- if (proc->paramCount != argCount) {
- return false;
- }
-
return rtCallHandler(rt, form, proc, proc->codeAddr, args, argCount, outArgs, outArgCount);
}
@@ -4221,15 +4451,44 @@ static BasValueT clipboardMethod(BasFormRtT *rt, const char *name, BasValueT *ar
}
+// A handle for a slot whose link CommOpen or CommOpenTcp opened: a hole
+// a close left, else one more. 0 (the slot freed) when the link did not
+// open.
+static int32_t commAddSlot(CommSlotT *slot) {
+ if (!slot->link) {
+ free(slot);
+ return 0;
+ }
+
+ int32_t handle = 0;
+
+ for (int32_t i = 0; i < (int32_t)arrlen(sCommSlots) && handle == 0; i++) {
+ if (!sCommSlots[i]) {
+ handle = i + 1;
+ }
+ }
+
+ if (handle == 0) {
+ arrput(sCommSlots, NULL);
+ handle = (int32_t)arrlen(sCommSlots);
+ }
+
+ slot->active = true;
+ slot->owner = curRt();
+ sCommSlots[handle - 1] = slot;
+ return handle;
+}
+
+
void CommAttach(int32_t handle, const char *termCtrlName, int32_t channel, int32_t encrypt) {
CommSlotT *slot = commGetSlot(handle);
- if (!slot || !sFormRt || !termCtrlName) {
+ if (!slot || !curRt() || !termCtrlName) {
return;
}
- // Find the terminal widget by control name on any loaded form
- WidgetT *termWidget = findCtrlWidgetByName(termCtrlName);
+ // The Terminal control of that name on any loaded form
+ WidgetT *termWidget = findTermWidget(termCtrlName);
if (!termWidget) {
return;
@@ -4329,14 +4588,35 @@ static CommSlotT *commGetSlot(int32_t handle) {
}
+// A Comm call that waits (CommHandshake, CommSend) goes on: the link is
+// polled, the program's events run (as a DoEvents loop would run them),
+// and it is still open, the program is not ending, and limitMs has not
+// passed since since.
+static bool commGoOn(BasFormRtT *rt, int32_t handle, PlatformTicksT since, uint32_t limitMs) {
+ CommSlotT *slot = commGetSlot(handle);
+
+ if (!slot || platformClock() - since >= PLATFORM_MS_TO_TICKS(limitMs)) {
+ return false;
+ }
+
+ sCommApi.poll(slot->link);
+ return !rt || (rtPump(rt) && commGetSlot(handle) != NULL);
+}
+
+
// Key exchange, optionally authenticated by a shared passphrase (NULL or
// "" = none; both sides must match). Returns True on success, False on
// any failure, including a passphrase mismatch or a peer that uses a
-// passphrase when this side does not (or the reverse).
+// passphrase when this side does not (or the reverse). It waits up to
+// SECLINK_HANDSHAKE_TIMEOUT_MS, a step at a time with the program's
+// events between steps (a ready link is re-keyed).
int32_t CommHandshake(int32_t handle, const char *passphrase) {
- CommSlotT *slot = commGetSlot(handle);
+ CommSlotT *slot = commGetSlot(handle);
+ BasFormRtT *rt = basFormRtCurrent();
+ PlatformTicksT since = platformClock();
+ int32_t rc;
- if (!slot || !sCommApi.handshake || !sCommApi.setPassphrase) {
+ if (!slot || slot->waiting || !sCommApi.handshakeStart || !sCommApi.handshakeStep || !sCommApi.setPassphrase) {
return 0;
}
@@ -4346,7 +4626,27 @@ int32_t CommHandshake(int32_t handle, const char *passphrase) {
return 0;
}
- return (commNote(slot, sCommApi.handshake(slot->link)) == SECLINK_SUCCESS) ? -1 : 0;
+ slot->waiting = true;
+ rc = commNote(slot, sCommApi.handshakeStart(slot->link));
+
+ while (rc == SECLINK_PENDING) {
+ if (!commGoOn(rt, handle, since, SECLINK_HANDSHAKE_TIMEOUT_MS)) {
+ rc = SECLINK_ERR_TIMEOUT;
+ break;
+ }
+
+ slot = commGetSlot(handle);
+ rc = commNote(slot, sCommApi.handshakeStep(slot->link));
+ }
+
+ slot = commGetSlot(handle);
+
+ if (slot != NULL) {
+ commNote(slot, rc);
+ slot->waiting = false;
+ }
+
+ return (rc == SECLINK_SUCCESS) ? -1 : 0;
}
@@ -4457,28 +4757,7 @@ static void commOnRecv(void *ctx, const uint8_t *data, int len, uint8_t channel,
int32_t CommOpen(int32_t com, int32_t baud, int32_t dataBits, const char *parity, int32_t stopBits, int32_t handshake) {
- if (!commResolveApi()) {
- return 0;
- }
-
- // Reuse an empty slot (hole from a previous close) if possible.
- // Otherwise grow the array by one pointer slot.
- int32_t handle = 0;
- int32_t n = (int32_t)arrlen(sCommSlots);
-
- for (int32_t i = 0; i < n; i++) {
- if (!sCommSlots[i]) {
- handle = i + 1;
- break;
- }
- }
-
- if (handle == 0) {
- arrput(sCommSlots, NULL);
- handle = (int32_t)arrlen(sCommSlots);
- }
-
- CommSlotT *slot = (CommSlotT *)calloc(1, sizeof(CommSlotT));
+ CommSlotT *slot = commResolveApi() ? (CommSlotT *)calloc(1, sizeof(CommSlotT)) : NULL;
if (!slot) {
return 0;
@@ -4491,15 +4770,27 @@ int32_t CommOpen(int32_t com, int32_t baud, int32_t dataBits, const char *parity
}
slot->link = sCommApi.open(com, baud, dataBits, parityChar, stopBits, handshake, commOnRecv, slot);
+ return commAddSlot(slot);
+}
- if (!slot->link) {
- free(slot);
+
+// The packet link over a net.bas TCP connection (TcpConnect, TcpAccept)
+// instead of a COM port: every channel, the handshake and encryption, as
+// CommOpen's. The connection stays the program's: TcpClose it after
+// CommClose. 0 when networking is not installed or the link cannot open.
+int32_t CommOpenTcp(int32_t tcp) {
+ bool ready = commResolveApi() && sCommApi.openStream && sCommApi.tcpRead && sCommApi.tcpWrite && tcp > 0;
+ CommSlotT *slot = ready ? (CommSlotT *)calloc(1, sizeof(CommSlotT)) : NULL;
+
+ if (!slot) {
return 0;
}
- slot->active = true;
- sCommSlots[handle - 1] = slot;
- return handle;
+ PktStreamT stream = { commTcpRead, commTcpWrite, slot, NULL };
+
+ slot->tcp = tcp;
+ slot->link = sCommApi.openStream(&stream, commOnRecv, slot);
+ return commAddSlot(slot);
}
@@ -4576,39 +4867,70 @@ static bool commResolveApi(void) {
return sCommApi.open != NULL;
}
- sCommApiResolved = true;
- sCommApi.open = dlsym(NULL, DVX_SYM("secLinkOpen"));
- sCommApi.close = dlsym(NULL, DVX_SYM("secLinkClose"));
- sCommApi.handshake = dlsym(NULL, DVX_SYM("secLinkHandshake"));
- sCommApi.isReady = dlsym(NULL, DVX_SYM("secLinkIsReady"));
- sCommApi.poll = dlsym(NULL, DVX_SYM("secLinkPoll"));
- sCommApi.send = dlsym(NULL, DVX_SYM("secLinkSend"));
- sCommApi.sendBuf = dlsym(NULL, DVX_SYM("secLinkSendBuf"));
- sCommApi.getPending = dlsym(NULL, DVX_SYM("secLinkGetPending"));
-
+ sCommApiResolved = true;
+ sCommApi.open = dlsym(NULL, DVX_SYM("secLinkOpen"));
+ sCommApi.close = dlsym(NULL, DVX_SYM("secLinkClose"));
+ sCommApi.handshakeStart = dlsym(NULL, DVX_SYM("secLinkHandshakeStart"));
+ sCommApi.isReady = dlsym(NULL, DVX_SYM("secLinkIsReady"));
+ sCommApi.poll = dlsym(NULL, DVX_SYM("secLinkPoll"));
+ sCommApi.send = dlsym(NULL, DVX_SYM("secLinkSend"));
+ sCommApi.getPending = dlsym(NULL, DVX_SYM("secLinkGetPending"));
sCommApi.setPassphrase = dlsym(NULL, DVX_SYM("secLinkSetPassphrase"));
sCommApi.setChannelPlain = dlsym(NULL, DVX_SYM("secLinkSetChannelPlain"));
sCommApi.handshakeStep = dlsym(NULL, DVX_SYM("secLinkHandshakeStep"));
sCommApi.getDropped = dlsym(NULL, DVX_SYM("secLinkGetDropped"));
+ sCommApi.openStream = dlsym(NULL, DVX_SYM("secLinkOpenStream"));
+ sCommApi.tcpRead = dlsym(NULL, DVX_SYM("netBasStreamRead"));
+ sCommApi.tcpWrite = dlsym(NULL, DVX_SYM("netBasStreamWrite"));
return sCommApi.open != NULL;
}
-// Send data (a BINARY STRING: every byte value) on channel.
+// Send data (a BINARY STRING: every byte value) on channel, a frame at a
+// time as the link takes it: while its window is full the link is polled
+// and the program's events run (as a DoEvents loop would run them). False
+// when the far side takes nothing for PKT_SEND_TIMEOUT_MS.
int32_t CommSend(int32_t handle, const uint8_t *data, int32_t len, int32_t channel, int32_t encrypt) {
- CommSlotT *slot = commGetSlot(handle);
+ CommSlotT *slot = commGetSlot(handle);
+ BasFormRtT *rt = basFormRtCurrent();
+ PlatformTicksT since = platformClock();
+ int32_t sent = 0;
+ int32_t rc = SECLINK_SUCCESS;
- if (!slot || !data || !sCommApi.sendBuf || channel < 0 || channel >= SECLINK_NUM_CHANNELS) {
+ if (!slot || slot->waiting || !data || !sCommApi.send || !sCommApi.poll || channel < 0 || channel >= SECLINK_NUM_CHANNELS) {
return 0;
}
- if (len == 0) {
- return -1;
+ slot->waiting = true;
+
+ while (sent < len) {
+ int32_t chunk = DVX_MIN(len - sent, SECLINK_MAX_PAYLOAD);
+
+ rc = sCommApi.send(slot->link, data + sent, chunk, (uint8_t)channel, encrypt != 0, false);
+
+ // The window is full: its acknowledgements come with the polls
+ if (rc == SECLINK_ERR_SEND && commGoOn(rt, handle, since, PKT_SEND_TIMEOUT_MS)) {
+ slot = commGetSlot(handle);
+ continue;
+ }
+
+ if (rc != SECLINK_SUCCESS) {
+ break;
+ }
+
+ sent += chunk;
+ since = platformClock();
}
- int32_t rc = commNote(slot, sCommApi.sendBuf(slot->link, data, len, (uint8_t)channel, encrypt != 0));
- return (rc == 0) ? -1 : 0;
+ slot = commGetSlot(handle);
+
+ if (slot != NULL) {
+ commNote(slot, rc);
+ slot->waiting = false;
+ }
+
+ return (rc == SECLINK_SUCCESS) ? -1 : 0;
}
@@ -4626,6 +4948,17 @@ int32_t CommSetPlain(int32_t handle, int32_t channel, int32_t plain) {
}
+// CommOpenTcp's byte stream: the slot's net.bas connection.
+static int32_t commTcpRead(void *ctx, uint8_t *buf, int32_t len) {
+ return sCommApi.tcpRead(((const CommSlotT *)ctx)->tcp, buf, len);
+}
+
+
+static int32_t commTcpWrite(void *ctx, const uint8_t *data, int32_t len) {
+ return sCommApi.tcpWrite(((const CommSlotT *)ctx)->tcp, data, len);
+}
+
+
// AnsiTerm read callback: drains the terminal channel's ring buffer
static int32_t commTermRead(void *ctx, uint8_t *buf, int32_t maxLen) {
CommSlotT *slot = (CommSlotT *)ctx;
@@ -4711,6 +5044,57 @@ static BasControlT *ctrlFromWidget(WidgetT *w, BasFormRtT **outRt) {
}
+// The runtime current on this task, or NULL.
+static BasFormRtT *curRt(void) {
+ uint32_t task = basFormRtTaskId();
+
+ for (int32_t i = 0; i < (int32_t)arrlen(sTaskRts); i++) {
+ if (sTaskRts[i].task == task) {
+ return sTaskRts[i].rt;
+ }
+ }
+
+ return NULL;
+}
+
+
+// rt is ending: no task has it current any more.
+static void curRtForget(const BasFormRtT *rt) {
+ for (int32_t i = (int32_t)arrlen(sTaskRts) - 1; i >= 0; i--) {
+ if (sTaskRts[i].rt == rt) {
+ arrdelswap(sTaskRts, i);
+ }
+ }
+}
+
+
+// Make rt (NULL: none) current on this task; the one it replaces.
+static BasFormRtT *curRtSet(BasFormRtT *rt) {
+ uint32_t task = basFormRtTaskId();
+ TaskRtT slot = { task, rt };
+
+ for (int32_t i = 0; i < (int32_t)arrlen(sTaskRts); i++) {
+ if (sTaskRts[i].task == task) {
+ BasFormRtT *prev = sTaskRts[i].rt;
+
+ if (rt) {
+ sTaskRts[i].rt = rt;
+ } else {
+ arrdelswap(sTaskRts, i);
+ }
+
+ return prev;
+ }
+ }
+
+ if (rt) {
+ arrput(sTaskRts, slot);
+ }
+
+ return NULL;
+}
+
+
// The default property of the control ref names (a store or read with no
// property name: Text1 = "x"); NULL after a runtime error when it has none.
static const char *defaultPropOf(BasFormRtT *rt, void *ref) {
@@ -4803,6 +5187,9 @@ static void deviceBind(BasFormRtT *rt) {
sDeviceApi.strig = (__typeof__(sDeviceApi.strig))dlsym(NULL, DVX_SYM("joyBasStrig"));
sDeviceApi.frameInit = (__typeof__(sDeviceApi.frameInit))dlsym(NULL, DVX_SYM("gameFrameInit"));
sDeviceApi.frameNext = (__typeof__(sDeviceApi.frameNext))dlsym(NULL, DVX_SYM("gameFrameNext"));
+ sDeviceApi.dosFor = (__typeof__(sDeviceApi.dosFor))dlsym(NULL, DVX_SYM("shellRunDosFor"));
+ sDeviceApi.dosDone = (__typeof__(sDeviceApi.dosDone))dlsym(NULL, DVX_SYM("shellRunDosDone"));
+ sDeviceApi.dosSplit = (__typeof__(sDeviceApi.dosSplit))dlsym(NULL, DVX_SYM("shellSplitCommand"));
memset(&cb, 0, sizeof(cb));
if (sDeviceApi.beep && sDeviceApi.busy && sDeviceApi.play && sDeviceApi.playCount && sDeviceApi.sound) {
@@ -4823,6 +5210,12 @@ static void deviceBind(BasFormRtT *rt) {
cb.frameBusy = deviceFrameBusy;
}
+ if (sDeviceApi.dosFor && sDeviceApi.dosDone && sDeviceApi.dosSplit) {
+ cb.shellStart = deviceShellStart;
+ cb.shellBusy = deviceShellBusy;
+ cb.shellResult = deviceShellResult;
+ }
+
cb.ctx = rt;
basVmSetDeviceCallbacks(rt->vm, &cb);
}
@@ -4861,6 +5254,38 @@ static int32_t devicePlayCount(void *ctx) {
}
+// SHELL: until the shell's main loop has run the command (or refused it).
+static bool deviceShellBusy(void *ctx) {
+ BasFormRtT *rt = (BasFormRtT *)ctx;
+
+ return !sDeviceApi.dosDone(rt->shellAppId, &rt->shellRc);
+}
+
+
+static int32_t deviceShellResult(void *ctx) {
+ return ((const BasFormRtT *)ctx)->shellRc;
+}
+
+
+// SHELL command: the shell runs it with DVX stepped aside for this
+// program, which waits (shellRunDosFor: the other programs are asked to
+// close first); a bare command (DIR) goes to the command interpreter.
+static bool deviceShellStart(void *ctx, const char *command) {
+ BasFormRtT *rt = (BasFormRtT *)ctx;
+ char program[DVX_MAX_PATH];
+ char args[SHELL_ARGS_MAX];
+
+ if (!rt->ctx || !sDeviceApi.dosSplit(command, program, (int32_t)sizeof(program), args, (int32_t)sizeof(args))) {
+ return false;
+ }
+
+ rt->shellAppId = rt->ctx->currentAppId;
+ rt->shellRc = -1;
+ sDeviceApi.dosFor(rt->shellAppId, program, args, NULL, false);
+ return true;
+}
+
+
static void deviceSound(void *ctx, double hz, double ticks) {
(void)ctx;
sDeviceApi.sound(hz, ticks);
@@ -4890,7 +5315,9 @@ static int32_t deviceStrig(void *ctx, int32_t n) {
// closing the box the last), or -1 for no buttons or more than
// DVX_BUTTON_BOX_MAX. icon is a MsgBox icon value (vbQuestion ...) or 0.
int32_t DlgButtons(const char *title, const char *message, const char *buttons, int32_t icon) {
- if (!sFormRt || !buttons) {
+ BasFormRtT *rt = curRt();
+
+ if (!rt || !buttons) {
return -1;
}
@@ -4905,12 +5332,14 @@ int32_t DlgButtons(const char *title, const char *message, const char *buttons,
return -1;
}
- return dvxButtonBox(sFormRt->ctx, title, message ? message : "", icon & MB_ICON_MASK, labels, count);
+ return dvxButtonBox(rt->ctx, title, message ? message : "", icon & MB_ICON_MASK, labels, count);
}
int32_t DlgChoice(const char *title, const char *prompt, const char *items, int32_t defaultIdx) {
- if (!sFormRt || !items) {
+ BasFormRtT *rt = curRt();
+
+ if (!rt || !items) {
return -1;
}
@@ -4926,7 +5355,7 @@ int32_t DlgChoice(const char *title, const char *prompt, const char *items, int3
int32_t count = (buf && ptrs) ? splitPipeList(buf, ptrs, maxItems) : 0;
int32_t chosen = -1;
- if (count > 0 && !dvxChoiceDialog(sFormRt->ctx, title, prompt, ptrs, count, defaultIdx, &chosen)) {
+ if (count > 0 && !dvxChoiceDialog(rt->ctx, title, prompt, ptrs, count, defaultIdx, &chosen)) {
chosen = -1;
}
@@ -4939,9 +5368,10 @@ int32_t DlgChoice(const char *title, const char *prompt, const char *items, int3
// The color dialog: the chosen color as RGB() makes it, or -1 when
// cancelled. defaultColor is the starting color.
int32_t DlgColor(const char *title, int32_t defaultColor) {
- uint32_t rgb = wgtBasicColor((uint32_t)defaultColor) & BAS_RGB_MASK;
+ uint32_t rgb = wgtBasicColor((uint32_t)defaultColor) & BAS_RGB_MASK;
+ BasFormRtT *rt = curRt();
- if (sFormRt && dvxColorDialog(sFormRt->ctx, title, &rgb)) {
+ if (rt && dvxColorDialog(rt->ctx, title, &rgb)) {
return (int32_t)wgtBasicColor(rgb);
}
@@ -4988,7 +5418,9 @@ int32_t DlgFont(const char *title, const char *family, int32_t size, int32_t sty
sDlgFont.size = size;
sDlgFont.style = (uint32_t)style & (FONT_STYLE_FACE | FONT_STYLE_LINES);
- return (sFormRt && dvxFontDialog(sFormRt->ctx, title, (uint32_t)flags, &sDlgFont)) ? -1 : 0;
+ BasFormRtT *rt = curRt();
+
+ return (rt && dvxFontDialog(rt->ctx, title, (uint32_t)flags, &sDlgFont)) ? -1 : 0;
}
@@ -5008,7 +5440,9 @@ int32_t DlgFontStyle(void) {
const char *DlgInputBox(const char *title, const char *prompt, const char *defaultText) {
- if (!sFormRt) {
+ BasFormRtT *rt = curRt();
+
+ if (!rt) {
sLastInputBoxCancelled = true;
return "";
}
@@ -5016,7 +5450,7 @@ const char *DlgInputBox(const char *title, const char *prompt, const char *defau
static char buf[BAS_INPUTBOX_BUF_LEN];
buf[0] = '\0';
- if (dvxInputBox(sFormRt->ctx, title, prompt, defaultText, buf, sizeof(buf))) {
+ if (dvxInputBox(rt->ctx, title, prompt, defaultText, buf, sizeof(buf))) {
sLastInputBoxCancelled = false;
return buf;
}
@@ -5039,19 +5473,22 @@ int32_t DlgIntInput(const char *title, const char *prompt, int32_t defaultVal, i
// The integer input box with the spinner moving by stepVal; the default
// value when cancelled (DlgInputCancelled tells).
int32_t DlgIntInputStep(const char *title, const char *prompt, int32_t defaultVal, int32_t minVal, int32_t maxVal, int32_t stepVal) {
- int32_t result = defaultVal;
+ int32_t result = defaultVal;
+ BasFormRtT *rt = curRt();
- sLastInputBoxCancelled = !sFormRt || !dvxIntInputBox(sFormRt->ctx, title, prompt, defaultVal, minVal, maxVal, stepVal, &result);
+ sLastInputBoxCancelled = !rt || !dvxIntInputBox(rt->ctx, title, prompt, defaultVal, minVal, maxVal, stepVal, &result);
return sLastInputBoxCancelled ? defaultVal : result;
}
int32_t DlgPromptSave(const char *title) {
- if (!sFormRt) {
+ BasFormRtT *rt = curRt();
+
+ if (!rt) {
return DVX_SAVE_NO;
}
- return dvxPromptSave(sFormRt->ctx, title);
+ return dvxPromptSave(rt->ctx, title);
}
@@ -5109,7 +5546,7 @@ static void dragBegin(BasFormRtT *rt, BasControlT *ctrl) {
sDrag.prevButtons = ctx->mouseButtons;
dragHookForms(rt, true);
- if (!ctrl->dragIcon || !dragShowIcon(ctx, ctrl->dragIcon)) {
+ if (!ctrl->dragIcon || !dragShowIcon(rt, ctx, ctrl->dragIcon)) {
dvxDragOutlineShow(ctx, w->w, w->h, win->x + win->contentX + w->x - ctx->mouseX, win->y + win->contentY + w->y - ctx->mouseY);
}
@@ -5309,23 +5746,26 @@ static void dragOverUpdate(void) {
}
-// Show the DragIcon picture at path as the drag feedback; false when it
-// cannot be read (the outline is shown instead).
-static bool dragShowIcon(AppContextT *ctx, const char *path) {
- int32_t len = 0;
- char *data = pictureFileRead(path, &len);
- int32_t w = 0;
- int32_t h = 0;
- int32_t pitch = 0;
- bool hasKey = false;
- uint32_t key = 0;
+// Show the DragIcon picture at path (a file, or a LoadPictureData
+// picture's name) as the drag feedback; false when it cannot be read (the
+// outline is shown instead).
+static bool dragShowIcon(const BasFormRtT *rt, AppContextT *ctx, const char *path) {
+ const BasPictureT *pic = pictureByDataName(rt, path);
+ int32_t len = pic ? pic->dataLen : 0;
+ char *read = pic ? NULL : pictureFileRead(path, &len);
+ const uint8_t *bytes = pic ? pic->data : (const uint8_t *)read;
+ int32_t w = 0;
+ int32_t h = 0;
+ int32_t pitch = 0;
+ bool hasKey = false;
+ uint32_t key = 0;
- if (!data) {
+ if (!bytes) {
return false;
}
- sDrag.icon = dvxLoadImageAlpha(ctx, (const uint8_t *)data, len, &w, &h, &pitch, &hasKey, &key);
- free(data);
+ sDrag.icon = dvxLoadImageAlpha(ctx, bytes, len, &w, &h, &pitch, &hasKey, &key);
+ free(read);
if (!sDrag.icon) {
return false;
@@ -5389,6 +5829,21 @@ static const BasProcEntryT *eventHandler(BasFormRtT *rt, const BasControlT *obj,
}
+// The rule callEventProc calls a handler by: a SUB taking the event's
+// arguments, or none.
+static bool eventProcFits(const BasProcEntryT *proc, int32_t argCount) {
+ return proc && !proc->isFunction && (proc->paramCount == 0 || proc->paramCount == argCount);
+}
+
+
+// True when the event has a handler that would run (eventProcFits).
+static bool eventWanted(BasFormRtT *rt, const BasControlT *obj, const char *ctrlName, const char *eventName, int32_t argCount) {
+ bool overridden;
+
+ return eventProcFits(eventHandler(rt, obj, ctrlName, eventName, &overridden), argCount);
+}
+
+
// The file dialogs of commdlg.bas: flags FD_OPEN, FD_SAVE, or FD_FOLDER,
// starting in folder (NULL or "" = the current folder), titled title (""
// = the dialog's own). The chosen path, or "" when cancelled.
@@ -5396,15 +5851,16 @@ static const char *fileDialog(const char *title, const char *filter, int32_t fla
static char path[DVX_MAX_PATH];
FileFilterT *filters = NULL;
char filterBuf[BAS_FILTER_BUF_LEN];
+ BasFormRtT *rt = curRt();
path[0] = '\0';
- if (!sFormRt) {
+ if (!rt) {
return path;
}
int32_t count = parseFileFilters(filter, &filters, filterBuf, sizeof(filterBuf));
- bool ok = dvxFileDialog(sFormRt->ctx, (title && title[0]) ? title : NULL, flags, (folder && folder[0]) ? folder : NULL, filters, count, path, sizeof(path));
+ bool ok = dvxFileDialog(rt->ctx, (title && title[0]) ? title : NULL, flags, (folder && folder[0]) ? folder : NULL, filters, count, path, sizeof(path));
free(filters);
@@ -5436,12 +5892,14 @@ static BasControlT *findButtonWith(BasFormT *form, const char *propName) {
// The running program's control called name, on any of its loaded forms
// (the first one found), or NULL.
static BasControlT *findCtrlByName(const char *name) {
- if (!sFormRt || !name) {
+ BasFormRtT *rt = curRt();
+
+ if (!rt || !name) {
return NULL;
}
- for (int32_t i = 0; i < (int32_t)arrlen(sFormRt->forms); i++) {
- BasControlT *hit = findCtrlInForm(sFormRt->forms[i], name);
+ for (int32_t i = 0; i < (int32_t)arrlen(rt->forms); i++) {
+ BasControlT *hit = findCtrlInForm(rt->forms[i], name);
if (hit) {
return hit;
@@ -5466,7 +5924,7 @@ static BasControlT *findCtrlByWidget(BasFormT *form, const WidgetT *w) {
// Case-insensitive scan of a single form's controls by bare name. The
// shared core of every unindexed control lookup (basFormRtFindCtrl,
-// basFormRtRemoveCtrl, findCtrlWidgetByName). Control-array elements
+// basFormRtRemoveCtrl, findCtrlByName). Control-array elements
// are never matched here: an unindexed reference to an array is an
// error in VB, and matching one would hit an arbitrary element.
static BasControlT *findCtrlInForm(BasFormT *form, const char *name) {
@@ -5480,15 +5938,6 @@ static BasControlT *findCtrlInForm(BasFormT *form, const char *name) {
}
-// Finds a control's live widget by name across every loaded form. Shared by
-// CommAttach/SerAttach to locate the terminal widget to bind a serial channel.
-static WidgetT *findCtrlWidgetByName(const char *name) {
- BasControlT *ctrl = findCtrlByName(name);
-
- return ctrl ? ctrl->widget : NULL;
-}
-
-
// rt's loaded form owning this window, or NULL (also when rt is NULL).
// Shared by every window callback (onForm*) and the drag.
static BasFormT *findFormByWindow(const BasFormRtT *rt, const WindowT *win) {
@@ -5574,6 +6023,17 @@ static const BasPropDescT *findPropDesc(const BasPropDescT *table, int32_t count
}
+// The live widget of the running program's Terminal control called name
+// (BAS_TERMINAL_TYPE), else NULL: a control of another type is no
+// terminal, and without the terminal widget no control is one. Shared by
+// SerAttach, CommAttach, and basFormRtTermBind.
+static WidgetT *findTermWidget(const char *name) {
+ BasControlT *ctrl = findCtrlByName(name);
+
+ return ctrl && strcasecmp(ctrl->typeName, BAS_TERMINAL_TYPE) == 0 ? ctrl->widget : NULL;
+}
+
+
static void fireCtrlEvent(BasFormRtT *rt, BasControlT *ctrl, const char *eventName, const BasValueT *args, int32_t argCount) {
fireCtrlEventOut(rt, ctrl, eventName, args, argCount, NULL, 0);
}
@@ -5596,6 +6056,15 @@ static void fireCtrlEventOut(BasFormRtT *rt, BasControlT *ctrl, const char *even
return;
}
+ // Dispatched on another task: the program's own task runs it
+ if (!outArgs && queueForeign(rt, eventName)) {
+ if (eventWanted(rt, ctrl, ctrl->name, eventName, argCount + (ctrl->index >= 0 ? 1 : 0))) {
+ queueEvent(rt, ctrl->form, ctrl, ctrl->name, eventName, args, argCount, BasQueuedCtrlEventE);
+ }
+
+ return;
+ }
+
// Re-entrancy guard: reject only same-event re-entry (runSubLoop
// pumps events during long handlers; without this, a lingering
// mouse-up can re-deliver Click on top of itself). But allow
@@ -5702,6 +6171,11 @@ static bool fireEventArgsOut(BasFormRtT *rt, BasFormT *form, BasControlT *obj, c
return false;
}
+ // Dispatched on another task: the program's own task runs it
+ if (!outArgs && queueForeign(rt, eventName)) {
+ return eventWanted(rt, obj, ctrlName, eventName, argCount) && queueEvent(rt, form, obj, ctrlName, eventName, args, argCount, BasQueuedEventE);
+ }
+
bool overridden;
const BasProcEntryT *proc = eventHandler(rt, obj, ctrlName, eventName, &overridden);
@@ -5836,6 +6310,18 @@ static void formatPropNames(const BasPropDescT *table, int32_t count, bool forWr
}
+// A form's window was closed: the form is unloaded (QueryUnload may keep
+// it); with no form left the program's code stops. A committed unload is
+// detached (removed from rt->forms) even when its frees are deferred.
+static void formClose(BasFormRtT *rt, BasFormT *form) {
+ basFormRtUnloadForm(rt, form);
+
+ if (arrlen(rt->forms) == 0 && rt->vm) {
+ rt->vm->running = false;
+ }
+}
+
+
// The object whose SetEvent overrides apply to a by-name form-level
// event: the form's own formCtrl, an already-created menu proxy (SetEvent
// on a menu name creates one), or NULL.
@@ -5858,6 +6344,28 @@ static BasControlT *formEventObject(BasFormT *form, const char *ctrlName) {
}
+// A full-screen form's screen came, went behind the desktop, or ended:
+// its FullScreen event (State: dvxFullScreenActive, dvxFullScreenSuspended,
+// or dvxFullScreenEnded).
+static void formFullChanged(WindowT *win, ExclusiveStateE state, void *userData) {
+ BasFormRtT *rt = (BasFormRtT *)userData;
+ BasFormT *form = findFormByWindow(rt, win);
+ int32_t code = BAS_FULLSCREEN_ENDED;
+
+ if (state == ExclusiveActiveE) {
+ code = BAS_FULLSCREEN_ACTIVE;
+ } else if (state == ExclusiveSuspendedE) {
+ code = BAS_FULLSCREEN_SUSPENDED;
+ }
+
+ if (form) {
+ BasValueT arg = basValInteger((int16_t)code);
+
+ basFormRtFireEventArgs(rt, form, form->name, BAS_EVT_FULLSCREEN, &arg, 1);
+ }
+}
+
+
// The form's own methods that reach the runtime as method calls:
// FitWidth / FitHeight (shrink-wrap one dimension to the controls, as
// AutoSize does both) and SaveScreenshot path$ (the window's picture to
@@ -6047,7 +6555,9 @@ static void formRtSerIdleSync(void) {
// ended when it was called) since the program started waiting at its
// current rate (FRAMEWAIT's own pacer, GameFramePacerT.missed).
int32_t FrameMissed(void) {
- return (sFormRt && sFormRt->frameHz > 0) ? (int32_t)sFormRt->framePacer.missed : 0;
+ BasFormRtT *rt = curRt();
+
+ return (rt && rt->frameHz > 0) ? (int32_t)rt->framePacer.missed : 0;
}
@@ -6378,8 +6888,17 @@ static void frmLoadOnFormProp(void *userData, const char *key, const char *value
case FORM_PROP_ICON:
case FORM_PROP_TAG:
- case FORM_PROP_MOUSEPOINTER: {
- BasValueT v = (pd->type == WGT_IFACE_INT) ? basValLong(num) : basValStringFromC(text);
+ case FORM_PROP_MOUSEPOINTER:
+ case FORM_PROP_MAXWIDTH:
+ case FORM_PROP_MAXHEIGHT:
+ case FORM_PROP_GAMEINPUT: {
+ BasValueT v;
+
+ if (pd->type == WGT_IFACE_STRING) {
+ v = basValStringFromC(text);
+ } else {
+ v = (pd->type == WGT_IFACE_BOOL) ? basValBool(flag) : basValLong(num);
+ }
setFormProp(ctx->rt, frm, pd->name, v);
basValRelease(&v);
@@ -6401,6 +6920,7 @@ static void frmLoadOnFormProp(void *userData, const char *key, const char *value
case FORM_PROP_DESIGNMODE:
case FORM_PROP_DESIGNSELECTION:
case FORM_PROP_ACTIVECONTROL:
+ case FORM_PROP_FULLSCREEN:
case FORM_PROP_COUNT:
// Name comes from the Begin line; the others are runtime-only.
break;
@@ -6603,7 +7123,7 @@ static BasValueT getCommonProp(BasControlT *ctrl, const char *propName, bool *ha
case CTRL_PROP_PARENT:
case CTRL_PROP_CONTAINER:
- return basFormRtDesignCtrlProp(sFormRt, ctrl, pd->name);
+ return basFormRtDesignCtrlProp(curRt(), ctrl, pd->name);
case CTRL_PROP_TAG:
return basValStringFromC(ctrl->tag ? ctrl->tag : "");
@@ -6711,6 +7231,18 @@ static BasValueT getFormProp(BasFormRtT *rt, BasFormT *frm, const char *propName
case FORM_PROP_ACTIVECONTROL:
return activeControlOf(rt, frm);
+ case FORM_PROP_MAXWIDTH:
+ return basValLong((win && win->maxW != WM_MAX_FROM_SCREEN) ? win->maxW : FORM_NO_MAX);
+
+ case FORM_PROP_MAXHEIGHT:
+ return basValLong((win && win->maxH != WM_MAX_FROM_SCREEN) ? win->maxH : FORM_NO_MAX);
+
+ case FORM_PROP_GAMEINPUT:
+ return basValBool(win && win->gameInput);
+
+ case FORM_PROP_FULLSCREEN:
+ return basValBool(win && rt->ctx->exclusive.owner == win);
+
case FORM_PROP_COUNT:
break;
}
@@ -6736,30 +7268,45 @@ static BasValueT getIfaceProp(const WgtIfaceT *iface, WidgetT *w, const char *pr
// The help compiler's errors and warnings are kept for
-// HelpCompileMessages$ (the last compile's only).
+// HelpCompileMessages$ (the last compile's only). .image names are
+// looked for beside the source. The compile lets the program's events
+// and DVX go on (helpCompileTick), so another HelpCompile (this program's
+// or another's) meanwhile is refused at once, the messages left alone.
int32_t HelpCompile(const char *inputFile, const char *outputFile) {
- if (!sHlpcResolved) {
- sHlpcResolved = true;
- sHlpcCompile = dlsym(NULL, DVX_SYM("hlpcCompile"));
- sHlpcSetMessage = dlsym(NULL, DVX_SYM("hlpcSetMessageFn"));
+ if (sHelpCompiling) {
+ return 0;
}
// cppcheck-suppress unsignedLessThanZero ; stb_ds arrsetlen(a, 0) compares the capacity with 0
arrsetlen(sHelpMessages, 0);
- if (!sHlpcCompile || !sHlpcSetMessage || !inputFile || !outputFile) {
+ HlpcModuleT hlpc;
+
+ if (!inputFile || !outputFile || !hlpcModuleOpen(&hlpc)) {
helpCompileMessage(NULL, HELP_COMPILER_MISSING);
return 0;
}
const char *inputs[1];
+ char imgDir[DVX_MAX_PATH];
+
inputs[0] = inputFile;
- sHlpcSetMessage(helpCompileMessage, NULL);
+ if (!platformPathDirName(inputFile, imgDir, sizeof(imgDir))) {
+ snprintf(imgDir, sizeof(imgDir), ".");
+ }
- int32_t rc = sHlpcCompile(inputs, 1, outputFile, NULL, NULL, HLPC_QUIET, NULL, NULL);
+ sHelpCompiling = true;
+ sHelpPumpAt = platformClock();
+ hlpc.setMessage(helpCompileMessage, NULL);
+ hlpc.setTick(helpCompileTick, curRt());
- sHlpcSetMessage(NULL, NULL);
+ int32_t rc = hlpc.compile(inputs, 1, outputFile, imgDir, NULL, HLPC_QUIET, NULL, NULL);
+
+ hlpc.setTick(NULL, NULL);
+ hlpc.setMessage(NULL, NULL);
+ hlpcModuleClose(&hlpc);
+ sHelpCompiling = false;
return (rc == 0) ? -1 : 0;
}
@@ -6784,6 +7331,24 @@ const char *HelpCompileMessages(void) {
}
+// The help compiler's tick (hlpcSetTickFn): every HELP_PUMP_MS the
+// program's events and DVX go on, as DoEvents does (not in a control
+// file's code, which must not wait); false (stop the compile) once the
+// program is ending.
+static bool helpCompileTick(void *ctx) {
+ BasFormRtT *rt = (BasFormRtT *)ctx;
+
+ if (!rt || rt->vm->runLimit > 0 || platformClock() - sHelpPumpAt < PLATFORM_MS_TO_TICKS(HELP_PUMP_MS)) {
+ return !rt || !rt->terminated;
+ }
+
+ bool goOn = rtPump(rt);
+
+ sHelpPumpAt = platformClock();
+ return goOn;
+}
+
+
// Open hlpFile in the Help Viewer (HelpViewTopic with no topic).
void HelpView(const char *hlpFile) {
helpViewOpen(hlpFile, "");
@@ -6800,7 +7365,9 @@ static void helpViewOpen(const char *hlpFile, const char *topicId) {
sShellOpenHelp = dlsym(NULL, DVX_SYM("shellOpenHelp"));
}
- if (!sShellOpenHelp || !sFormRt || !sFormRt->ctx || !hlpFile || !topicId) {
+ BasFormRtT *rt = curRt();
+
+ if (!sShellOpenHelp || !rt || !rt->ctx || !hlpFile || !topicId) {
return;
}
@@ -6810,8 +7377,8 @@ static void helpViewOpen(const char *hlpFile, const char *topicId) {
if (!hlpFile[0]) {
snprintf(resolved, sizeof(resolved), "%s", DVX_SYSTEM_HELP_FILE_LITERAL);
- } else if (!hasDir && sFormRt->vm && sFormRt->vm->appPath[0]) {
- snprintf(resolved, sizeof(resolved), "%s" DVX_PATH_SEP "%s", sFormRt->vm->appPath, hlpFile);
+ } else if (!hasDir && rt->vm && rt->vm->appPath[0]) {
+ snprintf(resolved, sizeof(resolved), "%s" DVX_PATH_SEP "%s", rt->vm->appPath, hlpFile);
} else {
snprintf(resolved, sizeof(resolved), "%s", hlpFile);
}
@@ -6819,7 +7386,7 @@ static void helpViewOpen(const char *hlpFile, const char *topicId) {
const char *quote = strchr(resolved, ' ') ? "\"" : "";
snprintf(args, sizeof(args), "%s%s%s%s%s", quote, resolved, quote, topicId[0] ? " " : "", topicId);
- sShellOpenHelp(sFormRt->ctx, args);
+ sShellOpenHelp(rt->ctx, args);
}
@@ -7181,6 +7748,8 @@ static void onFormActivate(WindowT *win) {
// basFormRtUnloadForm so the close box takes the exact same
// QueryUnload/Unload/teardown path (including the `unloading`
// re-entrancy guard and deferred frees) as the UNLOAD statement.
+// A form's window closed: on another task (the shell's frame took the
+// close) the program's own task unloads it at its turn.
static void onFormClose(WindowT *win) {
BasFormRtT *rt;
BasFormT *form = formOfWindow(win, &rt);
@@ -7189,14 +7758,12 @@ static void onFormClose(WindowT *win) {
return;
}
- basFormRtUnloadForm(rt, form);
-
- // If no forms are left, stop the VM. A QueryUnload cancel leaves the
- // form in rt->forms, so this stays false then; a committed unload is
- // detached (removed) even when its frees are deferred.
- if (arrlen(rt->forms) == 0 && rt->vm) {
- rt->vm->running = false;
+ if (queueForeign(rt, NULL)) {
+ queueEvent(rt, form, NULL, form->name, "", NULL, 0, BasQueuedCloseE);
+ return;
}
+
+ formClose(rt, form);
}
@@ -7652,17 +8219,17 @@ static int32_t parseFileFilters(const char *filter, FileFilterT **outFilters, ch
}
-// Name of the form's popup menu that owns this MenuT, or "" (also for
-// NULL, i.e. no context menu set).
-// A new Picture object of width x height pixels for file path, kept in
-// rt->pictures for the runtime's life. NULL when memory runs out.
+// A new Picture object of width x height pixels for file path (no bytes),
+// kept in rt->pictures for the runtime's life. NULL when memory runs out.
static BasPictureT *pictureAdd(BasFormRtT *rt, const char *path, int32_t width, int32_t height) {
size_t pathSize = strlen(path) + 1;
BasPictureT *pic = (BasPictureT *)malloc(sizeof(BasPictureT) + pathSize);
if (pic) {
- pic->width = width;
- pic->height = height;
+ pic->width = width;
+ pic->height = height;
+ pic->data = NULL;
+ pic->dataLen = 0;
memcpy(pic->path, path, pathSize);
arrput(rt->pictures, pic);
}
@@ -7671,6 +8238,34 @@ static BasPictureT *pictureAdd(BasFormRtT *rt, const char *path, int32_t width,
}
+// The LoadPictureData picture whose name (pictureDataName) path is, else
+// NULL.
+static BasPictureT *pictureByDataName(const BasFormRtT *rt, const char *path) {
+ char *end = NULL;
+ long index = (basPictureIsData(path) && isdigit((unsigned char)path[1])) ? strtol(path + 1, &end, BAS_PICTURE_NAME_RADIX) : -1;
+
+ if (index < 0 || *end != '\0' || index >= (long)arrlen(rt->pictures)) {
+ return NULL;
+ }
+
+ return rt->pictures[index]->data ? rt->pictures[index] : NULL;
+}
+
+
+// The name a property showing the LoadPictureData picture pic holds:
+// BAS_PICTURE_DATA_MARK and its place in rt->pictures (no file is named
+// so).
+static void pictureDataName(const BasFormRtT *rt, const BasPictureT *pic, char *out, int32_t outSize) {
+ int32_t index = 0;
+
+ while (index < (int32_t)arrlen(rt->pictures) && rt->pictures[index] != pic) {
+ index++;
+ }
+
+ snprintf(out, (size_t)outSize, "%c%" PRId32, BAS_PICTURE_DATA_MARK, index);
+}
+
+
// The bytes of a picture file, or of the icon a DVX program, widget or
// library file carries (dvxResLoadIconData), for dvxLoadImageAlpha. NULL
// when the file cannot be read. Free with free().
@@ -7703,6 +8298,12 @@ static BasPictureT *pictureFind(const BasFormRtT *rt, const void *ref) {
// one LoadPicture made for it, else a new one of the picture's size (0 x 0
// when the file cannot be read any more). NULL when memory runs out.
static BasPictureT *pictureFor(BasFormRtT *rt, const char *path) {
+ BasPictureT *named = pictureByDataName(rt, path);
+
+ if (named) {
+ return named;
+ }
+
int32_t err = 0;
BasPictureT *pic = (BasPictureT *)basFormRtLoadPicture(rt, path, &err);
@@ -7714,8 +8315,8 @@ static BasPictureT *pictureFor(BasFormRtT *rt, const char *path) {
// HiMetric units (0.01 mm, through the ScaleMode conversions: VB's 15
// twips a pixel, so a pixel is about 26.46), and Type (vbPicTypeBitmap,
// vbPicTypeNone for the empty picture); its default value (no name) is
-// its file name. Handle is not offered: DVX pictures have no system
-// handle.
+// its file name ("" for one from bytes). Handle is not offered: DVX
+// pictures have no system handle.
static BasValueT pictureProp(BasFormRtT *rt, const BasPictureT *pic, const char *propName) {
if (strcmp(propName, BAS_PIC_PROP_DEFAULT) == 0) {
return basValStringFromC(pic->path);
@@ -7730,7 +8331,7 @@ static BasValueT pictureProp(BasFormRtT *rt, const BasPictureT *pic, const char
}
if (strcasecmp(propName, BAS_PIC_PROP_TYPE) == 0) {
- return basValInteger(pic->path[0] ? BAS_PIC_TYPE_BITMAP : BAS_PIC_TYPE_NONE);
+ return basValInteger((pic->path[0] || pic->data) ? BAS_PIC_TYPE_BITMAP : BAS_PIC_TYPE_NONE);
}
basFormRtRuntimeError(rt,
@@ -7741,6 +8342,76 @@ static BasValueT pictureProp(BasFormRtT *rt, const BasPictureT *pic, const char
}
+// Show the LoadPictureData picture pic as ctrl's propName (one
+// pictureTakesData allows), the property holding the picture's name
+// (pictureDataName): a form's Icon, DragIcon (the drag shows it), or the
+// Picture of a control that takes picture bytes
+// (WGT_METHOD_SET_PICTURE_DATA). A runtime error is raised when it
+// cannot be shown.
+static void pictureShowData(BasFormRtT *rt, BasControlT *ctrl, const char *propName, const BasPictureT *pic) {
+ char name[BAS_PICTURE_NAME_MAX];
+
+ pictureDataName(rt, pic, name, (int32_t)sizeof(name));
+
+ if (strcasecmp(propName, BAS_PROP_DRAGICON) == 0) {
+ free(ctrl->dragIcon);
+ ctrl->dragIcon = strdup(name);
+ return;
+ }
+
+ if (ctrl->form && ctrl == &ctrl->form->formCtrl) {
+ BasFormT *frm = ctrl->form;
+ int32_t w = 0;
+ int32_t h = 0;
+ int32_t pitch = 0;
+ uint8_t *pixels = frm->window ? dvxLoadImageFromMemory(rt->ctx, pic->data, pic->dataLen, &w, &h, &pitch) : NULL;
+
+ if (frm->window && !pixels) {
+ basFormRtRuntimeError(rt, "Icon not loaded", "Form: %s\nThere is not enough memory for the picture.", frm->name);
+ return;
+ }
+
+ snprintf(frm->iconPath, sizeof(frm->iconPath), "%s", name);
+
+ if (frm->window) {
+ wmSetIconData(frm->window, pixels, w, h, pitch);
+ }
+
+ return;
+ }
+
+ const WgtMethodDescT *m = ctrl->iface ? ifaceFindMethod(ctrl->iface, WGT_METHOD_SET_PICTURE_DATA, WGT_SIG_DYNAMIC) : NULL;
+ WgtValueT args[WGT_PICTURE_DATA_ARGS_MAX];
+ WgtValueT ret;
+
+ if (!m) {
+ basFormRtRuntimeError(rt,
+ "Picture from LoadPictureData not shown",
+ "Control: %s\nType: %s\nOnly an Image, an ImageButton, or a PictureBox shows a picture made from bytes.",
+ ctrlDisplayName(ctrl), ctrl->typeName);
+ return;
+ }
+
+ args[0] = wgtValueBytes((const char *)pic->data, pic->dataLen);
+ args[1] = wgtValueStr(name);
+
+ if (!wgtIfaceCallMethod(ctrl->iface, ctrl->widget, m, args, WGT_PICTURE_DATA_ARGS_MAX, &ret) || !wgtValueToBool(&ret)) {
+ basFormRtRuntimeError(rt, "Picture not loaded", "Control: %s\nThere is not enough memory for the picture.", ctrlDisplayName(ctrl));
+ }
+}
+
+
+// Whether propName of ctrl takes a LoadPictureData picture: a form's
+// Icon, a control's Picture or DragIcon.
+static bool pictureTakesData(const BasControlT *ctrl, const char *propName) {
+ if (ctrl->form && ctrl == &ctrl->form->formCtrl) {
+ return strcasecmp(propName, BAS_PROP_ICON) == 0;
+ }
+
+ return strcasecmp(propName, BAS_PROP_PICTURE) == 0 || strcasecmp(propName, BAS_PROP_DRAGICON) == 0;
+}
+
+
// The DVX pointer (CURSOR_*) a MousePointer value shows; false for a
// value DVX has no pointer for.
static bool pointerCursor(int32_t pointer, int32_t *outCursor) {
@@ -7772,6 +8443,8 @@ static void popupMenuAdd(BasFormT *form, const char *name, MenuT *menu, bool own
}
+// Name of the form's popup menu that owns this MenuT, or "" (also for
+// NULL, i.e. no context menu set).
static const char *popupMenuNameFor(const BasFormT *form, const MenuT *menu) {
if (menu && form) {
for (int32_t i = 0; i < form->popupMenuCount; i++) {
@@ -7847,11 +8520,12 @@ int32_t PrintFormDialog(const char *formName) {
static int32_t printFormWindow(const char *formName, bool askFirst) {
- BasFormT *form = NULL;
+ BasFormRtT *rt = curRt();
+ BasFormT *form = NULL;
- for (int32_t i = 0; sFormRt && formName && i < (int32_t)arrlen(sFormRt->forms); i++) {
- if (strcasecmp(sFormRt->forms[i]->name, formName) == 0) {
- form = sFormRt->forms[i];
+ for (int32_t i = 0; rt && formName && i < (int32_t)arrlen(rt->forms); i++) {
+ if (strcasecmp(rt->forms[i]->name, formName) == 0) {
+ form = rt->forms[i];
break;
}
}
@@ -7860,7 +8534,171 @@ static int32_t printFormWindow(const char *formName, bool askFirst) {
return 0;
}
- return sPrintApi.window(sFormRt->ctx, form->window, askFirst) ? -1 : 0;
+ return sPrintApi.window(rt->ctx, form->window, askFirst) ? -1 : 0;
+}
+
+
+// A waiting event's copied arguments go.
+static void queueArgsFree(BasQueuedEventT *ev) {
+ for (int32_t i = 0; i < ev->argCount; i++) {
+ basValRelease(&ev->args[i]);
+ }
+
+ free(ev->args);
+ ev->args = NULL;
+ ev->argCount = 0;
+}
+
+
+// The events that waited for the program's own task run, oldest first
+// (a DoEvents in one of their handlers runs the next, as VB's does). Each
+// runs in its own eventDepth bracket, so the queue's goes once the last
+// is taken. A form or object gone meanwhile has its events dropped.
+static void queueDrain(BasFormRtT *rt) {
+ while (arrlen(rt->queued) > 0 && !rt->terminated && !queueForeign(rt, NULL)) {
+ BasQueuedEventT ev = rt->queued[0];
+
+ arrdel(rt->queued, 0);
+ sQueuedTotal--;
+ rtEventEnter(rt);
+
+ if (arrlen(rt->queued) == 0 && rt->queueHeld) {
+ rt->queueHeld = false;
+ rtEventLeave(rt);
+ }
+
+ // Booked as the program's handler (a Task Manager's "Not
+ // responding" counts from here, as for a window callback)
+ int32_t appId = rt->ctx->currentAppId;
+
+ dvxAppCallEnter(appId);
+
+ if (queueLive(rt, &ev)) {
+ switch (ev.kind) {
+ case BasQueuedCtrlEventE:
+ fireCtrlEventOut(rt, ev.obj, ev.eventName, ev.args, ev.argCount, NULL, 0);
+ break;
+
+ case BasQueuedCloseE:
+ formClose(rt, ev.form);
+ break;
+
+ case BasQueuedEventE:
+ fireEventArgsOut(rt, ev.form, ev.obj, ev.ctrlName, ev.eventName, ev.args, ev.argCount, NULL, 0);
+ break;
+ }
+ }
+
+ dvxAppCallLeave(appId);
+ queueArgsFree(&ev);
+ rtEventLeave(rt);
+ }
+}
+
+
+// Keep an event for the program's own task, its arguments copied; a newer
+// state (sQueueLatest) replaces one of the same object still waiting. The
+// first to wait takes the queue's eventDepth bracket. True (it will run).
+static bool queueEvent(BasFormRtT *rt, BasFormT *form, BasControlT *obj, const char *ctrlName, const char *eventName, const BasValueT *args, int32_t argCount, BasQueuedKindE kind) {
+ BasQueuedEventT *slot = NULL;
+
+ for (int32_t i = 0; !slot && i < DVX_ARRAY_LEN(sQueueLatest); i++) {
+ for (int32_t j = (int32_t)arrlen(rt->queued) - 1; j >= 0 && strcasecmp(eventName, sQueueLatest[i]) == 0; j--) {
+ BasQueuedEventT *ev = &rt->queued[j];
+
+ if (ev->form == form && ev->obj == obj && strcasecmp(ev->eventName, eventName) == 0 && strcasecmp(ev->ctrlName, ctrlName) == 0) {
+ slot = ev;
+ break;
+ }
+ }
+ }
+
+ if (slot) {
+ queueArgsFree(slot);
+ } else {
+ BasQueuedEventT ev;
+
+ memset(&ev, 0, sizeof(ev));
+ ev.form = form;
+ ev.obj = obj;
+ ev.kind = kind;
+ snprintf(ev.ctrlName, sizeof(ev.ctrlName), "%s", ctrlName);
+ snprintf(ev.eventName, sizeof(ev.eventName), "%s", eventName);
+ arrput(rt->queued, ev);
+ slot = &arrlast(rt->queued);
+ sQueuedTotal++;
+
+ if (!rt->queueHeld) {
+ rt->queueHeld = true;
+ rtEventEnter(rt);
+ }
+ }
+
+ slot->args = (argCount > 0) ? (BasValueT *)malloc(sizeof(BasValueT) * argCount) : NULL;
+
+ for (int32_t i = 0; slot->args && i < argCount; i++) {
+ slot->args[i] = basValCopy(args[i]);
+ }
+
+ slot->argCount = slot->args ? argCount : 0;
+ return true;
+}
+
+
+// True when this runs on a task other than the program's own, so the
+// event (eventName; NULL: any) must wait for the program's turn: not one
+// of a form's life (sQueueNever).
+static bool queueForeign(const BasFormRtT *rt, const char *eventName) {
+ for (int32_t i = 0; eventName && i < DVX_ARRAY_LEN(sQueueNever); i++) {
+ if (strcasecmp(eventName, sQueueNever[i]) == 0) {
+ return false;
+ }
+ }
+
+ return rt->ownTask != BAS_RT_ANY_TASK && basFormRtTaskId() != rt->ownTask;
+}
+
+
+// The runtime ends: its waiting events go unrun.
+static void queueFree(BasFormRtT *rt) {
+ for (int32_t i = 0; i < (int32_t)arrlen(rt->queued); i++) {
+ queueArgsFree(&rt->queued[i]);
+ }
+
+ sQueuedTotal -= (int32_t)arrlen(rt->queued);
+ arrfree(rt->queued);
+ rt->queued = NULL;
+ rt->queueHeld = false;
+}
+
+
+// A waiting event's form is still the program's and not unloading, and
+// its object (if any) still one of the program's. A Timer stopped
+// meanwhile fires no more (as in VB): its waiting tick goes too.
+static bool queueLive(BasFormRtT *rt, const BasQueuedEventT *ev) {
+ BasFormT *form = resolveFormRef(rt, ev->form);
+
+ if (!form || form != ev->form || form->detached) {
+ return false;
+ }
+
+ if (!ev->obj) {
+ return true;
+ }
+
+ if (resolveCtrlRef(rt, ev->obj) != ev->obj) {
+ return false;
+ }
+
+ bool handled = false;
+
+ if (strcasecmp(ev->eventName, BAS_EVT_TIMER) == 0 && ev->obj->iface && ev->obj->widget) {
+ BasValueT on = getIfaceProp(ev->obj->iface, ev->obj->widget, BAS_PROP_ENABLED, &handled);
+
+ return !handled || basValIsTruthy(on);
+ }
+
+ return true;
}
@@ -7952,119 +8790,6 @@ static void removeCtrlTree(BasFormRtT *rt, BasFormT *form, BasControlT *ctrl) {
}
-// ResAddData: a resource of any bytes (BINARY STRING); True on success.
-int32_t ResAddData(const char *path, const char *name, int32_t type, const uint8_t *data, int32_t len) {
- if (!path || !name || !data || len < 0) {
- return 0;
- }
-
- return dvxResAppend(path, name, (uint32_t)type, data, (uint32_t)len) == 0 ? -1 : 0;
-}
-
-
-int32_t ResAddFile(const char *path, const char *name, int32_t type, const char *srcFile) {
- if (!srcFile) {
- return 0;
- }
-
- int32_t fileSize = 0;
- uint8_t *buf = (uint8_t *)platformReadFile(srcFile, &fileSize);
- int32_t result = (buf && fileSize > 0) ? ResAddData(path, name, type, buf, fileSize) : 0;
-
- free(buf);
- return result;
-}
-
-
-int32_t ResAddText(const char *path, const char *name, const char *text) {
- if (!text) {
- text = "";
- }
-
- return ResAddData(path, name, DVX_RES_TEXT, (const uint8_t *)text, (int32_t)strlen(text) + 1);
-}
-
-
-void ResClose(int32_t handle) {
- int32_t idx = handle - 1;
-
- if (idx < 0 || idx >= RES_MAX_HANDLES || !sResHandles[idx]) {
- return;
- }
-
- dvxResClose(sResHandles[idx]);
- sResHandles[idx] = NULL;
-}
-
-
-int32_t ResCount(int32_t handle) {
- int32_t idx = handle - 1;
-
- if (idx >= 0 && idx < RES_MAX_HANDLES && sResHandles[idx]) {
- return (int32_t)sResHandles[idx]->entryCount;
- }
-
- return 0;
-}
-
-
-int32_t ResExtract(const char *path, const char *name, const char *outFile) {
- if (!outFile) {
- return 0;
- }
-
- uint32_t size = 0;
- void *data = resReadNamed(path, name, &size);
-
- if (!data) {
- return 0;
- }
-
- FILE *f = fopen(outFile, "wb");
-
- if (!f) {
- free(data);
- return 0;
- }
-
- int32_t result = (fwrite(data, 1, size, f) == size) ? -1 : 0;
- fclose(f);
- free(data);
- return result;
-}
-
-
-// ResGetData$: every byte of a resource (BINARY STRING); "" when it is
-// not there.
-const uint8_t *ResGetData(const char *path, const char *name, int32_t *outLen) {
- uint32_t size = 0;
-
- free(sResData);
- sResData = (uint8_t *)resReadNamed(path, name, &size);
- *outLen = sResData ? (int32_t)size : 0;
- return sResData;
-}
-
-
-const char *ResGetText(const char *path, const char *name) {
- static char sBuf[BAS_RES_TEXT_LEN];
- uint32_t size = 0;
- void *data = resReadNamed(path, name, &size);
-
- sBuf[0] = '\0';
-
- if (data) {
- uint32_t copyLen = DVX_MIN(size, (uint32_t)sizeof(sBuf) - 1);
-
- memcpy(sBuf, data, copyLen);
- sBuf[copyLen] = '\0';
- free(data);
- }
-
- return sBuf;
-}
-
-
// Widgets with a pixel buffer (Canvas, Image, PictureBox) expose
// Resize(w, h) so the buffer can follow the layout size once Width and
// Height are known. Runs after .frm parsing and on every runtime
@@ -8092,18 +8817,6 @@ static void resizeIfaceBuffer(BasControlT *ctrl) {
}
-const char *ResName(int32_t handle, int32_t index) {
- int32_t idx = handle - 1;
-
- if (idx >= 0 && idx < RES_MAX_HANDLES && sResHandles[idx] &&
- index >= 0 && index < (int32_t)sResHandles[idx]->entryCount) {
- return sResHandles[idx]->entries[index].name;
- }
-
- return "";
-}
-
-
// A control given as an argument: the control (an object) or its name on
// form; NULL when it is neither.
static BasControlT *resolveCtrlArg(BasFormRtT *rt, BasFormT *form, BasValueT arg) {
@@ -8258,82 +8971,6 @@ static bool resolveShellIdle(void) {
}
-int32_t ResOpen(const char *path) {
- if (!path) {
- return 0;
- }
-
- for (int32_t i = 0; i < RES_MAX_HANDLES; i++) {
- if (!sResHandles[i]) {
- sResHandles[i] = dvxResOpen(path);
-
- if (sResHandles[i]) {
- return i + 1;
- }
-
- return 0;
- }
- }
-
- return 0;
-}
-
-
-// A named resource of the file at path, read in one go (malloc'd, the
-// caller frees); NULL when the file or the resource is not there.
-static void *resReadNamed(const char *path, const char *name, uint32_t *outSize) {
- *outSize = 0;
-
- if (!path || !name) {
- return NULL;
- }
-
- DvxResHandleT *h = dvxResOpen(path);
-
- if (!h) {
- return NULL;
- }
-
- void *data = dvxResRead(h, name, outSize);
-
- dvxResClose(h);
- return data;
-}
-
-
-int32_t ResRemove(const char *path, const char *name) {
- if (!path || !name) {
- return 0;
- }
-
- return dvxResRemove(path, name) == 0 ? -1 : 0;
-}
-
-
-int32_t ResSize(int32_t handle, int32_t index) {
- int32_t idx = handle - 1;
-
- if (idx >= 0 && idx < RES_MAX_HANDLES && sResHandles[idx] &&
- index >= 0 && index < (int32_t)sResHandles[idx]->entryCount) {
- return (int32_t)sResHandles[idx]->entries[index].size;
- }
-
- return 0;
-}
-
-
-int32_t ResType(int32_t handle, int32_t index) {
- int32_t idx = handle - 1;
-
- if (idx >= 0 && idx < RES_MAX_HANDLES && sResHandles[idx] &&
- index >= 0 && index < (int32_t)sResHandles[idx]->entryCount) {
- return (int32_t)sResHandles[idx]->entries[index].type;
- }
-
- return 0;
-}
-
-
// The single VM-entry wrapper for firing BASIC handler code. Owns, in
// order: the prevForm/prevVars/prevVarCount save, the owning-form vars
// binding, the rtEventEnter/rtEventLeave pairing, and the restore --
@@ -8482,6 +9119,13 @@ static const BasProcEntryT *rtFindProc(BasFormRtT *rt, const char *name) {
}
+// One pump for code that waits in a program (DoEvents' own): DVX and the
+// program's waiting events run. False once the program is ending.
+static bool rtPump(BasFormRtT *rt) {
+ return basFormRtUpdate(rt) && !rt->terminated;
+}
+
+
// Runs the VM in step slices, giving DVX a turn between them, until the
// program halts or fails (an error is shown). Returns the last result.
static BasVmResultE runSliced(BasFormRtT *rt) {
@@ -8491,16 +9135,15 @@ static BasVmResultE runSliced(BasFormRtT *rt) {
do {
// This program's code runs (see rtCallHandler); the dvxUpdate
// between slices may run other programs' handlers.
- BasFormRtT *prevRt = sFormRt;
+ BasFormRtT *prevRt = curRtSet(rt);
- sFormRt = rt;
sCodeDepth++;
- result = basVmRun(vm);
+ result = basVmRun(vm);
sCodeDepth--;
- sFormRt = prevRt;
+ curRtSet(prevRt);
if (result == BAS_VM_STEP_LIMIT) {
- if (!dvxUpdate(rt->ctx)) {
+ if (!basFormRtUpdate(rt)) {
break;
}
} else if (result != BAS_VM_HALTED && result != BAS_VM_OK) {
@@ -8704,7 +9347,7 @@ static void screenSetProp(BasFormRtT *rt, const char *propName, BasValueT value)
void SerAttach(int32_t com, const char *termCtrlName) {
- if (!serResolveApi() || !sFormRt || !termCtrlName) {
+ if (!serResolveApi() || !curRt() || !termCtrlName) {
return;
}
@@ -8714,8 +9357,8 @@ void SerAttach(int32_t com, const char *termCtrlName) {
return;
}
- // Find the terminal widget by control name
- WidgetT *termWidget = findCtrlWidgetByName(termCtrlName);
+ // The Terminal control of that name on any loaded form
+ WidgetT *termWidget = findTermWidget(termCtrlName);
if (!termWidget) {
return;
@@ -8732,6 +9375,7 @@ void SerAttach(int32_t com, const char *termCtrlName) {
sSerAttach[idx].com = idx;
sSerAttach[idx].term = termWidget;
sSerAttach[idx].attached = true;
+ sSerAttach[idx].owner = curRt();
wgtAnsiTermSetComm(termWidget, &sSerAttach[idx], serTermRead, serTermWrite);
@@ -8946,7 +9590,7 @@ int32_t SerLineErrors(int32_t com) {
// 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) {
+static int32_t serLineGet(SerLineGetFnT *const *get, int32_t com) {
if (!serResolveApi() || !*get) {
return 0;
}
@@ -8957,7 +9601,7 @@ static int32_t serLineGet(int (*const *get)(int), int32_t com) {
// 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) {
+static void serLineSet(SerLineSetFnT *const *set, int32_t com, int32_t on) {
if (!serResolveApi() || !*set) {
return;
}
@@ -8994,6 +9638,7 @@ int32_t SerOpen(int32_t com, int32_t baud, int32_t dataBits, const char *parity,
// attached to a terminal.
if (idx >= 0 && idx < RS232_NUM_PORTS) {
sSerAttach[idx].opened = true;
+ sSerAttach[idx].owner = curRt();
}
return SER_BASIC_TRUE;
@@ -9023,7 +9668,7 @@ static bool serResolveApi(void) {
return sSerApi.open != NULL;
}
- sSerApiResolved = true;
+ sSerApiResolved = true;
sSerApi.open = dlsym(NULL, DVX_SYM("rs232Open"));
sSerApi.close = dlsym(NULL, DVX_SYM("rs232Close"));
sSerApi.read = dlsym(NULL, DVX_SYM("rs232Read"));
@@ -9126,8 +9771,9 @@ static int32_t serTermWrite(void *ctx, const uint8_t *data, int32_t len) {
return 0;
}
+ // What the port's buffer took (the terminal sends the rest later)
int32_t rc = sSerApi.writeBuf(sa->com, (const char *)data, len);
- return (rc >= 0) ? len : 0;
+ return (rc >= 0) ? rc : 0;
}
@@ -9191,7 +9837,7 @@ static bool setCommonProp(BasControlT *ctrl, const char *propName, BasValueT val
resizeIfaceBuffer(ctrl);
if (w->minW != old) {
- firePaintResized(sFormRt, ctrl);
+ firePaintResized(curRt(), ctrl);
}
break;
@@ -9206,7 +9852,7 @@ static bool setCommonProp(BasControlT *ctrl, const char *propName, BasValueT val
resizeIfaceBuffer(ctrl);
if (w->minH != old) {
- firePaintResized(sFormRt, ctrl);
+ firePaintResized(curRt(), ctrl);
}
break;
@@ -9256,7 +9902,7 @@ static bool setCommonProp(BasControlT *ctrl, const char *propName, BasValueT val
case CTRL_PROP_FONTUNDERLINE:
case CTRL_PROP_FONTSTRIKETHRU:
if (!fontPropSet(w, pd->name, value)) {
- basFormRtRuntimeError(sFormRt, "Out of memory", "Control: %s\nProperty: %s\nThere is not enough memory for the font.", ctrlDisplayName(ctrl), pd->name);
+ basFormRtRuntimeError(curRt(), "Out of memory", "Control: %s\nProperty: %s\nThere is not enough memory for the font.", ctrlDisplayName(ctrl), pd->name);
}
break;
@@ -9311,12 +9957,12 @@ static bool setCommonProp(BasControlT *ctrl, const char *propName, BasValueT val
break;
case CTRL_PROP_MOUSEPOINTER:
- setMousePointer(sFormRt, &ctrl->mousePointer, value);
+ setMousePointer(curRt(), &ctrl->mousePointer, value);
break;
case CTRL_PROP_CONTAINER:
// A failed move has reported its own error
- basFormRtMoveControl(sFormRt, ctrl, (value.type == BAS_TYPE_OBJECT) ? value.objVal : NULL, NULL);
+ basFormRtMoveControl(curRt(), ctrl, (value.type == BAS_TYPE_OBJECT) ? value.objVal : NULL, NULL);
break;
case CTRL_PROP_NAME:
@@ -9339,6 +9985,26 @@ static void setControlString(char *dst, int32_t dstSize, BasValueT value) {
}
+// FullScreen = on: the form's window takes the whole screen, showing its
+// own content (dvxExclusiveMirror; a hidden form is shown first), or
+// gives it back. When there is no video mode for it the form stays in
+// its window (FullScreen reads False).
+static void setFormFullScreen(BasFormRtT *rt, BasFormT *frm, bool on) {
+ if (!on) {
+ dvxExclusiveEnd(rt->ctx, frm->window);
+ return;
+ }
+
+ if (!frm->window->visible) {
+ basFormRtShowForm(rt, frm, false);
+ }
+
+ int32_t rc = dvxExclusiveMirror(rt->ctx, frm->window, formFullChanged, rt);
+
+ dvxLog("basrt: form %s full screen %s", frm->name, rc == DVX_EXCL_OK ? "on" : "unavailable");
+}
+
+
// Form.Icon = file$ or LoadPicture(file$): the picture of the minimized
// window ("" or LoadPicture() puts DVX's back).
static void setFormIcon(BasFormRtT *rt, BasFormT *frm, BasValueT value) {
@@ -9364,6 +10030,24 @@ static void setFormIcon(BasFormRtT *rt, BasFormT *frm, BasValueT value) {
}
+// MaxWidth (across) or MaxHeight = limit: how far the form's window can
+// be resized or maximized (FORM_NO_MAX or less: the screen's size); a
+// window larger than the new limit shrinks to it.
+static void setFormMax(BasFormRtT *rt, WindowT *win, bool across, int32_t limit) {
+ int32_t max = (limit > FORM_NO_MAX) ? limit : WM_MAX_FROM_SCREEN;
+
+ if (across) {
+ win->maxW = max;
+ } else {
+ win->maxH = max;
+ }
+
+ if (limit > FORM_NO_MAX && (across ? win->w : win->h) > limit) {
+ dvxResizeWindow(rt->ctx, win, across ? limit : win->w, across ? win->h : limit);
+ }
+}
+
+
// FormName.Prop = value: window geometry/state plus the BasFormT flags.
static void setFormProp(BasFormRtT *rt, BasFormT *frm, const char *propName, BasValueT value) {
WindowT *win = frm->window;
@@ -9513,6 +10197,25 @@ static void setFormProp(BasFormRtT *rt, BasFormT *frm, const char *propName, Bas
setCommonProp(&frm->formCtrl, pd->name, value);
break;
+ case FORM_PROP_MAXWIDTH:
+ case FORM_PROP_MAXHEIGHT:
+ if (hasWin) {
+ setFormMax(rt, win, pd == &sFormProps[FORM_PROP_MAXWIDTH], num);
+ }
+ break;
+
+ case FORM_PROP_GAMEINPUT:
+ if (hasWin) {
+ win->gameInput = flag;
+ }
+ break;
+
+ case FORM_PROP_FULLSCREEN:
+ if (hasWin) {
+ setFormFullScreen(rt, frm, flag);
+ }
+ break;
+
case FORM_PROP_NAME:
case FORM_PROP_LAYOUT:
case FORM_PROP_CONTROLS:
@@ -9711,6 +10414,14 @@ static void setPropValue(BasFormRtT *rt, void *ctrlRef, const char *propName, Ba
return;
}
+ // A LoadPictureData picture for Picture, DragIcon, or a form's Icon
+ BasPictureT *bytesPic = pictureTakesData(ctrl, propName) ? basFormRtPictureData(rt, value) : NULL;
+
+ if (bytesPic) {
+ pictureShowData(rt, ctrl, propName, bytesPic);
+ return;
+ }
+
// Drawing properties of forms and PictureBoxes
if (basFormRtDrawSetProp(rt, ctrl, propName, value)) {
return;
@@ -9881,16 +10592,18 @@ int32_t SQLAffected(int32_t db) {
// The SQL callback table of the running VM, or an all-NULL table when
// there is none, so each SQL* wrapper checks just its own entry. A VM
-// whose host bound no SQL callbacks (a compiled program's stub; only the
-// IDE binds them) gets the dvxsql library's functions by name.
+// whose host bound no SQL callbacks (basVmSetSqlCallbacks: no host does,
+// only the tests) gets the dvxsql library's functions by name.
static const BasSqlCallbacksT *sqlApi(void) {
static const BasSqlCallbacksT sNoSql;
- if (!sFormRt || !sFormRt->vm) {
+ BasFormRtT *rt = curRt();
+
+ if (!rt || !rt->vm) {
return &sNoSql;
}
- BasSqlCallbacksT *sql = &sFormRt->vm->sql;
+ BasSqlCallbacksT *sql = &rt->vm->sql;
if (!sql->sqlOpen) {
sqlResolveApi(sql);
@@ -9934,6 +10647,25 @@ const char *SQLError(int32_t db) {
}
+// SQLEscape$(text$): text with every ' doubled (dvxSqlEscape), to put
+// between single quotes in a statement. The text stays valid until the
+// next call.
+const char *SQLEscape(const char *text) {
+ static char *out = NULL; // stb_ds array, reused
+
+ const BasSqlCallbacksT *api = sqlApi();
+ int32_t len = text ? (int32_t)strlen(text) : 0;
+
+ if (!api->sqlEscape) {
+ return "";
+ }
+
+ // At most every character doubled, and the NUL
+ arrsetlen(out, len * BAS_SQL_ESC_GROWTH + 1);
+ return api->sqlEscape(text ? text : "", out, (int32_t)arrlen(out)) < 0 ? "" : out;
+}
+
+
int32_t SQLExec(int32_t db, const char *sql) {
const BasSqlCallbacksT *api = sqlApi();
@@ -10036,6 +10768,7 @@ int32_t SQLNext(int32_t rs) {
}
+// A program's database pumps while a statement runs long (sqlPump).
int32_t SQLOpen(const char *path) {
const BasSqlCallbacksT *api = sqlApi();
@@ -10043,7 +10776,37 @@ int32_t SQLOpen(const char *path) {
return 0;
}
- return api->sqlOpen(path);
+ int32_t db = api->sqlOpen(path);
+
+ if (db > 0 && api->sqlSetPump) {
+ api->sqlSetPump(db, sqlPump);
+ }
+
+ return db;
+}
+
+
+// The pump of a program's databases (dvxSqlSetPump): a statement running
+// into a new BIOS tick (~55 ms) lets DVX and the program's events run, as
+// DoEvents does (not in a control file's code, which must not wait), and
+// is stopped when the program ends.
+static bool sqlPump(int32_t db) {
+ BasFormRtT *rt = curRt();
+
+ (void)db;
+
+ if (!rt) {
+ return true;
+ }
+
+ uint32_t now = *platformBiosTicks();
+
+ if (rt->terminated || now == sSqlPumpTick || rt->vm->runLimit > 0) {
+ return !rt->terminated;
+ }
+
+ sSqlPumpTick = now;
+ return rtPump(rt);
}
@@ -10075,6 +10838,8 @@ static void sqlResolveApi(BasSqlCallbacksT *sql) {
sql->sqlFieldDbl = dlsym(NULL, DVX_SYM("dvxSqlFieldDbl"));
sql->sqlFreeResult = dlsym(NULL, DVX_SYM("dvxSqlFreeResult"));
sql->sqlAffectedRows = dlsym(NULL, DVX_SYM("dvxSqlAffectedRows"));
+ sql->sqlSetPump = dlsym(NULL, DVX_SYM("dvxSqlSetPump"));
+ sql->sqlEscape = dlsym(NULL, DVX_SYM("dvxSqlEscape"));
}
@@ -10189,6 +10954,22 @@ static void vmErrorEnd(BasFormRtT *rt) {
}
+// The wait pump (dvxSetWaitPump): a library waiting in a program's code
+// (SoundPlayEvent with wait, say) pumps as DoEvents does; the wait stops
+// when the program ends, and a control file's code does not wait. Code
+// that is no program's (the shell's) only yields.
+static bool waitPump(void) {
+ BasFormRtT *rt = curRt();
+
+ if (!rt) {
+ platformYield();
+ return true;
+ }
+
+ return rt->vm->runLimit <= 0 && rtPump(rt);
+}
+
+
// A BASIC value as a widget value of type want, with the BASIC
// conversions (basValToNumber, basValIsTruthy, basValFormatString).
// WGT_IFACE_NONE keeps the value's own kind (a dynamic method's
@@ -10228,7 +11009,7 @@ static WgtValueT wgtFromBas(BasValueT v, uint8_t want, BasStringT **hold) {
switch (want) {
case WGT_IFACE_STRING:
*hold = basValFormatString(v);
- return wgtValueStr((*hold)->data);
+ return wgtValueBytes((*hold)->data, (*hold)->len);
case WGT_IFACE_BOOL:
return wgtValueBool(basValIsTruthy(v));
diff --git a/src/apps/kpunch/dvxbasic/formrt/formrt.h b/src/apps/kpunch/dvxbasic/formrt/formrt.h
index 0e4e221f..c51ed6a3 100644
--- a/src/apps/kpunch/dvxbasic/formrt/formrt.h
+++ b/src/apps/kpunch/dvxbasic/formrt/formrt.h
@@ -61,7 +61,7 @@ typedef struct BasScreenWinT BasScreenWinT; // the program screen's window (f
// (vm.h) with the compiler, so name-keyed lookups never truncate.
// Longest menu Shortcut / ShortcutText ("Ctrl+Alt+Shift+Backspace"), and
// what separates it from the caption in an item's label ("&Open\tCtrl+O").
-#define BAS_MENU_KEY_TEXT_MAX 32
+#define BAS_MENU_KEY_TEXT_MAX DVX_ACCEL_TEXT_MAX
#define BAS_MENU_KEY_SEP '\t'
// Window size for a form that gives no Width/Height; the IDE designer
@@ -115,6 +115,8 @@ typedef struct BasScreenWinT BasScreenWinT; // the program screen's window (f
#define BAS_PROP_FONTSTRIKETHRU "FontStrikethru"
#define BAS_PROP_FONTUNDERLINE "FontUnderline"
#define BAS_PROP_FORECOLOR "ForeColor"
+#define BAS_PROP_FULLSCREEN "FullScreen"
+#define BAS_PROP_GAMEINPUT "GameInput"
#define BAS_PROP_HEIGHT "Height"
#define BAS_PROP_HELPTOPIC "HelpTopic"
#define BAS_PROP_ICON BAS_FORM_ICON
@@ -189,6 +191,10 @@ const BasPropDescT *basFormRtFindCommonProp(const char *name);
const BasPropDescT *basFormRtFindFormProp(const char *name);
const BasPropDescT *basFormRtFindMenuProp(const char *name);
+// True for a form property that is run-time state, never part of a .frm
+// (Visible, DesignMode, FullScreen ...; the object-typed ones aside).
+bool basFormRtFormPropRuntimeOnly(const char *name);
+
// ============================================================
// Font properties
// ============================================================
@@ -409,6 +415,28 @@ typedef struct {
char handlerName[BAS_MAX_IDENT]; // e.g. "HandleOkClick"
} BasEventOverrideT;
+// A LoadPicture or LoadPictureData Picture object: the picture's size in
+// pixels (0 x 0 for the empty picture), and its file name ("" for the
+// empty picture) or, from LoadPictureData, the bytes of its picture file
+// (data, dataLen bytes and a NUL after them; NULL for a file's picture).
+// A property showing one from bytes holds its name instead of a file's:
+// BAS_PICTURE_DATA_MARK and its place in BasFormRtT.pictures.
+#define BAS_PICTURE_DATA_MARK '*'
+
+// True when a picture property's text names a LoadPictureData picture
+// rather than a file (no file name starts with the mark).
+static inline bool basPictureIsData(const char *path) {
+ return path[0] == BAS_PICTURE_DATA_MARK;
+}
+
+typedef struct {
+ int32_t width;
+ int32_t height;
+ uint8_t *data;
+ int32_t dataLen;
+ char path[];
+} BasPictureT;
+
typedef struct BasControlT {
char name[BAS_MAX_IDENT]; // VB control name (e.g. "Command1")
char typeName[BAS_MAX_IDENT]; // VB type name (e.g. "CommandButton")
@@ -524,13 +552,47 @@ typedef struct {
int32_t frmSourceLen;
} BasFrmCacheT;
-// A LoadPicture Picture object: the picture's size in pixels (0 x 0 for
-// the empty picture) and its file name ("" for the empty picture).
+// A picture PaintPicture decoded, kept for the next call (formGfx's
+// drawPictureLoad): its pixels in the display format then, and the file's
+// size and time when it was read -- or the LoadPictureData picture it is
+// (picture; path "").
typedef struct {
- int32_t width;
- int32_t height;
- char path[];
-} BasPictureT;
+ uint8_t *pixels;
+ const BasPictureT *picture;
+ int64_t size;
+ int64_t time;
+ int32_t w;
+ int32_t h;
+ int32_t pitch;
+ int32_t bpp;
+ char path[DVX_MAX_PATH];
+} BasDrawPicT;
+
+// The task of a runtime whose code runs on any task (BasFormRtT.ownTask):
+// an installed control file's, shared by every program.
+#define BAS_RT_ANY_TASK UINT32_MAX
+
+// What a waiting event is: an event, a control's event (it runs through
+// the control-event path: Index argument, re-entry guard), or a form's
+// window closed (the form is unloaded, QueryUnload asked first).
+typedef enum {
+ BasQueuedEventE,
+ BasQueuedCtrlEventE,
+ BasQueuedCloseE
+} BasQueuedKindE;
+
+// An event another task dispatched for a program, kept until the
+// program's own task runs it (BasFormRtT.queued): what was fired, the
+// arguments copied.
+typedef struct {
+ BasFormT *form;
+ BasControlT *obj; // the control, form object, or menu proxy (NULL: none)
+ BasValueT *args; // copies (NULL: none), released once it has run
+ int32_t argCount;
+ BasQueuedKindE kind;
+ char ctrlName[BAS_MAX_IDENT];
+ char eventName[BAS_MAX_IDENT];
+} BasQueuedEventT;
typedef struct {
AppContextT *ctx; // DVX app context
@@ -545,6 +607,9 @@ typedef struct {
// loop exits at the next pump so the app doesn't stumble forward
// with a halted VM.
bool terminated;
+ // basFormRtLoadAllForms is loading the program's forms (the other
+ // tasks run between them): it counts as busy (basFormRtBusy).
+ bool formsLoading;
// Set true by hosts that want to handle runtime-error reporting
// themselves (the IDE shows the error in the output pane and
// navigates the editor to the line). When true,
@@ -587,6 +652,9 @@ typedef struct {
GameFramePacerT framePacer;
int32_t frameHz;
uint64_t frameDueUs;
+ // SHELL: the program waiting for its DOS run, and how the run ended
+ int32_t shellAppId;
+ int32_t shellRc;
// The program screen's window (made by the first console or graphics
// statement) and its caption: hosts set title to the program's name.
BasScreenWinT *screenWin;
@@ -595,6 +663,9 @@ typedef struct {
// (stb_ds array of heap objects; the object is that pointer), kept
// until the runtime is destroyed.
BasPictureT **pictures;
+ // PaintPicture's decoded picture files (stb_ds), and their bytes
+ BasDrawPicT *drawPics;
+ int32_t drawPicsBytes;
// Screen.MousePointer = vbHourglass is in force: the busy cursor is
// put back to the arrow when the runtime is destroyed.
bool pointerBusy;
@@ -608,6 +679,18 @@ typedef struct {
int32_t screenPointer;
// The Printer object's pages and document (NULL until it is used).
BasPrinterT *printer;
+ // A program's handlers run on its own task (the one that made the
+ // runtime; BAS_RT_ANY_TASK: any). An event another task dispatched
+ // (the shell's frame takes the input), and a form's window closed
+ // there, wait in queued (stb_ds, oldest first) for the program's next
+ // turn (basFormRtUpdate) -- except an event whose handler answers
+ // through its arguments (Cancel) and Load / QueryUnload / Unload,
+ // which run where they are fired. While any wait, queueHeld holds one
+ // eventDepth bracket so the forms and controls they name are not
+ // freed.
+ uint32_t ownTask;
+ BasQueuedEventT *queued;
+ bool queueHeld;
} BasFormRtT;
// ============================================================
@@ -625,6 +708,38 @@ void basFormRtLoadAllForms(BasFormRtT *rt, const char *startupFormName);
// Called after bytecode halts to keep the app alive for event handlers.
void basFormRtEventLoop(BasFormRtT *rt);
+// One turn of a program's event loop on its own task: the events another
+// task dispatched for it run first (in order), then a DVX frame. Every
+// pump of a running program (the stub's DoEvents, the IDE's, the
+// runtime's own loops) is one. False when DVX is shutting down.
+bool basFormRtUpdate(BasFormRtT *rt);
+
+// The running task (the task switcher's, found at run time; 0 without
+// one).
+uint32_t basFormRtTaskId(void);
+
+// Let the other tasks run once (no event of this program runs): between
+// the steps of a long call of the program's (starting it, a compile).
+void basFormRtYield(void);
+
+// rt's code runs on the calling task from now on (it was made on another:
+// the IDE makes a program's runtime in a menu command, then runs it on a
+// task of its own): the events other tasks dispatch for it wait for its
+// turn here, and it is this task's current runtime.
+void basFormRtAdopt(BasFormRtT *rt);
+
+// True while any program's BASIC code is running (a handler waiting in
+// DoEvents or a dialog box included), has events waiting for its turn
+// (its UserControls' too), or is loading its forms at its start: a host
+// that dispatched an event and waits for the program to be done with it
+// (a test driving a program) waits until this is false.
+bool basFormRtBusy(void);
+
+// True while a handler of rt's (or a native bridge holding its forms and
+// controls) is on some stack: its eventDepth beyond the bracket its
+// waiting events hold.
+bool basFormRtCodeOnStack(const BasFormRtT *rt);
+
// Simple non-debug execution: run bytecode in slices with dvxUpdate
// between slices, then enter the event loop. Used by the standalone stub.
void basFormRtRunSimple(BasFormRtT *rt);
@@ -658,9 +773,11 @@ WindowT *basFormRtScreenWindow(const BasFormRtT *rt);
// AutoRedraw, ScaleMode, ScaleLeft, ScaleTop, ScaleWidth, ScaleHeight).
bool basFormRtIsDrawMember(const char *name, bool isProp);
-// Close all serial/secLink resources and unregister the idle pollers.
-// Called by basFormRtDestroy and by the stub's _appShutdown force-kill hook.
-void basFormRtSerialShutdown(void);
+// Close the serial ports and secure links (comm.bas) the program rt
+// opened, NULL every one, and drop the idle pollers once none is left.
+// basFormRtDestroy calls it for its program (a force kill reaches that
+// through basRunHost's kill hook), so other programs' stay open.
+void basFormRtSerialShutdown(const BasFormRtT *rt);
#ifdef DVX_HOST_TEST
// Test-only: clear every file-scope static so a host test process can run
@@ -955,6 +1072,15 @@ bool basUcIsControlType(const char *typeName);
// host's design controls. NULL when it has none.
const WgtIfaceT *basUcOwnIface(const BasFormRtT *rt, const char *typeName);
+// A UserControl has events from another task waiting for its program's
+// turn (basFormRtBusy counts them).
+bool basUcBusy(void);
+
+// A program's turn on its own task (basFormRtUpdate): its UserControls'
+// events that came on other tasks (their code runs on its program's task
+// only), then their Resize and Paint.
+void basUcPump(void);
+
// The runtime is being destroyed: its UserControl classes and the ones it
// registered for design are unregistered (their widgets are gone with the
// forms).
@@ -1046,15 +1172,21 @@ void basFormRtDrawClear(BasFormRtT *rt, BasControlT *ctrl);
// Free a control's drawing state.
void basFormRtDrawFree(BasControlT *ctrl);
+// Free PaintPicture's kept picture files (the runtime is ending).
+void basFormRtDrawPicsFree(BasFormRtT *rt);
+
// The object is about to be repainted (shown, resized, Refresh): true
// when its Paint event should fire -- a form or PictureBox whose
// AutoRedraw is not True. One whose program set AutoRedraw = False is
// cleared to its BackColor first (VB); by default drawings are kept.
bool basFormRtDrawWantsPaint(BasControlT *ctrl);
+// The Picture object LOADPICTUREDATA made that v is, else NULL.
+BasPictureT *basFormRtPictureData(const BasFormRtT *rt, BasValueT v);
+
// The file name of a Picture object LOADPICTURE made ("" for an empty
// picture), NULL when v is not one. Picture properties and PaintPicture
-// take either a file name or such an object.
+// take a file name or a Picture object of either kind.
const char *basFormRtPicturePath(const BasFormRtT *rt, BasValueT v);
// The live control an object reference names, or NULL.
@@ -1082,6 +1214,11 @@ void basFormRtPrinterSetProp(BasFormRtT *rt, const char *name, BasValueT va
// and the Printer's state freed.
void basFormRtPrinterEnd(BasFormRtT *rt);
+// The program was ended by force: a page drawn is thrown away and a
+// document open abandoned (KillDoc) -- nothing is printed in the kill --
+// and the Printer's state freed.
+void basFormRtPrinterKill(BasFormRtT *rt);
+
// ============================================================
// Between formrt.c and formDesign.c
// ============================================================
diff --git a/src/apps/kpunch/dvxbasic/ide/ideDesigner.c b/src/apps/kpunch/dvxbasic/ide/ideDesigner.c
index 5c0cad31..e39dff4c 100644
--- a/src/apps/kpunch/dvxbasic/ide/ideDesigner.c
+++ b/src/apps/kpunch/dvxbasic/ide/ideDesigner.c
@@ -46,6 +46,7 @@
// ============================================================
#define MIN_CTRL_SIZE 8
+#define EMPTY_PICTURE_SIZE 32 // a new Image or ImageButton's size: it has none until it has a picture
#define DSGN_NO_SELECTION (-1) // selectedIdx: the form itself
// ============================================================
@@ -114,18 +115,12 @@ static WidgetT *containerChildParent(FrmCtrlT *pc) {
// Create a real DVX widget for design-time display through libdvx's
// wgtCreateByName (the project's own UserControls first, the designer's
-// registration of them: basUcResolveType); design-time refuses
-// WGT_CREATE_PARENT_DATA widgets (Image/ImageButton) since we have no
-// pixel data.
+// registration of them: basUcResolveType). An Image or ImageButton starts
+// with an empty picture; its Picture property fills it.
static WidgetT *createDesignWidget(const char *vbTypeName, WidgetT *parent) {
- const char *wgtName = basUcResolveType(NULL, vbTypeName);
- const WgtIfaceT *iface = wgtGetIface(wgtName);
+ const char *wgtName = basUcResolveType(NULL, vbTypeName);
- if (!wgtName || (iface && iface->createSig == WGT_CREATE_PARENT_DATA)) {
- return NULL;
- }
-
- return wgtCreateByName(wgtName, parent);
+ return wgtName ? wgtCreateByName(wgtName, parent) : NULL;
}
@@ -534,6 +529,15 @@ void dsgnOnMouse(DsgnStateT *ds, int32_t x, int32_t y, bool drag) {
return;
}
+ // A control made with an empty picture (Image, ImageButton) has no size
+ // of its own until it has one: a size to see and select it by
+ const WgtIfaceT *placed = basCtlIface(cp->typeName);
+
+ if (placed && placed->createSig == WGT_CREATE_PARENT_DATA) {
+ cp->width = EMPTY_PICTURE_SIZE;
+ cp->height = EMPTY_PICTURE_SIZE;
+ }
+
if (parentWidget) {
dsgnCreateControlWidget(ds->form, cp, parentWidget);
}
diff --git a/src/apps/kpunch/dvxbasic/ide/ideDesigner.h b/src/apps/kpunch/dvxbasic/ide/ideDesigner.h
index ed2a8089..e4c8966c 100644
--- a/src/apps/kpunch/dvxbasic/ide/ideDesigner.h
+++ b/src/apps/kpunch/dvxbasic/ide/ideDesigner.h
@@ -36,7 +36,7 @@
#include "dvxWgt.h"
#include "../compiler/lexer.h"
#include "../formrt/formrt.h"
-#include "../formrt/frmModel.h"
+#include "../basdev/frmModel.h"
#include "stb_ds_wrap.h"
diff --git a/src/apps/kpunch/dvxbasic/ide/ideMain.c b/src/apps/kpunch/dvxbasic/ide/ideMain.c
index 110cb79a..d11bec6b 100644
--- a/src/apps/kpunch/dvxbasic/ide/ideMain.c
+++ b/src/apps/kpunch/dvxbasic/ide/ideMain.c
@@ -92,26 +92,28 @@
// Constants
// ============================================================
-#define IDE_MAX_SOURCE PRJ_MAX_SOURCE // editor capacity and largest loadable project file
-#define IDE_MAX_OUTPUT 32768
-#define IDE_SOURCE_MARGIN 64
-#define IDE_FULL_NAME_BUF (BAS_MAX_IDENT * 2) // "Obj.Evt" / "Obj_Evt" combined names
-#define IDE_TITLE_BUF (PRJ_MAX_NAME + 32) // "DVX BASIC - [name] *"
-#define IDE_FORM_TITLE_BUF (FRM_MAX_TEXT + 32) // "caption [Design] *"
-#define IDE_MSG_BUF (DVX_MAX_PATH + 32) // path plus short message
-#define IDE_REPORT_BUF (DVX_MAX_PATH + BAS_MAX_IDENT * 4) // runtime error report
-#define IDE_PREF_KEY_BUF 32 // "colorN" style preference keys
-#define IDE_IMM_LINE_BUF 512 // one Immediate-window line
-#define IDE_SKELETON_BUF 512 // generated "Sub ... End Sub" skeleton
-#define IDE_EVENT_PARAMS_BUF 256 // an Event declaration's parameter list text
-#define IDE_RENAME_TITLE "Rename File"
-#define IDE_VALUE_BUF 256 // formatted variable value / watch text
-#define IDE_FIND_TEXT_BUF 256 // find / replace text
-#define IDE_LINE_NUM_BUF 16 // decimal line number text
-#define IDE_RGB_LABEL_BUF 16 // "Green 255"
-#define IDE_TAB_WIDTH_BUF 8 // tab width text input
-#define IDE_OUTPUT_LINE_RESERVE 256 // headroom kept in sOutputBuf per error line
-#define IDE_LOG_LINE_BUF 256 // one pending PRINT line for Output-to-log
+#define IDE_MAX_SOURCE PRJ_MAX_SOURCE // editor capacity and largest loadable project file
+#define IDE_MAX_OUTPUT 32768
+#define IDE_RUN_TASK_NAME "DVX BASIC program" // the task a run's program runs on (runModule)
+#define IDE_BUSY_PUMP_MS 50 // a compile, a debugger snippet, or a Replace All lets DVX go on this often (ideBusyPump)
+#define IDE_SOURCE_MARGIN 64
+#define IDE_FULL_NAME_BUF (BAS_MAX_IDENT * 2) // "Obj.Evt" / "Obj_Evt" combined names
+#define IDE_TITLE_BUF (PRJ_MAX_NAME + 32) // "DVX BASIC - [name] *"
+#define IDE_FORM_TITLE_BUF (FRM_MAX_TEXT + 32) // "caption [Design] *"
+#define IDE_MSG_BUF (DVX_MAX_PATH + 32) // path plus short message
+#define IDE_REPORT_BUF (DVX_MAX_PATH + BAS_MAX_IDENT * 4) // runtime error report
+#define IDE_PREF_KEY_BUF 32 // "colorN" style preference keys
+#define IDE_IMM_LINE_BUF 512 // one Immediate-window line
+#define IDE_SKELETON_BUF 512 // generated "Sub ... End Sub" skeleton
+#define IDE_EVENT_PARAMS_BUF 256 // an Event declaration's parameter list text
+#define IDE_RENAME_TITLE "Rename File"
+#define IDE_VALUE_BUF 256 // formatted variable value / watch text
+#define IDE_FIND_TEXT_BUF 256 // find / replace text
+#define IDE_LINE_NUM_BUF 16 // decimal line number text
+#define IDE_RGB_LABEL_BUF 16 // "Green 255"
+#define IDE_TAB_WIDTH_BUF 8 // tab width text input
+#define IDE_OUTPUT_LINE_RESERVE 256 // headroom kept in sOutputBuf per error line
+#define IDE_LOG_LINE_BUF 256 // one pending PRINT line for Output-to-log
#define IDE_MAIN_TITLE "DVX BASIC"
#define IDE_GENERAL_SECTION "(General)"
@@ -301,9 +303,23 @@ typedef enum {
DbgIdleE, // no program loaded
DbgRunningE, // program executing
DbgPausedE, // stopped at breakpoint/step
- DbgStoppingE // Stop requested; runModule has not unwound yet
+ DbgStoppingE // Stop requested; runBody has not unwound yet
} IdeDebugStateE;
+// shellAppTaskStart, found at run time (the host tests have no shell)
+typedef __typeof__(shellAppTaskStart) *IdeTaskStartFnT;
+
+// The run in progress (runStart, runBody): the IDE windows it hid, and
+// the owner of the fonts it added
+typedef struct {
+ bool hadFormWin;
+ bool hadToolbox;
+ bool hadProps;
+ bool hadCodeWin;
+ bool hadPrjWin;
+ int32_t fontOwner;
+} IdeRunT;
+
typedef struct {
int32_t fileIdx; // project file index
int32_t codeLine; // line within file's code section (1-based)
@@ -480,6 +496,8 @@ static bool hasProject(void);
static bool hasUnsavedData(void);
static void helpQueryHandler(void *ctx);
static void helpSetCtrlTopic(const char *typeName);
+static void ideBusyPump(void *ctx);
+static void ideBusyPumpEnd(void);
static void ideErrorBox(const char *msg);
static void idePump(void);
static void immPrintCallback(void *ctx, const char *text, bool newline);
@@ -609,10 +627,13 @@ static void renameProjectFile(int32_t fileIdx);
static bool renameProjectFileTo(int32_t fileIdx, const char *newName);
static int32_t replaceInSections(const char *objFilter, bool caseSensitive);
static void reportUnreadable(const char *path);
+static void runBody(void);
static void runCached(void);
static void runFontsAdd(int32_t owner);
static void runModule(BasModuleT *mod);
static BasVmResultE runProgramSlices(BasFormRtT *rt);
+static bool runStart(BasModuleT *mod);
+static void runTaskEntry(void *arg);
static bool saveActiveFile(void);
static bool saveAllModified(void);
static bool saveCurProc(bool discardSkeleton);
@@ -707,6 +728,12 @@ static WindowT *sLastFocusWin = NULL; // last focused non-toolbar window
static char sOutputBuf[IDE_MAX_OUTPUT];
static int32_t sOutputLen = 0;
+static PlatformTicksT sBusyPumpAt = 0; // when ideBusyPump last let DVX go on
+static bool sBusyPumpBusy = false; // ideBusyPump set the busy state (ideBusyPumpEnd clears it)
+static const BasDbgPumpT sBusyPump = { ideBusyPump, NULL }; // the debugger's snippets pump through it
+
+static IdeRunT sRun;
+
// Procedure view state -- the editor shows one procedure at a time.
// Each procedure is stored in its own malloc'd buffer. The editor
// swaps directly between buffers with no splicing needed.
@@ -1638,10 +1665,10 @@ static bool compileProject(void) {
IdeConcatE concat = concatProject(&project, &validatorCtx, true, &sProject.sourceMap, &failIdx);
if (concat == ConcatUnreadableE) {
- sOutputLen = frmEmitClamped(sOutputBuf, IDE_MAX_OUTPUT, 0, "COMPILE ERROR:\nCannot read %s (missing or larger than %d bytes).\n", sProject.files[failIdx].path, (int)IDE_MAX_SOURCE);
+ sOutputLen = dvxStrAppendfCut(sOutputBuf, IDE_MAX_OUTPUT, 0, "COMPILE ERROR:\nCannot read %s (missing or larger than %d bytes).\n", sProject.files[failIdx].path, (int)IDE_MAX_SOURCE);
showCompileError("Compilation failed: cannot read file.");
} else if (concat == ConcatTooLargeE) {
- sOutputLen = frmEmitClamped(sOutputBuf, IDE_MAX_OUTPUT, 0, "COMPILE ERROR:\nProject source exceeds %d bytes at %s.\n", (int)IDE_MAX_SOURCE, sProject.files[failIdx].path);
+ sOutputLen = dvxStrAppendfCut(sOutputBuf, IDE_MAX_OUTPUT, 0, "COMPILE ERROR:\nProject source exceeds %d bytes at %s.\n", (int)IDE_MAX_SOURCE, sProject.files[failIdx].path);
showCompileError("Compilation failed: source too large.");
} else if (concat == ConcatFailedE) {
setStatus(project.error);
@@ -1674,14 +1701,14 @@ static bool compileProject(void) {
// DXE. Those would silently drop the control at runtime and
// produce a misleading "control not found" error on first access.
if (arrlen(validatorCtx.badTypes) > 0) {
- int32_t n = frmEmitClamped(sOutputBuf, IDE_MAX_OUTPUT, 0, "COMPILE ERROR:\nUnknown widget type(s) in .frm files:\n");
+ int32_t n = dvxStrAppendfCut(sOutputBuf, IDE_MAX_OUTPUT, 0, "COMPILE ERROR:\nUnknown widget type(s) in .frm files:\n");
for (int32_t i = 0; i < (int32_t)arrlen(validatorCtx.badTypes) && n < IDE_MAX_OUTPUT - IDE_OUTPUT_LINE_RESERVE; i++) {
BasBadTypeT *b = &validatorCtx.badTypes[i];
- n = frmEmitClamped(sOutputBuf, IDE_MAX_OUTPUT, n, " " BAS_VALIDATE_BAD_TYPE_FMT "\n", b->formName, b->ctrlName, b->typeName);
+ n = dvxStrAppendfCut(sOutputBuf, IDE_MAX_OUTPUT, n, " " BAS_VALIDATE_BAD_TYPE_FMT "\n", b->formName, b->ctrlName, b->typeName);
}
- n = frmEmitClamped(sOutputBuf, IDE_MAX_OUTPUT, n, "\n" BAS_VALIDATE_BAD_TYPE_HINT "\n");
+ n = dvxStrAppendfCut(sOutputBuf, IDE_MAX_OUTPUT, n, "\n" BAS_VALIDATE_BAD_TYPE_HINT "\n");
sOutputLen = n;
showCompileError("Compilation failed: unknown widget type.");
basBuildFreeProject(&project);
@@ -1697,7 +1724,7 @@ static bool compileProject(void) {
basValidateCallbacks(&validatorCtx, &validator);
- BasModuleT *mod = basBuildCompile(src, srcLen, sProject.optionExplicit, &validator, &validatorCtx.compiler, &err);
+ BasModuleT *mod = basBuildCompile(src, srcLen, sProject.optionExplicit, &validator, &validatorCtx.compiler, &err, ideBusyPump, NULL);
basBuildFreeProject(&project);
basValidateFree(&validatorCtx);
@@ -1748,9 +1775,9 @@ static bool compileProject(void) {
const char *msg = basBuildErrorText(err.message);
if (procName[0]) {
- sOutputLen = frmEmitClamped(sOutputBuf, IDE_MAX_OUTPUT, 0, "COMPILE ERROR:\n%s line %d: %s\n", procName, (int)procLine, msg);
+ sOutputLen = dvxStrAppendfCut(sOutputBuf, IDE_MAX_OUTPUT, 0, "COMPILE ERROR:\n%s line %d: %s\n", procName, (int)procLine, msg);
} else {
- sOutputLen = frmEmitClamped(sOutputBuf, IDE_MAX_OUTPUT, 0, "COMPILE ERROR:\n%s line %d: %s\n", errFile, (int)errLocalLine, msg);
+ sOutputLen = dvxStrAppendfCut(sOutputBuf, IDE_MAX_OUTPUT, 0, "COMPILE ERROR:\n%s line %d: %s\n", errFile, (int)errLocalLine, msg);
}
showCompileError("Compilation failed.");
@@ -2027,8 +2054,8 @@ static void debugStartOrResume(int32_t cmd) {
}
-// debugStop -- request the running program to stop. runModule's loops and
-// doEventsCallback observe sStopRequested and unwind; until runModule has
+// debugStop -- request the running program to stop. runBody's loops and
+// doEventsCallback observe sStopRequested and unwind; until runBody has
// returned the state stays DbgStoppingE, so nothing can start a new run
// or free the module the unwinding VM is still executing.
static void debugStop(void) {
@@ -2136,7 +2163,7 @@ static void designCtlRefresh(void) {
if (count > 0 && concatProject(&ps, NULL, false, &map, &failIdx) == ConcatOkE) {
BasCompileErrorT err;
- module = basBuildCompile(ps.source, ps.sourceLen, sProject.optionExplicit, NULL, NULL, &err);
+ module = basBuildCompile(ps.source, ps.sourceLen, sProject.optionExplicit, NULL, NULL, &err, NULL, NULL);
}
basBuildFreeProject(&ps);
@@ -2560,15 +2587,15 @@ static void ensureProjectWindow(bool raise) {
}
-// evaluateImmediate -- compile and execute a single line from the
-// Immediate window. If the line doesn't start with a statement keyword,
-// wrap it in PRINT so expressions produce visible output.
-
// evaluateImmediate -- run a line typed into the Immediate window (an
// assignment to the paused program's variable, a statement, or an
// expression to print); an assignment refreshes the debug windows.
static void evaluateImmediate(const char *expr) {
- if (basDbgExec(sDbgState == DbgPausedE ? sVm : NULL, expr, immPrintCallback, NULL)) {
+ bool assigned = basDbgExec(sDbgState == DbgPausedE ? sVm : NULL, expr, immPrintCallback, NULL, &sBusyPump);
+
+ ideBusyPumpEnd();
+
+ if (assigned) {
updateLocalsWindow();
updateWatchWindow();
}
@@ -3570,6 +3597,39 @@ static void helpSetCtrlTopic(const char *typeName) {
}
+// ideBusyPump -- basBuildCompile's unit callback, the debugger's snippet
+// pump (sBusyPump), and Replace All's between procedures: a frame every
+// IDE_BUSY_PUMP_MS, so DVX goes on through a long compile, snippet, or
+// replace. The busy state keeps the user's input out meanwhile; set here
+// when the caller had not (ideBusyPumpEnd clears it).
+static void ideBusyPump(void *ctx) {
+ (void)ctx;
+
+ if (platformClock() - sBusyPumpAt < PLATFORM_MS_TO_TICKS(IDE_BUSY_PUMP_MS)) {
+ return;
+ }
+
+ if (!sAc->busy) {
+ dvxSetBusy(sAc, true);
+ sBusyPumpBusy = true;
+ }
+
+ dvxUpdate(sAc);
+ sBusyPumpAt = platformClock();
+}
+
+
+// ideBusyPumpEnd -- after a step ideBusyPump may have pumped: the busy
+// state it set goes.
+static void ideBusyPumpEnd(void) {
+ if (sBusyPumpBusy) {
+ dvxSetBusy(sAc, false);
+ }
+
+ sBusyPumpBusy = false;
+}
+
+
// ideErrorBox -- modal error message. The busy state swallows all input,
// so it is cleared first or the box could never be dismissed.
static void ideErrorBox(const char *msg) {
@@ -3579,10 +3639,15 @@ static void ideErrorBox(const char *msg) {
// idePump -- one pass of the GUI event loop while a program runs, with
-// its buffered output shown first
+// its buffered output shown first (and the events waiting for it run)
static void idePump(void) {
flushOutput();
- dvxUpdate(sAc);
+
+ if (sDbgFormRt) {
+ basFormRtUpdate(sDbgFormRt);
+ } else {
+ dvxUpdate(sAc);
+ }
}
@@ -4310,7 +4375,7 @@ static void makePackage(void) {
return;
}
- const char *result = idePackageMake(sAc, &sProject, makeOutput);
+ const char *result = idePackageMake(sAc, &sProject, makeOutput, setStatus);
if (result) {
setStatus(result);
@@ -4609,7 +4674,7 @@ static void onBreakpointHit(void *ctx, int32_t line) {
// handler finishes; that is not a real pause point (every genuine pause --
// breakpoint or Step Into/Over/Out -- arrives with a positive line), so
// ignore it. Otherwise Continue would re-pause and require a second press.
- // After Stop, a handler still run while runModule unwinds never pauses:
+ // After Stop, a handler still run while runBody unwinds never pauses:
// the state stays Stopping and no debugger UI is driven.
if (line < 0 || sStopRequested || sDbgState == DbgStoppingE) {
return;
@@ -4745,8 +4810,8 @@ static void onClose(WindowT *win) {
// win itself is destroyed last, below, once nothing refers to it
sWin = NULL;
- // A running program is still executing sCachedModule (this close
- // arrived through its event pump); runModule frees it on exit.
+ // A running program is still executing sCachedModule (on its task, or
+ // this close arrived through its event pump); runBody frees it on exit.
if (sCachedModule && !sVm) {
basModuleFree(sCachedModule);
sCachedModule = NULL;
@@ -5643,6 +5708,9 @@ static void onReplaceAll(WidgetT *w) {
}
}
+ // A long replace let DVX go on (replaceInSections pumps per procedure)
+ ideBusyPumpEnd();
+
char statusBuf[BAS_MAX_IDENT];
snprintf(statusBuf, sizeof(statusBuf), "%d replacement(s) made.", (int)totalCount);
setStatus(statusBuf);
@@ -6830,6 +6898,7 @@ static int32_t replaceInSections(const char *objFilter, bool caseSensitive) {
showProc(idx);
count += wgtTextAreaReplaceAll(sEditor, sFindText, sReplaceText, caseSensitive);
+ ideBusyPump(NULL);
}
arrfree(targets);
@@ -6846,144 +6915,12 @@ static void reportUnreadable(const char *path) {
}
-static void runCached(void) {
- if (!sCachedModule) {
- setStatus("No compiled program. Press F5 to compile first.");
- return;
- }
-
- // The forms are registered from the project buffers
- if (!stashCurrentFile()) {
- return;
- }
-
- clearOutput();
- runModule(sCachedModule);
-}
-
-
-// The project's fonts added for owner (the IDE) while a program runs; one
-// that cannot be added is reported in the Output window
-static void runFontsAdd(int32_t owner) {
- for (int32_t i = 0; i < prjFontCount(&sProject); i++) {
- char path[DVX_MAX_PATH];
-
- platformPathJoin(path, sizeof(path), sProject.projectDir, prjFontPath(&sProject, i));
-
- if (!dvxFontAddFile(owner, path)) {
- char msg[DVX_MAX_PATH + IDE_MSG_BUF];
-
- snprintf(msg, sizeof(msg), "Cannot use the font %s.\n", path);
- outputAppend(msg, (int32_t)strlen(msg));
- }
- }
-}
-
-
-static void runModule(BasModuleT *mod) {
- // Never nest a run: a stopped program may still be unwinding
- if (sVm) {
- return;
- }
-
- if (prjIsControlProject(&sProject)) {
- setStatus(IDE_CTL_NO_RUN);
- return;
- }
-
- // The program's VM and form runtime. In the IDE, App.Path is the
- // project's folder, App.Data its DATA folder (what the program will
- // ship), and App.User the user folder the project's program gets
- // once installed beside it; the program's screen window carries
- // the project's name and F1 opens the project's help file. The IDE
- // reports runtime errors through the Output pane and jumps the editor
- // to the faulting line, so the form runtime's modal error box is
- // suppressed (its nested event pump raced with events still queued
- // by the program: "press OK twice").
- char helpPath[DVX_MAX_PATH] = "";
- const char *hostError = NULL;
- BasRunSpecT spec;
-
- if (sProject.helpFile[0]) {
- platformPathJoin(helpPath, sizeof(helpPath), sProject.projectDir, sProject.helpFile);
- }
-
- memset(&spec, 0, sizeof(spec));
- spec.appDir = sProject.projectDir;
- spec.appFile = sProject.projectPath;
- spec.args = sProject.commandArgs;
- spec.title = sProject.name;
- spec.helpFile = helpPath[0] ? helpPath : NULL;
- spec.printFn = printCallback;
- spec.inputFn = inputCallback;
- spec.doEventsFn = doEventsCallback;
- spec.suppressErrorDialog = true;
-
- BasFormRtT *formRt = basRunHostCreate(sAc, mod, &spec, &hostError);
-
- if (!formRt) {
- setStatus(hostError);
- return;
- }
-
- BasVmT *vm = formRt->vm;
-
- setStatus("Running...");
- closeFindDialog();
-
- // Hide the designer, code and project windows while the program runs;
- // they are restored (where they were visible) when it ends.
- bool hadFormWin = sFormWin && sFormWin->visible;
- bool hadToolbox = sToolboxWin && sToolboxWin->visible;
- bool hadProps = sPropsWin && sPropsWin->visible;
- bool hadCodeWin = sCodeWin && sCodeWin->visible;
- bool hadPrjWin = sProjectWin && sProjectWin->visible;
-
- if (sFormWin) {
- dvxHideWindow(sAc, sFormWin);
- }
-
- if (sToolboxWin) {
- dvxHideWindow(sAc, sToolboxWin);
- }
-
- if (sPropsWin) {
- dvxHideWindow(sAc, sPropsWin);
- }
-
- if (sCodeWin) {
- dvxHideWindow(sAc, sCodeWin);
- }
-
- if (sProjectWin) {
- dvxHideWindow(sAc, sProjectWin);
- }
-
- basVmSetBreakpointCallback(vm, onBreakpointHit, NULL);
-
- sVm = vm;
- sDbgFormRt = formRt;
- sDbgModule = mod;
- sDbgState = DbgRunningE;
- sStopRequested = false;
-
- updateProjectMenuState();
-
- // Set breakpoints BEFORE loading forms so breakpoints in form
- // init code (module-level statements inside BEGINFORM) fire.
- if (sDbgEnabled) {
- syncVmBreakpoints();
-
- if (sDbgBreakOnStart) {
- basVmStepInto(vm);
- }
- }
-
- // The project's fonts, for the program run here as for the built one
- // (added for the IDE until the run ends)
- int32_t fontOwner = sAc->currentAppId;
-
- runFontsAdd(fontOwner);
+// runBody -- the rest of a run (runModule), on the task the program runs
+// on: its forms loaded and shown, its code run, then its event loop until
+// it ends; the error it ended on reported, the IDE's windows restored.
+static void runBody(void) {
+ BasFormRtT *formRt = sDbgFormRt;
+ BasVmT *vm = sVm;
// Load forms from project files (AFTER breakpoints are set so
// init code breakpoints fire), then show the startup form.
@@ -7094,7 +7031,7 @@ static void runModule(BasModuleT *mod) {
// The fonts the run added (the project's and any the program added):
// the IDE carries none of its own
- dvxFontRemoveOwner(fontOwner);
+ dvxFontRemoveOwner(sRun.fontOwner);
// If the IDE was closed while the program was running, skip all UI
// updates -- the windows are already destroyed -- and release the
@@ -7128,23 +7065,23 @@ static void runModule(BasModuleT *mod) {
dvxRaiseWindow(sAc, sCodeWin);
}
} else {
- if (hadFormWin && sFormWin) {
+ if (sRun.hadFormWin && sFormWin) {
dvxShowWindow(sAc, sFormWin);
}
- if (hadToolbox && sToolboxWin) {
+ if (sRun.hadToolbox && sToolboxWin) {
dvxShowWindow(sAc, sToolboxWin);
}
- if (hadProps && sPropsWin) {
+ if (sRun.hadProps && sPropsWin) {
dvxShowWindow(sAc, sPropsWin);
}
- if (hadCodeWin && sCodeWin) {
+ if (sRun.hadCodeWin && sCodeWin) {
dvxShowWindow(sAc, sCodeWin);
}
- if (hadPrjWin && sProjectWin) {
+ if (sRun.hadPrjWin && sProjectWin) {
dvxShowWindow(sAc, sProjectWin);
}
}
@@ -7154,6 +7091,58 @@ static void runModule(BasModuleT *mod) {
}
+static void runCached(void) {
+ if (!sCachedModule) {
+ setStatus("No compiled program. Press F5 to compile first.");
+ return;
+ }
+
+ // The forms are registered from the project buffers
+ if (!stashCurrentFile()) {
+ return;
+ }
+
+ clearOutput();
+ runModule(sCachedModule);
+}
+
+
+// The project's fonts added for owner (the IDE) while a program runs; one
+// that cannot be added is reported in the Output window
+static void runFontsAdd(int32_t owner) {
+ for (int32_t i = 0; i < prjFontCount(&sProject); i++) {
+ char path[DVX_MAX_PATH];
+
+ platformPathJoin(path, sizeof(path), sProject.projectDir, prjFontPath(&sProject, i));
+
+ if (!dvxFontAddFile(owner, path)) {
+ char msg[DVX_MAX_PATH + IDE_MSG_BUF];
+
+ snprintf(msg, sizeof(msg), "Cannot use the font %s.\n", path);
+ outputAppend(msg, (int32_t)strlen(msg));
+ }
+ }
+}
+
+
+// runModule -- run mod: set up here (runStart), then run (runBody) on a
+// task of the IDE's own (shellAppTaskStart), so the shell's main loop
+// goes on while the program does -- programs that end are reaped, Exit
+// and DOS requests carried out, the screen saver started. Without one
+// (the host tests have no shell) the program runs here, to its end.
+static void runModule(BasModuleT *mod) {
+ if (!runStart(mod)) {
+ return;
+ }
+
+ IdeTaskStartFnT taskStart = (IdeTaskStartFnT)dlsym(NULL, DVX_SYM("shellAppTaskStart"));
+
+ if (!taskStart || taskStart(sAc, IDE_RUN_TASK_NAME, BAS_RUN_STACK_SIZE, runTaskEntry, NULL) < 0) {
+ runBody();
+ }
+}
+
+
// runProgramSlices -- run the program in step slices, yielding to DVX
// between them, pausing at breakpoints, until it halts, fails (its
// message and line left in the VM, see basRunHostSlice) or is stopped.
@@ -7195,6 +7184,126 @@ static BasVmResultE runProgramSlices(BasFormRtT *rt) {
}
+// runStart -- the first part of a run (runModule): the program's runtime
+// made, the IDE's windows hidden, breakpoints set (and stepping when the
+// run breaks on start), the project's fonts added. False when it cannot
+// start.
+static bool runStart(BasModuleT *mod) {
+ // Never nest a run: a stopped program may still be unwinding
+ if (sVm) {
+ return false;
+ }
+
+ if (prjIsControlProject(&sProject)) {
+ setStatus(IDE_CTL_NO_RUN);
+ return false;
+ }
+
+ // The program's VM and form runtime. In the IDE, App.Path is the
+ // project's folder, App.Data its DATA folder (what the program will
+ // ship), and App.User the user folder the project's program gets
+ // once installed beside it; the program's screen window carries
+ // the project's name and F1 opens the project's help file. The IDE
+ // reports runtime errors through the Output pane and jumps the editor
+ // to the faulting line, so the form runtime's modal error box is
+ // suppressed (its nested event pump raced with events still queued
+ // by the program: "press OK twice").
+ char helpPath[DVX_MAX_PATH] = "";
+ const char *hostError = NULL;
+ BasRunSpecT spec;
+
+ if (sProject.helpFile[0]) {
+ platformPathJoin(helpPath, sizeof(helpPath), sProject.projectDir, sProject.helpFile);
+ }
+
+ memset(&spec, 0, sizeof(spec));
+ spec.appDir = sProject.projectDir;
+ spec.appFile = sProject.projectPath;
+ spec.args = sProject.commandArgs;
+ spec.title = sProject.name;
+ spec.helpFile = helpPath[0] ? helpPath : NULL;
+ spec.printFn = printCallback;
+ spec.inputFn = inputCallback;
+ spec.doEventsFn = doEventsCallback;
+ spec.suppressErrorDialog = true;
+
+ BasFormRtT *formRt = basRunHostCreate(sAc, mod, &spec, &hostError);
+
+ if (!formRt) {
+ setStatus(hostError);
+ return false;
+ }
+
+ BasVmT *vm = formRt->vm;
+
+ setStatus("Running...");
+ closeFindDialog();
+
+ // Hide the designer, code and project windows while the program runs;
+ // they are restored (where they were visible) when it ends.
+ sRun.hadFormWin = sFormWin && sFormWin->visible;
+ sRun.hadToolbox = sToolboxWin && sToolboxWin->visible;
+ sRun.hadProps = sPropsWin && sPropsWin->visible;
+ sRun.hadCodeWin = sCodeWin && sCodeWin->visible;
+ sRun.hadPrjWin = sProjectWin && sProjectWin->visible;
+
+ if (sFormWin) {
+ dvxHideWindow(sAc, sFormWin);
+ }
+
+ if (sToolboxWin) {
+ dvxHideWindow(sAc, sToolboxWin);
+ }
+
+ if (sPropsWin) {
+ dvxHideWindow(sAc, sPropsWin);
+ }
+
+ if (sCodeWin) {
+ dvxHideWindow(sAc, sCodeWin);
+ }
+
+ if (sProjectWin) {
+ dvxHideWindow(sAc, sProjectWin);
+ }
+
+ basVmSetBreakpointCallback(vm, onBreakpointHit, NULL);
+
+ sVm = vm;
+ sDbgFormRt = formRt;
+ sDbgModule = mod;
+ sDbgState = DbgRunningE;
+ sStopRequested = false;
+
+ updateProjectMenuState();
+
+ // Set breakpoints BEFORE loading forms so breakpoints in form
+ // init code (module-level statements inside BEGINFORM) fire.
+ if (sDbgEnabled) {
+ syncVmBreakpoints();
+
+ if (sDbgBreakOnStart) {
+ basVmStepInto(vm);
+ }
+ }
+
+ // The project's fonts, for the program run here as for the built one
+ // (added for the IDE until the run ends)
+ sRun.fontOwner = sAc->currentAppId;
+ runFontsAdd(sRun.fontOwner);
+ return true;
+}
+
+
+// runTaskEntry -- runModule's task: the program's runtime moves to it,
+// then the run goes on (runBody).
+static void runTaskEntry(void *arg) {
+ (void)arg;
+ basFormRtAdopt(sDbgFormRt);
+ runBody();
+}
+
+
// saveActiveFile -- write the active file (editor/designer state first
// stashed into its buffer). Returns false when nothing was written.
static bool saveActiveFile(void) {
@@ -9017,7 +9126,7 @@ static void updateWatchWindow(void) {
valBuf[0] = '\0';
- if (sDbgState == DbgPausedE && sVm && sDbgModule && !basDbgEval(sVm, sWatchExprs[i], valBuf, sizeof(valBuf))) {
+ if (sDbgState == DbgPausedE && sVm && sDbgModule && !basDbgEval(sVm, sWatchExprs[i], valBuf, sizeof(valBuf), &sBusyPump)) {
snprintf(valBuf, sizeof(valBuf), "%s", BAS_DBG_EVAL_ERROR);
}
@@ -9025,6 +9134,7 @@ static void updateWatchWindow(void) {
listRowsAdd(&sWatchRows, valBuf);
}
+ ideBusyPumpEnd();
listRowsCommit(&sWatchRows, sWatchList, count);
}
diff --git a/src/apps/kpunch/dvxbasic/ide/ideMenuEditor.c b/src/apps/kpunch/dvxbasic/ide/ideMenuEditor.c
index ea2a0c53..2cf45404 100644
--- a/src/apps/kpunch/dvxbasic/ide/ideMenuEditor.c
+++ b/src/apps/kpunch/dvxbasic/ide/ideMenuEditor.c
@@ -525,7 +525,7 @@ static void onOk(WidgetT *w) {
frmMenuStripBlank(&sMed.items);
char reason[MED_MSG_BUF];
- int32_t badIdx = frmModelMenuValidate(sMed.form, sMed.items, basIsValidIdent, reason, sizeof(reason));
+ int32_t badIdx = frmModelMenuValidate(sMed.form, sMed.items, reason, sizeof(reason));
if (badIdx >= 0) {
dvxErrorBox(sMed.ctx, MED_DLG_TITLE, reason);
diff --git a/src/apps/kpunch/dvxbasic/ide/idePackage.c b/src/apps/kpunch/dvxbasic/ide/idePackage.c
index ac079a0a..0dce4c29 100644
--- a/src/apps/kpunch/dvxbasic/ide/idePackage.c
+++ b/src/apps/kpunch/dvxbasic/ide/idePackage.c
@@ -60,6 +60,7 @@
#define PKG_NEED_SEP "," // between the needs of [Package] Needs
#define PKG_NEED_BLANKS " \t" // between a need's id and version
#define PKG_ID_DEFAULT_VENDOR "my" // a package name's vendor when the project has no publisher
+#define PKG_SLICE_MS 50 // making the package between frames
// The keys offered to sign with: their files and the list's lines
typedef struct {
@@ -74,7 +75,8 @@ typedef struct {
static bool askId(AppContextT *ctx, PrjStateT *prj);
static bool chooseKey(AppContextT *ctx, PrjStateT *prj, char *secretPath, int32_t secretSize);
-const char *idePackageMake(AppContextT *ctx, PrjStateT *prj, IdeBuildFnT build);
+const char *idePackageMake(AppContextT *ctx, PrjStateT *prj, IdeBuildFnT build, IdeStatusFnT status);
+static const char *jobRun(AppContextT *ctx, PkgDpkJobT *job, IdeStatusFnT status);
static void keysList(KeyListT *keys);
static bool manifestOf(const PrjStateT *prj, PkgManifestT *m);
static const char *say(const char *fmt, ...) __attribute__((format(printf, 1, 2)));
@@ -149,7 +151,7 @@ static bool chooseKey(AppContextT *ctx, PrjStateT *prj, char *secretPath, int32_
}
-const char *idePackageMake(AppContextT *ctx, PrjStateT *prj, IdeBuildFnT build) {
+const char *idePackageMake(AppContextT *ctx, PrjStateT *prj, IdeBuildFnT build, IdeStatusFnT status) {
static const FileFilterT filters[] = {
{ "Packages (*.dpk)" },
{ "All Files (*.*)" }
@@ -166,6 +168,7 @@ const char *idePackageMake(AppContextT *ctx, PrjStateT *prj, IdeBuildFnT build)
PkgManifestT m;
PkgSecretT secret;
PkgDpkFileT *files = NULL;
+ PkgDpkJobT *job = NULL;
const char *failure = NULL;
bool control = prjIsControlProject(prj);
bool ok;
@@ -217,8 +220,9 @@ const char *idePackageMake(AppContextT *ctx, PrjStateT *prj, IdeBuildFnT build)
failure = say("%s", err);
}
- if (!failure && !pkgDpkBuildFiles(&m, files, (int32_t)arrlen(files), outPath, secretPath[0] ? &secret : NULL, err, sizeof(err))) {
- failure = say("%s", err);
+ if (!failure) {
+ job = pkgDpkBuildStart(&m, files, (int32_t)arrlen(files), outPath, secretPath[0] ? &secret : NULL, err, sizeof(err));
+ failure = job ? jobRun(ctx, job, status) : say("%s", err);
}
pkgSecretWipe(&secret);
@@ -234,6 +238,32 @@ const char *idePackageMake(AppContextT *ctx, PrjStateT *prj, IdeBuildFnT build)
}
+// Run the package job (freed) to its end, a slice at a time with a frame
+// of DVX between slices (the busy cursor up: input waits) and how far it
+// is on the status line: NULL when the package is made, else why not.
+static const char *jobRun(AppContextT *ctx, PkgDpkJobT *job, IdeStatusFnT status) {
+ PkgDpkStepE step = PkgDpkMoreE;
+ const char *failure;
+
+ dvxSetBusy(ctx, true);
+
+ while (step == PkgDpkMoreE) {
+ PlatformTicksT end = platformClock() + PLATFORM_MS_TO_TICKS(PKG_SLICE_MS);
+
+ while ((step = pkgDpkJobStep(job, PKG_DPK_STEP)) == PkgDpkMoreE && platformClock() < end) {
+ }
+
+ status(say("Making the package: %d%%", (int)pkgDpkJobPercent(job)));
+ dvxUpdate(ctx);
+ }
+
+ dvxSetBusy(ctx, false);
+ failure = step == PkgDpkDoneE ? NULL : say("%s", pkgDpkJobMessage(job));
+ pkgDpkJobFree(job);
+ return failure;
+}
+
+
// The user's secret keys (the KEYS folder of the user folder), by the
// names in them, after "Not signed".
static void keysList(KeyListT *keys) {
diff --git a/src/apps/kpunch/dvxbasic/ide/idePackage.h b/src/apps/kpunch/dvxbasic/ide/idePackage.h
index e11b74af..1f903d79 100644
--- a/src/apps/kpunch/dvxbasic/ide/idePackage.h
+++ b/src/apps/kpunch/dvxbasic/ide/idePackage.h
@@ -45,12 +45,16 @@
// release: NULL when it is built, else why not.
typedef const char *(*IdeBuildFnT)(const char *outPath, bool release);
+// Show text on the IDE's status line.
+typedef void (*IdeStatusFnT)(const char *text);
+
// Make the project's package: its package name asked for when the
// project has none (and kept in it, as the key chosen is), the package
// file and the key to sign with asked for, the program built by build
// into a scratch folder, and the package made from it and the project's
-// details ([Project] and [Package]). The status line to show: the package
-// made, or why not; NULL when the user cancelled.
-const char *idePackageMake(AppContextT *ctx, PrjStateT *prj, IdeBuildFnT build);
+// details ([Project] and [Package]), a step at a time with DVX kept going
+// between steps and status showing how far it is. The status line to
+// show: the package made, or why not; NULL when the user cancelled.
+const char *idePackageMake(AppContextT *ctx, PrjStateT *prj, IdeBuildFnT build, IdeStatusFnT status);
#endif // IDE_PACKAGE_H
diff --git a/src/apps/kpunch/dvxbasic/ide/ideProperties.c b/src/apps/kpunch/dvxbasic/ide/ideProperties.c
index 0fffd44b..6efa8b30 100644
--- a/src/apps/kpunch/dvxbasic/ide/ideProperties.c
+++ b/src/apps/kpunch/dvxbasic/ide/ideProperties.c
@@ -39,6 +39,7 @@
#include "../basdev/basSource.h"
#include "../formrt/formrt.h"
#include "../formrt/frmParser.h"
+#include "../../../../libs/kpunch/sql/dvxSql.h"
#include "dvxDlg.h"
#include "dvxWm.h"
#include "box/box.h"
@@ -140,14 +141,14 @@ static const PrpFontWordT sFontWords[] = {
// here; the column-name and table-name probes resolve the subset they use.
// ============================================================
-typedef int32_t (*SqlOpenFnT)(const char *);
-typedef void (*SqlCloseFnT)(int32_t);
-typedef int32_t (*SqlQueryFnT)(int32_t, const char *);
-typedef int32_t (*SqlFieldCountFnT)(int32_t);
-typedef const char *(*SqlFieldNameFnT)(int32_t, int32_t);
-typedef bool (*SqlNextFnT)(int32_t);
-typedef const char *(*SqlFieldTextFnT)(int32_t, int32_t);
-typedef void (*SqlFreeResultFnT)(int32_t);
+typedef __typeof__(dvxSqlOpen) *SqlOpenFnT;
+typedef __typeof__(dvxSqlClose) *SqlCloseFnT;
+typedef __typeof__(dvxSqlQuery) *SqlQueryFnT;
+typedef __typeof__(dvxSqlFieldCount) *SqlFieldCountFnT;
+typedef __typeof__(dvxSqlFieldName) *SqlFieldNameFnT;
+typedef __typeof__(dvxSqlNext) *SqlNextFnT;
+typedef __typeof__(dvxSqlFieldText) *SqlFieldTextFnT;
+typedef __typeof__(dvxSqlFreeResult) *SqlFreeResultFnT;
// ============================================================
// Module state
@@ -215,11 +216,11 @@ static void addFontRow(const WidgetT *w) {
}
basFontChoiceOf(wgtFont(w), &font);
- pos = frmEmitClamped(text, sizeof(text), 0, "%s, %" PRId32, font.family, font.size);
+ pos = dvxStrAppendfCut(text, sizeof(text), 0, "%s, %" PRId32, font.family, font.size);
for (int32_t i = 0; i < DVX_ARRAY_LEN(sFontWords); i++) {
if (font.style & sFontWords[i].style) {
- pos = frmEmitClamped(text, sizeof(text), pos, ", %s", sFontWords[i].word);
+ pos = dvxStrAppendfCut(text, sizeof(text), pos, ", %s", sFontWords[i].word);
}
}
diff --git a/src/apps/kpunch/dvxbasic/ide/ideToolbox.c b/src/apps/kpunch/dvxbasic/ide/ideToolbox.c
index 6d174812..20acad0f 100644
--- a/src/apps/kpunch/dvxbasic/ide/ideToolbox.c
+++ b/src/apps/kpunch/dvxbasic/ide/ideToolbox.c
@@ -161,12 +161,6 @@ WindowT *tbxCreate(AppContextT *ctx, DsgnStateT *ds, int32_t y) {
break;
}
- // Types that need pixel data at creation (Image, ImageButton)
- // cannot be placed at design time.
- if (iface->createSig == WGT_CREATE_PARENT_DATA) {
- continue;
- }
-
// Add tool entry
TbxToolEntryT entry;
memset(&entry, 0, sizeof(entry));
diff --git a/src/apps/kpunch/dvxbasic/ideguide.dhs b/src/apps/kpunch/dvxbasic/ideguide.dhs
index abf5dff1..059ad670 100644
--- a/src/apps/kpunch/dvxbasic/ideguide.dhs
+++ b/src/apps/kpunch/dvxbasic/ideguide.dhs
@@ -779,7 +779,7 @@ Click a tool to select it (the active tool name is stored in the designer state)
WrapBox Flow layout container that wraps items to the next row.
.endtable
-Image and ImageButton controls are not in the Toolbox; they can still be written into a .frm file by hand and are then edited in the Properties panel like any other control.
+A new Image or ImageButton starts empty, 32 by 32 pixels so it can be seen and selected: set its Picture property to show a picture.
.link ide.designer Form Designer
.link ide.usercontrols UserControls in the IDE
diff --git a/src/apps/kpunch/dvxbasic/langref.dhs b/src/apps/kpunch/dvxbasic/langref.dhs
index 7a58b616..42a744b1 100644
--- a/src/apps/kpunch/dvxbasic/langref.dhs
+++ b/src/apps/kpunch/dvxbasic/langref.dhs
@@ -1760,6 +1760,7 @@ ErrorHandler:
11 Division by zero
13 Type mismatch (text that is not a number where a number is needed) / not an array / not a TYPE instance
14 Out of string space (no memory to build a string)
+ 18 User interrupt occurred (a control's code ran too long without returning and was stopped)
20 RESUME without error
26 FOR loop nesting too deep
28 Out of stack space (calls or expressions nested too deeply)
@@ -1800,13 +1801,15 @@ Executes an operating-system command.
SHELL "command"
.endcode
-When used as a function, SHELL returns the exit code of the command.
+When used as a function, SHELL returns the exit code of the command, or -1 when it did not run.
.code
Shell "DIR /B"
exitCode = Shell("COPY A.TXT B.TXT")
.endcode
+DVX steps aside while the command runs, as it does for a DOS program started from Program Manager: the screen goes back to text, and the command has the keyboard, the mouse, and the hardware to itself. Nothing of DVX runs meanwhile, so the other programs that are running are named first and asked to close (your program stays and waits); choosing No, or a program that keeps a window open, means the command does not run (-1). When the command ends, the desktop comes back and your program goes on. A command that prints and ends at once is gone from the screen as soon as it ends: ShellRunDos (shell.bas) can wait for a key first.
+
The command goes to DOS as it is written. DOS commands read / as the start of a switch (DIR /B), so write the paths in a command with \ (SHELL "DEL C:\TEMP\X.TXT"); FileOpsPathNative$ (fileops.bas) turns a path built with / into that form. Everywhere else in DVX BASIC a path may use / or \.
.h2 SLEEP
@@ -1897,6 +1900,8 @@ PLAY "T160 O3 L8 E D C D E E E4"
MF Music foreground: PLAY, SOUND, and BEEP wait until the sound has finished (the default).
MB Music background: the program goes on while the music plays; it only waits when more than 32 notes are waiting.
I n DVX's own: play the notes that follow on General MIDI instrument n (1 to 128; 1 is the piano, 41 the violin, 74 the flute) through the music synthesizer. I0 goes back to plain tones (the default).
+ V n Volume of the notes that follow, 0 to 100 percent (default 100), as in QB64. On an instrument it sets how hard the note is played.
+ @ n Waveform of the tones that follow, as in QB64: @1 square (the default, like the PC speaker), @3 triangle (softer), @5 noise (drums, explosions). QB64's @2 and @4 are not available.
X "X" + VARPTR$(s$) plays the commands in s$ at that point (s$ may use X itself, up to 16 levels deep).
= Any number above may be written as "=" + VARPTR$(n), n a numeric expression: "T=" + VARPTR$(tempo).
.endtable
@@ -1909,11 +1914,12 @@ tempo = 160
PLAY "T=" + VARPTR$(tempo) + "O3 X" + VARPTR$(verse$) + "X" + VARPTR$(verse$)
.endcode
-Building the string with + works too, such as PLAY "O" + STR$(n). Without I, the notes are square-wave tones like the PC speaker's; with it, they play on the same synthesizer as MIDI music.
+Building the string with + works too, such as PLAY "O" + STR$(n). Without I, the notes are tones -- square waves like the PC speaker's unless @ chooses another -- and with it, they play on the same synthesizer as MIDI music. The PC speaker plays only square tones at one volume: there V and @ make no difference, except that V0 is silent.
.code
PLAY "MB T120 L4 CDEF GAB>C" ' a scale in the background
PLAY "I74 O4 L8 GAB>C" ' on the flute
+PLAY "@3 V60 L8 CEG>C" ' softer, quieter
.endcode
.h2 PLAY Function
@@ -2057,8 +2063,11 @@ Shows the program's screen on the whole screen, or back in its window. Alt+Enter
.code
FULLSCREEN [ON | OFF]
FULLSCREEN expression ' True: whole screen, False: window
+isFull = FULLSCREEN ' True while the program's screen has the whole screen
.endcode
+Used as a value, FULLSCREEN tells whether the program's screen has the whole screen (also while the desktop is shown for another program, since the screen comes back with the focus): False when no suitable video mode could be set, or before SCREEN or any drawing made a screen. A form takes the whole screen with its FullScreen property instead.
+
.h2 Text: CLS, LOCATE, COLOR, CSRLIN, POS, SCREEN( )
.code
@@ -2516,7 +2525,7 @@ Name "draft.txt" As "final.txt"
.h2 FILECOPY
-Copies a file. If the destination already exists it is overwritten.
+Copies a file. If the destination already exists it is overwritten. While a big file copies, the rest of DVX and the program's own events go on, as with DoEvents (in a control's code they wait until the copy ends).
.code
FILECOPY source$, destination$
@@ -2584,7 +2593,7 @@ SetAttr "secret.dat", vbHidden + vbReadOnly
.h2 CURDIR$ / DIR$
-CURDIR$ returns the current directory. DIR$ returns the first file matching a pattern; subsequent calls with no arguments return additional matches (or "" when done). DIR$ returns only files: never ".", "..", or subdirectories. DIR$("C:\*.*") lists the root of C: (DIR$("C:*.*") the drive's current folder).
+CURDIR$ returns the current directory. DIR$ returns the first file matching a pattern; subsequent calls with no arguments return additional matches (or "" when done). DIR$ returns only files: never ".", "..", or subdirectories. The pattern uses DOS's wildcards, as the file dialog does: * stands for any characters and ? for one, case does not matter, and *.* is every file, with or without an extension. DIR$("C:\*.*") lists the root of C: (DIR$("C:*.*") the drive's current folder). Looking through a big folder for a rare match lets the rest of DVX and the program's own events go on, as with DoEvents; if one of those events starts a new DIR$ listing, the call returns "" (in a control's code nothing else runs meanwhile).
.code
cwd$ = CURDIR$
@@ -2608,7 +2617,9 @@ bytes = FILELEN(filename$)
.toc 1 Built-in Functions
.toc 2 String Functions
.index ASC
+.index ASCW
.index CHR$
+.index CHRW$
.index FORMAT$
.index HEX$
.index INSTR
@@ -2640,7 +2651,9 @@ bytes = FILELEN(filename$)
Function Returns Description
-------- ------- -----------
ASC(s$) Integer ASCII code of the first character of s$ (error 5 for an empty string)
+ ASCW(s$) Long Unicode code point of the first character of s$ (error 5 for an empty string)
CHR$(n) String Character with ASCII code n
+ CHRW$(n) String Character for Unicode code point n ("?" when DVX has no such character; error 5 outside 0 to &H10FFFF)
FILTER(arr, match$ [, include [, compare]]) String array The elements of arr that contain match$ (or, with include False, that do not)
FORMAT$(value [, fmt$]) String Formats a number or a date using a format string
HEX$(n) String Hexadecimal representation of n (uppercase, no leading &H)
@@ -2840,7 +2853,7 @@ If length is omitted, MID$ replaces up to LEN(replacement$) characters; a length
SIN(n) Double Sine of n (n in radians)
SQR(n) Double Square root of n
TAN(n) Double Tangent of n (n in radians)
- TIMER Double Seconds since midnight
+ TIMER Double Seconds since midnight, to a fraction of a millisecond (it keeps rising between the clock's 55 ms ticks)
.endtable
RND follows QBasic: RND or RND(n) with n > 0 returns the next number in the sequence, RND(0) repeats the last number, and RND(n) with n < 0 reseeds the generator from n (the same n always gives the same number). The result is a Double, 0 <= RND < 1.
@@ -3091,6 +3104,7 @@ File sizes and positions are Longs, so they stop at 2 GB (2,147,483,647 bytes).
.index IIF
.index CHOOSE
.index LOADPICTURE
+.index LOADPICTUREDATA
.h1 Miscellaneous Functions
@@ -3099,7 +3113,7 @@ File sizes and positions are Longs, so they stop at 2 GB (2,147,483,647 bytes).
-------- ------- -----------
DATE$ String Current date as "MM-DD-YYYY"
TIME$ String Current time as "HH:MM:SS" (24-hour)
- TIMER Double Seconds since midnight
+ TIMER Double Seconds since midnight, to a fraction of a millisecond
ENVIRON$(name$) String Value of the environment variable; empty string if not set
COMMAND$ String The arguments the program was started with; empty string if none
COMMAND$(n) String The nth argument (1 = first) without its quotes; empty string when there are fewer; COMMAND$(0) is the same as COMMAND$
@@ -3123,7 +3137,7 @@ Print Choose(Weekday(Date), "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat")
.h2 LOADPICTURE
-LOADPICTURE(file$) reads a picture file (BMP, PNG, JPEG, or GIF) -- or the icon of a DVX program, widget, or library file (.app, .wgt, .lib) -- and returns a Picture object (TypeName "Picture") that can be given to a picture property: a control's DragIcon, the Picture of an Image or ImageButton. Use SET, or a plain assignment, as with a file name; file names still work too. LOADPICTURE() or LOADPICTURE("") gives an empty picture, which clears the property. A missing file raises error 53 (File not found), a file that is not a picture error 481 (Invalid picture), so the check happens where the picture is loaded. Loading the same file again gives the same Picture object. Reading a picture property back gives a Picture object too (TypeName "Picture"), as in Visual Basic; used as a value (printed, joined to text, compared) it stands for its file name, so code that treats the property as a file name keeps working. The name of a program, widget, or library file works wherever a picture file's name does (a Picture property, DragIcon, PaintPicture, a Toolbar's AddButton) and shows its icon; such a file without an icon is not a picture (error 481).
+LOADPICTURE(file$) reads a picture file (BMP, PNG, JPEG, or GIF) -- or the icon of a DVX program, widget, or library file (.app, .wgt, .lib) -- and returns a Picture object (TypeName "Picture") that can be given to a picture property: a control's DragIcon, the Picture of an Image or ImageButton. Use SET, or a plain assignment, as with a file name; file names still work too. LOADPICTURE() or LOADPICTURE("") gives an empty picture, which clears the property. A missing file raises error 53 (File not found), a file that is not a picture error 481 (Invalid picture), so the check happens where the picture is loaded. The check reads the picture's header only (the control that shows it reads the rest): a picture file damaged after its header shows nothing. Loading the same file again gives the same Picture object. Reading a picture property back gives a Picture object too (TypeName "Picture"), as in Visual Basic; used as a value (printed, joined to text, compared) it stands for its file name, so code that treats the property as a file name keeps working. The name of a program, widget, or library file works wherever a picture file's name does (a Picture property, DragIcon, PaintPicture, a Toolbar's AddButton) and shows its icon; such a file without an icon is not a picture (error 481).
The optional size and colordepth arguments are Visual Basic's: size is vbLPSmall (0), vbLPLarge (1), vbLPSmallShell (2), vbLPLargeShell (3), or vbLPCustom (4, with x and y giving the size), colordepth vbLPDefault (0), vbLPMonochrome (1), vbLPVGAColor (2), or vbLPColor (3). They choose among the images of an icon file; every DVX picture is a single bitmap, so, as for a bitmap in Visual Basic, they are checked and otherwise make no difference. A value outside these, or a negative x or y, raises error 5.
@@ -3149,6 +3163,18 @@ Set pic = LoadPicture("logo.bmp")
Print Me.ScaleX(pic.Width, vbHimetric, vbPixels); "x"; Me.ScaleY(pic.Height, vbHimetric, vbPixels)
.endcode
+.h2 LOADPICTUREDATA
+
+LOADPICTUREDATA(bytes$) does what LOADPICTURE does for a picture held in a string rather than in a file: the bytes of a BMP, PNG, JPEG, or GIF file, such as HttpBody$ gives for a picture downloaded from the web, or ResGetData$ for a picture resource of a DVX program, widget, or library file (any of its resources, not only its icon). The Picture object it returns goes wherever a LOADPICTURE picture goes: the Picture of an Image, an ImageButton, or a PictureBox, a control's DragIcon, a form's Icon, and PaintPicture; reading such a property back gives the same object. Its Width, Height, and Type are a loaded picture's; used as a value it stands for "", since it has no file name. Bytes that are not a picture raise error 481 (Invalid picture); LOADPICTUREDATA("") gives an empty picture. Giving one to a control that shows no picture (a Label's Picture, for example) stops the program with an error.
+
+A Picture object made this way keeps its bytes until the program ends, and the same bytes give the same object, so loading a picture again costs nothing. To show many different pictures one after another (a slide show, pictures from the web), use the SetPictureData method of the Image, ImageButton, or PictureBox instead: Image1.SetPictureData bytes$ shows the picture without keeping it, and returns True, or False when the bytes are not a picture.
+
+.code
+Image1.Picture = LoadPictureData(HttpBody$(req))
+Set Label1.DragIcon = LoadPictureData(ResGetData$("mygame.app", "card")) ' resource.bas in the project
+If Not Picture1.SetPictureData(HttpBody$(req)) Then MsgBox "That is not a picture."
+.endcode
+
.h2 COMMAND$
COMMAND$ returns the program's arguments exactly as they were given, as in Visual Basic (QBasic turned them into capitals; DVX keeps them as they are). Arguments are separated by blanks; an argument with blanks in it is written in double quotes. COMMAND$(n) picks out one argument with its quotes removed, so a program that opens the document it was started with only needs:
@@ -3996,6 +4022,9 @@ Through a variable declared As Collection the members are checked when the progr
.index SQLFieldCount
.index SQLFreeResult
.index SQLError$
+.index SQLEscape$
+.index SQLFieldName$
+.index SQLFieldText$
.index SQLite
.h1 SQL Functions
@@ -4044,6 +4073,24 @@ ok = SQLExec(db, "DELETE FROM users WHERE id = 5")
Put text values in single quotes, as in the examples. Text in double quotes is read as a column name, so "Scott" in place of 'Scott' gives the error "no such column: Scott".
+.h3 SQLEscape$
+
+Returns text with every single quote doubled, the way SQL writes a quote inside a quoted value. Use it on text the user typed (or read from a file) before you put it between quotes in a statement: a name such as O'Brien would otherwise end the value early and break the statement.
+
+.code
+SQLExec db, "INSERT INTO users (name) VALUES ('" & SQLEscape$(name$) & "')"
+.endcode
+
+SQLExec runs every statement in sql$, one after another, before it returns.
+
+.h3 Long Statements
+
+A statement can take a while: one that changes many rows, CREATE INDEX on a big table, or the first SQLNext of a query that sorts or groups, which reads every row first. While a statement runs, DVX and the other programs keep going, and your program's events run as they do during DoEvents. Ending the program (End, or closing its last form) stops the statement, and what it changed is undone.
+
+An event that runs meanwhile cannot use the database the statement is running on: SQLExec, SQLQuery, and SQLNext return False or 0, and SQLError$ says "A statement is still running on this database". Its field functions return "" or 0. SQLFreeResult and SQLClose wait until the statement ends. Other databases can be used as always.
+
+Code in a control file (a control written in BASIC) does not wait like this: its statements run to the end.
+
.h3 SQLAffected
Returns the number of rows affected by the last INSERT, UPDATE, or DELETE.
@@ -4920,15 +4967,21 @@ Colors in the form RGB makes (&HBBGGRR, see Color Functions), as in Visual Basic
.index CommHandshake
.index CommRecvEncrypted
.index CommSetPlain
+.index CommOpenTcp
.index print.bas
.index PrintTextDialog
.index PrintPictureDialog
.index PrintPageSetup
.index net.bas
.index TcpConnect
+.index TcpConnectStart
.index TcpRecv$
+.index TcpPeek$
+.index NetState
+.index TcpSendSome
.index UdpSend
.index HostLookup$
+.index HostLookupStart
.index game.bas
.index GameKeyDown
.index GameKeyClear
@@ -5138,10 +5191,17 @@ Colors in the form RGB makes (&HBBGGRR, see Color Functions), as in Visual Basic
.index security.bas
.index SecRandom$
.index SecDhCreate
+.index SecDhStep
.index SecCipherCreate
.index SecSha256$
.index SecHmac$
+.index SecX25519$
+.index SecAeadLock$
+.index SecAeadUnlock$
+.index SecEd25519Sign$
+.index SecEd25519Check
.index Encryption
+.index Signatures
.index SerSet
.index SerLastError
.index CommHandshakeStep
@@ -5175,6 +5235,9 @@ Colors in the form RGB makes (&HBBGGRR, see Color Functions), as in Visual Basic
.index ShellRunningActivate
.index ShellRunningClose
.index ShellRunningKill
+.index ShellRunningPriority
+.index ShellRunDosWait
+.index ShellRunningSetPriority
.index ShellAppIsHung
.index ShellAppMemory
.index ShellAppId
@@ -5244,9 +5307,22 @@ Colors in the form RGB makes (&HBBGGRR, see Color Functions), as in Visual Basic
.index PkgInstallStart
.index PkgRunFor
.index PkgRemoveStart
-.index PkgBuild
-.index PkgSign
+.index PkgBuildStart
+.index PkgSignStart
.index PkgKeygen
+.index PkgOpenIn
+.index PkgKeysRows$
+.index PkgUntrustKey
+.index PkgVerifyStart
+.index PkgSignData$
+.index PkgCheckData
+.index PkgZipListStart
+.index PkgZipExtractStart
+.index PkgZipCreateStart
+.index PkgZipRows$
+.index PkgVersionCompare
+.index PkgNetOpenIn
+.index ZIP Files
.index json.bas
.index http.bas
.index pkgnet.bas
@@ -5254,6 +5330,7 @@ Colors in the form RGB makes (&HBBGGRR, see Color Functions), as in Visual Basic
.index Web requests
.index HttpOpen
.index HttpRunFor
+.index HttpSetBodyFile
.index JsonParse
.h1 Include Libraries
@@ -5265,7 +5342,7 @@ DVX ships a set of BASIC include files (.bas) in the sdk/include/basic directory
---- --------
commdlg.bas File Open/Save dialogs (DlgFileOpenIn$ and DlgFileSaveIn$ start in a folder you name), the folder dialog (DlgFolder$, for a Browse button), input dialogs, choice dialogs, save-prompt dialogs, the color dialog, the font dialog (DlgFont), message boxes with your own buttons (DlgButtons)
sql.bas SQLite database access (SQLOpen, SQLQuery, SQLNext, SQLField$, etc.)
- comm.bas Serial-port I/O (raw and packet) and optional encrypted links; port settings read back and changed, why a call failed, breaks, ring, and line errors (SerOpen, SerSet, SerLastError, CommOpen, CommHandshakeStep, ...)
+ comm.bas Serial-port I/O (raw and packet) and optional encrypted links; port settings read back and changed, why a call failed, breaks, ring, and line errors (SerOpen, SerSet, SerLastError, CommOpen, CommOpenTcp, CommHandshakeStep, ...)
conn.bas Connections the way Terminal makes them: a serial port, a modem that dials, the secure link to the DVX host proxy, or Telnet, shown on a Terminal control, with capture, answerback, and file transfers (ConnCreate, ConnOpen, ConnPoll, ConnAttach, ConnSendFiles, ...)
xfer.bas File transfers with XMODEM, XMODEM-1K, YMODEM, and ZMODEM, on a serial port or a connection, with their progress and questions (XferSendFile, XferStatus, XferBytes, XferAnswer, ...)
prefs.bas Settings files (INI): load one, read and change its settings, list the keys of a section, save it; DVX's own settings file, the screen mode it starts in, the names its choices are stored as and its default choices, and choices stored by names of your own (PrefsLoad, PrefsGet$, PrefsSet, PrefsKeyName$, PrefsSaveAs, PrefsDvxIni$, PrefsVideoWidth, PrefsChoiceLabel$, PrefsChoiceDefault, PrefsGetChoiceFrom, ...)
@@ -5275,7 +5352,7 @@ DVX ships a set of BASIC include files (.bas) in the sdk/include/basic directory
net.bas Network connections: TCP (TcpConnect, TcpListen, TcpSend, TcpRecv$, TcpPeer$, ...), UDP messages and name lookups, any bytes; the network settings and status (NetConfigLoad, NetConfigSet, NetConfigStore, NetRestart, NetStatus$, ...)
print.bas Printing: text with the page setup and pictures, with or without the Print dialog; a form; the Page Setup dialog; the printer and page settings; the printer ports (PrintModelLabel$, PrintSettingsStore, PrintPageSetting, PrintChoose, PrintForm, PrintPortStatus, ...)
listhelp.bas Lists and icon views you draw yourself (a UserControl, a PictureBox) that behave like ListBox and Program Manager: selection with Shift and Ctrl, the arrow keys in a list or a grid, Ctrl+A, Space, type-ahead, where a dragged item lands, and Program Manager's icon view rules -- its cells, the grid with a scroll bar, names on two lines, dragging and dropping (ListSelCreate, ListSelClick, ListSelNavigate, ListTypeAhead, ListIconMetric, ListIconLabel$, ...)
- security.bas Security: random bytes for keys, a key exchange, a cipher, SHA-256 hashes, HMAC signatures, key making, and a safe compare (SecRandom$, SecDhCreate, SecCipherCrypt$, SecSha256$, SecHmac$, SecHkdf$, ...)
+ security.bas Security: random bytes for keys, key exchanges, ciphers, sealing, SHA-256 hashes, HMAC and public-key signatures, key making, and a safe compare (SecRandom$, SecX25519$, SecAeadLock$, SecEd25519Sign$, SecSha256$, SecHkdf$, ...)
game.bas Games: whether a key is being held down right now, forgetting held keys, frames FRAMEWAIT skipped, waiting for the screen's retrace (GameKeyDown, GameKeyClear, FrameMissed, GameWaitRetrace)
joystick.bas Joysticks: how many there are, their names, where each stick points, and which buttons are pressed (JoyCount, JoyX, JoyY, JoyButton, ...); setting them up: calibration, dead zone, game port mode, keyboard joystick keys (JoySetCalibration, JoyCalibration, JoyRescan, JoyKeyIndex, JoySettingsStore, ...)
sound.bas Sound: play WAV and VOC files, tones and noise, set the volume (PlayWave, PlayTone, StopWave, SoundInfo$, ...); sounds loaded once and played with volume, pan, and pitch, sound made by the program as it goes, recording from a microphone or line input (RecordStart, RecordStop, RecordLevel, ...), the FM synthesizer chip, a wavetable card's voices and memory (SoundLoad, SoundPlay, SoundSetPitch, SoundCreate, SoundStreamOpen, SoundStreamWrite, FmAlloc, FmWrite, WtAlloc, WtStart, ...); DVX's own event sounds (SoundPlayEvent), master volume, sound driver, and MIDI settings (SoundEventFile$, SoundSetMasterVolume, SoundSettingsStore, ...)
@@ -5288,8 +5365,8 @@ DVX ships a set of BASIC include files (.bas) in the sdk/include/basic directory
compiler.bas The BASIC compiler: compile a project or files you give it, find compile errors in their files, list a program's procedures and the language's keywords, build an .app or a control file; put a project's UserControls on a form designer's toolbox; read source text as the compiler does (tokens, code or string or comment, renaming a control in code, WithEvents variables, class Events), F1 help topics; open, change, and save project files and UserControl headers (CompSourceFromProject, CompSourceAdd, CompCompile, CompError$, CompMapFile$, CompProcList$, CompBuildApp, CompDesignControls, CompTokens$, CompRenameInCode$, CompHelpTopic$, ProjOpen, ProjSave, ProjCtlSetHeader$, ...)
debug.bas Run a compiled program inside yours and debug it: breakpoints, stepping, its variables, expressions, Immediate-window lines, its PRINT output (RunCreate, RunStart, RunPoll, DbgSetBreakpoints, DbgStepOver, DbgLocals$, DbgEval$, DbgExec$, ...)
designer.bas Form designers: the control types, whether each holds other controls and can be placed, the names of new controls, default events, toolbox tooltips and icons, and each type's properties (with their types and value names), methods, and events (with each event's parameters); the names in .frm and class module text; .frm files as data to read, edit, and write, controls copied and pasted as text, and a loaded form written back as .frm text (CtlTypeCount, CtlTypeName$, CtlTypeFlags, CtlPropCount, CtlPropName$, CtlPropType, CtlEventList$, CtlEventParams$, CtlTypeEventParams$, FrmFormName$, FrmControlNames$, ClsClassName$, FrmOpen, FrmText$, FrmCtlAdd, FrmCtlText$, FrmCtlPaste, FrmMenuAdd, FormText$, ...)
- shell.bas DVX itself, everything Program Manager and Task Manager do: start programs (DVX's and DOS programs) and open documents, command lines (split into their parts) and the Run dialog, the installed programs and their names, file types, the running programs (find one, bring it to the front, ask it to close, or end it), this program's priority, keys that work in every program, Task Manager, system information, Exit DVX, the desktop and the program started after it, the DVX log; Program Manager's program groups with their items, shortcut keys, and icons (ShellRun, ShellRunProgram, ShellRunDos, ShellProgramScan, ShellAssocCount, ShellRunningId, ShellRunningActivate, ShellRunningClose, ShellArg$, ShellSetPriority, HotkeyAdd, ShellExitDvx, ShellGroupsLoad, ShellItemGet$, ShellItemIcon$, ...)
- pkg.bas Add-on packages: the packages installed in DVX and their details, files, and needs; reading .DPK files and planning what installing them takes; installing and removing as a job with progress; trusting a publisher's key; and making packages, signing them, and making keys (PkgOpen, PkgInstalledRows$, PkgOfferFile, PkgPlan, PkgInstallStart, PkgRunFor, PkgRemoveStart, PkgBuild, PkgSign, PkgKeygen, ...)
+ shell.bas DVX itself, everything Program Manager and Task Manager do: start programs (DVX's and DOS programs) and open documents, command lines (split into their parts) and the Run dialog, the installed programs and their names, file types, the running programs (find one, bring it to the front, ask it to close, or end it, and their priority), this program's priority, keys that work in every program, Task Manager, system information, Exit DVX, the desktop and the program started after it, the DVX log; Program Manager's program groups with their items, shortcut keys, and icons (ShellRun, ShellRunProgram, ShellRunDos, ShellProgramScan, ShellAssocCount, ShellRunningId, ShellRunningActivate, ShellRunningClose, ShellArg$, ShellSetPriority, HotkeyAdd, ShellExitDvx, ShellGroupsLoad, ShellItemGet$, ShellItemIcon$, ...)
+ pkg.bas Add-on packages: the packages installed in DVX and their details, files, and needs; reading .DPK files, installing, removing, making, and signing packages as jobs with progress, and planning what installing them takes; trusting a publisher's key; and making keys (PkgOpen, PkgInstalledRows$, PkgOfferFileStart, PkgPlan, PkgInstallStart, PkgRunFor, PkgRemoveStart, PkgBuildStart, PkgSignStart, PkgKeygen, ...)
json.bas JSON: read a document (a web service's reply, a settings file) and find values in it by path such as packages[3].version, and quote text for JSON of your own (JsonParse, JsonText$, JsonNumber, JsonCount, JsonKey$, JsonQuote$, ...)
http.bas Web requests: GET a page, a file, or a web service's answer, or POST to one, over the network a little at a time with progress, to memory or to a file (resuming one left part-way), following redirects (HttpOpen, HttpStart, HttpRunFor, HttpStatus, HttpBody$, HttpSaveTo, ...)
pkgnet.bas A package server: what it offers, what it has that is newer than what is installed, and fetching packages with what they need, for pkg.bas to install (PkgNetOpen, PkgNetCatalogue, PkgNetUpdates, PkgNetFetch, PkgNetRunFor, PkgNetRows$, ...)
@@ -5386,7 +5463,7 @@ End If
.h2 Resources in Files
-With resource.bas in the project, a program reads and writes the named resources DVX keeps at the end of program, widget, and library files: icons, text, and any bytes. ResOpen, ResCount, ResName$, ResType, and ResSize list them; ResGetText$ and ResAddText read and write text; ResAddFile and ResExtract copy a resource from or to a file; ResRemove takes one out. ResGetData$(file, name) gives every byte of a resource as a String (CHR$(0) included), and ResAddData file, name, type, bytes adds or replaces a resource of any bytes (type RES_TYPE_BINARY, RES_TYPE_TEXT, RES_TYPE_ICON, or RES_TYPE_FONT, a .DFN font the program carries; see Fonts). RES_NAME_ICON ("icon32") and RES_NAME_WIDGET_ICON ("icon24") are the names DVX gives a program's and a widget's icon; a name is at most RES_NAME_MAX - 1 characters.
+With resource.bas in the project, a program reads and writes the named resources DVX keeps at the end of program, widget, and library files: icons, text, and any bytes. ResOpen, ResCount, ResName$, ResType, and ResSize list them; ResGetText$ and ResAddText read and write text; ResAddFile and ResExtract copy a resource from or to a file; ResRemove takes one out. ResGetData$(file, name) gives every byte of a resource as a String (CHR$(0) included) -- LoadPictureData(ResGetData$(file, name)) shows a picture resource -- and ResAddData file, name, type, bytes adds or replaces a resource of any bytes (type RES_TYPE_BINARY, RES_TYPE_TEXT, RES_TYPE_ICON, or RES_TYPE_FONT, a .DFN font the program carries; see Fonts). RES_NAME_ICON ("icon32") and RES_NAME_WIDGET_ICON ("icon24") are the names DVX gives a program's and a widget's icon; a name is at most RES_NAME_MAX - 1 characters.
.code
Dim settings As String
@@ -5418,7 +5495,15 @@ End If
After the handshake, data that arrives unencrypted is ignored unless that channel is opened for it with CommSetPlain link, channel, True.
-CommHandshake waits until the key exchange is done. CommHandshakeStep(link, passphrase) does one step of it and returns at once, so a program can keep working: call it from a Timer until it returns something other than COMM_PENDING -- COMM_OK when the link is ready, else a COMM_ERR_ code. CommLastError(link) gives the COMM_ code of the last CommHandshake, CommHandshakeStep, CommSend, CommSetPlain, or CommPoll, which tells a wrong passphrase (COMM_ERR_AUTH) from a line that does not answer (COMM_ERR_TIMEOUT). CommDropped(link) counts arriving packets thrown away because they were tampered with or came unencrypted where that is refused. Channels are numbered 0 to COMM_MAX_CHANNEL.
+The same link runs over the network: with net.bas in the project too, CommOpenTcp(conn) opens it on a connection from TcpConnect or TcpAccept (both ends do), with every channel, the handshake, and encryption as on a COM port. The connection stays the program's: close it with TcpClose after CommClose.
+
+.code
+conn = TcpConnect("bbs.example.org", 2323)
+link = CommOpenTcp(conn)
+If CommHandshake(link, "shared secret") Then CommSend link, "hello", 5, True
+.endcode
+
+CommHandshake waits until the key exchange is done (up to 30 seconds), and CommSend waits while the link has as much in flight as it can; the program's events run while they wait, as they do in DoEvents. CommHandshakeStep(link, passphrase) does one step of it and returns at once, so a program can keep working: call it from a Timer until it returns something other than COMM_PENDING -- COMM_OK when the link is ready, else a COMM_ERR_ code. CommLastError(link) gives the COMM_ code of the last CommHandshake, CommHandshakeStep, CommSend, CommSetPlain, or CommPoll, which tells a wrong passphrase (COMM_ERR_AUTH) from a line that does not answer (COMM_ERR_TIMEOUT). CommDropped(link) counts arriving packets thrown away because they were tampered with or came unencrypted where that is refused. Channels are numbered 0 to COMM_MAX_CHANNEL.
.code
Sub Timer1_Timer ()
@@ -5550,7 +5635,7 @@ End Sub
.h2 Files and Folders
-With fileops.bas in the project, a program can show and change what is on the disks. FileOpsList(folder, filter) reads a folder once -- its folders and the files matching filter -- and returns a listing (a number; FileOpsListFree frees it). FileOpsListRows$ gives the whole listing as name, size, type, and date columns, ready for a ListView's SetRows, so even a folder of hundreds of files fills the list at once; FileOpsListSort re-sorts it by name, size, type, or date with folders first. FileOpsListName$, FileOpsListIsFolder, and FileOpsListSize read single entries, and FileOpsListFind finds one by name.
+With fileops.bas in the project, a program can show and change what is on the disks. FileOpsList(folder, filter) reads a folder once -- its folders and the files matching filter -- and returns a listing (a number; FileOpsListFree frees it). FileOpsListRows$ gives the whole listing as name, size, type, and date columns, ready for a ListView's SetRows, so even a folder of hundreds of files fills the list at once; FileOpsListSort re-sorts it by name, size, type, or date with folders first. FileOpsListName$, FileOpsListIsFolder, and FileOpsListSize read single entries, and FileOpsListFind finds one by name. FileOpsListFolders(listing) makes a new listing of just the folders in one, sorted by name, without reading the disk again (handy for a folder tree next to a file list).
Paths: FileOpsPathNormalize$ (the full path of an existing folder, or ""), FileOpsPathParent$, FileOpsPathJoin$, FileOpsPathSame, FileOpsPathIsRoot, FileOpsPathIsAbsolute (True for a path that is complete by itself, such as "C:\DOCS", False for one that counts from the current folder), FileOpsPathIsInside(path, folder) (True when path is folder itself or anywhere inside it: check it before copying or moving a folder, so it is never copied into itself), FileOpsPathSep$ (the character between folder names, "\" on DOS), FileOpsPathNative$ (a path written with that character, for showing it: "C:/DOCS/A.TXT" becomes "C:\DOCS\A.TXT"), FileOpsBaseName$, and FileOpsExtension$. Every one of them takes / and \ alike and gives back \. FileOpsFilterMatch(itemName, filter) tells whether a name matches a filter the way the file dialogs match it: wildcards * and ?, several patterns separated by ; such as "*.bmp;*.png", case not mattering.
@@ -5560,7 +5645,7 @@ Drives: FileOpsDriveList$ (the drive letters), FileOpsDriveType (FILEOPS_DRIVE_F
DVX's folders: FileOpsUserDir$() is the user folder (USER in the DVX folder, or the folder DVXUSER names), where everything DVX and its programs save goes; FileOpsUserPath$(name) is a file or folder in it, for example FileOpsUserPath$("DOWNLOAD"); FileOpsUserInTemp() is True when DVX runs from a disk it cannot write to and keeps the user folder in the TEMP folder. A program's own folder there is App.User. The fonts, sounds, themes, desktop pictures, MIDI instrument banks, and screen savers come in two folders each, DVX's own (and add-on packages') in SHARED and the user's in the user folder; the kinds are FILEOPS_KIND_FONTS, FILEOPS_KIND_SOUNDS, FILEOPS_KIND_THEMES, FILEOPS_KIND_WPAPER, FILEOPS_KIND_BANKS, and FILEOPS_KIND_SAVERS. An add-on's files are in a folder of its vendor's inside a kind's folder and are named VENDOR\NAME (for example ACME\SPACE.THM). FileOpsKindDir$(kind, user) is DVX's folder of a kind (user False) or the user's (True); FileOpsKindBrowseDir$(kind) is the one a file dialog starts in (the user's when it exists); FileOpsKindFind$(kind, fileName) finds a file by name, the user's first ("" when there is none); FileOpsKindName$(kind, path) is the name alone of a file in one of the two folders, the way settings store it ("" for any other path); FileOpsKindList(kind) lists both folders as one, sorted by name, a user's file hiding DVX's of the same name (a list for FileOpsListCount, FileOpsListName$, and the rest; FileOpsListFree when done). Programs read DVX's SHARED folder but never write in it.
-FileOpsCopyTree, FileOpsMoveTree, and FileOpsDeleteTree do a whole folder at once. For a long copy, move, or delete the program stays usable when it runs the work as a job, a little at a time from a Timer: FileOpsJobCreate, then FileOpsJobAddSource for each file or folder, then FileOpsJobRunFor from the timer until it returns FILEOPS_STEP_DONE or FILEOPS_STEP_CANCELLED. When it returns FILEOPS_STEP_ASK (a file already exists, a file is read-only) or FILEOPS_STEP_ERROR, show FileOpsJobMessage$ and pass the user's choice to FileOpsJobAnswer. FileOpsJobCurrent$, FileOpsJobFilesDone, FileOpsJobFilesTotal, FileOpsJobPercent, FileOpsJobBytesDone, and FileOpsJobBytesTotal show the progress, and FileOpsJobCancel stops the job. Files copied from a CD-ROM are read-only; FileOpsJobClearAttr job, FILEOPS_ATTR_READONLY before the job runs makes every copy changeable (FILEOPS_ATTR_EDITABLE clears all four attribute bits). Whatever a program leaves open is freed when it ends.
+FileOpsCopyTree, FileOpsMoveTree, and FileOpsDeleteTree do a whole folder in one call, as does FileOpsTreeSize. While one works, the rest of DVX goes on and so do the program's own events, as with DoEvents (in a control's code they wait until the call ends); if the program is ended meanwhile, the call stops and returns False. For a long copy, move, or delete with progress and questions, run the work as a job, a little at a time from a Timer: FileOpsJobCreate, then FileOpsJobAddSource for each file or folder, then FileOpsJobRunFor from the timer until it returns FILEOPS_STEP_DONE or FILEOPS_STEP_CANCELLED. When it returns FILEOPS_STEP_ASK (a file already exists, a file is read-only) or FILEOPS_STEP_ERROR, show FileOpsJobMessage$ and pass the user's choice to FileOpsJobAnswer. FileOpsJobCurrent$, FileOpsJobFilesDone, FileOpsJobFilesTotal, FileOpsJobFoldersTotal, FileOpsJobPercent, FileOpsJobBytesDone, and FileOpsJobBytesTotal show the progress, and FileOpsJobCancel stops the job. A FILEOPS_MEASURE job only counts: the files (FileOpsJobFilesTotal), the folders inside what was added (FileOpsJobFoldersTotal), and the bytes (FileOpsJobBytesTotal), changing nothing and asking nothing; FileOpsJobUnread says how many folders could not be read. A Properties dialog counts a big folder this way without stopping the program. Files copied from a CD-ROM are read-only; FileOpsJobClearAttr job, FILEOPS_ATTR_READONLY before the job runs makes every copy changeable (FILEOPS_ATTR_EDITABLE clears all four attribute bits). Whatever a program leaves open is freed when it ends.
.code
Dim job As Long
@@ -5593,7 +5678,7 @@ End Sub
With shell.bas in the project, a program works with DVX the way Program Manager does -- a whole desktop can be written in BASIC.
-Starting programs. ShellRun program, args starts a program (args is what it gets as COMMAND$), runs a DOS program, or opens a document in the program set for its type; it returns True when it started (or the DOS program will run). ShellRunProgram(program, args, folder) does the same the way a Program Manager item does: file names in args, and a document named without a folder, are looked for in folder first ("" for none), and it returns the new program's id (SHELL_RUN_DOS for a DOS program, 0 when it did not start; DVX has already said why). DOS programs are .EXE, .COM, and .BAT files (ShellIsDosProgram(path) says whether a file is one). ShellRunDos program, args, folder, pause runs one with DVX stepped aside: once your procedure has returned, DVX names the programs running besides the desktop, this one included, and asks them to close (each asks about unsaved work; No, or a program kept open, cancels the run); the screen then becomes the DOS screen until the program ends, and DVX comes back with the desktop. folder is where it runs ("" for its own folder); pause True shows "Press any key to return to DVX." when it ends. ShellMinimizeApp id runs that program minimized: its windows become icons, including the ones it opens while it starts. ShellRunCommand(commandLine) runs a whole command line (a program, in double quotes when its name has a blank, then its arguments) and returns the id too. ShellRunDialog$(history) shows DVX's Run dialog offering history (earlier commands, newest first, one per line) and returns what the user chose, "" for Cancel. ShellOpenDocument path opens a document in the program set for its type in the Control Panel.
+Starting programs. ShellRun program, args starts a program (args is what it gets as COMMAND$), runs a DOS program, or opens a document in the program set for its type; it returns True when it started (or the DOS program will run). ShellRunProgram(program, args, folder) does the same the way a Program Manager item does: file names in args, and a document named without a folder, are looked for in folder first ("" for none), and it returns the new program's id (SHELL_RUN_DOS for a DOS program, 0 when it did not start; DVX has already said why). DOS programs are .EXE, .COM, and .BAT files (ShellIsDosProgram(path) says whether a file is one). ShellRunDos program, args, folder, pause runs one with DVX stepped aside: once your procedure has returned, DVX names the programs running besides the desktop, this one included, and asks them to close (each asks about unsaved work; No, or a program kept open, cancels the run); the screen then becomes the DOS screen until the program ends, and DVX comes back with the desktop. folder is where it runs ("" for its own folder); pause True shows "Press any key to return to DVX." when it ends. ShellRunDosWait(program, args, folder, pause) does the same and waits for it, as SHELL does: your program is not asked to close, its events run meanwhile, and the DOS program's exit code (its ERRORLEVEL) comes back, -1 when it did not run. ShellMinimizeApp id runs that program minimized: its windows become icons, including the ones it opens while it starts. ShellRunCommand(commandLine) runs a whole command line (a program, in double quotes when its name has a blank, then its arguments) and returns the id too. ShellRunDialog$(history) shows DVX's Run dialog offering history (earlier commands, newest first, one per line) and returns what the user chose, "" for Cancel. ShellOpenDocument path opens a document in the program set for its type in the Control Panel.
Installed programs. ShellProgramScan(folder, skipPath) finds every program in folder and its subfolders ("" for the APPS folder) and returns a list: ShellProgramCount(list) programs, ShellProgramPath$(list, index) each one's path (index from 0), ShellProgramFree list when done (a list not freed is freed when the program ends). Give ShellAppPath$() as skipPath to leave this program out. ShellProgramName$(path) and ShellProgramDescription$(path) are the name and description a program shows, and ShellIsProgram(path) tells a program from a document. LOADPICTURE of a program gives its icon, so a PictureBox can show a grid of programs with PaintPicture. ShellProgramsFolder$() is the folder the installed programs are in (where ShellProgramScan("") looks), for example to start a Browse for a program there with DlgFileOpenIn$.
@@ -5673,30 +5758,47 @@ End Sub
.h2 Add-on Packages
-With pkg.bas in the project, a program installs, removes, and makes add-on packages -- the .DPK files Software installs -- the way Software and Package Builder (both written in BASIC) do. PkgOpen() starts a session (a number; PkgClose ends it), and PkgError$(session) says why the last call that failed did. Lists come back as rows for a ListView's SetRows: cells separated by TAB, each row ending in a line feed.
+With pkg.bas in the project, a program installs, removes, and makes add-on packages -- the .DPK files Software installs -- the way Software and Package Builder (both written in BASIC) do. PkgOpen() starts a session (a number; PkgClose ends it), and PkgError$(session) says why the last call that failed did. PkgOpenIn(root) starts one on another DVX folder (a copy being made ready on another disk, say): its installed packages and the keys its user folder trusts. Lists come back as rows for a ListView's SetRows: cells separated by TAB, each row ending in a line feed.
What is installed. PkgInstalledRows$ lists every installed package (DVX itself is kpunch.dvx): id, name, version, kind, and summary. A package is named by its id, vendor.name (acme.chess). PkgField$(s, id, field) gives one detail (PKG_FIELD_NAME, PKG_FIELD_VERSION, PKG_FIELD_PUBLISHER, PKG_FIELD_SIGNER_NAME, ...); PkgFilesRows$, PkgDependsRows$, and PkgNeededBy$ its files, what it needs, and what needs it. PkgFileOwner$(s, fileName) is the other way: the id of the package that put a file there (its name from the DVX folder, such as "SHARED\FONTS\KPUNCH\ARIMO.DFN"; "" when none did).
-Installing. PkgOfferFile(s, path) reads a package file and every package beside it (what it may need) as offers, numbered from 0, and returns the number of the one at path (-1 when it cannot be read). PkgOfferField$ and PkgOfferFilesRows$ describe an offer before it is installed; PKG_FIELD_TRUST says whether DVX trusts who signed it. PkgPlan(s, "0") plans installing it with everything it needs, checked against this DVX and the packages installed (False, with PkgError$ saying why, when it cannot be installed); PkgPlanRows$ lists what will go in, PkgPlanBytes the disk space, and PkgPlanRestart whether libraries or widgets are among them (they work from the next time DVX starts). PkgInstallStart(s) then starts the job, and PkgRunFor(s, milliseconds) runs it a little at a time, returning PKG_RUN_BUSY until it is PKG_RUN_DONE or PKG_RUN_FAILED (PkgRunMessage$ says why); PkgRunPercent and PkgRunCurrent$ show how far it is.
+Jobs. Reading package files, installing, removing, and making and signing packages each take a while (checking a signature alone takes a moment on a 486), so each is a job: a ...Start function starts it, and PkgRunFor(s, milliseconds) runs it a little at a time, returning PKG_RUN_BUSY until it is PKG_RUN_DONE or PKG_RUN_FAILED (PkgRunMessage$ says why); PkgRunPercent and PkgRunCurrent$ show how far it is. A session runs one job at a time.
+
+Installing. PkgOfferFileStart(s, path) reads a package file and every package beside it (what it may need) as offers, numbered from 0; the one at path is 0, and the job fails when it cannot be read. PkgOfferField$ and PkgOfferFilesRows$ describe an offer before it is installed; PKG_FIELD_TRUST says whether DVX trusts who signed it. PkgPlan(s, "0") plans installing it with everything it needs, checked against this DVX and the packages installed (False, with PkgError$ saying why, when it cannot be installed); PkgPlanRows$ lists what will go in, PkgPlanBytes the disk space, and PkgPlanRestart whether libraries or widgets are among them (they work from the next time DVX starts). PkgInstallStart(s) then starts installing.
.code
-offer = PkgOfferFile(s, path)
-If offer >= 0 And PkgPlan(s, CStr(offer)) Then
- If PkgInstallStart(s) Then
- Do
- rc = PkgRunFor(s, 60)
- pbJob.Value = PkgRunPercent(s)
- DoEvents
- Loop While rc = PKG_RUN_BUSY
+Function RunJob(s As Long) As Long
+ Dim rc As Long
+ Do
+ rc = PkgRunFor(s, 60)
+ pbJob.Value = PkgRunPercent(s)
+ DoEvents
+ Loop While rc = PKG_RUN_BUSY
+ RunJob = rc
+End Function
+
+If PkgOfferFileStart(s, path) Then
+ If RunJob(s) = PKG_RUN_DONE And PkgPlan(s, "0") Then
+ If PkgInstallStart(s) Then
+ rc = RunJob(s)
+ End If
End If
End If
.endcode
Removing. PkgDependents$(s, ids) lists the installed packages that need the ones going (they must go too), and PkgOrphans$(s, ids) those only they needed. PkgRemoveStart(s, ids, removeChanged) starts removing, run with PkgRunFor like an install; files changed since they were installed stay unless removeChanged, and PkgRunKept$ lists them.
-Trust. A publisher signs a package with a key so people can tell it came from them. PkgTrustKey(s, keyPath) trusts a publisher's key file (.KEY) from then on.
+Stopping. PkgRunCancel(s) stops a job after the package it is on: one still being unpacked is left as it was, and one whose files are already in is finished first, so keep calling PkgRunFor until it stops saying PKG_RUN_BUSY. A package being made or signed stops at once, and nothing of it is left behind.
-From a package server. With pkgnet.bas in the project too, a program gets packages from a package server the way Software does. PkgNetOpen("") returns a handle on the server DVX's settings name, with the account they give; PkgNetClose frees it. PkgNetServer$, PkgNetPort, PkgNetUser$, and PkgNetPassword$ read those settings (PkgNetUrl$ is the address they make), and PkgNetSaveSettings(server, port, user, password) changes them. A job is started with PkgNetCatalogue (every package the server offers), PkgNetUpdates (what it has that is newer than the packages installed, DVX itself among them), or PkgNetFetch(n, ids) (packages by id, with everything they need that is not installed), and run with PkgNetRunFor(n, milliseconds) until it is PKGNET_DONE or PKGNET_FAILED (PkgNetError$ says why); PkgNetPercent and PkgNetCurrent$ show how far a fetch is. PkgNetRows$ lists the answer (id, name, version, kind, summary, size, and the version installed, if any), and PkgNetField$ and PkgNetDependsRows$ describe one package in it. Each package fetched is checked against the size and fingerprint the server gives, and the server's signature of it comes with it, so it is trusted or not exactly as a package file is. What was fetched is in PkgNetDir$: PkgOfferFolder(s, folder) reads every package there as the offers, and PkgOfferIndex(s, id) gives the number of the one wanted, to plan and install as above; PkgNetClear removes them afterwards. With an account (the settings', or PkgNetAccount(n, email, password) for another server), PkgNetUpload(n, path) publishes a package file for the server's review, and PkgNetMine(n) asks what the account has sent: PkgNetMineRows$ lists it (id, name, version, state, and the review's note); both sign in first.
+Trust. A publisher signs a package with a key so people can tell it came from them. PkgTrustKey(s, keyPath) trusts a publisher's key file (.KEY) from then on. PkgKeysRows$(s) lists the keys DVX trusts (name, id, and "system" for DVX's own or "user" for one trusted this way), and PkgUntrustKey(s, keyId) stops trusting one of the user's.
+
+Checking a package. PkgVerifyStart(s, path, keyPath) checks a package file without installing it, a job run with PkgRunFor: every file in it against the size and fingerprint its manifest lists, so a damaged or changed package is found. With keyPath (a .KEY file) the package must also be signed by that key, in it or beside it, or nothing starts.
+
+Signing any data. PkgSignData$(s, bytes, secretPath) signs any bytes (a file read into a string, a message) with a secret key file, giving the signature as text; PkgCheckData(s, bytes, signature, keyPath) says whether a signature was made over those bytes by the key in keyPath, or with keyPath "" by a key DVX trusts.
+
+Plain ZIP files. A session also lists, unpacks, and makes ordinary .ZIP files, as jobs run with PkgRunFor: PkgZipListStart(s, zipPath) reads one to its end, every file in it checked; PkgZipExtractStart(s, zipPath, folder) unpacks it into folder (a name that would leave folder stops it); PkgZipCreateStart(s, zipPath, files) makes one from rows of a name in it and the file it comes from (as PkgFolderRows$ gives them). PkgZipRows$(s) lists what the last of them read or packed: name, size, date, and "folder" for a folder.
+
+From a package server. With pkgnet.bas in the project too, a program gets packages from a package server the way Software does. PkgNetOpen("") returns a handle on the server DVX's settings name, with the account they give; PkgNetClose frees it. PkgNetServer$, PkgNetPort, PkgNetUser$, and PkgNetPassword$ read those settings (PkgNetUrl$ is the address they make), and PkgNetSaveSettings(server, port, user, password) changes them. A job is started with PkgNetCatalogue (every package the server offers), PkgNetUpdates (what it has that is newer than the packages installed, DVX itself among them), or PkgNetFetch(n, ids) (packages by id, with everything they need that is not installed), and run with PkgNetRunFor(n, milliseconds) until it is PKGNET_DONE or PKGNET_FAILED (PkgNetError$ says why); PkgNetPercent and PkgNetCurrent$ show how far a fetch is. PkgNetRows$ lists the answer (id, name, version, kind, summary, size, and the version installed, if any), and PkgNetField$ and PkgNetDependsRows$ describe one package in it (PKG_FIELD_SHA256 is its file's fingerprint, PKG_FIELD_REPO_SIGNED whether the repository signed it). PkgNetOpenIn(server, root, downloadDir) works for another DVX folder and fetches into another folder. Each package fetched is checked against the size and fingerprint the server gives, and the server's signature of it comes with it, so it is trusted or not exactly as a package file is. What was fetched is in PkgNetDir$: PkgOfferFolderStart(s, folder) reads every package there as the offers (a job), and PkgOfferIndex(s, id) gives the number of the one wanted, to plan and install as above; PkgNetClear removes them afterwards. With an account (the settings', or PkgNetAccount(n, email, password) for another server), PkgNetUpload(n, path) publishes a package file for the server's review, and PkgNetMine(n) asks what the account has sent: PkgNetMineRows$ lists it (id, name, version, state, and the review's note); both sign in first.
.code
n = PkgNetOpen("")
@@ -5707,17 +5809,18 @@ If PkgNetFetch(n, "acme.chess") Then
DoEvents
Loop While rc = PKGNET_BUSY
End If
-If rc = PKGNET_DONE And PkgOfferFolder(s, PkgNetDir$(n)) > 0 Then
+If rc = PKGNET_DONE And PkgOfferFolderStart(s, PkgNetDir$(n)) Then
+ rc = RunJob(s)
offer = PkgOfferIndex(s, "acme.chess")
' plan and install offer as above, then PkgNetClear n
End If
.endcode
-Making packages. PkgBuild(s, template, files, out, secret) builds a package: template is a settings file with a [package] section (id, name, version, kind, and summary, publisher, author, licence, homepage, dvx as wanted) and a [depends] section (id = oldest version for each package it needs); files are rows of a name in the package (from the DVX folder, such as APPS\ACME\CHESS\CHESS.APP) and the file it is made from; secret is the secret key file (.SEC) to sign with, or "". PkgFolderRows$(s, folder, prefix) gives those rows for every file in a folder, and PkgKindFolder$(kind, id) the folder a package of that kind keeps its files in. PkgSign signs a package already built, PkgKeygen(s, name, base) makes a key pair (base.KEY to give out, base.SEC to keep private), and PkgKeyInfo$ reads a key file's name and id. PkgIdValid and PkgVersionValid check an id and a version before building; PkgKindName$ lists the kinds.
+Making packages. PkgBuildStart(s, template, files, out, secret) starts building a package, a job run with PkgRunFor like an install (PkgRunCurrent$ is the file it is on): template is a settings file with a [package] section (id, name, version, kind, and summary, publisher, author, licence, homepage, dvx as wanted) and a [depends] section (id = oldest version for each package it needs); files are rows of a name in the package (from the DVX folder, such as APPS\ACME\CHESS\CHESS.APP) and the file it is made from; secret is the secret key file (.SEC) to sign with, or "". PkgFolderRows$(s, folder, prefix) gives those rows for every file in a folder, and PkgKindFolder$(kind, id) the folder a package of that kind keeps its files in. PkgSignStart(s, path, secret, detached) signs a package already built: into it, a job run the same way, or beside it (NAME.SIG) when detached, done at once. PkgKeygen(s, name, base) makes a key pair (base.KEY to give out, base.SEC to keep private), and PkgKeyInfo$ reads a key file's name and id. PkgIdValid and PkgVersionValid check an id and a version before building; PkgVersionCompare(a, b) says which of two versions is newer (-1, 0, or 1: "1.10.0" is newer than "1.9.2"); PkgKindName$ lists the kinds.
.h2 Web Requests and JSON
-With http.bas in the project, a program asks web servers for pages, files, and web services' answers. HttpOpen(method, url) makes a request ("GET" to fetch, "POST" to send) and returns a handle; HttpClose frees it. Before starting it, HttpAddHeader adds a header, HttpSetBody gives a body to send with its type ("application/json", say), HttpSaveTo writes the answer's body to a file instead of keeping it (with keepPart True, a file left part-way is finished rather than started again), and HttpSetTimeout sets how many seconds without progress mean the server is not answering (30 to start with). HttpStart starts it (False for an address that is not http://; there is no encryption), and HttpRunFor(h, milliseconds) runs it a little at a time, returning HTTP_BUSY until it is HTTP_DONE or HTTP_FAILED:
+With http.bas in the project, a program asks web servers for pages, files, and web services' answers. HttpOpen(method, url) makes a request ("GET" to fetch, "POST" to send) and returns a handle; HttpClose frees it. Before starting it, HttpAddHeader adds a header, HttpSetBody gives a body to send with its type ("application/json", say), HttpSetBodyFile sends a file's bytes instead (a piece at a time, so a file of any size), HttpSaveTo writes the answer's body to a file instead of keeping it (with keepPart True, a file left part-way is finished rather than started again), and HttpSetTimeout sets how many seconds without progress mean the server is not answering (30 to start with). HttpStart starts it (False for an address that is not http://; there is no encryption), and HttpRunFor(h, milliseconds) runs it a little at a time, returning HTTP_BUSY until it is HTTP_DONE or HTTP_FAILED:
.code
h = HttpOpen("GET", "http://example.com/news.txt")
@@ -5735,7 +5838,7 @@ HttpClose h
HTTP_DONE means the server answered: HttpStatus is how (200 OK, 404 not found, ...), HttpBody$ what it sent, HttpReplyHeader$ one of its headers, and HttpUrl$ where the answer came from when the server sent the request elsewhere (redirects are followed). HTTP_FAILED means no answer came, and HttpError$ says why. HttpBytes and HttpTotal (-1 until the server says) show progress.
-With json.bas in the project, JsonParse(text) reads JSON -- what most web services answer with -- and returns a handle (0 when it is not JSON: JsonError$ says why and on which line); JsonFree frees it. The other functions find a value by its path from the top: member names and item numbers from 0, such as "packages[3].version". JsonType says what is there (JSON_STRING, JSON_NUMBER, JSON_ARRAY, ..., JSON_NONE for nothing), JsonText$ gives a string, JsonNumber a number, JsonBool True or False, JsonCount how many items an array or members an object has, and JsonKey$(h, path, i) the name of an object's member i. JsonQuote$(text) puts text in quotes with its special characters escaped, for building JSON to send:
+With json.bas in the project, JsonParse(text) reads JSON -- what most web services answer with -- and returns a handle (0 when it is not JSON: JsonError$ says why and on which line); JsonFree frees it. The other functions find a value by its path from the top: member names and item numbers from 0, such as "packages[3].version". JsonType says what is there (JSON_STRING, JSON_NUMBER, JSON_ARRAY, ..., JSON_NONE for nothing), JsonText$ gives a string, JsonNumber a number, JsonBool True or False, JsonCount how many items an array or members an object has, and JsonKey$(h, path, i) the name of an object's member i. JsonQuote$(text) puts text in quotes with its special characters escaped, for building JSON to send. Text comes and goes as DVX characters: an accented letter or a line-drawing character in a reply is the same character in JsonText$ ("?" stands for one DVX does not have), and JsonQuote$ writes such characters so that any web service reads them right:
.code
h = JsonParse(HttpBody$(req))
@@ -5766,7 +5869,7 @@ The pointer and the keyboard: SysMouseX() and SysMouseY() are where the mouse po
Memory: SysMemoryTotalKb() is the computer's memory in KB and SysMemoryFreeKb() how much of it is free (-1 when it cannot be told).
-Libraries: some parts of DVX are optional (Setup's "What to install" page; Software lists each as one package), and so are their libraries: Networking has dvxnet, http, and pkgnet (net.bas, http.bas, pkgnet.bas), Databases has dvxsql (sql.bas), and Communications has conn and xfer (conn.bas, xfer.bas). SysLibraryLoaded(libName) is True when the library a DECLARE LIBRARY line names (any case) is installed and was started with DVX. A program that can do without such a library asks first and leaves out what needs it: calling a function of a library that is not there stops the program with "External function not found". Software, for one, keeps its package server pages off without pkgnet.
+Libraries: some parts of DVX are optional (Setup's "What to install" page; Software lists each as one package), and so are their libraries: Networking has dvxnet, http, and pkgnet (net.bas, http.bas, pkgnet.bas), Databases has dvxsql (sql.bas), and Communications has conn and xfer (conn.bas, xfer.bas). SysLibraryLoaded(libName) is True when the library a DECLARE LIBRARY line names (any case) is installed (DVX loads a library when it is first needed, so asking loads it). A program that can do without such a library asks first and leaves out what needs it: calling a function of a library that is not there stops the program with "External function not found". Software, for one, keeps its package server pages off without pkgnet.
.code
If SysLibraryLoaded("pkgnet") Then
@@ -5915,6 +6018,26 @@ Sub Timer1_Timer ()
End Sub
.endcode
+A program can also hear of every change instead of reading the joystick: JoyWatch joy, True starts keeping that joystick's changes (False stops), and JoyEvent() gives the next one since it was last asked, each once and in order (the last 64 are kept): JOY_EVENT_AXIS (an axis moved), JOY_EVENT_BUTTON_DOWN, JOY_EVENT_BUTTON_UP, JOY_EVENT_HAT, or JOY_EVENT_NONE when nothing has changed. JoyEventJoy() and JoyEventIndex() say which joystick and which axis, button, or hat, and JoyEventValue() an axis's new position or a hat's direction. A button pressed and let go between two Timer ticks still gives both.
+
+.code
+Sub Form1_Load ()
+ ok = JoyWatch(1, True)
+End Sub
+
+Sub Timer1_Timer ()
+ Dim kind As Long
+
+ kind = JoyEvent()
+
+ Do While kind <> JOY_EVENT_NONE
+ If kind = JOY_EVENT_BUTTON_DOWN And JoyEventIndex() = 1 Then FireShot
+ If kind = JOY_EVENT_AXIS And JoyEventIndex() = 1 Then shipSpeed = JoyEventValue() \ 8192
+ kind = JoyEvent()
+ Loop
+End Sub
+.endcode
+
When no joystick is connected the keyboard stands in for one (the arrow keys, with Left Ctrl, Space, Left Shift, and Z as buttons), and it only answers while one of the program's windows is active. Joysticks are calibrated, and the keyboard joystick's keys chosen, on the Control Panel's Game Controllers tab. QBasic's STICK and STRIG work too, without joystick.bas.
joystick.bas can also set joysticks up, as the Control Panel does. JoyHats(joy) counts the hats, JoyAxisPresent(joy, axis) is True when an axis answers, JoyConnected(joy) is False once a joystick stops answering, and JoyCanCalibrate(joy) is True for one with analog axes. For calibrating, JoyRawAxis(joy, axis) gives an axis's raw reading and JoyRawButtons(joy) the buttons held (1 for button 1, 2 for button 2, 4 for button 3, ... added up); JoySetCalibration(joy, axis, minV, centerV, maxV) takes the readings at one end, the middle, and the other end, and JoyClearCalibration(joy, axis) lets the axis find its own range again. JoyDeadZone(joy) and JoySetDeadZone(joy, percent) are the dead zone round the middle (0 to 50 percent). JoyChoiceCount, JoyChoiceLabel$, JoyChoice, and JoySetChoice list and change the game port's mode (JOY_CHOICE_GAMEPORT) and when the keyboard joystick stands in (JOY_CHOICE_FALLBACK); JoyKeyCount and JoyKeyName$(index) list the keys the keyboard joystick can use, and JoyKbdSlot(slot) and JoySetKbdSlot(slot, keyIndex) its keys for up, down, left, right, and buttons 1 to 4 (slots 0 to 7). These lists are numbered from 0, so they fill a DropDown in order. JoyKeyIndex(keyName) gives a key's number by its name (-1 when unknown), JoyKeyCode(index) its raw key code, which GameKeyDown in game.bas takes, and JoyKeyNameOf$(rawCode) the name of a raw key code. JoyIsKeyboard(joy) is True for the keyboard standing in for a joystick, and JoyCalibration(joy, axis, part) reads back an axis's calibration: part JOY_CAL_MIN, JOY_CAL_CENTER, or JOY_CAL_MAX of the readings set with JoySetCalibration, or of the range the axis found by itself (0 for a joystick that cannot be calibrated). JoySetting$(key, default) and JoySetSetting key, value reach any other joystick setting; JOY_KEY_POLL_HZ is the key of how often joysticks are read, JOY_POLL_HZ_MIN to JOY_POLL_HZ_MAX times a second (JOY_POLL_HZ_DEFAULT normally), and dead zones go up to JOY_DEADZONE_MAX percent (JOY_DEADZONE_DEFAULT normally). After a new game port mode, JoyRescan() looks for joysticks again; it returns the new count, or -1 while another program is using a joystick. Every change applies at once; JoySettingsStore handle puts them all into a settings handle from prefs.bas for the program to save, and JoySettingsLoad handle takes them back from one, to undo.
@@ -5954,9 +6077,22 @@ End Sub
WAV files must be uncompressed (8 or 16 bits, mono or stereo); VOC files may be 8- or 16-bit. QBasic's BEEP, SOUND, and PLAY work too, without sound.bas.
-PlayTriangle(frequency, milliseconds, volume) plays a triangle tone, softer than PlayTone's square tone. SoundGetVolume() gives the volume of everything the program plays. SoundCaps(field) tells what the sound device can do, by name in any case: "dac" (it plays recorded sound), "synth" (it has a music synthesizer), "tone" (it plays tones on a tone generator), "running", and "record" (it records sound) are True or False; "voices" (how many sounds play at once), "rate" (hertz), "bits", "channels", and "recordsources" (the inputs it records from) are numbers. SoundPlayEvent eventIdx, wait plays one of DVX's own event sounds (SOUND_EVENT_DEFAULT, SOUND_EVENT_ERROR, SOUND_EVENT_WARNING, ...), the one the settings choose; with wait True it returns once the sound has played.
+PlayTriangle(frequency, milliseconds, volume) plays a triangle tone, softer than PlayTone's square tone. SoundGetVolume() gives the volume of everything the program plays. SoundCaps(field) tells what the sound device can do, by name in any case: "dac" (it plays recorded sound), "synth" (it has a music synthesizer), "tone" (it plays tones on a tone generator), "running", and "record" (it records sound) are True or False; "voices" (how many sounds play at once), "rate" (hertz), "bits", "channels", and "recordsources" (the inputs it records from) are numbers. SoundDriver$() is the short name of the sound driver playing ("sb", "speaker", ...; "" when none is), and SoundStats(field) what the mixer has done since the device started: "renders" (blocks of sound made), "frames" (samples made), "underruns" (stream samples that were due but not yet written), "clipped" (samples too loud), "dropped" (sounds dropped because too many played at once), and "playing" (voices playing now). SoundPlayEvent eventIdx, wait plays one of DVX's own event sounds (SOUND_EVENT_DEFAULT, SOUND_EVENT_ERROR, SOUND_EVENT_WARNING, ...), the one the settings choose; with wait True it returns once the sound has played (DVX and the program's events go on meanwhile, as in DoEvents; in a control file's code, and once the program is ending, it does not wait).
-A sound played often is best loaded once. SoundLoad(fileName) loads a WAV or VOC file and returns a sound number, or 0 (SoundError$() says why); SoundCreate(data, rate, bits, channels) makes a sound from raw sound data in a string: 8-bit samples (0 to 255, 128 is silence) or 16-bit ones (two bytes each, the low byte first), mono or stereo (left, right, left, ...), rate times a second (SOUND_RATE_MIN to SOUND_RATE_MAX). SoundPlay(sound, volume, pan, loopIt) plays it on a new voice and returns the voice number; pan goes from SOUND_PAN_LEFT (-100) through SOUND_PAN_CENTER (0) to SOUND_PAN_RIGHT (100). SoundLength(sound) is its length in milliseconds and SoundFree sound stops its voices and forgets it. While a voice plays, SoundSetVolume voice, volume, SoundSetPan voice, pan, and SoundSetPitch voice, percent change it (pitch is a percent of the recorded speed: SOUND_PITCH_NORMAL is 100, 200 is an octave higher, from SOUND_PITCH_MIN 25 to SOUND_PITCH_MAX 400), and SoundSetFrequency voice, hertz changes a tone's frequency. SoundPause voice holds a sound's voice where it is, silent, and SoundResume voice goes on from there (a tone cannot be paused). A program's sounds are freed when it ends.
+A sound played often is best loaded once. SoundLoad(fileName) loads a WAV or VOC file and returns a sound number, or 0 (SoundError$() says why); it reads and decodes the whole file before it returns, so for a long recording (a minute or more) use MusicLoad and MusicPlay (music.bas), which play a WAV or VOC file as they read it; SoundCreate(data, rate, bits, channels) makes a sound from raw sound data in a string: 8-bit samples (0 to 255, 128 is silence) or 16-bit ones (two bytes each, the low byte first), mono or stereo (left, right, left, ...), rate times a second (SOUND_RATE_MIN to SOUND_RATE_MAX). SoundLoadData(bytes) loads one from the bytes of a WAV or VOC file held in a string, such as ResGetData$ (resource.bas) or HttpBody$ (http.bas) gives. SoundPlay(sound, volume, pan, loopIt) plays it on a new voice and returns the voice number; pan goes from SOUND_PAN_LEFT (-100) through SOUND_PAN_CENTER (0) to SOUND_PAN_RIGHT (100). SoundLength(sound) is its length in milliseconds, SoundRate(sound), SoundBits(sound), and SoundChannels(sound) how it was recorded, and SoundFree sound stops its voices and forgets it. While a voice plays, SoundSetVolume voice, volume, SoundSetPan voice, pan, and SoundSetPitch voice, percent change it (pitch is a percent of the recorded speed: SOUND_PITCH_NORMAL is 100, 200 is an octave higher, from SOUND_PITCH_MIN 25 to SOUND_PITCH_MAX 400), and SoundSetFrequency voice, hertz changes a tone's frequency. SoundPause voice holds a sound's voice where it is, silent, and SoundResume voice goes on from there (a tone cannot be paused). A program's sounds are freed when it ends.
+
+To hear of every sound ending, call SoundEnded() from a Timer: it gives the next of the program's voices that ended since it was last asked (each once, in the order they ended; the last 64 are kept), or 0 when none has, and SoundEndReason() says why that one ended: SOUND_END_FINISHED (it played to its end), SOUND_END_STOPPED (StopWave, StopAllSounds, or its sound was freed), or SOUND_END_DROPPED (too many sounds played at once and it was the quietest).
+
+.code
+Sub Timer1_Timer ()
+ Dim v As Long
+ v = SoundEnded()
+ Do While v <> 0
+ If v = engine And SoundEndReason() = SOUND_END_FINISHED Then engine = SoundPlay(idle, 60, 0, True)
+ v = SoundEnded()
+ Loop
+End Sub
+.endcode
.code
Dim Shared laser As Long
@@ -5983,7 +6119,16 @@ Sub Timer1_Timer ()
End Sub
.endcode
-A program can record sound from the sound card's microphone, line, or CD input into a WAV file. SoundCaps("record") says whether the card can, and SoundCaps("recordsources") which inputs it has (the bit 2 ^ RECORD_MIC, RECORD_LINE, or RECORD_CD set for each). RecordStart(fileName, rate, bits, channels, inputSource) starts recording (0 for 22,050 Hz, 16-bit, mono) and returns True, or False with SoundError$ saying why; the card records at its nearest, which RecordRate(), RecordBits(), and RecordChannels() tell. While it records, RecordLength() is how long the recording is (milliseconds) and RecordLevel() the loudest sound since it was last asked (percent: a level meter); RecordLost() counts samples dropped because DVX was too busy. RecordStop() ends it and returns True when the file was all written; Recording() is True in between. Most sound cards record or play, not both, so nothing plays while a program records, and one program records at a time. A program's recording ends when the program does.
+A melody of tones -- a game's tune or a sound effect -- plays without a gap or a stumble in a note queue. NotesOpen(wave, volume) opens one in a waveform (SOUND_WAVE_SQUARE, SOUND_WAVE_TRIANGLE, or SOUND_WAVE_NOISE) and returns its number, or 0. NotesAdd(queue, hertz, milliseconds) adds a tone (0 hertz for a rest; fractions count) and returns True, or False when 256 are already waiting; each lasts exactly as long as asked, as long as the next is added before the one playing ends. NotesSetTone(queue, wave, volume) changes the waveform and volume (percent of the queue's) of the tones added from then on, so they change in time with the tune. NotesQueued(queue) is how many tones and rests have not finished (0 once it has gone quiet) and NotesSpace(queue) how many more fit; NotesVoice(queue) is its voice, for SoundSetVolume and SoundSetPan. NotesClear queue silences it and forgets what it holds, and NotesClose queue closes it. PLAY's V and @ commands give PLAY tunes a volume and a waveform too.
+
+.code
+q = NotesOpen(SOUND_WAVE_TRIANGLE, 80)
+For f = 200 To 800 Step 50
+ ok = NotesAdd(q, f, 15) ' a rising sweep, 15 ms a step
+Next f
+.endcode
+
+A program can record sound from the sound card's microphone, line, or CD input into a WAV file, or into memory. SoundCaps("record") says whether the card can, and SoundCaps("recordsources") which inputs it has (the bit 2 ^ RECORD_MIC, RECORD_LINE, or RECORD_CD set for each). RecordStart(fileName, rate, bits, channels, inputSource) starts recording (0 for 22,050 Hz, 16-bit, mono) and returns True, or False with SoundError$ saying why; the card records at its nearest, which RecordRate(), RecordBits(), and RecordChannels() tell. While it records, RecordLength() is how long the recording is (milliseconds) and RecordLevel() the loudest sound since it was last asked (percent: a level meter); RecordLost() counts samples dropped because DVX was too busy. RecordStop() ends it and returns True when the file was all written; Recording() is True in between. With a file name of "", RecordStart records into memory instead: RecordRead$(maxBytes) takes up to maxBytes of what was recorded and not yet taken, as raw sound data in the recording's format (as SoundCreate and SoundStreamWrite take it), and RecordQueued() is how many bytes are waiting. Take them often, from a Timer: about two seconds are kept, and what does not fit is lost (RecordLost()). A program can show the sound as it comes, change it, or send it over the network. Most sound cards record or play, not both, so nothing plays while a program records, and one program records at a time. A program's recording ends when the program does.
.code
If RecordStart("NOTE.WAV", 0, 0, 0, RECORD_MIC) Then
@@ -5994,7 +6139,7 @@ End If
On a computer with an FM synthesizer chip (AdLib and most Sound Blasters), a program can play instruments of its own. FmInfo$(field) describes the chip: "description" (like "OPL3 FM synthesizer at 388h", "" when there is none), "voices", "free" (voices no program has), "stereo", and "owned" (one program has the whole chip). FmAlloc() takes one of its two-operator voices (0 when none is free), FmWrite(voice, register, value) sets one of the voice's registers (the FM_ constants: FM_MOD_CHAR, FM_CAR_LEVEL, FM_FNUM_LOW, FM_KEY_BLOCK with FM_KEY_ON, ...; True when it worked), and FmFree voice gives it back. A player of chip music files can take the whole chip with FmRawAcquire() (False while another program has it), set any register with FmRawWrite(register, value) (bank * FM_BANK_REGS + register; the second bank only on an OPL3) and give it back with FmRawRelease. A frequency number for FM_FNUM_LOW and FM_KEY_BLOCK is hertz * 2 ^ 20 / (FM_CHIP_HZ * 2 ^ block).
-On a wavetable sound card (a Gravis UltraSound, a Sound Blaster AWE32 or AWE64), a program can play recorded sounds on the card's own voices from the card's memory. WtInfo$(field) describes the card: "description" ("" when there is none), "voices", "free" (voices no program has), "memory" and "freememory" (bytes), "level" (how many centibels louder a voice at full volume is than the loudest recorded sound the card plays: 0 when it is the same or not known), and what the card does itself ("envelopes", "lfos", "filter", "effects", "8bit", "rom"; True or False as text). WtMemAlloc(bytes, bits) takes memory for samples of 8 or 16 bits (its address, -1 when there is no room), WtUpload(address, pcm, bits) copies 16-bit mono samples there (a binary string, two bytes a sample; True when it worked), and WtMemFree address gives it back. WtAlloc() takes a voice (0 when none is free); WtStart(voice, address, startFrame, endFrame, loopStart, loopEnd, loopMode, bits, pitch, attenuation, pan) plays a sample on it from startFrame (0: its first; loopMode 0 plays once, 1 loops, 3 loops until WtRelease; pitch in cents, 0 for 44,100 samples a second and 1200 an octave higher; attenuation in centibels, 0 loudest and 60 half as loud; pan -100 to 100). WtSetPitch, WtSetAttenuation, and WtSetPan change a playing voice, WtStop silences it, WtActive(voice) is True while it sounds, and WtFree voice gives it back. Everything a program took is given back when it ends.
+On a wavetable sound card (a Gravis UltraSound, a Sound Blaster AWE32 or AWE64), a program can play recorded sounds on the card's own voices from the card's memory. WtInfo$(field) describes the card: "description" ("" when there is none), "voices", "free" (voices no program has), "memory", "freememory" (the largest free block), and "freetotal" (all free, in blocks maybe apart; bytes), "level" (how many centibels louder a voice at full volume is than the loudest recorded sound the card plays: 0 when it is the same or not known), and what the card does itself ("envelopes", "lfos", "filter", "effects", "8bit", "rom"; True or False as text). WtMemAlloc(bytes, bits) takes memory for samples of 8 or 16 bits (its address, -1 when there is no room), WtUpload(address, pcm, bits) copies 16-bit mono samples there (a binary string, two bytes a sample; True when it worked), and WtMemFree address gives it back. WtAlloc() takes a voice (0 when none is free); WtStart(voice, address, startFrame, endFrame, loopStart, loopEnd, loopMode, bits, pitch, attenuation, pan) plays a sample on it from startFrame (0: its first; loopMode 0 plays once, 1 loops, 3 loops until WtRelease; pitch in cents, 0 for 44,100 samples a second and 1200 an octave higher; attenuation in centibels, 0 loudest and 60 half as loud; pan -100 to 100); on a card with a General MIDI sample ROM (WtInfo$("rom")) the address may be WT_ADDR_ROM plus the bytes from the ROM's start. WtSetPitch, WtSetAttenuation, and WtSetPan change a playing voice, WtStop silences it, WtActive(voice) is True while it sounds, and WtFree voice gives it back. WtShape(voice, field, value) shapes the voice's notes as a SoundFont instrument does, one value by name, where the card can (WtInfo$ "envelopes", "lfos", "filter", "effects"): the volume envelope ("voldelay", "volattack", "volhold", "voldecay", "volsustain", "volrelease"), the modulation envelope ("moddelay" to "modrelease", "modenvpitch", "modenvfilter"), the modulation and vibrato LFOs ("modlfodelay", "modlfofreq", "modlfopitch", "modlfofilter", "modlfovolume", "viblfodelay", "viblfofreq", "viblfopitch"), the filter ("filterfc", "filterq"), and the effects ("reverb", "chorus"), in the units sound.bas lists. A value is kept for the voice's next WtStart; the filter, the effects, and the LFO depths also change a note that is sounding. Everything a program took is given back when it ends.
sound.bas also changes DVX's own sound settings, as the Control Panel's Sounds page does. DVX plays a sound for each of SoundEventCount() events, numbered from 0: SoundEventLabel$(eventIdx) names one ("Start DVX", "Error", ...) and SoundEventKey$(eventIdx) gives its key in DVX.INI. With a settings handle from prefs.bas (PrefsLoad(PrefsDvxIni$())), SoundEventFile$(handle, eventIdx) is the event's sound file ("" for none) and SoundSetEventFile handle, eventIdx, fileName changes it; after the program saves the file, SoundEventsReload makes DVX play the new sounds. SoundTestFile(fileName) plays a file the way DVX plays its event sounds (0 when it cannot, and SoundError$() says why), and SoundFolder$() is the folder of DVX's own sounds. SoundMasterVolume() and SoundSetMasterVolume percent are the volume of all sound from every program. SoundDriverCount() reads the list of sound drivers and SoundDriverName$(index) names each; SoundDriverAuto$() is the setting that means "find the best one". SoundMidiCount() and SoundMidiLabel$(index) list where MIDI music can play, SoundMidiInCount() and SoundMidiInLabel$(index) where MIDI input can come from, and SoundModulesCount() and SoundModulesLabel$(index) where music modules can play. SoundSettingDriver$(handle), SoundSettingMidi(handle), SoundSettingModules(handle), and SoundSettingVolume(handle) read the sound driver, MIDI choice, modules choice, and master volume from the settings, and SoundSettingsStore handle, driver, midi, modules, volume stores them (a new driver or MIDI choice takes effect when DVX next starts, a modules choice with the next music); SoundSettingMidiIn(handle) and SoundSettingMidiInStore handle, midiIn read and store the MIDI input choice (it takes effect when DVX next starts).
@@ -6015,7 +6160,7 @@ Next i
With music.bas in the project, a program plays music files: tracker modules (MOD, S3M, XM, and IT files, made of recorded instrument sounds) wherever the computer can play sampled sound (on a Gravis UltraSound's or Sound Blaster AWE's own voices, which takes the computer little time), MIDI files (.MID, types 0 and 1) on the computer's music synthesizer, or on a built-in synthesizer that sounds much the same when there is none, and WAV and VOC sound files wherever the computer can play sampled sound (a long WAV recording is read from the disk as it plays, so it needs no memory). Several programs can play music at once, and everything a program plays stops when it ends.
-MusicLoad(fileName) loads a music file and returns its music number, or MUSIC_FAILED (0) when it could not be loaded; MusicError$() then says why. MusicPlay(music, loopIt) plays it from where it is (the start after loading or stopping); with loopIt True it starts over at the end until it is stopped. MusicStop(music) stops and goes back to the start, MusicPause(music) and MusicResume(music) pause and go on, and MusicPlaying(music) is True while it plays. MusicVolume(music, volume) sets its volume in percent (0 to 100). MusicPosition(music) and MusicLength(music) give the place and the length in milliseconds, and MusicSeek(music, milliseconds) goes to a place (a module goes to the start of the part that holds it). MusicFree(music) stops a music and forgets it.
+MusicLoad(fileName) loads a music file and returns its music number, or MUSIC_FAILED (0) when it could not be loaded; MusicError$() then says why. MusicLoadData(bytes) does the same with the bytes of a music or sound file held in a string (from ResGetData$ or HttpBody$, say). MusicPlay(music, loopIt) plays it from where it is (the start after loading or stopping); with loopIt True it starts over at the end until it is stopped. MusicStop(music) stops and goes back to the start, MusicPause(music) and MusicResume(music) pause and go on, and MusicPlaying(music) is True while it plays. MusicEnded(music) is True once after the music played to its end (not when it was stopped) since MusicPlay started it: a Timer that asks it can start the next tune on time. MusicVolume(music, volume) sets its volume in percent (0 to 100). MusicPosition(music) and MusicLength(music) give the place and the length in milliseconds, and MusicSeek(music, milliseconds) goes to a place (a module goes to the start of the part that holds it). MusicFree(music) stops a music and forgets it.
MusicLoadMacro(macro) makes a music from a music macro string, the kind PLAY takes, with its notes on the General MIDI instruments its I commands name (the piano before any I): the tune can then be played in the background, paused, stopped, and made quieter like any music. MIDI music plays on the instruments chosen in Control Panel; MusicSetBank music, bank plays one music on another instrument bank, named as in the banks folders ("" is DVX's standard bank), before the music first plays (MusicError$ says when the bank is missing or damaged). A SoundFont (.SF2 or .SBK) gives the recorded instruments for a wavetable card or the built-in SoundFont synthesizer, as can a Gravis UltraSound's own patches: MusicPatchSet$() names them (their ULTRASND.INI, "" when there are none), and MusicRomBank$() names the instruments built into a Sound Blaster AWE card ("" when Creative's software is not installed).
@@ -6129,16 +6274,24 @@ End Sub
.table
Function Description
-------- -----------
- HostLookup$(hostName) The address of a name, such as "192.0.2.10", or "" when it is not found.
+ HostLookup$(hostName) The address of a name, such as "192.0.2.10", or "" when it is not found. Waits for the answer; other programs keep running.
+ HostLookupStart(hostName) The same without waiting: a lookup handle (0 when too many are under way).
+ HostLookupPoll(lookup) NET_BUSY while the lookup goes on, NET_DONE once found, NET_FAILED (NetError$ says why).
+ HostLookupResult$(lookup) The address found, once HostLookupPoll says NET_DONE.
+ HostLookupClose lookup Free a lookup handle (or give the lookup up).
HostName$(address) The name of an address; the address itself when no name is known for it.
NetAddress$() This computer's address ("" while it has none).
NetError$() Why the last call failed, in plain words.
TcpConnect(host, port) Connect; a handle, or 0. Waits up to 20 seconds; other programs keep running.
+ TcpConnectStart(host, port) The same without waiting: the connection's handle at once (0 when it cannot start).
+ TcpConnectPoll(handle) NET_BUSY while the host is looked up and the connection made, NET_DONE once it is (the handle then works as TcpConnect's), NET_FAILED (close it).
TcpListen(port) Wait for connections on port; a listening handle, or 0.
TcpAccept(handle) A connection that arrived on a listening handle, or 0 when none is waiting.
- TcpSend(handle, bytes) Send text or any other bytes; the bytes sent, or -1.
+ TcpSend(handle, bytes) Send text or any other bytes; the bytes sent, or -1. Waits while the connection's buffer is full.
+ TcpSendSome(handle, bytes) Send what the connection takes now, never waiting: the bytes taken from the start (0 while it is full), or -1.
TcpPending(handle) Bytes waiting to be read (-1 for a bad handle).
TcpRecv$(handle) What has arrived (up to 4096 bytes), or "".
+ TcpPeek$(handle) What has arrived (up to 4096 bytes), left waiting for the next TcpRecv$: look for a line's end or a message's length first.
TcpConnected(handle) 1 while the connection is open, 0 once the other side closed it.
TcpShutdown(handle) Tell the other side nothing more will be sent; what it sends can still be read.
TcpPeer$(handle) "address:port" of the other side ("" when not connected).
@@ -6155,6 +6308,29 @@ End Sub
The strings sent and received may hold any bytes, CHR$(0) too, so binary protocols work as well as text. Every connection a program opens is closed for it when the program ends.
+HostLookup$, TcpConnect, and TcpSend wait until they are done. Other programs keep running meanwhile, but yours does not: its windows do not answer, and the first lookup after DVX starts can also wait for the network to get its settings (DHCP). To stay responsive, start the lookup or connection and check on it from a Timer:
+
+.code
+Dim conn As Long
+
+Sub cmdConnect_Click ()
+ conn = TcpConnectStart("bbs.example.org", 23)
+ Timer1.Enabled = conn <> 0
+End Sub
+
+Sub Timer1_Timer ()
+ Select Case TcpConnectPoll(conn)
+ Case NET_DONE
+ Timer1.Enabled = False
+ lblStatus.Caption = "Connected"
+ Case NET_FAILED
+ Timer1.Enabled = False
+ lblStatus.Caption = "Could not connect: " & NetError$()
+ TcpClose conn
+ End Select
+End Sub
+.endcode
+
A program can also edit the network settings the way the Control Panel's Network page does, and show the network's state. The settings are kept in DVX's settings file, so NetConfigLoad and NetConfigStore take a handle from prefs.bas (add prefs.bas to the project too). NetConfigLoad gives a settings handle (0 when none is free); read and change its fields by name, check them, store them, and restart the network only when they changed:
.code
@@ -6198,6 +6374,7 @@ End Sub
NetVectorCount() / NetVector(index) The packet driver interrupts to choose from: index 0 is 0 (find it), then each one the settings accept.
NetRestart() Stop the network and start it with the saved settings. True when it is running.
NetStop Stop the network (every program's connections are closed) until NetRestart.
+ NetState() The network's state as a number: NET_STATE_STOPPED, NET_STATE_NO_DRIVER (no network card driver found), NET_STATE_CONFIGURING (waiting for an address from the network), or NET_STATE_UP.
NetStatus$(field) The network's state as text: "message" (in plain words), "up" and "link" ("-1" / "0"), "dhcp", "address", "netmask", "gateway", "dns1", "dns2", "hostname", "driver" (the network card's driver), "mac" (its hardware address), "vector", "sockets", "rxpackets", "rxbytes", "rxdropped", "txpackets", "txbytes". "" for an unknown field.
.endtable
@@ -6239,7 +6416,7 @@ End Sub
.h2 File Transfers
-With xfer.bas in the project, a program sends and receives files with XMODEM, XMODEM-1K, YMODEM, or ZMODEM. A transfer is a number: XferSendFile and XferReceive start one on a serial port opened with SerOpen (comm.bas), and ConnSendFiles and ConnReceiveFiles start one on a connection (conn.bas), with the options Terminal has: escaping every control character, asking the receiver to continue interrupted files, and what to do with a file that is already there. On a serial port XferPoll moves the transfer along; on a connection ConnPoll does. XferStatus tells where it is (XFER_RUNNING until it ends with XFER_DONE, XFER_FAILED, or XFER_CANCELLED), and XferBytes, XferSize, XferFile$, XferFileIndex, XferFileCount, XferBatchBytes, XferBatchSize, XferResumedAt, and XferErrors show its progress; XferMessage$ says how it ended. A receive with XFER_EXIST_ASK stops in XFER_ASKING when a file already exists: XferAskSize, XferAskRemoteSize (the size the sender offers, -1 when it did not say), and XferCanResume describe it, and XferAnswer continues with an XFER_EXIST_ choice. XferCancel stops a transfer and XferClose frees it.
+With xfer.bas in the project, a program sends and receives files with XMODEM, XMODEM-1K, YMODEM, or ZMODEM. A transfer is a number: XferSendFile and XferReceive start one on a serial port opened with SerOpen (comm.bas), and ConnSendFiles and ConnReceiveFiles start one on a connection (conn.bas). XferSendFiles and XferReceiveFiles (a serial port) and the Conn ones take the options Terminal has: a batch of files (one path per line), escaping every control character, asking the receiver to continue interrupted files, and what to do with a file that is already there. On a serial port XferPoll moves the transfer along; on a connection ConnPoll does. XferStatus tells where it is (XFER_RUNNING until it ends with XFER_DONE, XFER_FAILED, or XFER_CANCELLED), and XferBytes, XferSize, XferFile$, XferFileIndex, XferFileCount, XferBatchBytes, XferBatchSize, XferResumedAt, and XferErrors show its progress; XferMessage$ says how it ended. A receive with XFER_EXIST_ASK stops in XFER_ASKING when a file already exists: XferAskSize, XferAskRemoteSize (the size the sender offers, -1 when it did not say), and XferCanResume describe it, and XferAnswer continues with an XFER_EXIST_ choice. XferCancel stops a transfer and XferClose frees it.
XferCrc16(bytes) and XferCrc32(bytes) give the check values the protocols use (CRC-16 as XMODEM computes it, 0 to 65535; the CRC-32 of ZMODEM and ZIP files as a Long, negative when its top bit is set) for any bytes. XferSafeName$(name, folder) gives the name a receive into folder stores a sender's file name as: no folder or device name; a long name, its case kept, where folder's disk takes long names (a character a name cannot hold becomes _), else an 8.3 name of characters every disk accepts.
@@ -6321,11 +6498,20 @@ With security.bas in the project, a program has the building blocks of DVX's enc
Function Description
-------- -----------
SecRandom$(count) count random bytes, good for keys and salts (RND is not). SecAddEntropy bytes stirs something unpredictable (key timings, mouse positions) into the generator.
- SecDhCreate() A key exchange with fresh keys (a few seconds on a slow computer); a handle, or 0.
+ SecX25519PublicKey$(secretKey) The public key to send the other side, for a secret key of SEC_X25519_KEY_SIZE random bytes (SecRandom$).
+ SecX25519$(secretKey, otherPublicKey) The secret both sides now share, which nobody listening can work out ("" for a key the other side should not have sent). About a sixth of a second on a slow computer. Make keys from it with SecHkdf$.
+ SecX25519Start(secretKey, otherPublicKey) The same without the wait: SecX25519Step(h) then does a ninth of the work a call, returning SEC_X25519_PENDING until it is done (SEC_OK); SecX25519End$(h) gives the secret (with otherPublicKey "", the public key) and frees the handle.
+ SecAeadLock$(key, nonce, extra, bytes) bytes sealed: encrypted with a SEC_AEAD_KEY_SIZE-byte key and a SEC_AEAD_NONCE_SIZE-byte nonce, followed by a tag that proves them, and extra (sent in the clear), unchanged. Use a new nonce for every message: SecRandom$(SEC_AEAD_NONCE_SIZE) is safe.
+ SecAeadUnlock$(key, nonce, extra, sealed) What SecAeadLock$ sealed, or "" when anything was changed (or the key, nonce, or extra differs).
+ SecEd25519KeyPair$(seed) A signing key made from SecRandom$(SEC_ED25519_SEED_SIZE); keep it private. SecEd25519PublicKey$(key) is the key to give out.
+ SecEd25519Sign$(key, bytes) The signature of bytes; SecEd25519Check(signature, publicKey, bytes) is True only for exactly those bytes signed with that key.
+ SecDhCreate() The older key exchange (Diffie-Hellman), with fresh keys (a few seconds on a slow computer); a handle, or 0.
+ SecDhCreateStart() The same without the wait: SecDhStep(dh) then computes the keys a slice at a time, returning SEC_DH_PENDING until they are ready (SEC_OK).
SecDhPublicKey$(dh) This side's public key (SEC_DH_KEY_SIZE bytes), to send to the other side.
SecDhComputeSecret(dh, otherPublicKey) Combine with the other side's public key: SEC_OK, or SEC_ERR_PARAM for a key that is not acceptable.
+ SecDhComputeStart(dh, otherPublicKey) The same a slice at a time: SEC_DH_PENDING (or SEC_ERR_PARAM at once), then SecDhStep as above.
SecDhSecret$(dh) The secret both sides now share, which nobody listening can work out. Make keys from it with SecHkdf$; never use it as a key itself. SecDhFree dh frees the handle.
- SecCipherCreate(key) A cipher with a SEC_XTEA_KEY_SIZE-byte key; 0 for a key of another length.
+ SecCipherCreate(key) The older cipher (XTEA), with a SEC_XTEA_KEY_SIZE-byte key; 0 for a key of another length. It does not show tampering: add SecHmac$, or seal instead.
SecCipherCrypt$(cipher, bytes) Encrypt -- or decrypt, the same call -- bytes. SecCipherSetCounter cipher, n sets the position in the key stream; the same key and position must never encrypt two different messages. SecCipherFree cipher frees it.
SecSha256$(bytes) The SHA-256 hash (SEC_SHA256_SIZE bytes). For data in pieces: SecSha256Open, SecSha256Add h, piece, then SecSha256End$(h), which also frees the handle.
SecHmac$(key, bytes) The HMAC-SHA256 signature of bytes with key; SecHmacOpen(key), SecHmacAdd, and SecHmacEnd$ for pieces.
@@ -6334,16 +6520,15 @@ With security.bas in the project, a program has the building blocks of DVX's enc
.endtable
.code
-' Both sides: make keys, swap public keys (here over a TCP connection), agree a cipher key
-Dim dh As Long, cipher As Long, key As String
-dh = SecDhCreate()
-TcpSend conn, SecDhPublicKey$(dh)
-' ... receive the other side's key into theirs ...
-If SecDhComputeSecret(dh, theirs) = SEC_OK Then
- key = SecHkdf$("", SecDhSecret$(dh), "chat cipher", SEC_XTEA_KEY_SIZE)
- cipher = SecCipherCreate(key)
-End If
-SecDhFree dh
+' Both sides: make keys, swap public keys (here over a TCP connection), agree a sealing key
+Dim mine As String, theirs As String, key As String, nonce As String
+mine = SecRandom$(SEC_X25519_KEY_SIZE)
+TcpSend conn, SecX25519PublicKey$(mine)
+' ... receive the other side's public key into theirs ...
+key = SecHkdf$("", SecX25519$(mine, theirs), "chat", SEC_AEAD_KEY_SIZE)
+' Send a message: the nonce, then the sealed bytes
+nonce = SecRandom$(SEC_AEAD_NONCE_SIZE)
+TcpSend conn, nonce & SecAeadLock$(key, nonce, "", "Hello")
.endcode
Handles a program leaves open are freed, and their keys wiped, when it ends.
@@ -6376,7 +6561,7 @@ cboAccel.ListIndex = PrefsChoiceIndexOfValue(PREFS_CHOICE_ACCEL, SysMouseAccel()
.h2 Help Files
-With help.bas in the project, HelpCompile(source, output) compiles a help source file (.dhs) into a help file (.hlp), True when it worked; HelpCompileMessages$() gives the compiler's errors and warnings, one per line, each with the file and line ("error: NOTES.DHS:1: directive .h1 outside of a topic"). HelpView file opens a help file in the DVX Help Viewer (a file named without a folder is looked for beside the program; "" opens the DVX user guide). HelpViewTopic file, topicId opens it at a topic, the way F1 does. A Help Viewer that is already open shows the file in its window instead of a second copy starting.
+With help.bas in the project, HelpCompile(source, output) compiles a help source file (.dhs) into a help file (.hlp), True when it worked; its .image pictures are looked for in the source's folder. HelpCompileMessages$() gives the compiler's errors and warnings, one per line, each with the file and line ("error: NOTES.DHS:1: directive .h1 outside of a topic"). While a big file compiles, the rest of DVX and the program's own events go on, as with DoEvents: one compile runs at a time, so a second HelpCompile meanwhile returns False at once, and the compile stops if the program ends. HelpView file opens a help file in the DVX Help Viewer (a file named without a folder is looked for beside the program; "" opens the DVX user guide). HelpViewTopic file, topicId opens it at a topic, the way F1 does. A Help Viewer that is already open shows the file in its window instead of a second copy starting.
With hlpfile.bas, a program reads help files itself -- everything a help viewer needs. HlpOpen(path) opens a help file and returns a handle (0 when it is not one); HlpClose closes it (a program's open files are closed when it ends). Topics are numbered from 0: HlpTopicCount, HlpTopicTitle$ and HlpTopicId$ (the name links and HelpTopic use), HlpFindTopic(handle, topicId) (-1 when there is none), and HlpStartTopic(handle, topicId), the topic to show first (topicId when the file has it, else the file's own start page). HlpTocCount, HlpTocTitle$, HlpTocDepth, HlpTocTopic, and HlpTocExpanded give the table of contents in order -- a depth of 0 is the top level, a topic of -1 a heading that only groups others -- ready to fill a TreeView. HlpIndexCount, HlpIndexKeyword$, and HlpIndexTopic give the keyword index (sorted), for a list or DlgChoice. HlpSearch(handle, words) searches the titles, text, and keywords as the Help Viewer does and returns how many topics have every word; HlpSearchTopic(handle, n) gives them.
diff --git a/src/apps/kpunch/dvxbasic/runtime/basErrors.h b/src/apps/kpunch/dvxbasic/runtime/basErrors.h
index 801feb8e..33f72527 100644
--- a/src/apps/kpunch/dvxbasic/runtime/basErrors.h
+++ b/src/apps/kpunch/dvxbasic/runtime/basErrors.h
@@ -46,6 +46,7 @@
#define BAS_ERR_DIV_BY_ZERO 11
#define BAS_ERR_TYPE_MISMATCH 13
#define BAS_ERR_OUT_OF_STRING 14
+#define BAS_ERR_INTERRUPTED 18 // User interrupt occurred: code stopped for running too long (basVmSetRunLimit)
#define BAS_ERR_RESUME_WITHOUT_ERR 20
#define BAS_ERR_FOR_NESTING 26
#define BAS_ERR_OUT_OF_STACK 28
diff --git a/src/apps/kpunch/dvxbasic/runtime/gfx.c b/src/apps/kpunch/dvxbasic/runtime/gfx.c
index 9bd60a35..ba0686f8 100644
--- a/src/apps/kpunch/dvxbasic/runtime/gfx.c
+++ b/src/apps/kpunch/dvxbasic/runtime/gfx.c
@@ -70,6 +70,8 @@ typedef struct {
uint32_t border; // pixel the fill stops at
const BasGfxTileT *tile; // NULL: colour fill
uint8_t *done; // tile fill: one bit per view pixel
+ uint32_t *row; // a row of the view read at once (rowRead)
+ uint32_t *near; // and the run above or below it
} GfxFloodT;
// An ellipse outline (CIRCLE): its centre and radii in pixels, the arc
@@ -133,7 +135,7 @@ static int32_t drawNumber(const char *s, int32_t len, int32_t *pos, bool allowS
static void ellipse(BasGfxT *g, const GfxEllipseT *e);
static void ellipseFill(BasGfxT *g, int32_t cx, int32_t cy, int32_t rx, int32_t ry, uint32_t pixel);
static void ellipsePoint(BasGfxT *g, const GfxEllipseT *e, int32_t dx, int32_t dy);
-static bool fillable(const GfxFloodT *f, int32_t x, int32_t y);
+static bool fillable(const GfxFloodT *f, uint32_t pixel, int32_t x, int32_t y);
static void fillRect(BasGfxT *g, int32_t x0, int32_t y0, int32_t x1, int32_t y1, uint32_t pixel, int32_t style);
static void fillSpan(BasGfxT *g, int32_t y, int32_t x0, int32_t x1, uint32_t pixel, int32_t style);
static int32_t floodFill(BasGfxT *g, int32_t x, int32_t y, uint32_t paint, uint32_t border, const BasGfxTileT *tile);
@@ -151,6 +153,9 @@ static int32_t penSize(const BasGfxT *g);
static void plot(BasGfxT *g, int32_t x, int32_t y, uint32_t pixel);
static void plotPixel(BasGfxT *g, int32_t x, int32_t y, uint32_t pixel);
static void pointArg(const BasGfxT *g, BasValueT x, BasValueT y, bool step, double *outX, double *outY);
+static void rowRead(const BasGfxT *g, int32_t x0, int32_t x1, int32_t y, uint32_t *out);
+static int32_t rowsBottom(const BasGfxT *g);
+static int32_t rowsTop(const BasGfxT *g);
static double scaleLen(const BasGfxT *g, double len);
static bool shapeFilled(const BasGfxT *g);
static double spanX(const BasGfxT *g);
@@ -335,6 +340,7 @@ void basGfxInit(BasGfxT *g, const BasGfxSurfaceT *surf, uint32_t fore, uint32_t
g->surf = *surf;
g->viewX1 = surf->width - 1;
g->viewY1 = surf->height - 1;
+ g->bandY1 = BAS_GFX_BAND_ALL;
g->fore = fore;
g->back = back;
g->fillStyle = BAS_GFX_FILL_TRANSPARENT;
@@ -1174,9 +1180,8 @@ static void ellipsePoint(BasGfxT *g, const GfxEllipseT *e, int32_t dx, int32_t d
}
-static bool fillable(const GfxFloodT *f, int32_t x, int32_t y) {
- uint32_t pixel = f->g->surf.get(f->g->surf.ctx, x, y);
-
+// Whether the flood takes the pixel at (x, y), whose value is pixel.
+static bool fillable(const GfxFloodT *f, uint32_t pixel, int32_t x, int32_t y) {
if (pixel == f->border) {
return false;
}
@@ -1202,8 +1207,8 @@ static void fillRect(BasGfxT *g, int32_t x0, int32_t y0, int32_t x1, int32_t y1,
return;
}
- top = top < g->viewY0 ? g->viewY0 : top;
- bottom = bottom > g->viewY1 ? g->viewY1 : bottom;
+ top = top < rowsTop(g) ? rowsTop(g) : top;
+ bottom = bottom > rowsBottom(g) ? rowsBottom(g) : bottom;
for (int32_t y = top; y <= bottom; y++) {
fillSpan(g, y, x0, x1, pixel, style);
@@ -1222,7 +1227,7 @@ static void fillSpan(BasGfxT *g, int32_t y, int32_t x0, int32_t x1, uint32_t pix
int32_t left = x0 < x1 ? x0 : x1;
int32_t right = x0 < x1 ? x1 : x0;
- if (y < g->viewY0 || y > g->viewY1) {
+ if (y < rowsTop(g) || y > rowsBottom(g)) {
return;
}
@@ -1239,8 +1244,10 @@ static void fillSpan(BasGfxT *g, int32_t y, int32_t x0, int32_t x1, uint32_t pix
// Scan-line flood fill inside the view: everything reachable from (x, y)
// that is not the border colour (nor, for a colour, already that colour).
+// Rows are read a run at a time (rowRead), not a pixel at a time.
static int32_t floodFill(BasGfxT *g, int32_t x, int32_t y, uint32_t paint, uint32_t border, const BasGfxTileT *tile) {
GfxSeedT *seeds = NULL;
+ int32_t width = g->viewX1 - g->viewX0 + 1;
GfxFloodT f;
f.g = g;
@@ -1248,15 +1255,20 @@ static int32_t floodFill(BasGfxT *g, int32_t x, int32_t y, uint32_t paint, uint3
f.border = border;
f.tile = tile;
f.done = NULL;
+ f.row = (uint32_t *)malloc((size_t)width * sizeof(uint32_t));
+ f.near = (uint32_t *)malloc((size_t)width * sizeof(uint32_t));
if (tile) {
- size_t pixels = (size_t)(g->viewX1 - g->viewX0 + 1) * (size_t)(g->viewY1 - g->viewY0 + 1);
+ size_t pixels = (size_t)width * (size_t)(g->viewY1 - g->viewY0 + 1);
f.done = (uint8_t *)calloc(pixels / GFX_BITS_PER_BYTE + 1, 1);
+ }
- if (!f.done) {
- return BAS_ERR_OUT_OF_MEMORY;
- }
+ if (!f.row || !f.near || (tile && !f.done)) {
+ free(f.row);
+ free(f.near);
+ free(f.done);
+ return BAS_ERR_OUT_OF_MEMORY;
}
arrput(seeds, ((GfxSeedT){ x, y }));
@@ -1264,18 +1276,20 @@ static int32_t floodFill(BasGfxT *g, int32_t x, int32_t y, uint32_t paint, uint3
while (arrlen(seeds) > 0) {
GfxSeedT s = arrpop(seeds);
- if (!fillable(&f, s.x, s.y)) {
+ rowRead(g, g->viewX0, g->viewX1, s.y, f.row);
+
+ if (!fillable(&f, f.row[s.x - g->viewX0], s.x, s.y)) {
continue;
}
int32_t left = s.x;
int32_t right = s.x;
- while (left > g->viewX0 && fillable(&f, left - 1, s.y)) {
+ while (left > g->viewX0 && fillable(&f, f.row[left - 1 - g->viewX0], left - 1, s.y)) {
left--;
}
- while (right < g->viewX1 && fillable(&f, right + 1, s.y)) {
+ while (right < g->viewX1 && fillable(&f, f.row[right + 1 - g->viewX0], right + 1, s.y)) {
right++;
}
@@ -1288,8 +1302,10 @@ static int32_t floodFill(BasGfxT *g, int32_t x, int32_t y, uint32_t paint, uint3
bool inRun = false;
+ rowRead(g, left, right, ny, f.near);
+
for (int32_t nx = left; nx <= right; nx++) {
- bool can = fillable(&f, nx, ny);
+ bool can = fillable(&f, f.near[nx - left], nx, ny);
if (can && !inRun) {
arrput(seeds, ((GfxSeedT){ nx, ny }));
@@ -1301,6 +1317,8 @@ static int32_t floodFill(BasGfxT *g, int32_t x, int32_t y, uint32_t paint, uint3
}
arrfree(seeds);
+ free(f.row);
+ free(f.near);
free(f.done);
return 0;
}
@@ -1361,12 +1379,12 @@ static bool hatchOn(int32_t style, int32_t x, int32_t y) {
}
-// A horizontal span clipped to the view.
+// A horizontal span clipped to the view (and band).
static void hspan(BasGfxT *g, int32_t y, int32_t x0, int32_t x1, uint32_t pixel) {
int32_t left = x0 < x1 ? x0 : x1;
int32_t right = x0 < x1 ? x1 : x0;
- if (y < g->viewY0 || y > g->viewY1) {
+ if (y < rowsTop(g) || y > rowsBottom(g)) {
return;
}
@@ -1380,7 +1398,7 @@ static void hspan(BasGfxT *g, int32_t y, int32_t x0, int32_t x1, uint32_t pixel)
static bool inView(const BasGfxT *g, int32_t x, int32_t y) {
- return x >= g->viewX0 && x <= g->viewX1 && y >= g->viewY0 && y <= g->viewY1;
+ return x >= g->viewX0 && x <= g->viewX1 && y >= rowsTop(g) && y <= rowsBottom(g);
}
@@ -1507,7 +1525,11 @@ static void plot(BasGfxT *g, int32_t x, int32_t y, uint32_t pixel) {
return;
}
- for (int32_t dy = -r; dy <= r; dy++) {
+ // Only the disc's rows that are drawn (a band of a page: most are not)
+ int32_t from = y - r < rowsTop(g) ? rowsTop(g) - y : -r;
+ int32_t to = y + r > rowsBottom(g) ? rowsBottom(g) - y : r;
+
+ for (int32_t dy = from; dy <= to; dy++) {
int32_t half = basGfxRound(sqrt((double)(r * r - dy * dy)));
hspan(g, y + dy, x - half, x + half, pixel);
@@ -1529,6 +1551,32 @@ static void pointArg(const BasGfxT *g, BasValueT x, BasValueT y, bool step, doub
}
+// Pixels x0..x1 of row y into out: the surface's row read, or a pixel at
+// a time.
+static void rowRead(const BasGfxT *g, int32_t x0, int32_t x1, int32_t y, uint32_t *out) {
+ if (g->surf.getRow) {
+ g->surf.getRow(g->surf.ctx, x0, x1, y, out);
+ return;
+ }
+
+ for (int32_t x = x0; x <= x1; x++) {
+ out[x - x0] = g->surf.get(g->surf.ctx, x, y);
+ }
+}
+
+
+// The last row drawn: the view's, or the band's above it.
+static int32_t rowsBottom(const BasGfxT *g) {
+ return g->viewY1 < g->bandY1 ? g->viewY1 : g->bandY1;
+}
+
+
+// The first row drawn: the view's, or the band's below it.
+static int32_t rowsTop(const BasGfxT *g) {
+ return g->viewY0 > g->bandY0 ? g->viewY0 : g->bandY0;
+}
+
+
// A length in logical x units -> pixels.
static double scaleLen(const BasGfxT *g, double len) {
if (!g->world) {
diff --git a/src/apps/kpunch/dvxbasic/runtime/gfx.h b/src/apps/kpunch/dvxbasic/runtime/gfx.h
index bbcfdd3a..67cc8fab 100644
--- a/src/apps/kpunch/dvxbasic/runtime/gfx.h
+++ b/src/apps/kpunch/dvxbasic/runtime/gfx.h
@@ -107,6 +107,7 @@ typedef struct {
uint32_t colorMax; // largest colour number it takes (0: any value, e.g. RGB)
void (*span)(void *ctx, int32_t x0, int32_t x1, int32_t y, uint32_t pixel);
uint32_t (*get)(const void *ctx, int32_t x, int32_t y);
+ void (*getRow)(const void *ctx, int32_t x0, int32_t x1, int32_t y, uint32_t *out); // pixels x0..x1 of row y (NULL: get a pixel at a time)
uint32_t (*toPixel)(void *ctx, uint32_t color); // NULL: colours are pixels
uint32_t (*toColor)(const void *ctx, uint32_t pixel); // NULL: pixels are colours
const BasGfxVectorT *vec; // NULL: shapes are spans
@@ -120,6 +121,9 @@ typedef struct {
uint32_t colors[BAS_GFX_TILE_MAX_H][BAS_GFX_TILE_MAX_W];
} BasGfxTileT;
+// BasGfxT.bandY1 when no band limits the rows drawn
+#define BAS_GFX_BAND_ALL INT32_MAX
+
// Drawing state of one surface.
typedef struct {
BasGfxSurfaceT surf;
@@ -127,6 +131,8 @@ typedef struct {
int32_t viewY0;
int32_t viewX1;
int32_t viewY1;
+ int32_t bandY0; // rows drawn at all, with the view's: a printed page's band being drawn; 0..BAS_GFX_BAND_ALL otherwise
+ int32_t bandY1;
bool viewAbsolute; // VIEW SCREEN: coordinates are not moved to the view's corner
bool world; // WINDOW or a VB Scale is on: logical units are mapped onto the view
bool worldEdges; // VB Scale: the width covers the view's pixels edge to edge (WINDOW: first to last pixel centre)
diff --git a/src/apps/kpunch/dvxbasic/runtime/screen.c b/src/apps/kpunch/dvxbasic/runtime/screen.c
index f78cc1a7..87d5ad5f 100644
--- a/src/apps/kpunch/dvxbasic/runtime/screen.c
+++ b/src/apps/kpunch/dvxbasic/runtime/screen.c
@@ -286,6 +286,7 @@ static BasScreenT *screenEnsure(BasVmT *vm);
static void scrollUp(BasScreenT *scr);
static int32_t setMode(BasScreenT *scr, int32_t mode, int32_t cols, int32_t rows);
static uint32_t surfGet(const void *ctx, int32_t x, int32_t y);
+static void surfGetRow(const void *ctx, int32_t x0, int32_t x1, int32_t y, uint32_t *out);
static void surfSpan(void *ctx, int32_t x0, int32_t x1, int32_t y, uint32_t pixel);
static uint8_t textBackPixel(const BasScreenT *scr);
static void textRedraw(BasScreenT *scr, bool blinkOnly);
@@ -340,7 +341,7 @@ int32_t basScreenExec(BasVmT *vm, uint8_t fn, BasValueT *args, int32_t argc, Bas
*result = basValInteger(0);
// LINE INPUT and POS work without a screen (the host's input and
- // output); everything else makes one.
+ // output); everything else but FULLSCREEN's state makes one.
if (fn == BAS_FN_LINE_INPUT) {
return execLineInput(vm, args[0], result);
}
@@ -350,6 +351,12 @@ int32_t basScreenExec(BasVmT *vm, uint8_t fn, BasValueT *args, int32_t argc, Bas
return 0;
}
+ // FULLSCREEN's state makes no screen either
+ if (fn == BAS_FN_FULLSCREEN_STATE) {
+ *result = basValBool(vm->screen && vm->screen->fullScreen);
+ return 0;
+ }
+
BasScreenT *scr = screenEnsure(vm);
if (!scr) {
@@ -1563,6 +1570,7 @@ static int32_t setMode(BasScreenT *scr, int32_t mode, int32_t cols, int32_t rows
surf.colorMax = (uint32_t)info->colors - 1;
surf.span = surfSpan;
surf.get = surfGet;
+ surf.getRow = surfGetRow;
scr->fbAll = fb;
scr->charsAll = chars;
@@ -1610,6 +1618,16 @@ static uint32_t surfGet(const void *ctx, int32_t x, int32_t y) {
}
+static void surfGetRow(const void *ctx, int32_t x0, int32_t x1, int32_t y, uint32_t *out) {
+ const BasScreenT *scr = (const BasScreenT *)ctx;
+ const uint8_t *row = scr->fb + y * scr->width;
+
+ for (int32_t x = x0; x <= x1; x++) {
+ out[x - x0] = row[x];
+ }
+}
+
+
static void surfSpan(void *ctx, int32_t x0, int32_t x1, int32_t y, uint32_t pixel) {
BasScreenT *scr = (BasScreenT *)ctx;
diff --git a/src/apps/kpunch/dvxbasic/runtime/values.c b/src/apps/kpunch/dvxbasic/runtime/values.c
index dd56b82d..6b5ae52e 100644
--- a/src/apps/kpunch/dvxbasic/runtime/values.c
+++ b/src/apps/kpunch/dvxbasic/runtime/values.c
@@ -456,6 +456,11 @@ void basObjectClear(BasObjectT *obj) {
obj->itemCount = 0;
obj->itemCap = 0;
+ free(obj->keyIndex);
+ obj->keyIndex = NULL;
+ obj->keyIndexCap = 0;
+ obj->keyIndexUsed = 0;
+
for (int32_t i = 0; i < fieldCount; i++) {
basValRelease(&fields[i]);
}
diff --git a/src/apps/kpunch/dvxbasic/runtime/values.h b/src/apps/kpunch/dvxbasic/runtime/values.h
index fcc80a66..92422e0a 100644
--- a/src/apps/kpunch/dvxbasic/runtime/values.h
+++ b/src/apps/kpunch/dvxbasic/runtime/values.h
@@ -36,6 +36,7 @@
#include
#include "../compiler/opcodes.h"
+#include "dvxHash.h"
// ============================================================
// Reference-counted string
@@ -318,11 +319,6 @@ typedef struct {
BasObjectT *next;
} BasObjLinkT;
-// FNV-1a parameters shared by the constant-pool interning hash, the
-// case-insensitive symbol hash (compiler/scope.c) and Collection keys.
-#define BAS_FNV1A_OFFSET 0x811C9DC5u
-#define BAS_FNV1A_PRIME 0x01000193u
-
// One Collection item: an optional key (NULL = none), its hash (a lookup
// compares the text only when the hashes match) and the value.
typedef struct {
@@ -339,6 +335,9 @@ struct BasObjectS {
int32_t itemCap;
BasValueT *fields; // class instance: member variables
BasCollItemT *items; // Collection: items in order
+ int32_t *keyIndex; // Collection: item position + 1 by key hash, open addressing (NULL = not built)
+ int32_t keyIndexCap; // its slots (a power of two)
+ int32_t keyIndexUsed; // keyed items in it
BasObjFinalizeFnT finalize; // Class_Terminate hook (NULL = free at once)
void *finalizeCtx;
BasObjLinkT links[BAS_OBJ_LIST_COUNT]; // places on the owner's lists (BasObjListE)
diff --git a/src/apps/kpunch/dvxbasic/runtime/vm.c b/src/apps/kpunch/dvxbasic/runtime/vm.c
index 9814e9d9..68e4b626 100644
--- a/src/apps/kpunch/dvxbasic/runtime/vm.c
+++ b/src/apps/kpunch/dvxbasic/runtime/vm.c
@@ -26,7 +26,7 @@
// basVmStep() call, or runs until completion via basVmRun().
// PRINT/INPUT, forms and extern calls go through host-provided
// callbacks; file, directory and path statements use the C library
-// (stdio, dirent, fnmatch, stat) and the DVX platform helpers.
+// (stdio, dirent, stat) and the DVX platform helpers.
#include "vm.h"
#include "../compiler/opcodes.h"
@@ -36,10 +36,10 @@
#include "dvxPrefs.h"
#include "screen.h"
#include "dvxBas.h"
+#include "dvxFontFmt.h"
#include
#include
-#include
#include
#include
#include
@@ -161,8 +161,16 @@
#define BAS_SECS_PER_HOUR 3600.0
#define BAS_SECS_PER_MINUTE 60.0
#define BAS_USEC_PER_SEC 1000000.0
+#define BAS_USEC_PER_SEC_INT 1000000
+#define BAS_CLOCK_TICK_US ((int64_t)PLATFORM_US_PER_SEC * PLATFORM_PIT_BIOS_DIVISOR / PLATFORM_PIT_HZ) // a BIOS tick, the DOS wall clock's step
+#define BAS_TIMER_NO_READING (-1) // BasVmT.timerWall before TIMER's first reading
#define BAS_SECS_PER_DAY 86400.0
+// CHRW$: the highest Unicode code point, and the character for one code
+// page 437 does not have.
+#define BAS_UNICODE_MAX 0x10FFFF
+#define BAS_CHRW_NONE '?'
+
// struct tm counts years from 1900.
#define BAS_TM_YEAR_BASE 1900
@@ -183,6 +191,22 @@
// between checks (microseconds).
#define BAS_WAIT_POLL_USEC 1000
+// A long FileCopy or Dir$ lets the program's events run (DoEvents) every
+// BAS_PUMP_SEC, Dir$ looking every BAS_DIR_PUMP_ENTRIES entries it passes
+// over (not in a control's code)
+#define BAS_PUMP_SEC 0.05
+#define BAS_DIR_PUMP_ENTRIES 64
+
+// IniRead / IniWrite's kept file: a file changed this recently is read
+// again (FAT times are 2 seconds apart), and the size kept for no file
+#define BAS_INI_TIME_SEC 2
+#define BAS_INI_NO_FILE (-1)
+
+// A VM with a run limit (basVmSetRunLimit): why a run or call was stopped
+// (error 18), and why a wait was refused (error 5).
+#define BAS_RUNAWAY_MSG "The code ran too long without returning and was stopped"
+#define BAS_NO_WAIT_MSG "A control's code cannot wait (SLEEP, FRAMEWAIT, or SHELL)"
+
// RND: rand() scaled by RAND_MAX + 1 so the result stays below 1.0, and
// the multiplier that spreads a negative RND(n) argument's fraction into
// the reseed value (RND(-1.5) and RND(-1) must seed differently).
@@ -222,9 +246,15 @@
#define BAS_PLAY_TEXT_MAX 32767
#define BAS_PLAY_NUMBER_CHARS 12
-// How a Collection's item array grows.
+// How a Collection's item array grows, and its key index: at least
+// BAS_COLL_INDEX_MIN slots, BAS_COLL_INDEX_ROOM per key when built, and
+// built again once a key would take more than one slot in
+// BAS_COLL_INDEX_LOAD
#define BAS_COLL_MIN_CAP 4
#define BAS_COLL_GROWTH 2
+#define BAS_COLL_INDEX_MIN 16
+#define BAS_COLL_INDEX_ROOM 4
+#define BAS_COLL_INDEX_LOAD 2
#define BAS_COLL_KEY_ARGS 2 // Add item, key
// TYPENAME / TYPEOF names that are not class or control names, and the
@@ -315,6 +345,13 @@ typedef struct {
int32_t len;
} BasPlayTextT;
+// FileCopy's pump between pieces (fileCopyGoOn)
+typedef struct {
+ BasVmT *vm;
+ double pumpAt; // wall seconds of the last pump
+ bool stopped; // the program was asked to end meanwhile
+} BasCopyPumpT;
+
// Output sink for PUT: writes to fp or into mem (up to memCap bytes), or
// with neither only counts the bytes (a RANDOM PUT measures its record
// before writing it).
@@ -392,8 +429,11 @@ static bool closeChannel(BasVmT *vm, int32_t channel);
static bool collectCycles(BasVmT *vm, int32_t returnPc);
static BasVmResultE collectionAccess(BasVmT *vm, BasObjectT *coll, int32_t member, BasValueT *args, int32_t argc, int32_t op);
static int32_t collectionAdd(BasObjectT *coll, BasValueT *args, int32_t argc);
-static int32_t collectionFind(const BasObjectT *coll, BasValueT index, int32_t *outPos);
-static int32_t collectionKeyPos(const BasObjectT *coll, const BasStringT *key);
+static int32_t collectionFind(BasObjectT *coll, BasValueT index, int32_t *outPos);
+static void collectionIndexDrop(BasObjectT *coll);
+static void collectionIndexPut(BasObjectT *coll, int32_t pos);
+static bool collectionIndexReady(BasObjectT *coll);
+static int32_t collectionKeyPos(BasObjectT *coll, const BasStringT *key);
static BasStringT *commandArg(const char *args, int32_t n);
static bool conversionFailed(void);
static BasVmResultE convertValue(BasVmT *vm, uint8_t op, BasValueT *val);
@@ -402,7 +442,8 @@ static bool debugChecksDue(const BasVmT *vm);
static void defaultPrint(void *ctx, const char *text, bool newline);
static void derefArg(BasValueT *arg);
static void dirClose(BasVmT *vm);
-static const char *dirNext(BasVmT *vm);
+static const char *dirNext(BasVmT *vm, bool *stopped);
+static BasVmResultE dirPush(BasVmT *vm);
static void discardFrame(BasVmT *vm);
static void discardResult(BasVmT *vm, int32_t depth, int32_t sp);
static bool dispatchError(BasVmT *vm, BasVmResultE result, int32_t floorDepth, int32_t faultPc);
@@ -436,6 +477,7 @@ static BasVmResultE execObjMember(BasVmT *vm, uint8_t op);
static BasVmResultE execPrint(BasVmT *vm);
static BasVmResultE execRaiseEvent(BasVmT *vm);
static BasVmResultE execReturn(BasVmT *vm, bool withValue);
+static BasVmResultE execShell(BasVmT *vm);
static BasVmResultE execStoreVar(BasVmT *vm, uint8_t op);
static BasVmResultE execStrAppend(BasVmT *vm);
static BasVmResultE execStringOp(BasVmT *vm, uint8_t op);
@@ -463,6 +505,7 @@ static bool fastType(uint8_t type);
static bool fastValues(const BasValueT *v, int32_t count);
static int32_t fetchI32(const uint8_t *p);
static uint16_t fetchU16(const uint8_t *p);
+static bool fileCopyGoOn(void *ctx);
static int32_t fileWidth(BasVmT *vm, BasValueT target, int32_t width);
static BasStringT *fillString(char ch, int32_t count);
static int32_t filterScan(const BasArrayT *arr, const BasStringT *match, bool keep, bool ignoreCase, BasArrayT *out);
@@ -483,7 +526,8 @@ static void gcScan(BasGcT *gc, int32_t idx, bool mark);
static bool hostCallFinish(BasVmT *vm, BasVmResultE rc, int32_t savedPc, int32_t savedDepth, bool savedRunning, int32_t savedSp, BasValueT *result);
static bool hostCallReady(const BasVmT *vm);
static int32_t indexArray(const BasArrayT *arr, BasValueT *args, int32_t argc, int32_t *outIdx);
-static BasValueT iniRead(const char *path, const char *sec, const char *key, BasStringT *defStr);
+static PrefsHandleT *iniPrefs(BasVmT *vm, const char *path);
+static BasValueT iniRead(BasVmT *vm, const char *path, const char *sec, const char *key, BasStringT *defStr);
static BasVmResultE iniWrite(BasVmT *vm, const char *path, const char *sec, const char *key, const char *val);
static BasValueT integralValue(double v);
static BasValueT intResult(int32_t v);
@@ -504,7 +548,7 @@ static bool layoutNext(const BasStringT *layout, int32_t *pos, BasLayo
static bool leaveFast(BasVmT *vm, uint8_t op, int32_t safeEnd);
static int32_t likeElement(const char *p, int32_t pLen, int32_t pos, uint8_t ch, bool text, bool *hit);
static int32_t likeMatch(const char *s, int32_t sLen, const char *p, int32_t pLen, bool text);
-static void localNow(struct tm *out, int32_t *outUsec);
+static void localNow(BasVmT *vm, struct tm *out, int32_t *outUsec);
static BasVmResultE logicalNull(BasVmT *vm, uint8_t op, BasValueT a, BasValueT b);
static BasValueT logicalResult(int32_t r, bool isBool, bool isLong);
static void logicalSet(BasValueT *v, int32_t r, bool isBool, bool isLong);
@@ -579,6 +623,7 @@ static void releaseVmState(BasVmT *vm);
static int32_t replaceScan(const BasStringT *s, int32_t start, const BasStringT *find, const BasStringT *repl, int32_t count, bool ignoreCase, char *out);
static void restoreFormContext(BasVmT *vm, BasCallFrameT *frame);
static BasVmResultE roundedOperand(BasVmT *vm, BasValueT v, int32_t *out);
+static bool runaway(BasVmT *vm, int32_t *slices);
static BasVmResultE runFast(BasVmT *vm, int32_t budget, int32_t floorDepth, int32_t *outSteps, int32_t *outPc);
static bool runSubLoop(BasVmT *vm, int32_t savedPc, int32_t savedCallDepth, bool savedRunning);
static void runtimeError(BasVmT *vm, int32_t errNum, const char *msg);
@@ -589,6 +634,7 @@ static bool sendSpool(BasVmT *vm, BasFileChannelT *ch);
static void sinkFill(BasPutSinkT *sink, char c, int32_t count);
static void sinkWrite(BasPutSinkT *sink, const void *data, int32_t len);
static bool sleepSeconds(BasVmT *vm, int32_t secs);
+static void sliceBegin(BasVmT *vm);
static int32_t sourceRead(BasGetSourceT *src, void *dst, int32_t len);
static int32_t splitScan(const BasStringT *s, const BasStringT *delim, int32_t limit, bool ignoreCase, BasArrayT *out);
static BasVmResultE startTerminate(BasVmT *vm);
@@ -597,6 +643,7 @@ static void storeValue(BasValueT *target, BasValueT val);
static int32_t stringFind(const BasStringT *haystack, int32_t start, const BasStringT *needle, bool ignoreCase);
static bool stringMatchAt(const BasStringT *haystack, int32_t i, const BasStringT *needle, bool ignoreCase);
static BasValueT strValue(BasStringT *s);
+static bool subLoopFail(BasVmT *vm, int32_t savedPc, int32_t savedCallDepth, bool savedRunning);
static void sweepObjects(BasVmT *vm);
static const char *typeNameOf(const BasVmT *vm, uint8_t type, int32_t typeId);
static BasUdtT *udtFromLayout(BasVmT *vm, const BasStringT *layout, int32_t *pos, int32_t typeId, int32_t fieldCount, int32_t depth);
@@ -611,6 +658,7 @@ static bool valueless(const BasValueT *v);
static int32_t valueOperands(const BasVmT *vm);
static int32_t varptrString(BasValueT v, BasValueT *out);
static int32_t varTypeOf(uint8_t type);
+static bool waitRefused(BasVmT *vm);
static bool waitWhile(BasVmT *vm, bool (*busy)(void *ctx));
static double wallSeconds(void);
static bool wholeOrCurrency(uint8_t type);
@@ -1021,6 +1069,7 @@ void basVmDestroy(BasVmT *vm) {
}
dirClose(vm);
+ prefsClose((PrefsHandleT *)vm->iniPrefs);
basScreenDestroy(vm->screen);
free(vm->commandArgs);
free(vm->externCache);
@@ -1280,8 +1329,11 @@ BasVmResultE basVmRun(BasVmT *vm) {
return BAS_VM_HALTED;
}
+ int32_t slices = 0; // a run limit's count (no step limit: one run to the end)
+
vm->running = true;
vm->stepCount = 0;
+ sliceBegin(vm);
while (vm->running) {
// Check step limit: the host gets a turn, with the screen shown
@@ -1290,14 +1342,19 @@ BasVmResultE basVmRun(BasVmT *vm) {
return BAS_VM_STEP_LIMIT;
}
- // savedPc: where the instruction that failed started, for RESUME
- int32_t budget = (vm->stepLimit > 0) ? vm->stepLimit - vm->stepCount : BAS_RUN_UNLIMITED;
+ // savedPc: where the instruction that failed started, for RESUME.
+ // A run limit without a step limit counts the run in slices.
+ int32_t budget = (vm->stepLimit > 0) ? vm->stepLimit - vm->stepCount : (vm->runLimit > 0 ? BAS_VM_DEFAULT_STEP_SLICE : BAS_RUN_UNLIMITED);
int32_t steps = 0;
int32_t savedPc = vm->pc;
BasVmResultE result = runFast(vm, budget, BAS_NO_FLOOR_DEPTH, &steps, &savedPc);
if (vm->stepLimit > 0) {
vm->stepCount += steps;
+ } else if (result == BAS_VM_OK && vm->running && runaway(vm, &slices)) {
+ vm->running = false;
+ basScreenPresent(vm);
+ return BAS_VM_ERROR;
}
if (result != BAS_VM_OK) {
@@ -1419,6 +1476,12 @@ void basVmSetInputCallback(BasVmT *vm, BasInputFnT fn, void *ctx) {
}
+void basVmSetMicrosClock(BasVmT *vm, uint64_t (*micros)(void)) {
+ vm->microsFn = micros;
+ vm->timerWall = BAS_TIMER_NO_READING;
+}
+
+
void basVmSetPrintCallback(BasVmT *vm, BasPrintFnT fn, void *ctx) {
vm->printFn = fn;
vm->printCtx = ctx;
@@ -1430,11 +1493,22 @@ void basVmSetPrinterCallbacks(BasVmT *vm, const BasPrinterCallbacksT *printer) {
}
+void basVmSetRunLimit(BasVmT *vm, int32_t slices) {
+ vm->runLimit = slices;
+}
+
+
void basVmSetScreenHost(BasVmT *vm, const BasScreenHostT *host) {
vm->screenHost = *host;
}
+void basVmSetSliceClock(BasVmT *vm, const volatile uint32_t *clock, uint32_t span) {
+ vm->sliceClock = clock;
+ vm->sliceSpan = span;
+}
+
+
void basVmSetSqlCallbacks(BasVmT *vm, const BasSqlCallbacksT *sql) {
vm->sql = *sql;
}
@@ -2605,7 +2679,7 @@ static BasVmResultE basVmStepExec(BasVmT *vm) {
struct tm t;
int32_t usec;
- localNow(&t, &usec);
+ localNow(vm, &t, &usec);
double secs = (double)t.tm_hour * BAS_SECS_PER_HOUR + (double)t.tm_min * BAS_SECS_PER_MINUTE + (double)t.tm_sec + (double)usec / BAS_USEC_PER_SEC;
@@ -2621,7 +2695,7 @@ static BasVmResultE basVmStepExec(BasVmT *vm) {
struct tm t;
char buf[BAS_DATE_STR_LEN];
- localNow(&t, NULL);
+ localNow(vm, &t, NULL);
snprintf(buf, sizeof(buf), "%02d-%02d-%04d", t.tm_mon + 1, t.tm_mday, t.tm_year + 1900);
if (!push(vm, basValStringFromC(buf))) {
@@ -2636,7 +2710,7 @@ static BasVmResultE basVmStepExec(BasVmT *vm) {
struct tm t;
char buf[BAS_TIME_STR_LEN];
- localNow(&t, NULL);
+ localNow(vm, &t, NULL);
snprintf(buf, sizeof(buf), "%02d:%02d:%02d", t.tm_hour, t.tm_min, t.tm_sec);
if (!push(vm, basValStringFromC(buf))) {
@@ -2656,6 +2730,10 @@ static BasVmResultE basVmStepExec(BasVmT *vm) {
int32_t secs = basValToInt32(val);
basValRelease(&val);
+ if (secs > 0 && waitRefused(vm)) {
+ return BAS_VM_ERROR;
+ }
+
if (!sleepSeconds(vm, secs)) {
vm->running = false;
return BAS_VM_HALTED;
@@ -3311,25 +3389,10 @@ static BasVmResultE basVmStepExec(BasVmT *vm) {
// ============================================================
case OP_SHELL: {
- BasValueT cmdVal;
+ BasVmResultE shellRes = execShell(vm);
- if (!pop(vm, &cmdVal)) {
- return BAS_VM_STACK_UNDERFLOW;
- }
-
- BasValueT cmdStr = basValToString(cmdVal);
- basValRelease(&cmdVal);
-
- int32_t result = 0;
-
- if (cmdStr.strVal && cmdStr.strVal->len > 0) {
- result = system(cmdStr.strVal->data);
- }
-
- basValRelease(&cmdStr);
-
- if (!push(vm, basValInteger((int16_t)result))) {
- return BAS_VM_STACK_OVERFLOW;
+ if (shellRes != BAS_VM_OK) {
+ return shellRes;
}
break;
@@ -4132,7 +4195,7 @@ static BasVmResultE basVmStepExec(BasVmT *vm) {
BasStringT *secStr = basValFormatString(secVal);
BasStringT *keyStr = basValFormatString(keyVal);
BasStringT *defStr = basValFormatString(defVal);
- BasValueT resultVal = iniRead(fileStr->data, secStr->data, keyStr->data, defStr);
+ BasValueT resultVal = iniRead(vm, fileStr->data, secStr->data, keyStr->data, defStr);
basStringUnref(fileStr);
basStringUnref(secStr);
@@ -4294,7 +4357,7 @@ static int32_t builtinDate(uint8_t fn, BasValueT *args, BasValueT *out) {
double datePart = 0.0;
double timePart = 0.0;
- localNow(&t, NULL);
+ localNow(NULL, &t, NULL);
if (fn != BAS_FN_TIME && !basDateSerial(t.tm_year + BAS_TM_YEAR_BASE, t.tm_mon + 1, t.tm_mday, &datePart)) {
return BAS_ERR_OVERFLOW;
@@ -4994,6 +5057,31 @@ static int32_t builtinString(BasVmT *vm, uint8_t fn, BasValueT *args, int32_t ar
return 0;
}
+ case BAS_FN_ASCW: {
+ BasStringT *s = argString(&args[0]);
+
+ if (s->len == 0) {
+ return BAS_ERR_ILLEGAL_FUNC_CALL;
+ }
+
+ *out = basValLong((int32_t)dfnTextToUnicode((uint8_t)s->data[0]));
+ return 0;
+ }
+
+ case BAS_FN_CHRW: {
+ double code = basValToNumber(args[0]);
+
+ if (code < 0 || code > BAS_UNICODE_MAX) {
+ return BAS_ERR_ILLEGAL_FUNC_CALL;
+ }
+
+ int32_t c = dfnTextFromUnicode((uint32_t)code);
+ char ch = (c >= 0) ? (char)c : BAS_CHRW_NONE;
+
+ *out = strValue(basStringNew(&ch, 1));
+ return 0;
+ }
+
case BAS_FN_STRREVERSE: {
BasStringT *s = argString(&args[0]);
BasStringT *result = basStringAlloc(s->len + 1);
@@ -5081,6 +5169,23 @@ static int32_t builtinString(BasVmT *vm, uint8_t fn, BasValueT *args, int32_t ar
return 0;
}
+ case BAS_FN_LOADPICTUREDATA: {
+ if (!vm->ui.loadPictureData) {
+ return BAS_ERR_NO_FORM_RUNTIME;
+ }
+
+ BasStringT *bytes = argString(&args[0]);
+ int32_t picErr = 0;
+ void *pic = vm->ui.loadPictureData(vm->ui.ctx, bytes->data, bytes->len, &picErr);
+
+ if (!pic) {
+ return picErr;
+ }
+
+ *out = basValObject(pic);
+ return 0;
+ }
+
case BAS_FN_NEW_FORM:
if (!vm->ui.newForm) {
return BAS_ERR_NO_FORM_RUNTIME;
@@ -5552,6 +5657,7 @@ static BasVmResultE collectionAccess(BasVmT *vm, BasObjectT *coll, int32_t membe
coll->itemCount--;
memmove(&coll->items[pos], &coll->items[pos + 1], (size_t)(coll->itemCount - pos) * sizeof(BasCollItemT));
+ collectionIndexDrop(coll);
basStringUnref(gone.key);
basValRelease(&gone.value);
}
@@ -5623,6 +5729,15 @@ static int32_t collectionAdd(BasObjectT *coll, BasValueT *args, int32_t argc) {
coll->items[pos].keyHash = hasKey ? keyHash(args[1].strVal) : 0;
coll->items[pos].value = valueDetach(basValCopy(args[0]));
coll->itemCount++;
+
+ // An item added at the end keeps the key index (while it has room);
+ // one put before others moves their positions
+ if (pos < coll->itemCount - 1 || (hasKey && coll->keyIndex && (coll->keyIndexUsed + 1) * BAS_COLL_INDEX_LOAD > coll->keyIndexCap)) {
+ collectionIndexDrop(coll);
+ } else if (hasKey && coll->keyIndex) {
+ collectionIndexPut(coll, pos);
+ }
+
return 0;
}
@@ -5630,7 +5745,7 @@ static int32_t collectionAdd(BasObjectT *coll, BasValueT *args, int32_t argc) {
// The 0-based position of the item index (1-based) or key (a string,
// matched without regard to case) names. Returns 0 and sets *outPos,
// else the BASIC error number.
-static int32_t collectionFind(const BasObjectT *coll, BasValueT index, int32_t *outPos) {
+static int32_t collectionFind(BasObjectT *coll, BasValueT index, int32_t *outPos) {
if (index.type == BAS_TYPE_STRING) {
int32_t pos = collectionKeyPos(coll, index.strVal);
@@ -5657,10 +5772,85 @@ static int32_t collectionFind(const BasObjectT *coll, BasValueT index, int32_t *
}
-// The position of the item with key (case ignored), or -1.
-static int32_t collectionKeyPos(const BasObjectT *coll, const BasStringT *key) {
+// Forget the key index (the next lookup builds it again): positions moved.
+static void collectionIndexDrop(BasObjectT *coll) {
+ free(coll->keyIndex);
+ coll->keyIndex = NULL;
+ coll->keyIndexCap = 0;
+ coll->keyIndexUsed = 0;
+}
+
+
+// Put keyed item pos in the key index (which has room).
+static void collectionIndexPut(BasObjectT *coll, int32_t pos) {
+ uint32_t mask = (uint32_t)coll->keyIndexCap - 1;
+ uint32_t slot = coll->items[pos].keyHash & mask;
+
+ while (coll->keyIndex[slot] != 0) {
+ slot = (slot + 1) & mask;
+ }
+
+ coll->keyIndex[slot] = pos + 1;
+ coll->keyIndexUsed++;
+}
+
+
+// The key index, built when missing; false when there is no memory for it
+// (the caller looks through the items instead).
+static bool collectionIndexReady(BasObjectT *coll) {
+ if (coll->keyIndex) {
+ return true;
+ }
+
+ int32_t keyed = 0;
+ int32_t cap = BAS_COLL_INDEX_MIN;
+
+ for (int32_t i = 0; i < coll->itemCount; i++) {
+ keyed += coll->items[i].key ? 1 : 0;
+ }
+
+ while (cap < keyed * BAS_COLL_INDEX_ROOM) {
+ cap *= 2;
+ }
+
+ coll->keyIndex = (int32_t *)calloc((size_t)cap, sizeof(int32_t));
+
+ if (!coll->keyIndex) {
+ return false;
+ }
+
+ coll->keyIndexCap = cap;
+ coll->keyIndexUsed = 0;
+
+ for (int32_t i = 0; i < coll->itemCount; i++) {
+ if (coll->items[i].key) {
+ collectionIndexPut(coll, i);
+ }
+ }
+
+ return true;
+}
+
+
+// The position of the item with key (case ignored), or -1: through the
+// key index, so a big keyed collection is not read item by item.
+static int32_t collectionKeyPos(BasObjectT *coll, const BasStringT *key) {
uint32_t hash = keyHash(key);
+ if (collectionIndexReady(coll)) {
+ uint32_t mask = (uint32_t)coll->keyIndexCap - 1;
+
+ for (uint32_t slot = hash & mask; coll->keyIndex[slot] != 0; slot = (slot + 1) & mask) {
+ const BasCollItemT *item = &coll->items[coll->keyIndex[slot] - 1];
+
+ if (item->keyHash == hash && basStringCompareCI(item->key, key) == 0) {
+ return coll->keyIndex[slot] - 1;
+ }
+ }
+
+ return -1;
+ }
+
for (int32_t i = 0; i < coll->itemCount; i++) {
if (coll->items[i].keyHash == hash && coll->items[i].key && basStringCompareCI(coll->items[i].key, key) == 0) {
return i;
@@ -5799,27 +5989,39 @@ static void dirClose(BasVmT *vm) {
// Next Dir$ match. Like QB/VB Dir$ without a directory attribute, only
-// files are returned: ".", ".." and subdirectories are skipped.
-static const char *dirNext(BasVmT *vm) {
+// files are returned: ".", ".." and subdirectories are skipped. NULL at
+// the end, or when the program's events (run along the way) started a new
+// Dir$ or said stop (*stopped).
+static const char *dirNext(BasVmT *vm, bool *stopped) {
if (!vm->dirHandle) {
return NULL;
}
+ DIR *dir = (DIR *)vm->dirHandle;
+ int32_t passed = 0;
struct dirent *ent;
- while ((ent = readdir((DIR *)vm->dirHandle)) != NULL) {
+ while ((ent = readdir(dir)) != NULL) {
+ // A long run of entries passed over lets the program's events run;
+ // one that starts a new Dir$ meanwhile ends this one
+ if (++passed % BAS_DIR_PUMP_ENTRIES == 0 && vm->runLimit <= 0 && vm->doEventsFn) {
+ *stopped = !basVmDoEvents(vm);
+
+ if (*stopped || vm->dirHandle != dir) {
+ return NULL;
+ }
+ }
+
if (ent->d_name[0] == '.' && (ent->d_name[1] == '\0' || (ent->d_name[1] == '.' && ent->d_name[2] == '\0'))) {
continue;
}
- if (fnmatch(vm->dirPattern, ent->d_name, FNM_CASEFOLD) != 0) {
+ // DOS's wildcards, as the file dialog has them: "*.*" is every name
+ if (strcmp(vm->dirPattern, PLATFORM_FILTER_ALL) != 0 && !platformGlobMatch(vm->dirPattern, ent->d_name)) {
continue;
}
- char full[DVX_MAX_PATH];
- struct stat st;
-
- if (platformPathJoin(full, sizeof(full), vm->dirPath, ent->d_name) && stat(full, &st) == 0 && S_ISDIR(st.st_mode)) {
+ if (platformDirEntryIsDir(vm->dirPath, ent->d_name, ent->d_type)) {
continue;
}
@@ -5831,6 +6033,21 @@ static const char *dirNext(BasVmT *vm) {
}
+// Dir$'s next name on the stack ("" when there is none); the program
+// halts if its events said stop meanwhile.
+static BasVmResultE dirPush(BasVmT *vm) {
+ bool stopped = false;
+ const char *name = dirNext(vm, &stopped);
+
+ if (stopped) {
+ vm->running = false;
+ return BAS_VM_HALTED;
+ }
+
+ return push(vm, basValStringFromC(name)) ? BAS_VM_OK : BAS_VM_STACK_OVERFLOW;
+}
+
+
// Releases the top call frame without returning through it: its locals
// are released, its FOR frames dropped and the caller's statement
// boundary restored. Used when an error unwinds frames (dispatchError,
@@ -6236,6 +6453,9 @@ static const char *errorText(int32_t errNum) {
case BAS_ERR_OUT_OF_STRING:
return "Out of string space";
+ case BAS_ERR_INTERRUPTED:
+ return "User interrupt occurred";
+
case BAS_ERR_RESUME_WITHOUT_ERR:
return "RESUME without error";
@@ -6939,6 +7159,10 @@ static BasVmResultE execDevice(BasVmT *vm, uint8_t op) {
return BAS_VM_ERROR;
}
+ if (waitRefused(vm)) {
+ return BAS_VM_ERROR;
+ }
+
d->frameStart(d->ctx, hz);
if (!waitWhile(vm, d->frameBusy)) {
@@ -6977,8 +7201,9 @@ static BasVmResultE execDevice(BasVmT *vm, uint8_t op) {
}
}
- // BEEP, SOUND and PLAY wait while the music is in the foreground.
- if (d->soundBusy && !waitWhile(vm, d->soundBusy)) {
+ // BEEP, SOUND and PLAY wait while the music is in the foreground (not
+ // in code that must not block: it plays on as background music).
+ if (d->soundBusy && vm->runLimit <= 0 && !waitWhile(vm, d->soundBusy)) {
vm->running = false;
return BAS_VM_HALTED;
}
@@ -7722,11 +7947,22 @@ static BasVmResultE execFsOp(BasVmT *vm, uint8_t op) {
return BAS_VM_STACK_UNDERFLOW;
}
- bool copied = platformCopyFile(srcStr.strVal->data, dstStr.strVal->data);
+ BasCopyPumpT pump;
+
+ pump.vm = vm;
+ pump.pumpAt = wallSeconds();
+ pump.stopped = false;
+
+ bool copied = platformCopyFileWith(srcStr.strVal->data, dstStr.strVal->data, fileCopyGoOn, &pump);
basValRelease(&srcStr);
basValRelease(&dstStr);
+ if (pump.stopped) {
+ vm->running = false;
+ return BAS_VM_HALTED;
+ }
+
if (!copied) {
runtimeError(vm, BAS_ERR_FILE_NOT_FOUND, "File not found or cannot create destination");
return BAS_VM_FILE_ERROR;
@@ -7856,21 +8092,11 @@ static BasVmResultE execFsOp(BasVmT *vm, uint8_t op) {
}
vm->dirHandle = opendir(vm->dirPath);
-
- if (!push(vm, basValStringFromC(dirNext(vm)))) {
- return BAS_VM_STACK_OVERFLOW;
- }
-
- break;
+ return dirPush(vm);
}
- case OP_FS_DIR_NEXT: {
- if (!push(vm, basValStringFromC(dirNext(vm)))) {
- return BAS_VM_STACK_OVERFLOW;
- }
-
- break;
- }
+ case OP_FS_DIR_NEXT:
+ return dirPush(vm);
case OP_FS_FILELEN: {
BasValueT fnStr;
@@ -8561,6 +8787,46 @@ static BasVmResultE execReturn(BasVmT *vm, bool withValue) {
}
+// SHELL command: run it with DVX stepped aside through the host (keeping
+// its events running until the command is over) and push its exit code;
+// outside DVX, system(). An empty command does nothing (0).
+static BasVmResultE execShell(BasVmT *vm) {
+ BasValueT cmdVal;
+ const BasDeviceCallbacksT *d = &vm->device;
+ int32_t result = 0;
+
+ if (!pop(vm, &cmdVal)) {
+ return BAS_VM_STACK_UNDERFLOW;
+ }
+
+ BasValueT cmdStr = basValToString(cmdVal);
+
+ basValRelease(&cmdVal);
+
+ if (cmdStr.strVal && cmdStr.strVal->len > 0 && waitRefused(vm)) {
+ basValRelease(&cmdStr);
+ return BAS_VM_ERROR;
+ }
+
+ if (cmdStr.strVal && cmdStr.strVal->len > 0) {
+ if (!d->shellStart) {
+ result = system(cmdStr.strVal->data);
+ } else if (!d->shellStart(d->ctx, cmdStr.strVal->data)) {
+ result = -1;
+ } else if (!waitWhile(vm, d->shellBusy)) {
+ basValRelease(&cmdStr);
+ vm->running = false;
+ return BAS_VM_HALTED;
+ } else {
+ result = d->shellResult(d->ctx);
+ }
+ }
+
+ basValRelease(&cmdStr);
+ return push(vm, basValInteger((int16_t)result)) ? BAS_VM_OK : BAS_VM_STACK_OVERFLOW;
+}
+
+
// OP_STORE_LOCAL / OP_STORE_GLOBAL [uint16 idx] and their _CONV forms
// [uint16 idx] [uint8 conv], which first run conversion opcode conv on the
// value: pops the value into the variable (a local holding a reference
@@ -9448,6 +9714,23 @@ static inline uint16_t fetchU16(const uint8_t *p) {
}
+// FileCopy between pieces: every BAS_PUMP_SEC the program's events run,
+// as DoEvents (not in a control's code); false (the copy stops) once the
+// program is asked to end.
+static bool fileCopyGoOn(void *ctx) {
+ BasCopyPumpT *pump = (BasCopyPumpT *)ctx;
+ double now = wallSeconds();
+
+ if (pump->vm->runLimit > 0 || !pump->vm->doEventsFn || now - pump->pumpAt < BAS_PUMP_SEC) {
+ return true;
+ }
+
+ pump->pumpAt = now;
+ pump->stopped = !basVmDoEvents(pump->vm);
+ return !pump->stopped;
+}
+
+
// WIDTH for a file or device (BAS_FN_FILE_WIDTH): target is a channel,
// BAS_WIDTH_LPRINT or a device -- SCRN: (the screen's WIDTH), LPT1: ..
// LPT3: or PRN (a channel opened on it later). width 1 .. 255; 255 is no
@@ -10139,15 +10422,36 @@ static int32_t indexArray(const BasArrayT *arr, BasValueT *args, int32_t argc, i
}
+// The file IniRead and IniWrite work on, parsed: the one kept from the
+// last call when it is the same file and unchanged since (its size and
+// time the same, and not changed in the last BAS_INI_TIME_SEC seconds,
+// which a FAT time cannot tell apart), else read again and kept. NULL
+// when out of memory.
+static PrefsHandleT *iniPrefs(BasVmT *vm, const char *path) {
+ struct stat st;
+ bool have = stat(path, &st) == 0;
+ int64_t size = have ? (int64_t)st.st_size : BAS_INI_NO_FILE;
+ int64_t when = have ? (int64_t)st.st_mtime : 0;
+
+ if (vm->iniPrefs && platformPathSame(path, vm->iniPath) && size == vm->iniSize && when == vm->iniTime && (!have || (int64_t)time(NULL) - when > BAS_INI_TIME_SEC)) {
+ return (PrefsHandleT *)vm->iniPrefs;
+ }
+
+ prefsClose((PrefsHandleT *)vm->iniPrefs);
+ vm->iniPrefs = prefsLoad(path);
+ vm->iniSize = size;
+ vm->iniTime = when;
+ snprintf(vm->iniPath, sizeof(vm->iniPath), "%s", path);
+ return (PrefsHandleT *)vm->iniPrefs;
+}
+
+
// IniRead: the value of key in [sec] of path through dvxPrefs, or a
// copy of defStr when the file, section or key is missing.
-static BasValueT iniRead(const char *path, const char *sec, const char *key, BasStringT *defStr) {
- PrefsHandleT *h = prefsLoad(path);
- const char *value = prefsGetString(h, sec, key, NULL);
- BasValueT result = value ? basValStringFromC(value) : basValString(defStr);
+static BasValueT iniRead(BasVmT *vm, const char *path, const char *sec, const char *key, BasStringT *defStr) {
+ const char *value = prefsGetString(iniPrefs(vm, path), sec, key, NULL);
- prefsClose(h);
- return result;
+ return value ? basValStringFromC(value) : basValString(defStr);
}
@@ -10157,7 +10461,7 @@ static BasValueT iniRead(const char *path, const char *sec, const char *key, Bas
// end of the file. A file that exists but cannot be read is never
// rewritten, and a failed save leaves the old file intact (error 75).
static BasVmResultE iniWrite(BasVmT *vm, const char *path, const char *sec, const char *key, const char *val) {
- PrefsHandleT *h = prefsLoad(path);
+ PrefsHandleT *h = iniPrefs(vm, path);
if (!h) {
runtimeError(vm, BAS_ERR_OUT_OF_MEMORY, "Out of memory");
@@ -10166,9 +10470,12 @@ static BasVmResultE iniWrite(BasVmT *vm, const char *path, const char *sec, cons
prefsSetString(h, sec, key, val);
+ // What was saved is read back next time (its new size and time are
+ // not known here); what failed to save is not kept either
bool saved = prefsSave(h);
prefsClose(h);
+ vm->iniPrefs = NULL;
if (!saved) {
runtimeError(vm, BAS_ERR_PATH_FILE_ACCESS, "Cannot write INI file");
@@ -10428,11 +10735,10 @@ static inline int32_t jumpTarget(const BasVmT *vm, int32_t offset) {
// A Collection key's hash: FNV-1a of its text with case folded as
// basStringCompareCI folds it, so keys that match have equal hashes.
static uint32_t keyHash(const BasStringT *key) {
- uint32_t h = BAS_FNV1A_OFFSET;
+ uint32_t h = DVX_FNV_BASIS;
for (int32_t i = 0; i < key->len; i++) {
- h ^= (uint32_t)toupper((unsigned char)key->data[i]);
- h *= BAS_FNV1A_PRIME;
+ h = dvxFnvByte(h, (uint8_t)toupper((unsigned char)key->data[i]));
}
return h;
@@ -10457,15 +10763,15 @@ static bool layoutNext(const BasStringT *layout, int32_t *pos, BasLayoutEntryT *
return false;
}
- out->fixedLen = d[*pos] | (d[*pos + 1] << 8);
+ out->fixedLen = basReadU16LE(d + *pos);
*pos += 2;
} else if (out->type == BAS_TYPE_UDT) {
if (*pos + 4 > layout->len) {
return false;
}
- out->typeId = d[*pos] | (d[*pos + 1] << 8);
- out->fieldCount = d[*pos + 2] | (d[*pos + 3] << 8);
+ out->typeId = basReadU16LE(d + *pos);
+ out->fieldCount = basReadU16LE(d + *pos + 2);
*pos += 4;
}
@@ -10628,13 +10934,30 @@ static int32_t likeMatch(const char *s, int32_t sLen, const char *p, int32_t pLe
// Local wall-clock time, with the sub-second part in microseconds when
-// outUsec is non-NULL. Shared by TIMER, DATE$ and TIME$.
-static void localNow(struct tm *out, int32_t *outUsec) {
+// outUsec is non-NULL. Shared by TIMER, DATE$, TIME$ and NOW (vm NULL:
+// whole seconds only). With a fine
+// clock (basVmSetMicrosClock) the time since the wall clock's reading last
+// moved is added -- never a whole BIOS tick, its step on DOS -- so TIMER
+// keeps rising in fine steps and never passes the next reading.
+static void localNow(BasVmT *vm, struct tm *out, int32_t *outUsec) {
struct timeval tv;
gettimeofday(&tv, NULL);
- time_t secs = tv.tv_sec;
+ int64_t wall = (int64_t)tv.tv_sec * BAS_USEC_PER_SEC_INT + tv.tv_usec;
+
+ if (vm && vm->microsFn) {
+ uint64_t now = vm->microsFn();
+
+ if (wall != vm->timerWall) {
+ vm->timerWall = wall;
+ vm->timerSeen = now;
+ }
+
+ wall += DVX_MIN((int64_t)(now - vm->timerSeen), BAS_CLOCK_TICK_US - 1);
+ }
+
+ time_t secs = (time_t)(wall / BAS_USEC_PER_SEC_INT);
struct tm *local = localtime(&secs);
if (local) {
@@ -10644,7 +10967,7 @@ static void localNow(struct tm *out, int32_t *outUsec) {
}
if (outUsec) {
- *outUsec = (int32_t)tv.tv_usec;
+ *outUsec = (int32_t)(wall % BAS_USEC_PER_SEC_INT);
}
}
@@ -11378,13 +11701,16 @@ static bool operandDefaults(BasVmT *vm, int32_t count, BasVmResultE *rc) {
BasValueT *v = &vm->stack[slot];
if (v->type == BAS_TYPE_OBJECT && v->objVal != NULL && vm->ui.defaultProp != NULL && vm->ui.getProp != NULL) {
+ // A default can be an object with a default of its own (an
+ // Image's Picture, whose default is its file name)
const char *name = vm->ui.defaultProp(vm->ui.ctx, v->objVal);
- if (name != NULL) {
+ while (name != NULL) {
BasValueT val = vm->ui.getProp(vm->ui.ctx, v->objVal, name);
basValRelease(v);
- *v = val;
+ *v = val;
+ name = (v->type == BAS_TYPE_OBJECT && v->objVal != NULL) ? vm->ui.defaultProp(vm->ui.ctx, v->objVal) : NULL;
}
continue;
@@ -12696,6 +13022,18 @@ static BasVmResultE roundedOperand(BasVmT *vm, BasValueT v, int32_t *out) {
// labels for unused and the label addresses in the table for Booleans.
// cppcheck-suppress-begin [unusedLabelConfiguration, assignBoolToPointer]
__attribute__((optimize("no-gcse", "omit-frame-pointer", "align-labels=16")))
+// basVmSetRunLimit: one more slice of a run or call; true (error 18 set)
+// when that is more than the limit.
+static bool runaway(BasVmT *vm, int32_t *slices) {
+ if (vm->runLimit <= 0 || ++*slices <= vm->runLimit) {
+ return false;
+ }
+
+ runtimeError(vm, BAS_ERR_INTERRUPTED, BAS_RUNAWAY_MSG);
+ return true;
+}
+
+
static BasVmResultE runFast(BasVmT *vm, int32_t budget, int32_t floorDepth, int32_t *outSteps, int32_t *outPc) {
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Woverride-init"
@@ -12780,6 +13118,7 @@ static BasVmResultE runFast(BasVmT *vm, int32_t budget, int32_t floorDepth, int3
int32_t left = budget;
int32_t startPc = 0;
bool lineHooks = false;
+ bool timed = vm->sliceClock != NULL && budget != BAS_RUN_UNLIMITED;
uint8_t op = OP_NOP;
BasVmResultE rc = BAS_VM_OK;
@@ -13605,6 +13944,11 @@ fastAfter:
return BAS_VM_OK;
}
+ // The slice's time is up (a long call): it ends as its steps would
+ if (timed && *vm->sliceClock - vm->sliceStart >= vm->sliceSpan) {
+ left = 0;
+ }
+
FAST_LOAD();
FAST_NEXT();
@@ -13652,8 +13996,11 @@ fastResume:
// event handlers.
static bool runSubLoop(BasVmT *vm, int32_t savedPc, int32_t savedCallDepth, bool savedRunning) {
int32_t stepsSinceYield = 0;
+ int32_t slices = 0; // a run limit's count
bool hadBreakpoint = false;
+ sliceBegin(vm);
+
while (vm->running && vm->callDepth > savedCallDepth) {
int32_t steps = 0;
int32_t stepPc = vm->pc; // where a failing instruction started, for ON ERROR dispatch
@@ -13681,18 +14028,13 @@ static bool runSubLoop(BasVmT *vm, int32_t savedPc, int32_t savedCallDepth, bool
if (!vm->doEventsFn(vm->doEventsCtx)) {
// Host is shutting down: drop the paused frames like
// every other exit path before handing back.
- while (vm->callDepth > savedCallDepth) {
- discardFrame(vm);
- }
-
- vm->pc = savedPc;
- vm->running = savedRunning;
- return false;
+ return subLoopFail(vm, savedPc, savedCallDepth, savedRunning);
}
}
// User resumed -- continue executing the sub
stepsSinceYield = 0;
+ sliceBegin(vm);
continue;
}
@@ -13705,27 +14047,24 @@ static bool runSubLoop(BasVmT *vm, int32_t savedPc, int32_t savedCallDepth, bool
continue;
}
- // Untrapped: the frames still hold refcounted locals; discard
- // them so force-restoring callDepth cannot orphan them above
- // releaseVmState's reach. Not popCallFrame: it would clear the
- // error state the host reads.
- while (vm->callDepth > savedCallDepth) {
- discardFrame(vm);
- }
-
- vm->pc = savedPc;
- vm->running = savedRunning;
- return false;
+ return subLoopFail(vm, savedPc, savedCallDepth, savedRunning);
}
- // Yield periodically to keep the GUI responsive
+ // Yield periodically to keep the GUI responsive; code with a run
+ // limit that has run too long is stopped (ON ERROR cannot trap it)
if (stepsSinceYield >= BAS_VM_DEFAULT_STEP_SLICE) {
stepsSinceYield = 0;
+ if (runaway(vm, &slices)) {
+ return subLoopFail(vm, savedPc, savedCallDepth, savedRunning);
+ }
+
if (!basVmDoEvents(vm)) {
vm->running = false;
break;
}
+
+ sliceBegin(vm);
}
}
@@ -13912,6 +14251,12 @@ static bool sleepSeconds(BasVmT *vm, int32_t secs) {
}
+// A slice starts now: the slice clock's reading it is measured from.
+static void sliceBegin(BasVmT *vm) {
+ vm->sliceStart = vm->sliceClock ? *vm->sliceClock : 0;
+}
+
+
// Reads up to len bytes into dst (none from a source with neither a file
// nor memory). Returns how many there were.
static int32_t sourceRead(BasGetSourceT *src, void *dst, int32_t len) {
@@ -14094,6 +14439,22 @@ static BasValueT strValue(BasStringT *s) {
}
+// runSubLoop's way out without finishing the call (an untrapped error, a
+// run limit, the host shutting down): the frames still hold refcounted
+// locals; they are discarded so force-restoring callDepth cannot orphan
+// them above releaseVmState's reach (not popCallFrame: it would clear the
+// error state the host reads). Returns false, runSubLoop's failure.
+static bool subLoopFail(BasVmT *vm, int32_t savedPc, int32_t savedCallDepth, bool savedRunning) {
+ while (vm->callDepth > savedCallDepth) {
+ discardFrame(vm);
+ }
+
+ vm->pc = savedPc;
+ vm->running = savedRunning;
+ return false;
+}
+
+
// Releases every object the VM made that is still alive when it is
// destroyed. Each object is held while all of them let go of what they
// hold (breaking reference cycles), then the holds are dropped: objects
@@ -14692,6 +15053,18 @@ static int32_t varTypeOf(uint8_t type) {
}
+// basVmSetRunLimit: code that must not block refuses to wait (error 5
+// set: true).
+static bool waitRefused(BasVmT *vm) {
+ if (vm->runLimit <= 0) {
+ return false;
+ }
+
+ runtimeError(vm, BAS_ERR_ILLEGAL_FUNC_CALL, BAS_NO_WAIT_MSG);
+ return true;
+}
+
+
// Keep the host's events running until busy says the wait is over (or
// just check again after a pause without an event pump). False when the
// host asks the program to stop.
diff --git a/src/apps/kpunch/dvxbasic/runtime/vm.h b/src/apps/kpunch/dvxbasic/runtime/vm.h
index 7f2bda2f..14ca05e4 100644
--- a/src/apps/kpunch/dvxbasic/runtime/vm.h
+++ b/src/apps/kpunch/dvxbasic/runtime/vm.h
@@ -41,6 +41,7 @@
#include "values.h"
#include "../compiler/basEvents.h"
#include "../compiler/opcodes.h"
+#include "../../../../libs/kpunch/sql/dvxSql.h"
#include "dvxTypes.h"
#include "dvxPlat.h"
@@ -228,56 +229,65 @@ typedef const char *(*BasUiDefaultPropFnT)(void *ctx, void *objRef);
// cannot be read.
typedef void *(*BasUiLoadPictureFnT)(void *ctx, const char *path, int32_t *outErr);
+// LoadPictureData(bytes): a Picture object for the len bytes of a picture
+// file ("" = an empty picture; the same bytes give the same object). NULL
+// with *outErr set to a BASIC error (invalid picture, out of memory) when
+// they are not a picture DVX can show.
+typedef void *(*BasUiLoadPictureDataFnT)(void *ctx, const char *data, int32_t len, int32_t *outErr);
+
// LoadFormText(text): a form loaded from .frm text (its own copy when a
// form of its name is loaded), NULL when the text does not load.
typedef void *(*BasUiLoadFormTextFnT)(void *ctx, const char *text);
// Collected UI callbacks
typedef struct {
- BasUiGetPropFnT getProp;
- BasUiSetPropFnT setProp;
- BasUiCallMethodFnT callMethod;
- BasUiCreateCtrlFnT createCtrl;
- BasUiFindCtrlFnT findCtrl;
- BasUiFindCtrlIdxFnT findCtrlIdx;
- BasUiLoadFormFnT loadForm;
- BasUiUnloadFormFnT unloadForm;
- BasUiShowFormFnT showForm;
- BasUiHideFormFnT hideForm;
- BasUiMsgBoxFnT msgBox;
- BasUiInputBoxFnT inputBox;
- BasUiCreateFormFnT createForm;
- BasUiNewFormFnT newForm;
- BasUiCreateCtrlExFnT createCtrlEx;
- BasUiSetEventFnT setEvent;
- BasUiRemoveCtrlFnT removeCtrl;
- BasUiTypeNameFnT typeName;
- BasUiLoadPictureFnT loadPicture;
- BasUiDefaultPropFnT defaultProp;
- BasUiLoadFormTextFnT loadFormText;
- void *ctx; // passed as first arg to all callbacks
+ BasUiGetPropFnT getProp;
+ BasUiSetPropFnT setProp;
+ BasUiCallMethodFnT callMethod;
+ BasUiCreateCtrlFnT createCtrl;
+ BasUiFindCtrlFnT findCtrl;
+ BasUiFindCtrlIdxFnT findCtrlIdx;
+ BasUiLoadFormFnT loadForm;
+ BasUiUnloadFormFnT unloadForm;
+ BasUiShowFormFnT showForm;
+ BasUiHideFormFnT hideForm;
+ BasUiMsgBoxFnT msgBox;
+ BasUiInputBoxFnT inputBox;
+ BasUiCreateFormFnT createForm;
+ BasUiNewFormFnT newForm;
+ BasUiCreateCtrlExFnT createCtrlEx;
+ BasUiSetEventFnT setEvent;
+ BasUiRemoveCtrlFnT removeCtrl;
+ BasUiTypeNameFnT typeName;
+ BasUiLoadPictureFnT loadPicture;
+ BasUiDefaultPropFnT defaultProp;
+ BasUiLoadFormTextFnT loadFormText;
+ BasUiLoadPictureDataFnT loadPictureData;
+ void *ctx; // passed as first arg to all callbacks
} BasUiCallbacksT;
// ============================================================
-// SQL callbacks (host-provided)
+// SQL callbacks (host-provided: the dvxsql library's functions, by type)
// ============================================================
typedef struct {
- int32_t (*sqlOpen)(const char *path);
- void (*sqlClose)(int32_t db);
- bool (*sqlExec)(int32_t db, const char *sql);
- const char *(*sqlError)(int32_t db);
- int32_t (*sqlQuery)(int32_t db, const char *sql);
- bool (*sqlNext)(int32_t rs);
- bool (*sqlEof)(int32_t rs);
- int32_t (*sqlFieldCount)(int32_t rs);
- const char *(*sqlFieldName)(int32_t rs, int32_t col);
- const char *(*sqlFieldText)(int32_t rs, int32_t col);
- const char *(*sqlFieldByName)(int32_t rs, const char *name);
- int32_t (*sqlFieldInt)(int32_t rs, int32_t col);
- double (*sqlFieldDbl)(int32_t rs, int32_t col);
- void (*sqlFreeResult)(int32_t rs);
- int32_t (*sqlAffectedRows)(int32_t db);
+ __typeof__(dvxSqlOpen) *sqlOpen;
+ __typeof__(dvxSqlClose) *sqlClose;
+ __typeof__(dvxSqlExec) *sqlExec;
+ __typeof__(dvxSqlError) *sqlError;
+ __typeof__(dvxSqlQuery) *sqlQuery;
+ __typeof__(dvxSqlNext) *sqlNext;
+ __typeof__(dvxSqlEof) *sqlEof;
+ __typeof__(dvxSqlFieldCount) *sqlFieldCount;
+ __typeof__(dvxSqlFieldName) *sqlFieldName;
+ __typeof__(dvxSqlFieldText) *sqlFieldText;
+ __typeof__(dvxSqlFieldByName) *sqlFieldByName;
+ __typeof__(dvxSqlFieldInt) *sqlFieldInt;
+ __typeof__(dvxSqlFieldDbl) *sqlFieldDbl;
+ __typeof__(dvxSqlFreeResult) *sqlFreeResult;
+ __typeof__(dvxSqlAffectedRows) *sqlAffectedRows;
+ __typeof__(dvxSqlSetPump) *sqlSetPump;
+ __typeof__(dvxSqlEscape) *sqlEscape;
} BasSqlCallbacksT;
// ============================================================
@@ -313,9 +323,13 @@ typedef struct {
// soundBusy says the statement must still wait (foreground music); after
// frameStart(hz) (the host's frame clock moves on to its next frame at hz
// frames a second) likewise while frameBusy says that frame has not
-// begun. Without beep / sound / play the statements do nothing and
-// PLAY(n) is 0, without stick / strig they read 0, and FRAMEWAIT without
-// frameStart raises "Device unavailable".
+// begun. SHELL hands its command line to shellStart (the host runs it
+// with DVX stepped aside: false when it cannot), keeps the host's events
+// running while shellBusy, and returns shellResult (the exit code, -1 when
+// it did not run). Without beep / sound / play the statements do nothing
+// and PLAY(n) is 0, without stick / strig they read 0, FRAMEWAIT without
+// frameStart raises "Device unavailable", and SHELL without shellStart
+// runs the command with system() (BASIC outside DVX: the runner, BASCOMP).
// QBasic's ranges: SOUND frequency (Hz) and duration (clock ticks, 18.2
// a second), STICK(0..3), STRIG(0..7)
@@ -335,6 +349,9 @@ typedef struct {
int32_t (*strig)(void *ctx, int32_t n);
void (*frameStart)(void *ctx, int32_t hz);
bool (*frameBusy)(void *ctx);
+ bool (*shellStart)(void *ctx, const char *command);
+ bool (*shellBusy)(void *ctx);
+ int32_t (*shellResult)(void *ctx);
void *ctx;
} BasDeviceCallbacksT;
@@ -837,6 +854,17 @@ typedef struct {
void *inputCtx;
BasDoEventsFnT doEventsFn;
void *doEventsCtx;
+
+ // The slice clock (basVmSetSliceClock): a counter the host's timer
+ // interrupt advances, read after each instruction run out of line
+ // (calls, properties, built-ins); NULL: slices count steps only.
+ const volatile uint32_t *sliceClock;
+ uint32_t sliceSpan; // its counts a slice may last
+ uint32_t sliceStart; // its value when the slice began
+ uint64_t (*microsFn)(void); // basVmSetMicrosClock: TIMER's fine clock (NULL: the wall clock alone)
+ int64_t timerWall; // TIMER: the wall clock's last reading, in microseconds (-1: none yet)
+ uint64_t timerSeen; // microsFn when it came
+ int32_t runLimit; // slices one run or call may last, waits refused (basVmSetRunLimit; 0: no limit)
BasBreakpointFnT breakpointFn;
void *breakpointCtx;
BasEventHostFnT eventHostFn; // UserControl events to the host (NULL: none)
@@ -907,6 +935,12 @@ typedef struct {
void *dirHandle; // DIR * (opaque here; cast in vm.c)
char dirPattern[DVX_MAX_PATH]; // filename glob for Dir$
char dirPath[DVX_MAX_PATH]; // directory being enumerated
+
+ // IniRead / IniWrite's last file, kept parsed (iniPrefs in vm.c)
+ void *iniPrefs; // PrefsHandleT * (NULL = none kept)
+ char iniPath[DVX_MAX_PATH];
+ int64_t iniSize; // its size and time when kept (-1: there was no file)
+ int64_t iniTime;
} BasVmT;
// ============================================================
@@ -955,6 +989,30 @@ void basVmSetDoEventsCallback(BasVmT *vm, BasDoEventsFnT fn, void *ctx);
void basVmSetInputCallback(BasVmT *vm, BasInputFnT fn, void *ctx);
void basVmSetPrintCallback(BasVmT *vm, BasPrintFnT fn, void *ctx);
+// For a VM with no event pump that must never keep the CPU (an installed
+// control file's, shared by every program): a run, or a call into it (an
+// event), that lasts more than slices slices is stopped with error 18,
+// which ON ERROR cannot trap; SLEEP, FRAMEWAIT, and SHELL raise error 5
+// instead of blocking, and BEEP, SOUND, and PLAY do not wait for
+// foreground music. 0: no limit (the default).
+void basVmSetRunLimit(BasVmT *vm, int32_t slices);
+
+// Give the VM a clock for its slices: clock is a counter the host's timer
+// interrupt advances (reading it costs one load), span how far it may move
+// in one slice. A slice (a basVmRun with a step limit, or the stretch of
+// an event handler between two doEvents) then also ends once the clock has
+// moved span, at the first instruction run out of line after that, so a
+// loop of long calls (a file copy, a SQL statement) gives the host its turn
+// as often as a loop of quick instructions does. NULL: steps only.
+void basVmSetSliceClock(BasVmT *vm, const volatile uint32_t *clock, uint32_t span);
+
+// A fine monotonic clock in microseconds (platformMicros) for TIMER: the
+// wall clock moves in coarse steps on DOS (a BIOS tick, about 55 ms), and
+// the time since it last moved is added from this, never a whole step, so
+// TIMER rises smoothly and never passes the wall clock's next reading.
+// NULL: the wall clock alone.
+void basVmSetMicrosClock(BasVmT *vm, uint64_t (*micros)(void));
+
// Set the printer callbacks (LPRINT, OPEN "LPTn:").
void basVmSetPrinterCallbacks(BasVmT *vm, const BasPrinterCallbacksT *printer);
@@ -965,8 +1023,10 @@ void basVmSetDeviceCallbacks(BasVmT *vm, const BasDeviceCallbacksT *device);
// Set the program screen callbacks.
void basVmSetScreenHost(BasVmT *vm, const BasScreenHostT *host);
-// Set UI callbacks (for form/control system).
+// Set the SQL callbacks (the dvxsql library's functions).
void basVmSetSqlCallbacks(BasVmT *vm, const BasSqlCallbacksT *sql);
+
+// Set UI callbacks (for form/control system).
void basVmSetUiCallbacks(BasVmT *vm, const BasUiCallbacksT *ui);
// Set external library callbacks (for DECLARE LIBRARY support).
diff --git a/src/apps/kpunch/dvxbasic/stub/bascomp.c b/src/apps/kpunch/dvxbasic/stub/bascomp.c
index 78684fba..e935b2e6 100644
--- a/src/apps/kpunch/dvxbasic/stub/bascomp.c
+++ b/src/apps/kpunch/dvxbasic/stub/bascomp.c
@@ -181,7 +181,7 @@ int main(int argc, char **argv) {
printf("Compiling %d file(s)...\n", (int)fileCount);
BasCompileErrorT err;
- BasModuleT *mod = basBuildCompile(project.source, project.sourceLen, project.optionExplicit, NULL, NULL, &err);
+ BasModuleT *mod = basBuildCompile(project.source, project.sourceLen, project.optionExplicit, NULL, NULL, &err, NULL, NULL);
if (!mod) {
const char *errPath = NULL;
diff --git a/src/apps/kpunch/dvxbasic/stub/basstub.c b/src/apps/kpunch/dvxbasic/stub/basstub.c
index 979cb7d9..948c111b 100644
--- a/src/apps/kpunch/dvxbasic/stub/basstub.c
+++ b/src/apps/kpunch/dvxbasic/stub/basstub.c
@@ -51,13 +51,11 @@
// App descriptor
// ============================================================
-#define STUB_STACK_SIZE 65536
-
SHELL_APP_DESCRIPTOR(
.name = BAS_DEFAULT_APP_NAME,
.hasMainLoop = true,
.multiInstance = false,
- .stackSize = STUB_STACK_SIZE,
+ .stackSize = BAS_RUN_STACK_SIZE,
.priority = TS_PRIORITY_NORMAL
);
@@ -71,7 +69,6 @@ static BasFormRtT *sRt = NULL; // live while appMain runs the program (F1 que
// Function prototypes (alphabetical; appMain last)
-void appShutdown(void);
static bool stubDoEvents(void *ctx);
static void stubHelpQuery(void *ctx);
static bool stubInput(void *ctx, const char *prompt, char *buf, int32_t bufSize);
@@ -80,16 +77,6 @@ static void stubPrint(void *ctx, const char *text, bool newline);
int32_t appMain(DxeAppContextT *ctx);
-// Resolved by the shell as _appShutdown and called when the app is reaped
-// after appMain returned (which already ran basFormRtDestroy, so this
-// finds the serial ports closed). A force kill runs none of the app's
-// code: basrt's kill hook (basRunHostAppKilled) ends the runtime then,
-// serial ports and idle pollers included, while its windows are there.
-void appShutdown(void) {
- basFormRtSerialShutdown();
-}
-
-
static bool stubDoEvents(void *ctx) {
(void)ctx;
@@ -97,7 +84,7 @@ static bool stubDoEvents(void *ctx) {
return false;
}
- return dvxUpdate(sAc);
+ return sRt ? basFormRtUpdate(sRt) : dvxUpdate(sAc);
}
diff --git a/src/apps/kpunch/dvxbasic/test_compiler.c b/src/apps/kpunch/dvxbasic/test_compiler.c
index 23bec59a..b1917963 100644
--- a/src/apps/kpunch/dvxbasic/test_compiler.c
+++ b/src/apps/kpunch/dvxbasic/test_compiler.c
@@ -7100,8 +7100,9 @@ static void testScreenModesPages(void) {
// Phase 1 library functions: the string functions (REPLACE$, SPLIT, JOIN$,
-// INSTRREV, STRCOMP, STRREVERSE$, FILTER, LSET / RSET, IIF, CHOOSE) and
-// the DATE type with its functions, literals, arithmetic and Format$.
+// INSTRREV, STRCOMP, STRREVERSE$, CHRW$, ASCW, FILTER, LSET / RSET, IIF,
+// CHOOSE) and the DATE type with its functions, literals, arithmetic and
+// Format$.
static void testStringsAndDates(void) {
runProgram("Replace$",
"PRINT Replace$(\"aXbXc\", \"X\", \"-\")\n"
@@ -7164,6 +7165,25 @@ static void testStringsAndDates(void) {
"apple-banana-cherry\napple banana cherry\nxyz\n1,22,333\n[]\n0 banana\napple+cherry\nbanana\n-1 \n"
);
+ runProgram("Dir$ takes DOS's wildcards: *.* is every file, [ is a plain character",
+ "MKDIR \"dirwild\"\n"
+ "OPEN \"dirwild/plain\" FOR OUTPUT AS #1: CLOSE #1\n"
+ "OPEN \"dirwild/a[1].txt\" FOR OUTPUT AS #1: CLOSE #1\n"
+ "OPEN \"dirwild/b.TXT\" FOR OUTPUT AS #1: CLOSE #1\n"
+ "n = 0\n"
+ "f$ = Dir$(\"dirwild/*.*\")\n"
+ "DO WHILE f$ <> \"\"\n"
+ " n = n + 1\n"
+ " f$ = Dir$()\n"
+ "LOOP\n"
+ "PRINT n; Dir$(\"dirwild/a[1]*\"); \" \"; Dir$(\"dirwild/B.t?t\")\n"
+ "KILL \"dirwild/plain\"\n"
+ "KILL \"dirwild/a[1].txt\"\n"
+ "KILL \"dirwild/b.TXT\"\n"
+ "RMDIR \"dirwild\"\n",
+ "3 a[1].txt b.TXT\n"
+ );
+
runProgram("InStrRev, StrComp and StrReverse$",
"PRINT InStrRev(\"abcabc\", \"bc\")\n"
"PRINT InStrRev(\"abcabc\", \"bc\", 4)\n"
@@ -7178,6 +7198,26 @@ static void testStringsAndDates(void) {
"5 \n2 \n0 \n3 \n2 \n0 \n0 \n-1 1 0 \n-1 0 -1 \ncba[]\n"
);
+ runProgram("FULLSCREEN reads the program screen's state without making one",
+ "PRINT FULLSCREEN; NOT FULLSCREEN\n",
+ "False True \n"
+ );
+
+ runProgram("ChrW$ and AscW: Unicode code points as DVX text",
+ "c = 130\n"
+ "PRINT AscW(\"A\"); AscW(Chr$(10)); AscW(Chr$(c)); AscW(Chr$(196))\n"
+ "PRINT Asc(ChrW$(&HE9)); Asc(ChrW(&H2500)); Asc(ChrW$(65)); Asc(ChrW$(&H263A))\n"
+ "PRINT ChrW$(&H4E2D); LEN(ChrW$(&H1F600))\n"
+ "e$ = \"\"\n"
+ "ON ERROR GOTO h\n"
+ "PRINT AscW(e$)\n"
+ "PRINT ChrW$(-1)\n"
+ "PRINT ChrW$(&H110000)\n"
+ "END\n"
+ "h: PRINT \"err\"; ERR: RESUME NEXT\n",
+ "65 10 233 9472 \n130 196 65 1 \n?1 \nerr5 \nerr5 \nerr5 \n"
+ );
+
runProgram("Drag and clipboard constants; Control, Form and Object types",
"PRINT vbManual; vbAutomatic; vbBeginDrag; vbEndDrag\n"
"PRINT vbEnter; vbLeave; vbOver; vbCFText\n"
@@ -8716,7 +8756,9 @@ int main(void) {
}
printf(found ? "PASS: OP_CALL_EXTERN emitted\n" : "FAIL: OP_CALL_EXTERN not emitted\n");
- if (!found) { gFailCount++; }
+ if (!found) {
+ gFailCount++;
+ }
basModuleFree(mod);
} else {
printf("MODULE BUILD FAILED\n");
@@ -8792,7 +8834,9 @@ int main(void) {
printf("Calling Command1_Click via basVmCallSub: ");
bool callOk = basVmCallSub(vm, p->codeAddr);
printf("result=%s\n", callOk ? "ok" : "FAIL");
- if (!callOk) { gFailCount++; }
+ if (!callOk) {
+ gFailCount++;
+ }
}
basVmDestroy(vm);
@@ -9835,10 +9879,14 @@ int main(void) {
const BasProcEntryT *p1 = basModuleFindProc(mod, "Form1_Load");
const BasProcEntryT *p2 = basModuleFindProc(mod, "Command1_Click");
- if (p1) { basVmCallSub(vm, p1->codeAddr); }
+ if (p1) {
+ basVmCallSub(vm, p1->codeAddr);
+ }
else { printf("Form1_Load NOT FOUND\n"); }
- if (p2) { basVmCallSub(vm, p2->codeAddr); }
+ if (p2) {
+ basVmCallSub(vm, p2->codeAddr);
+ }
else { printf("Command1_Click NOT FOUND\n"); }
basVmDestroy(vm);
diff --git a/src/apps/kpunch/dvxbasic/test_suite.c b/src/apps/kpunch/dvxbasic/test_suite.c
index 83c3210f..b9823456 100644
--- a/src/apps/kpunch/dvxbasic/test_suite.c
+++ b/src/apps/kpunch/dvxbasic/test_suite.c
@@ -41,6 +41,8 @@
#include "dvxRes.h"
#include
+#include
+#include
#include
#include "compiler/strip.h"
#include "runtime/serialize.h"
@@ -73,6 +75,8 @@
// Failure detail text of a check.
#define TEST_DETAIL_LEN 512
+#define SLICE_SPAN 2 // testSliceClock: clock counts a slice may last
+#define SLICE_STEPS 1000000 // testSliceClock: far more steps than the slice runs
#define TEST_IMPL_CORRUPTIONS 3 // testInterfaceMaps: bad entry, short map, bad interface
#define TEST_ENUM_CORRUPTIONS 7 // testUserControlEnums: index past the table, below none, on a method, in another class, an Enum without members, an empty name, two names alike
#define TEST_ENUM_WIRE_NAME "\x06\x00" "Circle" // the member Circle on the wire: uint16 length, then the name
@@ -80,6 +84,13 @@
#define TEST_ENUM_WIRE_NUL 5 // a byte of the name, made NUL
#define TS_BIG_FILE "ts_big.tmp" // a sparse file past 2 GB (testBattery)
#define TS_BIG_SIZE 3000000000LL // bytes: a FAT32 file may reach 4 GB - 1
+#define TS_COMPILE_UNITS 4 // testCompileUnits: two assignments, a SUB, a PRINT
+#define TS_DIR_PUMP_DIR "ts_dirp" // testDirPumps' folder
+#define TS_DIR_PUMP_FILES 200 // files in it Dir$ passes over
+#define TS_DIR_PUMPS_MIN 2 // pumps at least: 200 passed over in two calls, a pump per 64
+#define TS_PUMPS_MANY 1000 // a countdown no test runs out
+#define TS_INI_FILE "ts_inic.tmp" // testIniCache's file
+#define TS_INI_SETTLED_SEC 3600 // its time: an hour old, past the 2 s a FAT time cannot tell apart
static CaptureT *sUiCap = NULL;
static char sUiRefs[UI_STUB_REF_COUNT][UI_STUB_REF_LEN];
@@ -238,6 +249,7 @@ static bool deviceSoundBusyStub(void *ctx);
static void deviceSoundStub(void *ctx, double hz, double ticks);
static int32_t deviceStickStub(void *ctx, int32_t n);
static int32_t deviceStrigStub(void *ctx, int32_t n);
+static void dirPumpFiles(bool make);
static bool doEventsAbortStub(void *ctx);
static bool doEventsCountdownStub(void *ctx);
static bool doEventsResumeStub(void *ctx);
@@ -248,6 +260,7 @@ static void handEmit16(HandCodeT *h, int16_t v);
static void handEmit32(HandCodeT *h, int32_t v);
static void handEmit8(HandCodeT *h, uint8_t b);
static void handEmitU16(HandCodeT *h, uint16_t v);
+static bool iniRewriteStub(void *ctx);
static bool inputStub(void *ctx, const char *prompt, char *buf, int32_t bufSize);
static int32_t memberIndex(const BasClassInfoT *cls, const char *memberName, uint8_t kind);
static MiniCtrlT *miniAddCtrl(MiniFormT *f, const char *name, const char *typeName);
@@ -256,6 +269,7 @@ static void miniFireCtrlEvent(MiniRtT *rt, MiniCtrlT *ctrl, const char *eventNam
static MiniFormT *miniResolveOwningForm(MiniRtT *rt, const BasProcEntryT *proc);
static void miniSetEvent(MiniCtrlT *ctrl, const char *eventName, const char *handlerName);
static bool moduleHasOpcode(const BasModuleT *mod, uint8_t op);
+static void printTickStub(void *ctx, const char *text, bool newline);
static void reportFail(const char *name, const char *detail);
static void reportFailDiff(const char *name, const char *expected, const char *got);
static void reportPass(const char *name);
@@ -267,9 +281,11 @@ static void testBytecode(const char *name, const char *source, const uint8_t *ex
static void testClassRoundTrip(const char *name);
static void testCommandArgs(const char *name);
static void testCompileError(const char *name, const char *source, const char *needle);
+static void testCompileUnits(void);
static void testDebuggerApi(void);
static void testDebugInfoRoundTrip(const char *name);
static void testDeviceStatements(void);
+static void testDirPumps(void);
static void testDispatchEq(const char *name, const TestDispatchT *d, const char *expected);
static void testEarlyBoundOpcodes(void);
static void testEq(const char *name, const char *source, const char *expected);
@@ -278,6 +294,7 @@ static void testHandAssembled(void);
static void testHandBattery(void);
static void testHandRun(const char *name, const HandCodeT *h, BasStringT **consts, int32_t constCount, int32_t localCount, const char *expected);
static void testHandRunError(const char *name, const HandCodeT *h, BasStringT **consts, int32_t constCount, int32_t localCount, const char *needle);
+static void testIniCache(void);
static void testInputCallback(const char *name);
static void testInterfaceMaps(const char *name);
static void testLexerTokens(const char *name, const char *source, const ExpectedTokenT *expected, int32_t expectedCount);
@@ -290,6 +307,7 @@ static void testRuntimeError(const char *name, const char *source, const char *n
static void testSerializeCorrupt(const char *name);
static void testSharedFormScope(const char *name);
static void testSleepPumpsEvents(const char *name);
+static void testSliceClock(void);
static void testStrAppend(void);
static void testSubEq(const char *name, const char *source, const char *subName, const char *expected);
static void testUiEq(const char *name, const char *source, const char *expected);
@@ -314,6 +332,12 @@ static void uiStubSetEvent(void *ctx, void *ctrlRef, const char *eventName, cons
static void uiStubSetProp(void *ctx, void *ctrlRef, const char *propName, BasValueT value);
static void uiStubShowForm(void *ctx, void *formRef, bool modal);
static void uiStubUnloadForm(void *ctx, void *formRef);
+static void unitCountStub(void *ctx);
+
+
+// testSliceClock's slice clock (printTickStub moves it) and its prints
+static volatile uint32_t sSliceTicks = 0;
+static int32_t sSlicePrints = 0;
static const BasUiCallbacksT sUiStubs = {
@@ -482,6 +506,31 @@ static int32_t deviceStrigStub(void *ctx, int32_t n) {
}
+// testDirPumps' files: TS_DIR_PUMP_FILES that do not match and one that
+// does, made (empty) or removed.
+static void dirPumpFiles(bool make) {
+ char path[TEST_DETAIL_LEN];
+
+ for (int32_t i = 0; i <= TS_DIR_PUMP_FILES; i++) {
+ if (i < TS_DIR_PUMP_FILES) {
+ snprintf(path, sizeof(path), TS_DIR_PUMP_DIR "/f%03d.dat", (int)i);
+ } else {
+ snprintf(path, sizeof(path), TS_DIR_PUMP_DIR "/match.zzz");
+ }
+
+ FILE *f = make ? fopen(path, "w") : NULL;
+
+ if (f) {
+ fclose(f);
+ }
+
+ if (!make) {
+ remove(path);
+ }
+ }
+}
+
+
// DoEvents stub that tells the VM to stop (host shutting down).
static bool doEventsAbortStub(void *ctx) {
(void)ctx;
@@ -573,6 +622,38 @@ static void handEmitU16(HandCodeT *h, uint16_t v) {
}
+// testIniCache's DoEvents: the nth call rewrites the file -- the same size
+// and time, then another size, then the same size at the time now.
+static bool iniRewriteStub(void *ctx) {
+ static const char *texts[] = { "[A]\nk=2\n", "[A]\nk=33\n", "[A]\nk=44\n" };
+ int32_t *calls = (int32_t *)ctx;
+
+ if (*calls >= (int32_t)(sizeof(texts) / sizeof(texts[0]))) {
+ return false;
+ }
+
+ FILE *f = fopen(TS_INI_FILE, "w");
+
+ if (!f) {
+ return false;
+ }
+
+ fputs(texts[*calls], f);
+ fclose(f);
+
+ if (*calls < 2) {
+ struct utimbuf settled;
+
+ settled.actime = time(NULL) - TS_INI_SETTLED_SEC;
+ settled.modtime = settled.actime;
+ utime(TS_INI_FILE, &settled);
+ }
+
+ (*calls)++;
+ return true;
+}
+
+
// INPUT stub: logs the prompt, answers "42" on the first call and
// cancels every later one.
static bool inputStub(void *ctx, const char *prompt, char *buf, int32_t bufSize) {
@@ -797,6 +878,18 @@ static bool moduleHasOpcode(const BasModuleT *mod, uint8_t op) {
}
+// testSliceClock's print callback: each line moves the slice clock.
+static void printTickStub(void *ctx, const char *text, bool newline) {
+ (void)ctx;
+ (void)text;
+
+ if (newline) {
+ sSliceTicks++;
+ sSlicePrints++;
+ }
+}
+
+
static void reportFail(const char *name, const char *detail) {
printf("FAIL %s\n", name);
@@ -1435,6 +1528,26 @@ static void testCompileError(const char *name, const char *source, const char *n
}
+// basBuildCompile's unit callback runs after every unit compiled (a
+// statement, or a whole procedure), and the module is the same as
+// without it.
+static void testCompileUnits(void) {
+ const char *src = "a = 1\nb = 2\nSUB S\n PRINT 1\n PRINT 2\nEND SUB\nPRINT a + b\n";
+ BasCompileErrorT err;
+ int32_t units = 0;
+ char detail[TEST_DETAIL_LEN];
+ BasModuleT *mod = basBuildCompile(src, (int32_t)strlen(src), false, NULL, NULL, &err, unitCountStub, &units);
+ BasModuleT *plain = basBuildCompile(src, (int32_t)strlen(src), false, NULL, NULL, &err, NULL, NULL);
+
+ snprintf(detail, sizeof(detail), "units=%d", (int)units);
+ expectTrue("compile-unit-callback", mod && plain && units == TS_COMPILE_UNITS && mod->codeLen == plain->codeLen && memcmp(mod->code, plain->code, (size_t)mod->codeLen) == 0, detail);
+ basModuleFree(mod);
+ basModuleFree(plain);
+}
+
+
+// Breakpoints, step into/over/out, run to cursor, and the runSubLoop
+// pause/resume/abort protocol used by the IDE debugger.
static void testDebuggerApi(void) {
const char *source =
"DIM x AS INTEGER\n"
@@ -1623,6 +1736,9 @@ static void testDebuggerApi(void) {
}
+// Debug-info side channel: serialize the debugVars table, wipe it on a
+// second compile of the same source, deserialize, and compare. Also
+// drives the truncated-stream rejection paths.
static void testDebugInfoRoundTrip(const char *name) {
const char *source =
"DIM g AS INTEGER\n"
@@ -1728,9 +1844,6 @@ static void testDebugInfoRoundTrip(const char *name) {
}
-// Debug-info side channel: serialize the debugVars table, wipe it on a
-// second compile of the same source, deserialize, and compare. Also
-// drives the truncated-stream rejection paths.
// BEEP, SOUND, PLAY, PLAY(n), STICK, STRIG and FRAMEWAIT reach the device
// callbacks with QBasic's ranges checked, and wait (pumping events) while
// the host says so.
@@ -1775,8 +1888,42 @@ static void testDeviceStatements(void) {
}
-// Breakpoints, step into/over/out, run to cursor, and the runSubLoop
-// pause/resume/abort protocol used by the IDE debugger.
+// Dir$ lets the program's events run every BAS_DIR_PUMP_ENTRIES entries it
+// passes over, and halts the program when they say stop.
+static void testDirPumps(void) {
+ mkdir(TS_DIR_PUMP_DIR, 0755);
+ dirPumpFiles(true);
+
+ BasModuleT *mod = compileSource("dir-pumps", "n = 0\nf$ = DIR$(\"" TS_DIR_PUMP_DIR "/*.zzz\")\nDO WHILE f$ <> \"\"\n n = n + 1\n f$ = DIR$\nLOOP\nPRINT n\n");
+
+ for (int32_t stop = 0; mod && stop <= 1; stop++) {
+ BasVmT *vm = basVmCreate();
+ CaptureT cap;
+ int32_t countdown = stop ? 0 : TS_PUMPS_MANY;
+ char detail[TEST_DETAIL_LEN];
+
+ captureReset(&cap);
+ basVmLoadModule(vm, mod);
+ basVmSetPrintCallback(vm, capturePrint, &cap);
+ basVmSetDoEventsCallback(vm, doEventsCountdownStub, &countdown);
+
+ BasVmResultE r = basVmRun(vm);
+
+ snprintf(detail, sizeof(detail), "r=%d pumps=%d out=[%s]", (int)r, (int)(TS_PUMPS_MANY - countdown), cap.buf);
+
+ if (stop) {
+ expectTrue("dir-pumps-stop-halts", r == BAS_VM_HALTED && cap.len == 0, detail);
+ } else {
+ expectTrue("dir-pumps-pass-over", strcmp(cap.buf, "1 \n") == 0 && TS_PUMPS_MANY - countdown >= TS_DIR_PUMPS_MIN, detail);
+ }
+
+ basVmDestroy(vm);
+ }
+
+ basModuleFree(mod);
+ dirPumpFiles(false);
+ rmdir(TS_DIR_PUMP_DIR);
+}
static void testDispatchEq(const char *name, const TestDispatchT *d, const char *expected) {
@@ -2390,6 +2537,53 @@ static void testHandRunError(const char *name, const HandCodeT *h, BasStringT **
}
+// IniRead keeps the file parsed while its size and time are the same and
+// it is older than a FAT time can tell apart; a new size or a new time
+// reads it again.
+static void testIniCache(void) {
+ FILE *f = fopen(TS_INI_FILE, "w");
+
+ if (f) {
+ fputs("[A]\nk=1\n", f);
+ fclose(f);
+ }
+
+ struct utimbuf settled;
+
+ settled.actime = time(NULL) - TS_INI_SETTLED_SEC;
+ settled.modtime = settled.actime;
+ utime(TS_INI_FILE, &settled);
+
+ BasModuleT *mod = compileSource("ini-cache",
+ "PRINT INIREAD(\"" TS_INI_FILE "\", \"A\", \"k\", \"?\")\n"
+ "DOEVENTS\n"
+ "PRINT INIREAD(\"" TS_INI_FILE "\", \"A\", \"k\", \"?\")\n"
+ "DOEVENTS\n"
+ "PRINT INIREAD(\"" TS_INI_FILE "\", \"A\", \"k\", \"?\")\n"
+ "DOEVENTS\n"
+ "PRINT INIREAD(\"" TS_INI_FILE "\", \"A\", \"k\", \"?\")\n");
+
+ if (mod) {
+ BasVmT *vm = basVmCreate();
+ CaptureT cap;
+ int32_t calls = 0;
+ char detail[TEST_DETAIL_LEN];
+
+ captureReset(&cap);
+ basVmLoadModule(vm, mod);
+ basVmSetPrintCallback(vm, capturePrint, &cap);
+ basVmSetDoEventsCallback(vm, iniRewriteStub, &calls);
+ basVmRun(vm);
+ snprintf(detail, sizeof(detail), "calls=%d out=[%s]", (int)calls, cap.buf);
+ expectTrue("ini-cache-kept-and-reread", calls == 3 && strcmp(cap.buf, "1\n1\n33\n44\n") == 0, detail);
+ basVmDestroy(vm);
+ basModuleFree(mod);
+ }
+
+ remove(TS_INI_FILE);
+}
+
+
// INPUT through the host callback: prompt text, an accepted line, and a
// cancelled read (empty result).
static void testInputCallback(const char *name) {
@@ -3635,6 +3829,49 @@ static void testSleepPumpsEvents(const char *name) {
}
+// The slice clock: a slice ends once the host's clock has moved its span,
+// at the first instruction run out of line after that, however few steps
+// it has run -- here each PRINT line moves the clock one count, as a long
+// native call would. Without a clock the same slice runs to its steps.
+static void testSliceClock(void) {
+ BasModuleT *mod = compileSource("slice-clock", "DIM i AS LONG\nFOR i = 1 TO 1000000\n PRINT i\nNEXT i\n");
+
+ if (!mod) {
+ return;
+ }
+
+ BasVmT *vm = basVmCreate();
+ char detail[TEST_DETAIL_LEN];
+
+ basVmLoadModule(vm, mod);
+ basVmSetPrintCallback(vm, printTickStub, NULL);
+ basVmSetStepLimit(vm, SLICE_STEPS);
+ basVmSetSliceClock(vm, &sSliceTicks, SLICE_SPAN);
+ sSliceTicks = 0;
+ sSlicePrints = 0;
+
+ BasVmResultE r = basVmRun(vm);
+
+ snprintf(detail, sizeof(detail), "r=%d prints=%d", (int)r, (int)sSlicePrints);
+ expectTrue("slice-clock-ends-slice", r == BAS_VM_STEP_LIMIT && sSlicePrints == SLICE_SPAN, detail);
+
+ // The next slice measures from its own start
+ sSlicePrints = 0;
+ r = basVmRun(vm);
+ snprintf(detail, sizeof(detail), "r=%d prints=%d", (int)r, (int)sSlicePrints);
+ expectTrue("slice-clock-next-slice", r == BAS_VM_STEP_LIMIT && sSlicePrints == SLICE_SPAN, detail);
+
+ basVmSetSliceClock(vm, NULL, 0);
+ sSlicePrints = 0;
+ r = basVmRun(vm);
+ snprintf(detail, sizeof(detail), "r=%d prints=%d", (int)r, (int)sSlicePrints);
+ expectTrue("slice-clock-none-steps", r == BAS_VM_STEP_LIMIT && sSlicePrints > SLICE_SPAN, detail);
+
+ basVmDestroy(vm);
+ basModuleFree(mod);
+}
+
+
// "s = s + x" / "s = s & x" on a STRING variable compile to OP_STR_APPEND
// (in-place growth); anything whose operand could observe or change the
// variable keeps the ordinary load/concat/store form. Both forms must
@@ -4349,6 +4586,12 @@ static void uiStubUnloadForm(void *ctx, void *formRef) {
}
+// testCompileUnits' unit callback: counts.
+static void unitCountStub(void *ctx) {
+ (*(int32_t *)ctx)++;
+}
+
+
int main(void) {
printf("DVX BASIC Regression Suite\n");
printf("==========================\n");
@@ -5136,6 +5379,20 @@ int main(void) {
"IF t >= 0 THEN PRINT \"ok\"\n",
"ok\n");
+ // --- TIMER keeps rising in fine steps: readings in a busy loop never
+ // go back, and some differ by less than a 55 ms clock tick ---
+ TEST_EQ("timer-fine",
+ "DIM a AS DOUBLE\nDIM b AS DOUBLE\nDIM i AS LONG\nDIM fine AS LONG\nDIM back AS LONG\n"
+ "a = TIMER\n"
+ "FOR i = 1 TO 20000\n"
+ " b = TIMER\n"
+ " IF b < a AND a - b < 1 THEN back = back + 1\n"
+ " IF b > a AND b - a < 0.05 THEN fine = fine + 1\n"
+ " a = b\n"
+ "NEXT i\n"
+ "PRINT back; fine > 0\n",
+ "0 True \n");
+
// --- Integer overflow promotes to LONG ---
TEST_EQ("int-to-long-promotion",
"DIM a AS INTEGER\nDIM b AS INTEGER\n"
@@ -7777,9 +8034,48 @@ int main(void) {
"KILL \"ts_dird/f1.tmp\"\nRMDIR \"ts_dird/sub\"\nRMDIR \"ts_dird\"\n",
"[f1.tmp]\n");
+ // FileCopy copies a file of many pieces whole (each piece may let the
+ // program's events run).
+ TEST_EQ("filecopy-many-pieces",
+ "OPEN \"ts_fc.tmp\" FOR OUTPUT AS #1\n"
+ "FOR i = 1 TO 2000\n PRINT #1, \"line\"; i; STRING$(40, \"x\")\nNEXT i\n"
+ "CLOSE #1\n"
+ "FILECOPY \"ts_fc.tmp\", \"ts_fc2.tmp\"\n"
+ "PRINT FILELEN(\"ts_fc.tmp\") = FILELEN(\"ts_fc2.tmp\"); FILELEN(\"ts_fc2.tmp\") > 65536\n"
+ "OPEN \"ts_fc2.tmp\" FOR INPUT AS #1\n"
+ "DO WHILE NOT EOF(1)\n LINE INPUT #1, l$\nLOOP\n"
+ "CLOSE #1\n"
+ "PRINT LEFT$(l$, 9)\n"
+ "KILL \"ts_fc.tmp\"\nKILL \"ts_fc2.tmp\"\n",
+ "True True \nline2000 \n");
+
+ // Keyed Collection lookups through the key index: built on the first
+ // lookup, kept as keys are added (and rebuilt as it fills), rebuilt
+ // after a removal or an insert before the end; keys ignore case.
+ TEST_EQ("collection-key-index",
+ "DIM c AS NEW Collection\nDIM i AS LONG\nDIM s AS LONG\n"
+ "FOR i = 1 TO 5\n c.Add i, \"k\" & i\nNEXT i\n"
+ "PRINT c(\"k3\")\n"
+ "FOR i = 6 TO 100\n c.Add i, \"k\" & i\nNEXT i\n"
+ "FOR i = 1 TO 100\n s = s + c(\"K\" & i)\nNEXT i\n"
+ "PRINT s; c.Count\n"
+ "c.Remove \"k50\"\nc.Remove 1\n"
+ "PRINT c.Count; c(\"k51\"); c(1); c(49)\n"
+ "c.Add 777, \"new\", 1\n"
+ "PRINT c(1); c(\"NEW\"); c(\"k100\"); c(\"k2\")\n"
+ "ON ERROR GOTO EH\n"
+ "c.Add 1, \"K2\"\n"
+ "x = c(\"k50\")\n"
+ "PRINT \"end\"\nEND\n"
+ "EH:\nPRINT \"err\"; ERR\nRESUME NEXT\n",
+ "3 \n5050 100 \n98 51 2 51 \n777 777 100 2 \nerr457 \nerr5 \nend\n");
+
// SLEEP keeps pumping the host's events and stops when told to.
testSleepPumpsEvents("sleep-pumps-events");
+ testSliceClock();
testDeviceStatements();
+ testDirPumps();
+ testIniCache();
testStrAppend();
testObfuscateCaption("obfuscate-caption-kept");
testObfuscateKeepsNames("obfuscate-keeps-names-name", "SUB Total_Click\n PRINT Total.Name\nEND SUB\n");
@@ -7799,6 +8095,7 @@ int main(void) {
testHandBattery();
testOpcodeGuards();
testBasBuild();
+ testCompileUnits();
// Coverage battery: debugger, externs, serialization, values API.
testDebuggerApi();
diff --git a/src/apps/kpunch/dvxdemo/dvxdemo.c b/src/apps/kpunch/dvxdemo/dvxdemo.c
index 724f8ea6..baf3f859 100644
--- a/src/apps/kpunch/dvxdemo/dvxdemo.c
+++ b/src/apps/kpunch/dvxdemo/dvxdemo.c
@@ -1115,7 +1115,11 @@ static void setupMainWindow(void) {
MenuT *viewMenu = wmAddMenu(bar, "&View");
if (viewMenu) {
- wmAddMenuItem(viewMenu, "&Terminal", CMD_VIEW_TERM);
+ // The terminal widget is part of Communications
+ if (dvxAnsiTermApi()) {
+ wmAddMenuItem(viewMenu, "&Terminal", CMD_VIEW_TERM);
+ }
+
wmAddMenuItem(viewMenu, "&Controls", CMD_VIEW_CTRL);
wmAddMenuSeparator(viewMenu);
wmAddMenuCheckItem(viewMenu, "Tool&bar", CMD_VIEW_TOOLBAR, true);
@@ -1222,7 +1226,12 @@ static void setupMainWindow(void) {
// supported attributes (bold, reverse, blink), 8+8 colors, background
// colors, and CP437 box-drawing/graphic characters. The terminal is
// minimized on creation to avoid cluttering the initial demo view.
+// Nothing without the terminal widget (part of Communications).
static void setupTerminalWindow(void) {
+ if (!dvxAnsiTermApi()) {
+ return;
+ }
+
WindowT *win = dvxCreateWindow(sAc, "ANSI Terminal", DEMO_TERM_X, DEMO_TERM_Y, DEMO_TERM_W, DEMO_TERM_H, true);
if (!win) {
diff --git a/src/apps/kpunch/dvxdemo/dvxdemo.dhs b/src/apps/kpunch/dvxdemo/dvxdemo.dhs
index 93263053..7c2f0bec 100644
--- a/src/apps/kpunch/dvxdemo/dvxdemo.dhs
+++ b/src/apps/kpunch/dvxdemo/dvxdemo.dhs
@@ -1,24 +1,24 @@
-// 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.
+# 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.
# DVX Demo help. Compiled to dvxdemo.hlp next to dvxdemo.app; F1 and
# Help > Contents in the DVX Information window open it.
diff --git a/src/apps/kpunch/dvxdemo/dvxdemo.res b/src/apps/kpunch/dvxdemo/dvxdemo.res
index dc033e3b..6030f379 100644
--- a/src/apps/kpunch/dvxdemo/dvxdemo.res
+++ b/src/apps/kpunch/dvxdemo/dvxdemo.res
@@ -1,3 +1,25 @@
+# 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.
+
# dvxdemo.res -- Resource manifest for DVX Demo
icon32 icon icon32.bmp
name text "DVX Demo"
diff --git a/src/apps/kpunch/dvxhelp/dvxhelp.res b/src/apps/kpunch/dvxhelp/dvxhelp.res
index 539a1654..df6100a9 100644
--- a/src/apps/kpunch/dvxhelp/dvxhelp.res
+++ b/src/apps/kpunch/dvxhelp/dvxhelp.res
@@ -1,3 +1,25 @@
+# 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.
+
# dvxhelp.res -- Resource manifest for DVX Help Viewer
icon32 icon icon32.bmp
name text "DVX Help"
diff --git a/src/apps/kpunch/fileMgr/fileMgr.bas b/src/apps/kpunch/fileMgr/fileMgr.bas
index 6d232314..9702379b 100644
--- a/src/apps/kpunch/fileMgr/fileMgr.bas
+++ b/src/apps/kpunch/fileMgr/fileMgr.bas
@@ -35,7 +35,9 @@
' the variables of the form whose code it is, and a finished job shows
' the main window's folder again.
'
-' The Properties dialog leaves its answer here too.
+' The Properties dialog leaves its answer here too, and counts the size
+' of the selection here: its timer works on a measure job a slice at a
+' time (fmProps.frm), so a big folder does not stop DVX.
OPTION EXPLICIT
@@ -66,12 +68,24 @@ DIM SHARED fmJobCount AS LONG
DIM SHARED fmPropsAccepted AS BOOLEAN
DIM SHARED fmPropsAttr AS LONG
+' Its count of the selection: the measure job (0 once done), the selected
+' items that are folders, and whether one could not be found
+DIM SHARED fmPropsJob AS LONG
+DIM SHARED fmPropsFolders AS LONG
+DIM SHARED fmPropsMissing AS BOOLEAN
+
SUB ShowError(BYVAL title AS STRING, BYVAL message AS STRING)
MsgBox message, vbOKOnly + vbCritical, title
END SUB
+' "1 file", "3 files": count things, thing in the singular.
+FUNCTION CountOf$(BYVAL count AS LONG, BYVAL thing AS STRING) AS STRING
+ CountOf$ = CStr(count) & " " & thing & IIF(count = 1, "", "s")
+END FUNCTION
+
+
' The running job whose window is called windowName, or Nothing.
FUNCTION FindJob(BYVAL windowName AS STRING) AS FmJob
DIM job AS FmJob
@@ -213,6 +227,44 @@ END SUB
' Progress line, file count and bar of the job's window.
SUB JobShowProgress(BYVAL job AS FmJob)
job.Window.lblWhat.Caption = job.Verb & " " & FileOpsJobCurrent$(job.Handle)
- job.Window.lblCounts.Caption = CStr(FileOpsJobFilesDone(job.Handle)) & " of " & CStr(FileOpsJobFilesTotal(job.Handle)) & " files"
+ job.Window.lblCounts.Caption = CStr(FileOpsJobFilesDone(job.Handle)) & " of " & CountOf$(FileOpsJobFilesTotal(job.Handle), "file")
job.Window.pbJob.Value = FileOpsJobPercent(job.Handle)
END SUB
+
+
+' ============================================================
+' The Properties dialog's count
+' ============================================================
+
+' The size and contents of the selection in the Properties dialog: so far
+' while counting, then (done) all of it, the job freed and the timer
+' stopped. The dialog grows when a line gets longer.
+SUB PropsShowCount(BYVAL done AS BOOLEAN)
+ DIM size AS STRING
+ DIM contains AS STRING
+ DIM grow AS BOOLEAN
+
+ size = "Size: " & FileOpsFormatSize$(FileOpsJobBytesTotal(fmPropsJob)) & " bytes"
+ contains = "Contains: " & CountOf$(FileOpsJobFilesTotal(fmPropsJob), "file") & ", " & CountOf$(FileOpsJobFoldersTotal(fmPropsJob) + fmPropsFolders, "folder")
+
+ IF NOT done THEN
+ size = size & " so far"
+ contains = contains & " so far"
+ ELSEIF fmPropsMissing OR FileOpsJobUnread(fmPropsJob) > 0 THEN
+ size = size & " (some items could not be read)"
+ END IF
+
+ grow = Len(size) > Len(PropsForm.lblSize.Caption) OR Len(contains) > Len(PropsForm.lblContains.Caption)
+ PropsForm.lblSize.Caption = size
+ PropsForm.lblContains.Caption = contains
+
+ IF grow THEN
+ PropsForm.AutoSize = True
+ END IF
+
+ IF done THEN
+ PropsForm.tmrCount.Enabled = False
+ FileOpsJobFree fmPropsJob
+ fmPropsJob = 0
+ END IF
+END SUB
diff --git a/src/apps/kpunch/fileMgr/fileMgr.dhs b/src/apps/kpunch/fileMgr/fileMgr.dhs
index 0ed22e22..1ff4a095 100644
--- a/src/apps/kpunch/fileMgr/fileMgr.dhs
+++ b/src/apps/kpunch/fileMgr/fileMgr.dhs
@@ -216,7 +216,7 @@ Files and disks bigger than 2 GB (a FAT32 disk) show their full size.
.h1 Properties
-Select one or more items and choose File > Properties or press Alt+Enter. The Properties window shows the name, the folder it is in, the total size (for a folder, everything in it) with the number of files and folders, and the date the item was last changed.
+Select one or more items and choose File > Properties or press Alt+Enter. The Properties window shows the name, the folder it is in, the total size (for a folder, everything in it) with the number of files and folders, and the date the item was last changed. A big folder takes a moment to count: the size and the numbers grow, marked "so far", until the count is done, and you can change the attributes or close the window meanwhile.
The Attributes boxes can be changed; click OK to apply them to every selected item, or Cancel to leave them as they were.
diff --git a/src/apps/kpunch/fileMgr/fileMgr.frm b/src/apps/kpunch/fileMgr/fileMgr.frm
index 67d17645..2d01b2ee 100644
--- a/src/apps/kpunch/fileMgr/fileMgr.frm
+++ b/src/apps/kpunch/fileMgr/fileMgr.frm
@@ -513,14 +513,27 @@ SUB UpdateTitle
END SUB
-' "N items" or "S of N selected (B bytes)", and the free space of the
-' folder's drive.
+' The free space of the folder's drive. Asking DOS takes a while on a
+' floppy or a network drive, so it is asked on navigate and refresh only.
+SUB UpdateFree
+ DIM freeBytes AS DOUBLE
+
+ freeBytes = FileOpsFreeBytes(curFolder)
+
+ IF freeBytes >= 0 THEN
+ lblFree.Caption = FileOpsFormatSize$(freeBytes) & " bytes free"
+ ELSE
+ lblFree.Caption = ""
+ END IF
+END SUB
+
+
+' "N items" or "S of N selected (B bytes)".
SUB UpdateStatus
DIM rows AS STRING
DIM row AS VARIANT
DIM selected AS LONG
DIM bytes AS DOUBLE
- DIM freeBytes AS DOUBLE
rows = fmList.SelectedRows()
@@ -534,15 +547,7 @@ SUB UpdateStatus
IF selected > 0 THEN
fmStatus.Caption = CStr(selected) & " of " & CStr(FileOpsListCount(listing)) & " selected (" & FileOpsFormatSize$(bytes) & " bytes)"
ELSE
- fmStatus.Caption = CStr(FileOpsListCount(listing)) & " items"
- END IF
-
- freeBytes = FileOpsFreeBytes(curFolder)
-
- IF freeBytes >= 0 THEN
- lblFree.Caption = FileOpsFormatSize$(freeBytes) & " bytes free"
- ELSE
- lblFree.Caption = ""
+ fmStatus.Caption = CountOf$(FileOpsListCount(listing), "item")
END IF
END SUB
@@ -611,7 +616,7 @@ FUNCTION Navigate(BYVAL path AS STRING, BYVAL fromTree AS BOOLEAN) AS BOOLEAN
fmPath.Text = curFolder
UpdateTitle
- UpdateStatus
+ UpdateFree
IF fromTree THEN
item = fmTree.ListIndex
@@ -655,12 +660,13 @@ SUB FmRefresh
FreeListing
fmList.SetRows ""
+ UpdateStatus
END IF
SelectByName keep
TreeRebuild
UpdateTitle
- UpdateStatus
+ UpdateFree
END SUB
@@ -727,7 +733,8 @@ END FUNCTION
' Add folder's sub-folders (sorted by name) under the tree item parentItem,
' which has none yet, so they follow it. Returns the item of the one at
-' expandPath, -1 when none.
+' expandPath, -1 when none. The current folder's come from its listing,
+' which already holds them, instead of reading the disk again.
FUNCTION TreeAddChildren(BYVAL parentItem AS LONG, BYVAL folder AS STRING, BYVAL expandPath AS STRING) AS LONG
DIM subList AS LONG
DIM count AS LONG
@@ -737,7 +744,12 @@ FUNCTION TreeAddChildren(BYVAL parentItem AS LONG, BYVAL folder AS STRING, BYVAL
DIM found AS LONG
found = -1
- subList = FileOpsList(folder, "")
+
+ IF listing <> 0 AND FileOpsPathSame(folder, curFolder) THEN
+ subList = FileOpsListFolders(listing)
+ ELSE
+ subList = FileOpsList(folder, "")
+ END IF
IF subList = 0 THEN
TreeAddChildren = found
@@ -1163,14 +1175,12 @@ END SUB
' Size, dates and editable attributes of the selection. Attribute changes
' apply to every selected item.
+' The dialog counts the size itself, from its timer (fmProps.frm), with a
+' measure job of the selection started here.
SUB CmdProperties
DIM paths AS Collection
DIM path AS VARIANT
DIM first AS STRING
- DIM bytes AS DOUBLE
- DIM files AS LONG
- DIM folders AS LONG
- DIM allRead AS BOOLEAN
DIM attr AS LONG
DIM location AS STRING
DIM modified AS STRING
@@ -1183,16 +1193,15 @@ SUB CmdProperties
EXIT SUB
END IF
- allRead = True
+ fmPropsJob = FileOpsJobCreate(FILEOPS_MEASURE, "")
+ fmPropsFolders = 0
+ fmPropsMissing = False
FOR EACH path IN paths
- allRead = FileOpsTreeSize(path) AND allRead
- bytes = bytes + FileOpsTreeBytes()
- files = files + FileOpsTreeFiles()
- folders = folders + FileOpsTreeFolders()
-
- IF FileOpsIsFolder(path) THEN
- folders = folders + 1
+ IF NOT FileOpsJobAddSource(fmPropsJob, path) THEN
+ fmPropsMissing = True
+ ELSEIF FileOpsIsFolder(path) THEN
+ fmPropsFolders = fmPropsFolders + 1
END IF
NEXT path
@@ -1218,14 +1227,7 @@ SUB CmdProperties
END IF
PropsForm.lblLocation.Caption = "Location: " & location
-
- IF allRead THEN
- PropsForm.lblSize.Caption = "Size: " & FileOpsFormatSize$(bytes) & " bytes"
- ELSE
- PropsForm.lblSize.Caption = "Size: " & FileOpsFormatSize$(bytes) & " bytes (some items could not be read)"
- END IF
-
- PropsForm.lblContains.Caption = "Contains: " & CStr(files) & " files, " & CStr(folders) & " folders"
+ PropsShowCount False
IF paths.Count = 1 AND modified <> "" THEN
PropsForm.lblModified.Caption = "Modified: " & modified
@@ -1239,9 +1241,12 @@ SUB CmdProperties
PropsForm.chkArchive.Value = (attr AND FILEOPS_ATTR_ARCHIVE) <> 0
' Fit the dialog to what it now says and centre it, then wait for it
- fmPropsAccepted = False
- PropsForm.AutoSize = True
- PropsForm.Centered = True
+ ' while its timer counts
+ fmPropsAccepted = False
+ PropsForm.AutoSize = True
+ PropsForm.Centered = True
+ PropsForm.tmrCount.Interval = FM_JOB_TICK
+ PropsForm.tmrCount.Enabled = True
PropsForm.Show vbModal
IF NOT fmPropsAccepted THEN
diff --git a/src/apps/kpunch/fileMgr/fmProps.frm b/src/apps/kpunch/fileMgr/fmProps.frm
index 23153ad7..1af2ec96 100644
--- a/src/apps/kpunch/fileMgr/fmProps.frm
+++ b/src/apps/kpunch/fileMgr/fmProps.frm
@@ -20,6 +20,9 @@ Begin Form PropsForm
Begin Label lblModified
Caption = ""
End
+ Begin Timer tmrCount
+ Enabled = False
+ End
Begin Frame fraAttributes
Caption = "Attributes"
Begin CheckBox chkReadOnly
@@ -78,7 +81,8 @@ End
' fmProps.frm -- the File Manager's Properties dialog: what the selection
' is (the File Manager fills in the labels and ticks the attributes before
-' showing it with Show vbModal) and its Read-only, Hidden, System and
+' showing it with Show vbModal; tmrCount counts its size meanwhile, a
+' slice of a measure job at a time) and its Read-only, Hidden, System and
' Archive attributes. OK leaves the ticked attributes in fmPropsAttr and
' sets fmPropsAccepted (fileMgr.bas); Cancel, Escape and the close box
' leave fmPropsAccepted False. Its buttons are the size of the buttons of
@@ -114,3 +118,15 @@ END SUB
SUB cmdCancel_Click
Unload PropsForm
END SUB
+
+
+' Closed before the count is done: the count stops.
+SUB PropsForm_Unload
+ FileOpsJobFree fmPropsJob
+ fmPropsJob = 0
+END SUB
+
+
+SUB tmrCount_Timer
+ PropsShowCount FileOpsJobRunFor(fmPropsJob, FM_SLICE_MS) <> FILEOPS_STEP_BUSY
+END SUB
diff --git a/src/apps/kpunch/helpedit/helpedit.dhs b/src/apps/kpunch/helpedit/helpedit.dhs
index 826b94a4..f2194bab 100644
--- a/src/apps/kpunch/helpedit/helpedit.dhs
+++ b/src/apps/kpunch/helpedit/helpedit.dhs
@@ -1,24 +1,24 @@
-// 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.
+# 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.
# Help Editor help. Compiled to helpedit.hlp next to helpedit.app; F1
# and Help > Contents in the Help Editor open it. The help source
diff --git a/src/apps/kpunch/helpedit/helpedit.frm b/src/apps/kpunch/helpedit/helpedit.frm
index 04c4d86c..4803a8ba 100644
--- a/src/apps/kpunch/helpedit/helpedit.frm
+++ b/src/apps/kpunch/helpedit/helpedit.frm
@@ -98,16 +98,23 @@ End
CONST HELP_FILE = "helpedit.hlp"
CONST VIEWER_HELP = "\kpunch\dvxhelp\dvxhelp.hlp"
CONST FORMAT_TOPIC = "help.format"
-CONST HASH_MUL = 31
-CONST HASH_MOD = 16777213
+
+' Loaded text is gathered in chunks of about this many bytes before being
+' appended to the whole source, so a big file is not rebuilt per line;
+' saved text is written this many bytes at a time
+CONST OPEN_CHUNK_BYTES = 4096
+CONST SAVE_CHUNK_BYTES = 16384
+
+' Room left in the editor past a loaded source for typing
+CONST EDIT_ROOM_BYTES = 65536
DIM SHARED currentFile AS STRING
DIM SHARED hlpFile AS STRING
-DIM SHARED savedHash AS LONG
+DIM SHARED savedText AS STRING
DIM SHARED dirty AS INTEGER
currentFile = ""
hlpFile = ""
-savedHash = 0
+savedText = ""
dirty = 0
Load HelpEdit
@@ -121,7 +128,7 @@ Editor.SetCaptureTabs True
Editor.SetTabWidth 2
' Start with a template
-Editor.Text = ".topic intro" + CHR$(10) + ".title My Help File" + CHR$(10) + ".toc 0 My Help File" + CHR$(10) + ".default" + CHR$(10) + CHR$(10) + ".h1 Welcome" + CHR$(10) + CHR$(10) + "This is your help file. Edit the .dhs source here," + CHR$(10) + "then click Compile and Preview to see the result." + CHR$(10)
+Editor.Text = NewSource$()
MarkClean
LblStatus.Caption = "Ready."
@@ -133,23 +140,10 @@ IF COMMAND$(1) <> "" THEN
END IF
-' A hash of the editor's text (kept below HASH_MOD so h * HASH_MUL
-' always fits a LONG).
-FUNCTION TextHash() AS LONG
- DIM h AS LONG
- DIM t AS STRING
- DIM i AS LONG
- t = Editor.Text
- h = 0
- FOR i = 1 TO LEN(t)
- h = (h * HASH_MUL + ASC(MID$(t, i, 1))) MOD HASH_MOD
- NEXT i
- TextHash = h
-END FUNCTION
-
-
+' The text differs from what was last saved or loaded (typing it back as
+' it was is not a change).
FUNCTION IsDirty() AS INTEGER
- IsDirty = (TextHash() <> savedHash)
+ IsDirty = (Editor.Text <> savedText)
END FUNCTION
@@ -187,7 +181,7 @@ END SUB
SUB MarkClean
dirty = 0
- savedHash = TextHash()
+ savedText = Editor.Text
UpdateTitle
END SUB
@@ -206,13 +200,18 @@ SUB mnuNew_Click
END IF
currentFile = ""
hlpFile = ""
- Editor.Text = ".topic intro" + CHR$(10) + ".title My Help File" + CHR$(10) + ".toc 0 My Help File" + CHR$(10) + ".default" + CHR$(10) + CHR$(10) + ".h1 Welcome" + CHR$(10) + CHR$(10)
+ Editor.Text = NewSource$()
MarkClean
LblStatus.Caption = "New file."
- UpdateTitle
END SUB
+' The source a new file starts with.
+FUNCTION NewSource$() AS STRING
+ NewSource$ = ".topic intro" + CHR$(10) + ".title My Help File" + CHR$(10) + ".toc 0 My Help File" + CHR$(10) + ".default" + CHR$(10) + CHR$(10) + ".h1 Welcome" + CHR$(10) + CHR$(10) + "This is your help file. Edit the .dhs source here," + CHR$(10) + "then click Compile and Preview to see the result." + CHR$(10)
+END FUNCTION
+
+
SUB mnuOpen_Click
IF AskSave() THEN
EXIT SUB
@@ -227,15 +226,20 @@ SUB mnuOpen_Click
END SUB
-' Loads the help source in path; a file that cannot be read is reported
-' and the current text stays.
+' Loads the help source in path; a file that cannot be read, or is too
+' big for the editor, is reported and the current text stays. The
+' editor is made big enough for it (and some typing).
SUB OpenSource(path AS STRING)
DIM f AS INTEGER
DIM opened AS BOOLEAN
+ DIM firstLine AS BOOLEAN
DIM text AS STRING
+ DIM chunk AS STRING
DIM ln AS STRING
- opened = False
- text = ""
+ opened = False
+ firstLine = True
+ text = ""
+ chunk = ""
ON ERROR GOTO openFailed
f = FREEFILE
@@ -243,19 +247,35 @@ SUB OpenSource(path AS STRING)
opened = True
DO WHILE NOT EOF(f)
LINE INPUT #f, ln
- IF text <> "" THEN
- text = text + CHR$(10)
+ IF firstLine THEN
+ firstLine = False
+ ELSE
+ chunk = chunk + CHR$(10)
+ END IF
+ chunk = chunk + ln
+ IF LEN(chunk) >= OPEN_CHUNK_BYTES THEN
+ text = text + chunk
+ chunk = ""
END IF
- text = text + ln
LOOP
CLOSE #f
+ text = text + chunk
+
+ ' The editor holds what MaxLength allows, at most its own limit
+ IF LEN(text) + EDIT_ROOM_BYTES > Editor.MaxLength THEN
+ Editor.MaxLength = LEN(text) + EDIT_ROOM_BYTES
+ END IF
+
+ IF LEN(text) > Editor.MaxLength THEN
+ MsgBox path + " is too big for the Help Editor."
+ EXIT SUB
+ END IF
Editor.Text = text
currentFile = path
hlpFile = ""
MarkClean
LblStatus.Caption = path
- UpdateTitle
EXIT SUB
openFailed:
@@ -272,16 +292,46 @@ SUB FileSave
EXIT SUB
END IF
- OPEN currentFile FOR OUTPUT AS #1
- PRINT #1, Editor.Text
- CLOSE #1
+ IF NOT WriteSource(currentFile) THEN
+ EXIT SUB
+ END IF
MarkClean
LblStatus.Caption = "Saved: " + currentFile
- UpdateTitle
END SUB
+' Writes the editor text to path, SAVE_CHUNK_BYTES at a time (a big
+' source does not stop DVX for the whole write). False (after telling
+' the user) on failure.
+FUNCTION WriteSource(path AS STRING) AS BOOLEAN
+ DIM f AS INTEGER
+ DIM opened AS BOOLEAN
+ DIM text AS STRING
+ DIM at AS LONG
+ WriteSource = False
+ opened = False
+ text = Editor.Text
+ ON ERROR GOTO writeFailed
+ f = FREEFILE
+ OPEN path FOR OUTPUT AS #f
+ opened = True
+ FOR at = 1 TO LEN(text) STEP SAVE_CHUNK_BYTES
+ PRINT #f, MID$(text, at, SAVE_CHUNK_BYTES);
+ NEXT at
+ PRINT #f, ""
+ CLOSE #f
+ WriteSource = True
+ EXIT FUNCTION
+
+writeFailed:
+ IF opened THEN
+ CLOSE #f
+ END IF
+ MsgBox "Could not save " + path + " (error" + STR$(ERR) + ")."
+END FUNCTION
+
+
SUB FileSaveAs
DIM path AS STRING
path = DlgFileSave$("Save Help Source", "Help Source (*.dhs)|All Files (*.*)")
@@ -317,11 +367,11 @@ SUB DoCompile
LblStatus.Caption = "Save cancelled."
EXIT SUB
END IF
- ELSE
- OPEN currentFile FOR OUTPUT AS #1
- PRINT #1, Editor.Text
- CLOSE #1
+ ELSEIF WriteSource(currentFile) THEN
MarkClean
+ ELSE
+ LblStatus.Caption = "Not compiled: the source could not be saved."
+ EXIT SUB
END IF
' Derive .hlp filename from .dhs filename
@@ -336,15 +386,23 @@ SUB DoCompile
hlpFile = baseName + ".hlp"
- LblStatus.Caption = "Compiling..."
- DoEvents
+ ' The compile lets this program's events run: no second one meanwhile
+ LblStatus.Caption = "Compiling..."
+ mnuCompile.Enabled = False
+ mnuPreview.Enabled = False
IF HelpCompile(currentFile, hlpFile) THEN
LblStatus.Caption = "Compiled: " + hlpFile
ELSE
LblStatus.Caption = "Compile failed!"
hlpFile = ""
+ IF HelpCompileMessages$() <> "" THEN
+ MsgBox HelpCompileMessages$(), vbOKOnly + vbExclamation, "Compile failed"
+ END IF
END IF
+
+ mnuCompile.Enabled = True
+ mnuPreview.Enabled = True
END SUB
diff --git a/src/apps/kpunch/notepad/notepad.frm b/src/apps/kpunch/notepad/notepad.frm
index 6ab70f63..7717034d 100644
--- a/src/apps/kpunch/notepad/notepad.frm
+++ b/src/apps/kpunch/notepad/notepad.frm
@@ -106,6 +106,13 @@ CONST HELP_FILE = "notepad.hlp"
' appended to the whole document, so large files are not rebuilt per line.
CONST OPEN_CHUNK_BYTES = 4096
+' Saved text is written this many bytes at a time, so a big file does not
+' stop DVX for the whole write (the program yields between pieces).
+CONST SAVE_CHUNK_BYTES = 16384
+
+' Room left in the editor past a loaded file for typing
+CONST EDIT_ROOM_BYTES = 65536
+
DIM SHARED currentFile AS STRING
DIM SHARED dirty AS BOOLEAN
currentFile = ""
@@ -218,13 +225,19 @@ END FUNCTION
FUNCTION WriteTextFile(path AS STRING) AS BOOLEAN
DIM f AS INTEGER
DIM opened AS BOOLEAN
+ DIM text AS STRING
+ DIM at AS LONG
WriteTextFile = False
opened = False
+ text = Editor.Text
ON ERROR GOTO writeFailed
f = FREEFILE
OPEN path FOR OUTPUT AS #f
opened = True
- PRINT #f, Editor.Text
+ FOR at = 1 TO LEN(text) STEP SAVE_CHUNK_BYTES
+ PRINT #f, MID$(text, at, SAVE_CHUNK_BYTES);
+ NEXT at
+ PRINT #f, ""
CLOSE #f
WriteTextFile = True
EXIT FUNCTION
@@ -278,6 +291,18 @@ SUB OpenFile(path AS STRING)
IF NOT ReadTextFile(path, text) THEN
EXIT SUB
END IF
+
+ ' The editor holds what MaxLength allows, at most its own limit: a
+ ' file it cannot hold whole is not opened (saving it would cut it)
+ IF LEN(text) + EDIT_ROOM_BYTES > Editor.MaxLength THEN
+ Editor.MaxLength = LEN(text) + EDIT_ROOM_BYTES
+ END IF
+
+ IF LEN(text) > Editor.MaxLength THEN
+ MsgBox path + " is too big for Notepad."
+ EXIT SUB
+ END IF
+
Editor.Text = text
currentFile = path
MarkClean
diff --git a/src/apps/kpunch/pkgbuild/pbProg.frm b/src/apps/kpunch/pkgbuild/pbProg.frm
new file mode 100644
index 00000000..ffb8e08d
--- /dev/null
+++ b/src/apps/kpunch/pkgbuild/pbProg.frm
@@ -0,0 +1,70 @@
+VERSION DVX 1.00
+Begin Form ProgForm
+ Caption = "Building"
+ Layout = VBox
+ AutoSize = False
+ Resizable = False
+ Centered = True
+ Width = 360
+ Height = 130
+ Begin Label lblWhat
+ Caption = ""
+ Weight = 1
+ End
+ Begin ProgressBar pbJob
+ End
+ Begin HBox boxCancel
+ Alignment = Center
+ Begin CommandButton cmdProgCancel
+ Caption = "&Cancel"
+ Cancel = True
+ MinWidth = 80
+ MinHeight = 24
+ End
+ 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.
+
+
+' pbProg.frm -- the progress of building a package: PbRunJob
+' (pkgbuild.bas) runs the build and keeps this window up to date, and
+' closes it when the build ends. Cancel (and the close box) stop the
+' build; nothing of the package is left behind.
+
+OPTION EXPLICIT
+
+
+SUB cmdProgCancel_Click
+ PbCancel
+END SUB
+
+
+SUB ProgForm_QueryUnload(Cancel AS INTEGER)
+ ' Still building: stop it, and close when it has
+ IF pbBuilding THEN
+ PbCancel
+ Cancel = 1
+ END IF
+END SUB
diff --git a/src/apps/kpunch/pkgbuild/pkgbuild.bas b/src/apps/kpunch/pkgbuild/pkgbuild.bas
index b377cb60..cf08b235 100644
--- a/src/apps/kpunch/pkgbuild/pkgbuild.bas
+++ b/src/apps/kpunch/pkgbuild/pkgbuild.bas
@@ -29,7 +29,7 @@
' file dvxpkg build takes too), plus a [builder] section with the folder
' of files, how that folder is laid out, and the key. Building writes the template beside
' the new package for a moment and hands it, the files, and the key to
-' PkgBuild.
+' PkgBuildStart, then runs the build with its progress window up.
OPTION EXPLICIT
@@ -48,6 +48,7 @@ CONST PB_FIRST_VERSION = "1.0.0"
CONST PB_NOT_SIGNED = "Not signed"
CONST PB_PAGE_PACKAGE = 0 ' the tabs: the Package page
CONST PB_PAGE_FILES = 3 ' the Files page
+CONST PB_SLICE_MS = 60 ' building between screen updates
' The template: the manifest's sections and keys, and the builder's own
CONST PB_SECT_PACKAGE = "package"
@@ -80,6 +81,11 @@ DIM SHARED pbKeys AS STRING
' PbState$ when the template was last opened or saved
DIM SHARED pbSaved AS STRING
+' A build is running (its progress window is up), and its Cancel was
+' pressed
+DIM SHARED pbBuilding AS BOOLEAN
+DIM SHARED pbCancelled AS BOOLEAN
+
' Build the package: checked first, then saved where the user says, from
' the template, the files, and the key.
@@ -89,7 +95,7 @@ SUB PbBuild
DIM rows AS STRING
DIM ok AS BOOLEAN
- IF NOT PbCheck() THEN
+ IF pbBuilding OR NOT PbCheck() THEN
EXIT SUB
END IF
@@ -114,7 +120,8 @@ SUB PbBuild
EXIT SUB
END IF
- ok = PkgBuild(pbSession, tmplPath, rows, outPath, PbKeyPath$())
+ ' The template is read as the build starts
+ ok = PkgBuildStart(pbSession, tmplPath, rows, outPath, PbKeyPath$())
KILL tmplPath
IF NOT ok THEN
@@ -122,12 +129,28 @@ SUB PbBuild
EXIT SUB
END IF
+ IF PbRunJob() <> PKG_RUN_DONE THEN
+ IF NOT pbCancelled THEN
+ PbError "The package cannot be built. " & PkgRunMessage$(pbSession)
+ END IF
+
+ EXIT SUB
+ END IF
+
BuilderForm.lblStatus.Caption = "Built " & outPath
ShellLog "Package Builder: built " & outPath
MsgBox "The package is built:" & PB_EOL & PB_EOL & outPath, vbOKOnly + vbInformation, PB_TITLE
END SUB
+' Stop the build the progress window shows (nothing of the package is
+' left behind).
+SUB PbCancel
+ pbCancelled = True
+ PkgRunCancel pbSession
+END SUB
+
+
' What a package must have before it is built; False (and a word why)
' when something is missing or wrong.
FUNCTION PbCheck() AS BOOLEAN
@@ -456,6 +479,7 @@ SUB PbNewKey
EXIT SUB
END IF
+ ShellLog "Package Builder: key made " & keyBase & PB_SEC_EXT
PbKeysFill keyBase & PB_SEC_EXT
MsgBox "Your key is made." & PB_EOL & PB_EOL & "Give " & FileOpsBaseName$(keyBase & PB_KEY_EXT) & " to the people who install your packages: it tells DVX that they are yours. Keep " & FileOpsBaseName$(keyBase & PB_SEC_EXT) & " private: anyone who has it can sign packages as you.", vbOKOnly + vbInformation, PB_TITLE
END SUB
@@ -486,6 +510,31 @@ END SUB
' Save the template: to pbPath, or where the user says when it has none
' or asAs. False when it was not saved.
+' Run the build just started, a slice at a time, with the progress window
+' up; its Cancel stops the build. How it ended (PKG_RUN_*).
+FUNCTION PbRunJob() AS LONG
+ DIM rc AS LONG
+
+ Load ProgForm
+ pbBuilding = True
+ pbCancelled = False
+ ProgForm.lblWhat.Caption = "Building"
+ ProgForm.pbJob.Value = 0
+ ProgForm.Show
+
+ DO
+ rc = PkgRunFor(pbSession, PB_SLICE_MS)
+ ProgForm.lblWhat.Caption = Trim$("Building " & PkgRunCurrent$(pbSession))
+ ProgForm.pbJob.Value = PkgRunPercent(pbSession)
+ DoEvents
+ LOOP WHILE rc = PKG_RUN_BUSY
+
+ pbBuilding = False
+ Unload ProgForm
+ PbRunJob = rc
+END FUNCTION
+
+
FUNCTION PbSave(BYVAL asAs AS BOOLEAN) AS BOOLEAN
DIM path AS STRING
diff --git a/src/apps/kpunch/pkgbuild/pkgbuild.dbp b/src/apps/kpunch/pkgbuild/pkgbuild.dbp
index 65d7e33f..46c486a1 100644
--- a/src/apps/kpunch/pkgbuild/pkgbuild.dbp
+++ b/src/apps/kpunch/pkgbuild/pkgbuild.dbp
@@ -19,7 +19,8 @@ Count = 7
[Forms]
File0 = pkgbuild.frm
-Count = 1
+File1 = pbProg.frm
+Count = 2
[Settings]
StartupForm = BuilderForm
diff --git a/src/apps/kpunch/pkgbuild/pkgbuild.dhs b/src/apps/kpunch/pkgbuild/pkgbuild.dhs
index 14424e99..15e55140 100644
--- a/src/apps/kpunch/pkgbuild/pkgbuild.dhs
+++ b/src/apps/kpunch/pkgbuild/pkgbuild.dhs
@@ -44,6 +44,8 @@ Describe the package on the five pages of the window, then choose Build Package
.item Signing: the key that shows the package came from you.
.endlist
+While the package is built, a window shows how far it has got; choose Cancel to stop, and nothing of the package is left behind. You can go on using DVX while it builds.
+
What you fill in can be saved as a template (File > Save) and opened again (File > Open) to build the next version.
.link pkgbuild.package The Package page
@@ -158,4 +160,4 @@ DVXPKG build CHESS.INI FOLDER CHESS.DPK MYKEY.SEC
where FOLDER is laid out like the DVX folder. DVXPKG with no arguments lists its other commands: signing a package, checking one, showing what is in one, making keys, and installing, removing, and listing packages.
-Programs make packages with the package library (pkg.bas): PkgBuild, PkgSign, PkgKeygen, and the functions around them.
+Programs make packages with the package library (pkg.bas): PkgBuildStart and PkgSignStart (jobs run a little at a time with PkgRunFor, as installing is), PkgKeygen, and the functions around them.
diff --git a/src/apps/kpunch/pkgbuild/pkgbuild.frm b/src/apps/kpunch/pkgbuild/pkgbuild.frm
index aec51661..3825d277 100644
--- a/src/apps/kpunch/pkgbuild/pkgbuild.frm
+++ b/src/apps/kpunch/pkgbuild/pkgbuild.frm
@@ -329,7 +329,11 @@ END SUB
SUB BuilderForm_QueryUnload(Cancel AS INTEGER)
- IF NOT PbSaveFirst() THEN
+ ' Building: stop it first (close again when it has)
+ IF pbBuilding THEN
+ PbCancel
+ Cancel = 1
+ ELSEIF NOT PbSaveFirst() THEN
Cancel = 1
END IF
END SUB
diff --git a/src/apps/kpunch/player/player.frm b/src/apps/kpunch/player/player.frm
index 80663aa5..11fba1c0 100644
--- a/src/apps/kpunch/player/player.frm
+++ b/src/apps/kpunch/player/player.frm
@@ -1117,15 +1117,10 @@ SUB lstTracks_DblClick
END SUB
-' Enter plays the selected track, Del takes it out of the list.
+' Del takes the selected track out of the list. (Enter plays it: a list
+' box's Enter is a double-click.)
SUB lstTracks_KeyDown(KeyCode AS INTEGER, Shift AS INTEGER)
- IF Shift <> 0 OR lstTracks.ListIndex < 0 THEN
- EXIT SUB
- END IF
-
- IF KeyCode = vbKeyReturn THEN
- PlayTrack lstTracks.ListIndex
- ELSEIF KeyCode = vbKeyDelete THEN
+ IF Shift = 0 AND KeyCode = vbKeyDelete AND lstTracks.ListIndex >= 0 THEN
RemoveTrack lstTracks.ListIndex
END IF
END SUB
diff --git a/src/apps/kpunch/progman/pmView.c b/src/apps/kpunch/progman/pmView.c
index ca30c98e..1a7f9b61 100644
--- a/src/apps/kpunch/progman/pmView.c
+++ b/src/apps/kpunch/progman/pmView.c
@@ -34,8 +34,10 @@
#include "listhelp/listHelp.h"
#include "dvxApp.h"
#include "dvxDraw.h"
+#include "dvxPlat.h"
#include "dvxVideo.h"
#include "dvxWgtP.h"
+#include "taskSwch.h"
#include
#include
@@ -49,6 +51,15 @@
#define PV_BUTTON_RIGHT 2 // onMouseDown button number of the right button
#define PV_CELL_PARTS 2 // a cell's column and row (navigate's buffer)
+// Icon decoding one paint may spend (at least one icon); the cells after
+// it show placeholders until a later frame (viewPoll). While programs
+// run, a frame that decoded is followed by as long again without decoding
+// (iconTurn): on a 486 such a frame (the decode, the paint, and the flush)
+// takes 100 ms or more, and the shell's priority gives it several turns in
+// a row, so back-to-back decoding frames kept the programs from running
+// for most of a second.
+#define PV_ICON_BUDGET_US 20000
+
// Program Manager's views as the icon view rules name them
static const ListIconViewE sIconView[PmViewCountE] = {
[PmViewLargeE] = ListIconLargeE,
@@ -59,22 +70,27 @@ static const ListIconViewE sIconView[PmViewCountE] = {
typedef struct {
PmModelT *model;
const PmViewCallbacksT *cb;
- PmViewEntryT *entries; // stb_ds copy
+ PmViewEntryT *entries; // stb_ds copy
const char *emptyText;
+ uint64_t paintUs; // when the paint under way began (platformMicros)
+ uint64_t iconDueUs; // decoding may go on from then (iconTurn)
int32_t selected;
int32_t scrollRow;
int32_t lastCols;
- int32_t dragEntry; // pressed entry that may be dragged, or -1
+ int32_t dragEntry; // pressed entry that may be dragged, or -1
int32_t dropCol;
int32_t dropRow;
int32_t pressX;
int32_t pressY;
int32_t sbDragOff;
+ int32_t paintIcons; // icons the paint under way has loaded
bool manual;
bool canDrag;
bool dragging;
bool sbDragging;
- bool placeDirty; // entries changed: tell onColumns again
+ bool placeDirty; // entries changed: tell onColumns again
+ bool iconsPending; // a paint left icons for the next frame (viewPoll)
+ bool iconsDecoded; // the last paint decoded: viewPoll starts the rest
} PmViewDataT;
static int32_t sTypeId = -1;
@@ -89,6 +105,7 @@ static void ensureVisible(WidgetT *w, const ListIconGridT *g, int32_t e);
static void entryCell(const PmViewDataT *d, int32_t e, int32_t cols, int32_t *col, int32_t *row);
static PmItemT *entryItem(const PmViewDataT *d, int32_t e);
static int32_t hitEntry(const WidgetT *w, const ListIconGridT *g, int32_t vx, int32_t vy, int32_t *outCol, int32_t *outRow);
+static bool iconTurn(const PmViewDataT *d);
static void manualCell(const PmItemT *item, int32_t *col, int32_t *row);
static int32_t manualRows(const PmViewDataT *d);
static int32_t navigate(const PmViewDataT *d, const ListIconGridT *g, int32_t key);
@@ -113,11 +130,12 @@ static void viewOnKey(WidgetT *w, int32_t key, int32_t mod);
static void viewOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy);
static bool viewOnWheel(WidgetT *w, int32_t delta);
static void viewPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors);
+static void viewPoll(WidgetT *w, WindowT *win);
static void viewSync(WidgetT *w, const BitmapFontT *font, ListIconGridT *g);
static const WidgetClassT sClassView = {
.version = WGT_CLASS_VERSION,
- .flags = WCLASS_FOCUSABLE,
+ .flags = WCLASS_FOCUSABLE | WCLASS_NEEDS_POLL,
.handlers = {
[WGT_METHOD_PAINT] = (void *)viewPaint,
[WGT_METHOD_CALC_MIN_SIZE] = (void *)viewCalcMinSize,
@@ -128,6 +146,7 @@ static const WidgetClassT sClassView = {
[WGT_METHOD_ON_DRAG_END] = (void *)viewDragEnd,
[WGT_METHOD_ON_WHEEL] = (void *)viewOnWheel,
[WGT_METHOD_GET_TEXT] = (void *)viewGetText,
+ [WGT_METHOD_POLL] = (void *)viewPoll,
}
};
@@ -154,7 +173,13 @@ static void drawCell(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapF
uint32_t bg = selected ? colors->menuHighlightBg : colors->contentBg;
if (!item->iconTried && vd->cb->loadIcon) {
- vd->cb->loadIcon(item);
+ if (iconTurn(vd)) {
+ vd->cb->loadIcon(item);
+ vd->paintIcons++;
+ vd->iconsDecoded = true;
+ } else {
+ vd->iconsPending = true;
+ }
}
const uint8_t *picture = large ? item->icon : item->iconSmall;
@@ -293,6 +318,16 @@ static int32_t hitEntry(const WidgetT *w, const ListIconGridT *g, int32_t vx, in
}
+// Whether the paint under way may decode an icon: the first once the
+// rest after the last decoding frame is over, more while the paint's
+// budget lasts.
+static bool iconTurn(const PmViewDataT *d) {
+ uint64_t now = platformMicros();
+
+ return d->paintIcons == 0 ? now >= d->iconDueUs : now - d->paintUs < PV_ICON_BUDGET_US;
+}
+
+
// An item's own cell (manual layout; a negative col / row counts as 0).
static void manualCell(const PmItemT *item, int32_t *col, int32_t *row) {
*col = DVX_MAX(item->col, 0);
@@ -696,7 +731,9 @@ static void viewPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bitmap
ListIconGridT g;
viewSync(w, font, &g);
- vd->scrollRow = clampInt(vd->scrollRow, 0, g.maxScroll);
+ vd->scrollRow = clampInt(vd->scrollRow, 0, g.maxScroll);
+ vd->paintUs = platformMicros();
+ vd->paintIcons = 0;
drawBevel(d, ops, w->x, w->y, w->w, w->h, &bevel);
// Intersect the widget's inside with the clip already in force.
@@ -742,6 +779,29 @@ static void viewPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bitmap
}
+// WGT_METHOD_POLL, at the start of a frame: while programs run, a frame
+// that decoded (the paint, the composite, and the other tasks' turns
+// since) is followed by as long a rest; a paint that left icons is
+// followed by another once decoding may go on, until every icon shown is
+// in.
+static void viewPoll(WidgetT *w, WindowT *win) {
+ PmViewDataT *vd = (PmViewDataT *)w->data;
+ uint64_t now = platformMicros();
+
+ (void)win;
+
+ if (vd->iconsDecoded) {
+ vd->iconsDecoded = false;
+ vd->iconDueUs = tsActiveCount() > 1 ? now + (now - vd->paintUs) : now;
+ }
+
+ if (vd->iconsPending && now >= vd->iconDueUs) {
+ vd->iconsPending = false;
+ wgtInvalidatePaint(w);
+ }
+}
+
+
// Geometry, first telling Program Manager the column count whenever it
// changed or the entries were replaced (it places unplaced items then).
// Nothing is reported before the first layout gives the view a width.
diff --git a/src/apps/kpunch/progman/progman.c b/src/apps/kpunch/progman/progman.c
index 4ea17412..56e82168 100644
--- a/src/apps/kpunch/progman/progman.c
+++ b/src/apps/kpunch/progman/progman.c
@@ -167,6 +167,7 @@ static char sIniPath[DVX_MAX_PATH];
static char sFilter[PM_SEARCH_MAX];
static bool sStartupPending = true;
static bool sScanning = false;
+static uint32_t sInstalledSeen = 0; // platformInstalledChangeCount at the last scan
static void (*sPrevOnFocus)(WindowT *win) = NULL;
// ============================================================
@@ -738,11 +739,17 @@ static void launchItem(const PmItemT *item) {
}
-// The shell's item icon rule (shellItemIcon), at both sizes.
+// The shell's item icon rule (shellItemIcons), at both sizes from one
+// decode.
static void loadItemIcon(PmItemT *item) {
- item->iconTried = true;
- item->icon = shellItemIcon(sAc, item->iconPath, item->program, SHELL_PROG_ICON, &item->iconPitch);
- item->iconSmall = shellItemIcon(sAc, item->iconPath, item->program, SHELL_PROG_ICON_SMALL, &item->iconSmallPitch);
+ ShellIconsT icons;
+
+ shellItemIcons(sAc, item->iconPath, item->program, SHELL_PROG_ICON, SHELL_PROG_ICON_SMALL, &icons);
+ item->iconTried = true;
+ item->icon = icons.icon;
+ item->iconPitch = icons.iconPitch;
+ item->iconSmall = icons.iconSmall;
+ item->iconSmallPitch = icons.iconSmallPitch;
}
@@ -791,13 +798,21 @@ static void onPmClose(WindowT *win) {
}
-// Back at the front: pick up programs installed or removed meanwhile.
+// Back at the front: pick up programs installed or removed meanwhile,
+// looking only when something may have changed (a scan reads the whole
+// programs folder).
static void onPmFocus(WindowT *win) {
if (sPrevOnFocus) {
sPrevOnFocus(win);
}
- if (!sScanning && pmInstalledChanged(&sModel, sCtx->appPath)) {
+ if (sScanning || platformInstalledChangeCount() == sInstalledSeen) {
+ return;
+ }
+
+ sInstalledSeen = platformInstalledChangeCount();
+
+ if (pmInstalledChanged(&sModel, sCtx->appPath)) {
scanInstalled(false);
refreshView();
}
@@ -1101,7 +1116,8 @@ static void scanInstalled(bool pump) {
return;
}
- sScanning = true;
+ sScanning = true;
+ sInstalledSeen = platformInstalledChangeCount();
int32_t count = pmInstalledScan(&sModel, sCtx->appPath, pump ? scanProgress : NULL, NULL);
diff --git a/src/apps/kpunch/progman/progman.dhs b/src/apps/kpunch/progman/progman.dhs
index 382aca5c..906ccd8c 100644
--- a/src/apps/kpunch/progman/progman.dhs
+++ b/src/apps/kpunch/progman/progman.dhs
@@ -1,24 +1,24 @@
-// 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.
+# 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.
# Program Manager help. Compiled to progman.hlp next to progman.app; F1
# and Help > Contents in the Program Manager open it. The user guide
@@ -70,7 +70,7 @@ A group is a set of program icons you put together, such as Games or Writing. Th
.h2 Installed Programs
-The first group, Installed Programs, is kept up to date for you: it always shows every program installed in DVX, in alphabetical order. A newly installed program appears there by itself (the Program Manager looks again whenever its window comes to the front, and View > Refresh looks at once), and a removed program disappears. You cannot change this group, but you can copy its icons into your own groups with File > Copy.
+The first group, Installed Programs, is kept up to date for you: it always shows every program installed in DVX, in alphabetical order. A newly installed program appears there by itself (when its window comes to the front after a package was installed or removed, files were copied, moved, or deleted, or a DOS program ran, the Program Manager looks again; View > Refresh looks at once), and a removed program disappears. You cannot change this group, but you can copy its icons into your own groups with File > Copy.
.h2 Main and Startup
diff --git a/src/apps/kpunch/progman/userHelp.dhs b/src/apps/kpunch/progman/userHelp.dhs
index 7f2c7bc0..05fe500c 100644
--- a/src/apps/kpunch/progman/userHelp.dhs
+++ b/src/apps/kpunch/progman/userHelp.dhs
@@ -964,6 +964,8 @@ When you exit DVX, each program is closed as if you had closed it yourself. If y
DVX writes a record of what happened to the file DVX.LOG, in your USER folder (for example C:\DVX\USER\DVX.LOG). If a program crashes, this file contains details that help the program's author find the problem. The file is started fresh each time DVX starts, so copy it before starting DVX again.
+DVX also remembers the programs' names and icons in PROGRAMS.CAC in the same folder, so Program Manager starts quickly. If a program shows an old name or icon after you changed it, delete PROGRAMS.CAC: DVX makes it again.
+
.link user.settings Changing Settings
.link user.taskmgr Task Manager
diff --git a/src/apps/kpunch/resedit/resedit.dhs b/src/apps/kpunch/resedit/resedit.dhs
index 33ea97e7..2eae3d60 100644
--- a/src/apps/kpunch/resedit/resedit.dhs
+++ b/src/apps/kpunch/resedit/resedit.dhs
@@ -1,24 +1,24 @@
-// 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.
+# 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.
# Resource Editor help. Compiled to resedit.hlp next to resedit.app; F1
# and Help > Contents in the Resource Editor open it.
diff --git a/src/apps/kpunch/software/software.bas b/src/apps/kpunch/software/software.bas
index 737aaf15..8f9ef95c 100644
--- a/src/apps/kpunch/software/software.bas
+++ b/src/apps/kpunch/software/software.bas
@@ -145,7 +145,12 @@ SUB SwFetchInstall(BYVAL net AS LONG, BYVAL ids AS STRING, BYVAL what AS STRING)
EXIT SUB
END IF
- IF PkgOfferFolder(swSession, PkgNetDir$(net)) <= 0 THEN
+ IF NOT SwRead(PkgOfferFolderStart(swSession, PkgNetDir$(net)), what & " cannot be installed: ") THEN
+ PkgNetClear net
+ EXIT SUB
+ END IF
+
+ IF PkgOfferCount(swSession) = 0 THEN
SwError what & " cannot be installed: what was fetched cannot be read."
PkgNetClear net
EXIT SUB
@@ -195,16 +200,10 @@ END FUNCTION
' Install the package file path, with what it needs from the packages
' beside it: what will happen is shown first, then it runs.
SUB SwInstallFile(BYVAL path AS STRING)
- DIM offer AS LONG
-
- offer = PkgOfferFile(swSession, path)
-
- IF offer < 0 THEN
- SwError path & " cannot be installed: " & PkgError$(swSession)
- EXIT SUB
+ ' The file is offer 0
+ IF SwRead(PkgOfferFileStart(swSession, path), path & " cannot be installed: ") THEN
+ SwInstallOffers "0", PkgOfferField$(swSession, 0, PKG_FIELD_NAME), path
END IF
-
- SwInstallOffers CStr(offer), PkgOfferField$(swSession, offer, PKG_FIELD_NAME), path
END SUB
@@ -438,28 +437,42 @@ END FUNCTION
' Publish the package file path on the package server with the
' settings' account: True when the server took it in for review.
FUNCTION SwPublish(BYVAL path AS STRING) AS BOOLEAN
- DIM offer AS LONG
DIM what AS STRING
- offer = PkgOfferFile(swSession, path)
-
- IF offer < 0 THEN
- SwError path & " cannot be published: " & PkgError$(swSession)
+ ' The file is offer 0
+ IF NOT SwRead(PkgOfferFileStart(swSession, path), path & " cannot be published: ") THEN
EXIT FUNCTION
END IF
- what = PkgOfferField$(swSession, offer, PKG_FIELD_NAME) & " " & PkgOfferField$(swSession, offer, PKG_FIELD_VERSION)
+ what = PkgOfferField$(swSession, 0, PKG_FIELD_NAME) & " " & PkgOfferField$(swSession, 0, PKG_FIELD_VERSION)
IF NOT SwNetRun(swMine, PkgNetUpload(swMine, path), "Publishing") THEN
EXIT FUNCTION
END IF
- ShellLog "Software: published " & PkgOfferField$(swSession, offer, PKG_FIELD_ID) & " " & PkgOfferField$(swSession, offer, PKG_FIELD_VERSION)
+ ShellLog "Software: published " & PkgOfferField$(swSession, 0, PKG_FIELD_ID) & " " & PkgOfferField$(swSession, 0, PKG_FIELD_VERSION)
MsgBox what & " is on the package server, waiting for its review. The Published page shows how the review goes.", vbOKOnly + vbInformation, SW_TITLE
SwPublish = True
END FUNCTION
+' Read packages as the offers (started: whether the reading did) with the
+' progress window up. True when they were read; else failText and why
+' are shown, unless it was stopped.
+FUNCTION SwRead(BYVAL started AS BOOLEAN, BYVAL failText AS STRING) AS BOOLEAN
+ IF NOT started THEN
+ SwError failText & "another job is running."
+ EXIT FUNCTION
+ END IF
+
+ IF SwRunJob("Reading") = PKG_RUN_DONE THEN
+ SwRead = True
+ ELSEIF NOT swCancelled THEN
+ SwError failText & PkgRunMessage$(swSession)
+ END IF
+END FUNCTION
+
+
' Remove the installed package id, after asking about what needs it (it
' goes too, or nothing does), what only it needed (offered), and files
' changed since they were installed (kept unless the user says).
@@ -542,6 +555,7 @@ FUNCTION SwRunJob(BYVAL verb AS STRING) AS LONG
Load ProgForm
swJob = SW_JOB_PKG
+ swCancelled = False
ProgForm.Caption = verb
ProgForm.lblWhat.Caption = verb
ProgForm.pbJob.Value = 0
diff --git a/src/apps/kpunch/terminal/terminal.dhs b/src/apps/kpunch/terminal/terminal.dhs
index 14a51007..3436ac46 100644
--- a/src/apps/kpunch/terminal/terminal.dhs
+++ b/src/apps/kpunch/terminal/terminal.dhs
@@ -82,7 +82,7 @@ Settings > Session Settings... changes the current session. Changes to the conne
Setting Meaning
------- -------
Name The name shown in the phone book and the window title.
- Connect with Serial port (a cable to the other computer), Modem (dials a phone number), Secure link (the encrypted link to the DVX secure-link proxy), or Telnet (not available yet).
+ Connect with Serial port (a cable to the other computer), Modem (dials a phone number), Secure link (the encrypted link to the DVX secure-link proxy), or Telnet (over the network; it needs Networking installed).
Port COM1 to COM4.
Speed (bps) 300 to 115200 bits per second. Both sides must use the same speed.
Data bits 5 to 8; almost always 8.
@@ -124,7 +124,7 @@ A passphrase typed on this tab is saved scrambled in Terminal's settings file, s
Port Its port number: usually 23 for Telnet. A secure-link proxy usually listens on 2323.
.endtable
-Telnet uses these settings, and so does the secure link when "Connect over the network" is ticked on the Secure Link tab. The network itself is set up in the Control Panel (Network page).
+Telnet uses these settings, and so does the secure link when "Connect over the network" is ticked on the Secure Link tab. Both need Networking, a part of DVX you choose when you install it (Setup can add it later). The network itself is set up in the Control Panel (Network page).
.h2 Screen Tab
@@ -365,6 +365,7 @@ On a serial port or modem without flow control, ZMODEM downloads automatically a
The modem did not answer Check that the modem is on and connected to the chosen port, and that the setup command is right.
The passphrase does not match the host's Use exactly the same passphrase as the secure-link proxy, or none on both sides.
The host did not answer the secure link Check that the secure-link proxy is running and that the speed matches.
+ Networking is not installed Telnet and the secure link over the network need Networking: run Setup and add it.
The host ... was not found Check the host name, and that a name server is set up (Control Panel, Network page).
... refused the connection on port ... The computer is there but nothing answers on that port: check the port number.
... did not answer Check the address, and that the network is working (Control Panel, Network page, Status...).
diff --git a/src/apps/kpunch/terminal/terminal.frm b/src/apps/kpunch/terminal/terminal.frm
index b9991582..8f776e84 100644
--- a/src/apps/kpunch/terminal/terminal.frm
+++ b/src/apps/kpunch/terminal/terminal.frm
@@ -399,20 +399,27 @@ END SUB
SUB ConnectSession
DIM host AS STRING
DIM pass AS STRING
+ DIM overNet AS BOOLEAN
IF transferring THEN
EXIT SUB
END IF
' The host and port are for Telnet, and for a secure link only when it
- ' is set to go over the network.
- host = session.host
+ ' is set to go over the network; both need Networking installed.
+ overNet = (session.connType = CONN_TELNET) OR (session.connType = CONN_SECURE AND session.secureNet)
+ host = session.host
- IF session.connType = CONN_SECURE AND NOT session.secureNet THEN
+ IF NOT overNet THEN
host = ""
END IF
- IF (session.connType = CONN_TELNET OR (session.connType = CONN_SECURE AND session.secureNet)) AND host = "" THEN
+ IF overNet AND NOT ConnTypeAvailable(CONN_TELNET) THEN
+ MsgBox "Telnet and the secure link over the network need Networking, which is not installed. Setup can add it.", vbOKOnly + vbInformation, TERM_TITLE
+ EXIT SUB
+ END IF
+
+ IF overNet AND host = "" THEN
MsgBox "Enter the host name on the Network page of Session Settings first.", vbOKOnly + vbInformation, TERM_TITLE
EXIT SUB
END IF
diff --git a/src/apps/kpunch/widshow/widshow.dhs b/src/apps/kpunch/widshow/widshow.dhs
index 0ff0e879..e050c063 100644
--- a/src/apps/kpunch/widshow/widshow.dhs
+++ b/src/apps/kpunch/widshow/widshow.dhs
@@ -1,24 +1,24 @@
-// 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.
+# 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.
# Widget Showcase help. Compiled to widshow.hlp next to widshow.app; F1
# and Help > Contents in the Widget Showcase open it.
diff --git a/src/common.mk b/src/common.mk
index bb2ee279..a3ea9872 100644
--- a/src/common.mk
+++ b/src/common.mk
@@ -98,6 +98,13 @@ endif
CC = $(DJGPP_PREFIX)/bin/$(DJGPP_TRIPLE)-gcc
DXE3GEN = PATH=$(DJGPP_PREFIX)/bin:$(PATH) DJDIR=$(DJGPP_PREFIX)/$(DJGPP_TRIPLE) $(DVX_ROOT)/src/tools/dxeGen.sh $(DJGPP_PREFIX)/$(DJGPP_TRIPLE)/bin/dxe3gen
EXE2COFF = $(DJGPP_PREFIX)/$(DJGPP_TRIPLE)/bin/exe2coff
+# Pre-link a module with third-party code down to what its own objects
+# reach (tools/dxeGc.sh): $(DXE_GC) OUT OWN.o... -- ALL.o..., the objects
+# built with $(DXE_GC_CFLAGS). Functions only: -fdata-sections would put
+# this COFF target's zero-filled variables in data sections, stored in
+# the module file (lwIP's buffer pools alone are about 100 KB).
+DXE_GC = $(DVX_ROOT)/src/tools/dxeGc.sh $(DJGPP_PREFIX)/bin/$(DJGPP_TRIPLE)
+DXE_GC_CFLAGS = -ffunction-sections
# ---- Flags ----
WARNFLAGS = -Wall -Wextra -Werror -Wno-type-limits -Wno-sign-compare -Wno-format-truncation
diff --git a/src/include/basic/comm.bas b/src/include/basic/comm.bas
index 5e5333f5..c65a2644 100644
--- a/src/include/basic/comm.bas
+++ b/src/include/basic/comm.bas
@@ -74,6 +74,7 @@ CONST SER_ERR_INVALID_FIFO = -13
CONST SER_ERR_NULL_PTR = -14
CONST SER_ERR_IRQ_NOT_FOUND = -15
CONST SER_ERR_LOCK_MEM = -16
+CONST SER_ERR_NO_MEMORY = -17
' Errors seen on received data (SerLineErrors; add them up)
CONST SER_LINE_OVERRUN = &H02
@@ -86,7 +87,6 @@ CONST SER_LINE_BREAK = &H10
CONST COMM_OK = 0
CONST COMM_ERR_PARAM = -1
CONST COMM_ERR_SERIAL = -2
-CONST COMM_ERR_ALLOC = -3
CONST COMM_ERR_HANDSHAKE = -4
CONST COMM_ERR_NOT_READY = -5
CONST COMM_ERR_SEND = -6
@@ -97,7 +97,9 @@ CONST COMM_PENDING = 1
' The highest channel number (channels are 0 to COMM_MAX_CHANNEL)
CONST COMM_MAX_CHANNEL = 127
-DECLARE LIBRARY "basrt"
+' The library named is the one these need (the serial ports and secure
+' links); BASIC finds the functions by name.
+DECLARE LIBRARY "serial"
' =========================================================
' UART Detection and Configuration
@@ -201,6 +203,11 @@ DECLARE LIBRARY "basrt"
' Send a break: hold the line in the break state (True), then release
' it (False).
DECLARE SUB SerSetBreak(BYVAL com AS LONG, BYVAL state AS LONG)
+END DECLARE
+
+' The packet and encrypted links are the secure link library's (part of
+' Communications): without it CommOpen and CommOpenTcp return 0.
+DECLARE LIBRARY "seclink"
' =========================================================
' Packet Serial (reliable delivery + channel multiplexing)
@@ -209,11 +216,21 @@ DECLARE LIBRARY "basrt"
' Open a packet serial link. Returns a handle (>0), or 0 on failure.
DECLARE FUNCTION CommOpen(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
+ ' Open the same link over a network connection instead of a COM port:
+ ' tcp is a connection handle from net.bas (TcpConnect or TcpAccept;
+ ' the other side opens its link the same way). Every channel, the
+ ' handshake, and encryption work as on a COM port. The connection
+ ' stays yours: close it with TcpClose after CommClose. Returns a
+ ' handle (>0), or 0 (the network part of DVX not installed).
+ DECLARE FUNCTION CommOpenTcp(BYVAL tcp AS LONG) AS LONG
+
' Close a packet serial link.
DECLARE SUB CommClose(BYVAL handle AS LONG)
- ' Send data on a logical channel (0 to COMM_MAX_CHANNEL).
- ' encrypt: True to encrypt (requires CommHandshake first).
+ ' Send data on a logical channel (0 to COMM_MAX_CHANNEL), waiting while
+ ' the link's window is full (the program's events run meanwhile, as
+ ' with DoEvents); False when the far side takes nothing for 15
+ ' seconds. encrypt: True to encrypt (requires CommHandshake first).
DECLARE FUNCTION CommSend(BYVAL handle AS LONG, BYVAL payload AS BINARY STRING, BYVAL channel AS LONG, BYVAL encrypt AS LONG) AS LONG
' Receive pending data from a specific channel. Returns "" if none.
@@ -245,7 +262,8 @@ DECLARE LIBRARY "basrt"
' Encryption (authenticated key exchange)
' =========================================================
- ' Perform the key exchange. Blocks until both sides complete, then
+ ' Perform the key exchange. Waits until both sides complete (up to 30
+ ' seconds; the program's events run meanwhile, as with DoEvents), then
' CommSend with encrypt=True encrypts. passphrase is optional: both
' sides must give the same one, or both none. Returns False on a
' mismatch, when only one side uses a passphrase, or on a failed link.
diff --git a/src/include/basic/conn.bas b/src/include/basic/conn.bas
index e94254b6..9f0588a2 100644
--- a/src/include/basic/conn.bas
+++ b/src/include/basic/conn.bas
@@ -67,10 +67,10 @@ CONST CONN_OPENING = 1
CONST CONN_OPEN = 2
' ConnOpen results
-CONST CONN_OK = 0
-CONST CONN_ERR_PARAM = -1
-CONST CONN_ERR_PORT = -3
-CONST CONN_ERR_HOST = -4
+CONST CONN_OK = 0
+CONST CONN_ERR_PARAM = -1
+CONST CONN_ERR_PORT = -3
+CONST CONN_ERR_NO_NET = -4
' Flow control (ConnSetSerial)
CONST CONN_FLOW_NONE = 0
@@ -160,6 +160,11 @@ DECLARE LIBRARY "conn"
' and modems).
DECLARE FUNCTION ConnCanBreak(BYVAL connType AS LONG) AS LONG
+ ' True when connections of a type can be made here: every type but
+ ' CONN_TELNET, which needs Networking installed (a secure link over the
+ ' network needs it too; without it ConnOpen gives CONN_ERR_NO_NET).
+ DECLARE FUNCTION ConnTypeAvailable(BYVAL connType AS LONG) AS LONG
+
' Hold the line in the break state for a moment. True when it did.
DECLARE FUNCTION ConnSendBreak(BYVAL handle AS LONG) AS LONG
diff --git a/src/include/basic/designer.bas b/src/include/basic/designer.bas
index 11b0c9c9..deb8341b 100644
--- a/src/include/basic/designer.bas
+++ b/src/include/basic/designer.bas
@@ -147,7 +147,11 @@ DECLARE LIBRARY "basrt"
DECLARE FUNCTION FrmControlNames$(BYVAL frmText AS STRING) AS STRING
DECLARE FUNCTION FrmCode$(BYVAL frmText AS STRING) AS STRING
DECLARE FUNCTION ClsClassName$(BYVAL clsText AS STRING, BYVAL path AS STRING) AS STRING
+END DECLARE
+' The form model (the Frm* functions with a handle, and FormText$) is the
+' BASIC development library's, part of DVX BASIC.
+DECLARE LIBRARY "basdev"
' A form model from .frm text, from a file, or a new blank form;
' 0 when the text is not a form. FrmFree frees it.
DECLARE FUNCTION FrmOpen(BYVAL frmText AS STRING) AS LONG
diff --git a/src/include/basic/fileops.bas b/src/include/basic/fileops.bas
index db0fea9e..b6baaa81 100644
--- a/src/include/basic/fileops.bas
+++ b/src/include/basic/fileops.bas
@@ -41,9 +41,10 @@ CONST FILEOPS_SORT_TYPE = 2
CONST FILEOPS_SORT_DATE = 3
' FileOpsJobCreate operations
-CONST FILEOPS_COPY = 0
-CONST FILEOPS_MOVE = 1
-CONST FILEOPS_DELETE = 2
+CONST FILEOPS_COPY = 0
+CONST FILEOPS_MOVE = 1
+CONST FILEOPS_DELETE = 2
+CONST FILEOPS_MEASURE = 3
' FileOpsJobRunFor results
CONST FILEOPS_STEP_BUSY = 0 ' more to do
@@ -199,6 +200,11 @@ DECLARE LIBRARY "fileops"
' -1 when there is none.
DECLARE FUNCTION FileOpsListFind(BYVAL list AS LONG, BYVAL itemName AS STRING) AS LONG
+ ' A new listing of a listing's folders alone, sorted by name, made
+ ' without reading the disk again (the listing itself is unchanged); 0
+ ' when out of memory. FileOpsListFree it when done.
+ DECLARE FUNCTION FileOpsListFolders(BYVAL list AS LONG) AS LONG
+
' An entry's name, whether it is a folder, and its size in bytes.
DECLARE FUNCTION FileOpsListName$(BYVAL list AS LONG, BYVAL index AS LONG) AS STRING
DECLARE FUNCTION FileOpsListIsFolder(BYVAL list AS LONG, BYVAL index AS LONG) AS LONG
@@ -324,10 +330,14 @@ DECLARE LIBRARY "fileops"
' A byte count with thousands separators, "1,234,567".
DECLARE FUNCTION FileOpsFormatSize$(BYVAL bytes AS DOUBLE) AS STRING
- ' ---- Jobs: copy, move, or delete with progress and questions ----
+ ' ---- Jobs: copy, move, delete, or measure with progress and questions ----
- ' Start a FILEOPS_COPY, FILEOPS_MOVE (into destFolder), or
- ' FILEOPS_DELETE job. Returns a job handle, 0 when out of memory.
+ ' Start a FILEOPS_COPY, FILEOPS_MOVE (into destFolder), FILEOPS_DELETE,
+ ' or FILEOPS_MEASURE job. A measure only counts the files, folders,
+ ' and bytes (FileOpsJobFilesTotal, FileOpsJobFoldersTotal, and
+ ' FileOpsJobBytesTotal) without changing anything or asking anything;
+ ' FileOpsJobUnread says how many folders it could not read. Returns a
+ ' job handle, 0 when out of memory.
DECLARE FUNCTION FileOpsJobCreate(BYVAL op AS LONG, BYVAL destFolder AS STRING) AS LONG
' Add a file or folder (full path) before the job runs. False when it
@@ -353,10 +363,13 @@ DECLARE LIBRARY "fileops"
DECLARE SUB FileOpsJobCancel(BYVAL job AS LONG)
' Progress: the path being worked on, files done and found so far,
- ' and percent done (0-100).
+ ' folders found so far inside the job's own files and folders, folders
+ ' a measure could not read (all or part), and percent done (0-100).
DECLARE FUNCTION FileOpsJobCurrent$(BYVAL job AS LONG) AS STRING
DECLARE FUNCTION FileOpsJobFilesDone(BYVAL job AS LONG) AS LONG
DECLARE FUNCTION FileOpsJobFilesTotal(BYVAL job AS LONG) AS LONG
+ DECLARE FUNCTION FileOpsJobFoldersTotal(BYVAL job AS LONG) AS LONG
+ DECLARE FUNCTION FileOpsJobUnread(BYVAL job AS LONG) AS LONG
DECLARE FUNCTION FileOpsJobPercent(BYVAL job AS LONG) AS LONG
' Progress in bytes: the size of the files found so far, and how much
diff --git a/src/include/basic/game.bas b/src/include/basic/game.bas
index b80b7e60..9949831b 100644
--- a/src/include/basic/game.bas
+++ b/src/include/basic/game.bas
@@ -65,12 +65,15 @@ DECLARE LIBRARY "game"
' program's windows is active.
DECLARE SUB GameKeyClear()
- ' How many frames FRAMEWAIT has skipped because the program was too
- ' slow to draw them in time, since it began waiting at its present
- ' rate (0 before the first FRAMEWAIT).
- DECLARE FUNCTION FrameMissed() AS LONG
-
' Wait until the screen starts drawing its next picture from the top;
' a full-screen game that draws right after it does not tear.
DECLARE SUB GameWaitRetrace()
END DECLARE
+
+' FRAMEWAIT's own counter, kept by the BASIC runtime
+DECLARE LIBRARY "basrt"
+ ' How many frames FRAMEWAIT has skipped because the program was too
+ ' slow to draw them in time, since it began waiting at its present
+ ' rate (0 before the first FRAMEWAIT).
+ DECLARE FUNCTION FrameMissed() AS LONG
+END DECLARE
diff --git a/src/include/basic/help.bas b/src/include/basic/help.bas
index a5cd67a5..ab185296 100644
--- a/src/include/basic/help.bas
+++ b/src/include/basic/help.bas
@@ -29,9 +29,15 @@
'
' Usage: Add this file to your project.
-DECLARE LIBRARY "basrt"
- ' Compile a .dhs source file to a binary .hlp file.
- ' Returns True on success; HelpCompileMessages$ says what was wrong.
+' The library named is the one these need (the shell's help viewer);
+' BASIC finds the functions by name.
+DECLARE LIBRARY "dvxshell"
+ ' Compile a .dhs source file to a binary .hlp file (its .image
+ ' pictures are looked for beside it). Returns True on success;
+ ' HelpCompileMessages$ says what was wrong. While it compiles, DVX
+ ' and your program's events go on, as with DoEvents; a second
+ ' HelpCompile meanwhile (yours or another program's) returns False at
+ ' once, and the compile stops if your program ends.
DECLARE FUNCTION HelpCompile(BYVAL inputFile AS STRING, BYVAL outputFile AS STRING) AS INTEGER
' The errors and warnings of the last HelpCompile, one per line
diff --git a/src/include/basic/http.bas b/src/include/basic/http.bas
index 4ba45b9c..b10bebde 100644
--- a/src/include/basic/http.bas
+++ b/src/include/basic/http.bas
@@ -30,7 +30,7 @@
' HttpOpen(method, url) makes a request and returns a number ("handle",
' 0 when out of memory); close it with HttpClose when done (whatever a
' program leaves open is closed when it ends). Set it up (HttpAddHeader,
-' HttpSetBody, HttpSaveTo, HttpSetTimeout), HttpStart it, then call
+' HttpSetBody or HttpSetBodyFile, HttpSaveTo, HttpSetTimeout), HttpStart it, then call
' HttpRunFor until it is HTTP_DONE or HTTP_FAILED:
'
' h = HttpOpen("GET", "http://example.com/news.txt")
@@ -60,12 +60,14 @@ DECLARE LIBRARY "http"
DECLARE SUB HttpClose(BYVAL h AS LONG)
' Before starting: a header to send, a body to send (with its type,
- ' such as "application/json"), a file to save the reply's body in
+ ' such as "application/json"; or a file's bytes, sent a piece at a
+ ' time, so a file of any size), a file to save the reply's body in
' (with keepPart True, a part already there is kept and only the rest
' fetched), and the seconds without progress before giving up (30 to
' start with).
DECLARE FUNCTION HttpAddHeader(BYVAL h AS LONG, BYVAL headerName AS STRING, BYVAL value AS STRING) AS LONG
DECLARE FUNCTION HttpSetBody(BYVAL h AS LONG, BYVAL body AS BINARY STRING, BYVAL contentType AS STRING) AS LONG
+ DECLARE FUNCTION HttpSetBodyFile(BYVAL h AS LONG, BYVAL path AS STRING, BYVAL contentType AS STRING) AS LONG
DECLARE SUB HttpSaveTo(BYVAL h AS LONG, BYVAL path AS STRING, BYVAL keepPart AS LONG)
DECLARE SUB HttpSetTimeout(BYVAL h AS LONG, BYVAL seconds AS LONG)
diff --git a/src/include/basic/joystick.bas b/src/include/basic/joystick.bas
index b37a69cb..820a43ca 100644
--- a/src/include/basic/joystick.bas
+++ b/src/include/basic/joystick.bas
@@ -49,6 +49,13 @@ CONST JOY_HAT_CENTERED = -1
CONST JOY_AXIS_MIN = -32768
CONST JOY_AXIS_MAX = 32767
+' What changed, as JoyEvent tells it
+CONST JOY_EVENT_NONE = 0
+CONST JOY_EVENT_AXIS = 1
+CONST JOY_EVENT_BUTTON_DOWN = 2
+CONST JOY_EVENT_BUTTON_UP = 3
+CONST JOY_EVENT_HAT = 4
+
' The listed settings for JoyChoice and friends (the library's own
' numbers, which do not change): the game port's mode, and when the
' keyboard joystick stands in for a missing one.
@@ -108,6 +115,22 @@ DECLARE LIBRARY "joystick"
' False when the joystick stopped answering (unplugged).
DECLARE FUNCTION JoyConnected(BYVAL joy AS LONG) AS LONG
+ ' Changes as they happen, instead of reading the joystick: JoyWatch
+ ' joy, True starts keeping that joystick's changes (False stops), and
+ ' JoyEvent() gives the next one since it was last asked (each once,
+ ' in order; the last 64 are kept): JOY_EVENT_AXIS (an axis moved),
+ ' JOY_EVENT_BUTTON_DOWN, JOY_EVENT_BUTTON_UP, JOY_EVENT_HAT, or
+ ' JOY_EVENT_NONE when nothing has changed. JoyEventJoy() and
+ ' JoyEventIndex() say which joystick and which axis, button, or hat
+ ' (from 1), and JoyEventValue() an axis's new position or a hat's
+ ' direction. Call JoyEvent from a Timer until it gives
+ ' JOY_EVENT_NONE. JoyWatch is True when there is such a joystick.
+ DECLARE FUNCTION JoyWatch(BYVAL joy AS LONG, BYVAL watching AS LONG) AS LONG
+ DECLARE FUNCTION JoyEvent() AS LONG
+ DECLARE FUNCTION JoyEventJoy() AS LONG
+ DECLARE FUNCTION JoyEventIndex() AS LONG
+ DECLARE FUNCTION JoyEventValue() AS LONG
+
' ---- Setting joysticks up ----
' Look for joysticks again (one plugged in later, a new game port
diff --git a/src/include/basic/music.bas b/src/include/basic/music.bas
index 09c3fcb6..4f24aed0 100644
--- a/src/include/basic/music.bas
+++ b/src/include/basic/music.bas
@@ -75,6 +75,10 @@ DECLARE LIBRARY "music"
' until MusicPlay.
DECLARE FUNCTION MusicLoad(BYVAL fileName AS STRING) AS LONG
+ ' The same from the bytes of a music or sound file, such as
+ ' ResGetData$ or HttpBody$ gives.
+ DECLARE FUNCTION MusicLoadData(BYVAL bytes AS BINARY STRING) AS LONG
+
' A MIDI music made from a music macro string (as PLAY takes it): its
' notes play on the General MIDI instruments its I commands name, the
' piano before any I. Nothing plays until MusicPlay.
@@ -112,6 +116,11 @@ DECLARE LIBRARY "music"
' True while the music is playing (not stopped, finished, or paused).
DECLARE FUNCTION MusicPlaying(BYVAL music AS LONG) AS LONG
+ ' True once after the music played to its end (not when stopped)
+ ' since MusicPlay started it: call it from a Timer to start the next
+ ' tune.
+ DECLARE FUNCTION MusicEnded(BYVAL music AS LONG) AS LONG
+
' Set the volume of this music (percent).
DECLARE SUB MusicVolume(BYVAL music AS LONG, BYVAL volume AS LONG)
diff --git a/src/include/basic/net.bas b/src/include/basic/net.bas
index ad374b86..9e4f2c31 100644
--- a/src/include/basic/net.bas
+++ b/src/include/basic/net.bas
@@ -26,9 +26,11 @@
' Control Panel (Network page). Handles are numbers; 0 means the call
' failed, and NetError$ then says why in plain words.
'
-' TcpConnect and TcpSend wait until they are done (other programs keep
-' running meanwhile). Everything else returns at once, so a program stays
-' event driven by checking from a Timer:
+' HostLookup$, TcpConnect, and TcpSend wait until they are done (other
+' programs keep running meanwhile, but yours waits). Everything else
+' returns at once, so a program stays event driven by checking from a
+' Timer, and those three have forms that do not wait either:
+' HostLookupStart, TcpConnectStart, and TcpSendSome.
'
' Sub Timer1_Timer ()
' IF TcpPending(conn) > 0 THEN PRINT TcpRecv$(conn)
@@ -43,12 +45,34 @@
'
' Usage: Add this file to your project.
+' HostLookupPoll and TcpConnectPoll results
+CONST NET_BUSY = 0 ' still going: check again later (from a Timer)
+CONST NET_DONE = 1
+CONST NET_FAILED = 2 ' NetError$ says why
+
+' NetState results
+CONST NET_STATE_STOPPED = 0 ' not started, or stopped (NetStop)
+CONST NET_STATE_NO_DRIVER = 1 ' started, but no network card driver was found
+CONST NET_STATE_CONFIGURING = 2 ' waiting for an address from the network
+CONST NET_STATE_UP = 3 ' has an address: ready
+
DECLARE LIBRARY "dvxnet"
' The address of hostName (like "bbs.example.org"), as text such as
' "192.0.2.10", or "" when it cannot be found.
DECLARE FUNCTION HostLookup$(BYVAL hostName AS STRING) AS STRING
+ ' The same without waiting: HostLookupStart gives a lookup handle (0
+ ' when too many are under way), HostLookupPoll says NET_BUSY, NET_DONE,
+ ' or NET_FAILED, HostLookupResult$ gives the address once it is
+ ' NET_DONE, and HostLookupClose frees the handle (or gives the lookup
+ ' up). The first lookup after DVX starts may wait for the network's
+ ' settings (DHCP) too.
+ DECLARE FUNCTION HostLookupStart(BYVAL hostName AS STRING) AS LONG
+ DECLARE FUNCTION HostLookupPoll(BYVAL lookup AS LONG) AS LONG
+ DECLARE FUNCTION HostLookupResult$(BYVAL lookup AS LONG) AS STRING
+ DECLARE SUB HostLookupClose(BYVAL lookup AS LONG)
+
' The name of an address such as "192.0.2.10"; the address itself
' when no name is known for it, "" when it is not an address.
DECLARE FUNCTION HostName$(BYVAL address AS STRING) AS STRING
@@ -63,6 +87,14 @@ DECLARE LIBRARY "dvxnet"
' or 0 when the connection could not be made (waits up to 20 seconds).
DECLARE FUNCTION TcpConnect(BYVAL host AS STRING, BYVAL port AS LONG) AS LONG
+ ' The same without waiting: TcpConnectStart gives the connection's
+ ' handle at once (0 when it cannot start), and TcpConnectPoll says
+ ' NET_BUSY while the host is looked up and the connection made,
+ ' NET_DONE once it is (the handle is then like TcpConnect's), or
+ ' NET_FAILED (close it with TcpClose).
+ DECLARE FUNCTION TcpConnectStart(BYVAL host AS STRING, BYVAL port AS LONG) AS LONG
+ DECLARE FUNCTION TcpConnectPoll(BYVAL handle AS LONG) AS LONG
+
' Wait for connections on port. Returns a listening handle, or 0.
DECLARE FUNCTION TcpListen(BYVAL port AS LONG) AS LONG
@@ -74,12 +106,22 @@ DECLARE LIBRARY "dvxnet"
' of bytes sent, or -1.
DECLARE FUNCTION TcpSend(BYVAL handle AS LONG, BYVAL bytes AS BINARY STRING) AS LONG
+ ' Send what the connection takes now, never waiting: the number of
+ ' bytes taken from the start of bytes (0 while it is full: send the
+ ' rest later), or -1.
+ DECLARE FUNCTION TcpSendSome(BYVAL handle AS LONG, BYVAL bytes AS BINARY STRING) AS LONG
+
' How many bytes have arrived and wait to be read (-1 for a bad handle).
DECLARE FUNCTION TcpPending(BYVAL handle AS LONG) AS LONG
' What has arrived (up to 4096 bytes), or "" when nothing has.
DECLARE FUNCTION TcpRecv$(BYVAL handle AS LONG) AS BINARY STRING
+ ' What has arrived (up to 4096 bytes), left waiting for the next
+ ' TcpRecv$: look for the end of a line, or a message's length, before
+ ' taking any of it. "" when nothing has.
+ DECLARE FUNCTION TcpPeek$(BYVAL handle AS LONG) AS BINARY STRING
+
' 1 while the connection is open, 0 once the other side closed it
' (text may still be waiting: read it with TcpRecv$ first).
DECLARE FUNCTION TcpConnected(BYVAL handle AS LONG) AS LONG
@@ -179,4 +221,8 @@ DECLARE LIBRARY "dvxnet"
' "rxpackets", "rxbytes", "rxdropped", "txpackets", "txbytes" ("" for
' an unknown field).
DECLARE FUNCTION NetStatus$(BYVAL field AS STRING) AS STRING
+
+ ' The network's state as a number: NET_STATE_STOPPED,
+ ' NET_STATE_NO_DRIVER, NET_STATE_CONFIGURING, or NET_STATE_UP.
+ DECLARE FUNCTION NetState() AS LONG
END DECLARE
diff --git a/src/include/basic/pkg.bas b/src/include/basic/pkg.bas
index f47226da..f6ca0e28 100644
--- a/src/include/basic/pkg.bas
+++ b/src/include/basic/pkg.bas
@@ -24,9 +24,9 @@
' pkg.bas -- Add-on Package Library for DVX BASIC
' Lists the packages installed in DVX, reads add-on packages (.DPK files),
' plans what installing them takes (the packages they need, checked
-' against this DVX, its other packages, and the keys DVX trusts), and
-' installs and removes packages as a job a program runs a little at a
-' time with progress; and makes packages, signs them, and makes keys.
+' against this DVX, its other packages, and the keys DVX trusts),
+' installs and removes packages, and makes and signs packages, each as a
+' job a program runs a little at a time with progress; and makes keys.
' Usage: Add this file to your project.
'
' A session (PkgOpen) is a number ("handle", 0 = none); close it with
@@ -55,6 +55,8 @@ CONST PKG_FIELD_EXPLICIT = 13 ' installed: "yes" chosen, "no" only needed
CONST PKG_FIELD_TRUST = 14 ' offer: "system" (DVX's own key), "user" (a key you trust), "none"
CONST PKG_FIELD_WHERE = 15 ' offer: its file
CONST PKG_FIELD_SIZE = 16 ' the bytes of its files
+CONST PKG_FIELD_SHA256 = 17 ' catalogue (PkgNetField$): its package file's SHA-256, as hex
+CONST PKG_FIELD_REPO_SIGNED = 18 ' catalogue (PkgNetField$): "yes" when the repository signed it, else "no"
' PkgRunFor results
CONST PKG_RUN_BUSY = 0 ' more to do
@@ -68,6 +70,11 @@ DECLARE LIBRARY "pkg"
' handle, or 0 when out of memory.
DECLARE FUNCTION PkgOpen() AS LONG
+ ' A session on another DVX folder (root; "" is this DVX's), such as a
+ ' copy being made ready on another disk: its installed packages and
+ ' the keys its user folder trusts. 0 when its records cannot be read.
+ DECLARE FUNCTION PkgOpenIn(BYVAL root AS STRING) AS LONG
+
' Close a session (a running job stops; one whose files are already in
' place is finished first).
DECLARE SUB PkgClose(BYVAL s AS LONG)
@@ -100,15 +107,17 @@ DECLARE LIBRARY "pkg"
' ---- Packages from files ----
- ' Read the package file path, and every other package in its folder
- ' (what it may need), as the offers (the ones before are gone).
- ' Returns path's offer number (0), or -1: PkgError$ says why.
- DECLARE FUNCTION PkgOfferFile(BYVAL s AS LONG, BYVAL path AS STRING) AS LONG
+ ' Start reading the package file path, and every other package in its
+ ' folder (what it may need), as the offers (the ones before are
+ ' gone): a job, run with PkgRunFor (checking a package's signature
+ ' takes a moment). Path's offer is 0; the job fails when path cannot
+ ' be read (PkgRunMessage$ says why). False while a job runs.
+ DECLARE FUNCTION PkgOfferFileStart(BYVAL s AS LONG, BYVAL path AS STRING) AS LONG
- ' Read every package file in folder as the offers (the ones before
- ' are gone; one that cannot be read is left out): how many, or -1
- ' while a job runs. What pkgnet.bas fetched is planned this way.
- DECLARE FUNCTION PkgOfferFolder(BYVAL s AS LONG, BYVAL folder AS STRING) AS LONG
+ ' Start reading every package file in folder as the offers, the same
+ ' way (one that cannot be read is left out; PkgOfferCount says how
+ ' many there were). What pkgnet.bas fetched is planned this way.
+ DECLARE FUNCTION PkgOfferFolderStart(BYVAL s AS LONG, BYVAL folder AS STRING) AS LONG
' How many offers there are, and the number of the newest offer of
' the package id (-1 when none is).
@@ -122,9 +131,17 @@ DECLARE LIBRARY "pkg"
DECLARE FUNCTION PkgOfferDependsRows$(BYVAL s AS LONG, BYVAL offer AS LONG) AS STRING
' Trust the key file keyPath (NAME.KEY) from now on. Read the offers
- ' again (PkgOfferFile) to see their new trust.
+ ' again (PkgOfferFileStart) to see their new trust.
DECLARE FUNCTION PkgTrustKey(BYVAL s AS LONG, BYVAL keyPath AS STRING) AS LONG
+ ' The keys DVX trusts, as rows: name, id, and "system" (DVX's own) or
+ ' "user" (one you chose to trust with PkgTrustKey).
+ DECLARE FUNCTION PkgKeysRows$(BYVAL s AS LONG) AS STRING
+
+ ' Stop trusting the key with id keyId (as PkgKeysRows$ gives it): it
+ ' leaves your KEYS folder. DVX's own keys stay. True when done.
+ DECLARE FUNCTION PkgUntrustKey(BYVAL s AS LONG, BYVAL keyId AS STRING) AS LONG
+
' ---- Planning ----
' Plan installing the offers numbered in offers ("0" or "0 3") with
@@ -160,7 +177,9 @@ DECLARE LIBRARY "pkg"
' Start installing the plan, or removing the installed packages ids
' (one per line, in that order; files changed since they were
' installed stay unless removeChanged). False when a job is running
- ' (or, installing, there is no plan).
+ ' (or, installing, there is no plan). Reading packages, and building
+ ' and signing one, are jobs too (PkgOfferFileStart,
+ ' PkgOfferFolderStart, PkgBuildStart, PkgSignStart).
DECLARE FUNCTION PkgInstallStart(BYVAL s AS LONG) AS LONG
DECLARE FUNCTION PkgRemoveStart(BYVAL s AS LONG, BYVAL ids AS STRING, BYVAL removeChanged AS LONG) AS LONG
@@ -168,7 +187,8 @@ DECLARE LIBRARY "pkg"
' PKG_RUN_FAILED. Call it again (from a Timer, say) while BUSY.
DECLARE FUNCTION PkgRunFor(BYVAL s AS LONG, BYVAL milliseconds AS LONG) AS LONG
- ' Progress: percent done (0-100) and the package it is on.
+ ' Progress: percent done (0-100) and the package it is on (reading:
+ ' the package file; building or signing: the file in it).
DECLARE FUNCTION PkgRunPercent(BYVAL s AS LONG) AS LONG
DECLARE FUNCTION PkgRunCurrent$(BYVAL s AS LONG) AS STRING
@@ -179,8 +199,11 @@ DECLARE LIBRARY "pkg"
' were installed, one per line.
DECLARE FUNCTION PkgRunKept$(BYVAL s AS LONG) AS STRING
- ' Stop the job after the package it is on (one still being unpacked
- ' is left as it was).
+ ' Stop the job: reading, building, or signing at once (nothing read;
+ ' nothing left behind);
+ ' installing or removing after the package it is on (one still being
+ ' unpacked is left as it was; one whose files are in is finished first,
+ ' so keep calling PkgRunFor while it says PKG_RUN_BUSY).
DECLARE SUB PkgRunCancel(BYVAL s AS LONG)
' ---- Making packages ----
@@ -200,28 +223,45 @@ DECLARE LIBRARY "pkg"
DECLARE FUNCTION PkgIdPart$(BYVAL text AS STRING) AS STRING
DECLARE FUNCTION PkgVersionValid(BYVAL text AS STRING) AS LONG
+ ' -1 when version a is older than b, 0 when they are the same, 1 when
+ ' a is newer: "1.10.0" is newer than "1.9.2", which comparing the
+ ' texts gets wrong. A text that is not a version counts as older than
+ ' any version.
+ DECLARE FUNCTION PkgVersionCompare(BYVAL a AS STRING, BYVAL b AS STRING) AS LONG
+
' This DVX's version (a package's oldest DVX) and the ABI its programs,
' libraries, and widgets are made for (a package's abi).
DECLARE FUNCTION PkgDvxVersion$() AS STRING
DECLARE FUNCTION PkgDvxAbi() AS LONG
- ' Every file under folder as the files for PkgBuild: rows of its name
+ ' Every file under folder as the files for PkgBuildStart: rows of its name
' in the package (prefix, then its name under folder; "" names it from
' folder, laid out like the DVX folder) and the file, in name order.
' "" with the reason in PkgError$ when folder cannot be read.
DECLARE FUNCTION PkgFolderRows$(BYVAL s AS LONG, BYVAL folder AS STRING, BYVAL prefix AS STRING) AS STRING
- ' Build the package outPath from templatePath (a PACKAGE.INI without
- ' [files] or format; other sections are ignored; a program, library,
- ' or widget with no abi gets PkgDvxAbi's) and files (rows of a name
- ' in the package TAB the file it is made from), signed with the secret
- ' key file secretPath unless it is "". True, or False: PkgError$ says
- ' why.
- DECLARE FUNCTION PkgBuild(BYVAL s AS LONG, BYVAL templatePath AS STRING, BYVAL files AS STRING, BYVAL outPath AS STRING, BYVAL secretPath AS STRING) AS LONG
+ ' Start building the package outPath from templatePath (a PACKAGE.INI
+ ' without [files] or format; other sections are ignored; a program,
+ ' library, or widget with no abi gets PkgDvxAbi's) and files (rows of
+ ' a name in the package TAB the file it is made from), signed with the
+ ' secret key file secretPath unless it is "": a job, run with
+ ' PkgRunFor. False when it cannot start (PkgError$ says why) or a job
+ ' is running.
+ DECLARE FUNCTION PkgBuildStart(BYVAL s AS LONG, BYVAL templatePath AS STRING, BYVAL files AS STRING, BYVAL outPath AS STRING, BYVAL secretPath AS STRING) AS LONG
- ' Sign the package dpkPath with the secret key file secretPath: into it,
- ' or beside it (NAME.SIG) when detached.
- DECLARE FUNCTION PkgSign(BYVAL s AS LONG, BYVAL dpkPath AS STRING, BYVAL secretPath AS STRING, BYVAL detached AS LONG) AS LONG
+ ' Start signing the package dpkPath with the secret key file
+ ' secretPath: into it (a job, run with PkgRunFor), or beside it
+ ' (NAME.SIG) when detached (done at once: PkgRunFor says PKG_RUN_DONE).
+ ' False as PkgBuildStart.
+ DECLARE FUNCTION PkgSignStart(BYVAL s AS LONG, BYVAL dpkPath AS STRING, BYVAL secretPath AS STRING, BYVAL detached AS LONG) AS LONG
+
+ ' Check the package dpkPath without installing it: every file against
+ ' its manifest (each file's size and SHA-256, so a damaged or changed
+ ' package is found), a job run with PkgRunFor like an install. With
+ ' keyPath (a NAME.KEY file) the package must also be signed by that key
+ ' -- in it or beside it (NAME.SIG) -- or nothing starts. False with the
+ ' reason in PkgError$ (none while a job is running).
+ DECLARE FUNCTION PkgVerifyStart(BYVAL s AS LONG, BYVAL dpkPath AS STRING, BYVAL keyPath AS STRING) AS LONG
' Make a key pair called keyName: basePath.KEY, the public key to give out
' (and to trust: PkgTrustKey), and basePath.SEC, the secret key to sign
@@ -231,4 +271,34 @@ DECLARE LIBRARY "pkg"
' The name and id of the key in a key file (.KEY) or secret key file
' (.SEC) as a row; "" with the reason in PkgError$ when it is neither.
DECLARE FUNCTION PkgKeyInfo$(BYVAL s AS LONG, BYVAL path AS STRING) AS STRING
+
+ ' Sign any bytes (a file read into a string, a message) with the
+ ' secret key file secretPath: the signature as text, to keep or send
+ ' with them; "" with the reason in PkgError$.
+ DECLARE FUNCTION PkgSignData$(BYVAL s AS LONG, BYVAL bytes AS BINARY STRING, BYVAL secretPath AS STRING) AS STRING
+
+ ' True when signature (PkgSignData$'s text, or a .SIG file's) was made
+ ' over bytes by the key in the key file keyPath -- or, with keyPath "",
+ ' by a key DVX trusts. False with the reason in PkgError$.
+ DECLARE FUNCTION PkgCheckData(BYVAL s AS LONG, BYVAL bytes AS BINARY STRING, BYVAL signature AS STRING, BYVAL keyPath AS STRING) AS LONG
+
+ ' ---- Plain ZIP archives (jobs run with PkgRunFor, like an install) ----
+
+ ' Read the ZIP zipPath to its end, every file in it checked; then
+ ' PkgZipRows$ lists what is in it.
+ DECLARE FUNCTION PkgZipListStart(BYVAL s AS LONG, BYVAL zipPath AS STRING) AS LONG
+
+ ' Unpack the ZIP zipPath into folder (made as needed; a file already
+ ' there is replaced). A name in the ZIP that would leave folder stops
+ ' it.
+ DECLARE FUNCTION PkgZipExtractStart(BYVAL s AS LONG, BYVAL zipPath AS STRING, BYVAL folder AS STRING) AS LONG
+
+ ' Make the ZIP zipPath from files: rows of a name in the ZIP and the
+ ' file it is made from, TAB between them (PkgFolderRows$ gives them
+ ' for a folder).
+ DECLARE FUNCTION PkgZipCreateStart(BYVAL s AS LONG, BYVAL zipPath AS STRING, BYVAL files AS STRING) AS LONG
+
+ ' What the last ZIP job listed, unpacked, or packed, as rows: name,
+ ' size, date (YYYY-MM-DD HH:MM), and "folder" for a folder.
+ DECLARE FUNCTION PkgZipRows$(BYVAL s AS LONG) AS STRING
END DECLARE
diff --git a/src/include/basic/pkgnet.bas b/src/include/basic/pkgnet.bas
index cab64831..9f73f601 100644
--- a/src/include/basic/pkgnet.bas
+++ b/src/include/basic/pkgnet.bas
@@ -25,7 +25,7 @@
' that is newer than the packages installed (DVX itself among them), and
' fetches packages, with what they need that is not installed, into a
' download folder. With an account on the server, it also publishes
-' packages there and lists what the account has sent. There pkg.bas reads them (PkgOfferFolder), plans
+' packages there and lists what the account has sent. There pkg.bas reads them (PkgOfferFolderStart), plans
' installing them, and installs them as it does a package file. Every
' package fetched is checked against the size and fingerprint (SHA-256)
' the server gives, and the server's signature of it is fetched beside
@@ -62,6 +62,11 @@ DECLARE LIBRARY "pkgnet"
' memory. An account for a handle on another server:
' PkgNetAccount(n, email, password).
DECLARE FUNCTION PkgNetOpen(BYVAL server AS STRING) AS LONG
+
+ ' PkgNetOpen for another DVX folder (root, "" for this DVX: what is
+ ' installed there decides the updates), fetching into downloadDir (""
+ ' for the user folder's DOWNLOAD).
+ DECLARE FUNCTION PkgNetOpenIn(BYVAL server AS STRING, BYVAL root AS STRING, BYVAL downloadDir AS STRING) AS LONG
DECLARE SUB PkgNetClose(BYVAL n AS LONG)
DECLARE SUB PkgNetAccount(BYVAL n AS LONG, BYVAL user AS STRING, BYVAL password AS STRING)
@@ -124,7 +129,7 @@ DECLARE LIBRARY "pkgnet"
DECLARE FUNCTION PkgNetField$(BYVAL n AS LONG, BYVAL id AS STRING, BYVAL field AS LONG) AS STRING
DECLARE FUNCTION PkgNetDependsRows$(BYVAL n AS LONG, BYVAL id AS STRING) AS STRING
- ' The download folder (for pkg.bas's PkgOfferFolder after a fetch),
+ ' The download folder (for pkg.bas's PkgOfferFolderStart after a fetch),
' and removing what the last fetch put there (once it is installed,
' or not wanted).
DECLARE FUNCTION PkgNetDir$(BYVAL n AS LONG) AS STRING
diff --git a/src/include/basic/print.bas b/src/include/basic/print.bas
index a426e3e6..25495a73 100644
--- a/src/include/basic/print.bas
+++ b/src/include/basic/print.bas
@@ -157,12 +157,6 @@ DECLARE LIBRARY "print"
DECLARE FUNCTION PrintChoiceFirst() AS LONG
DECLARE FUNCTION PrintChoiceLast() AS LONG
- ' Print what a form shows (it must be loaded), scaled to the page, on
- ' a printer model that prints pictures; PrintFormDialog shows the
- ' Print dialog first. True when it was printed.
- DECLARE FUNCTION PrintForm(BYVAL formName AS STRING) AS LONG
- DECLARE FUNCTION PrintFormDialog(BYVAL formName AS STRING) AS LONG
-
' ---- The printer ports themselves (LPT1 is 1, up to 3)
' True when the computer has printer port LPTn.
@@ -177,6 +171,15 @@ DECLARE LIBRARY "print"
DECLARE FUNCTION PrintStatusProblem$(BYVAL status AS LONG, BYVAL afterWrite AS LONG, BYVAL where AS STRING) AS STRING
END DECLARE
+' Printing a form, done by the BASIC runtime (which has the forms)
+DECLARE LIBRARY "basrt"
+ ' Print what a form shows (it must be loaded), scaled to the page, on
+ ' a printer model that prints pictures; PrintFormDialog shows the
+ ' Print dialog first. True when it was printed.
+ DECLARE FUNCTION PrintForm(BYVAL formName AS STRING) AS LONG
+ DECLARE FUNCTION PrintFormDialog(BYVAL formName AS STRING) AS LONG
+END DECLARE
+
' The state of a printer (PrintPortStatus)
CONST PRINT_STATUS_TIMEOUT = &H01
CONST PRINT_STATUS_IO_ERROR = &H08
diff --git a/src/include/basic/resource.bas b/src/include/basic/resource.bas
index eb1a152b..06e4149a 100644
--- a/src/include/basic/resource.bas
+++ b/src/include/basic/resource.bas
@@ -39,9 +39,10 @@ CONST RES_NAME_ICON = "icon32"
CONST RES_NAME_WIDGET_ICON = "icon24"
CONST RES_NAME_MAX = 32
-DECLARE LIBRARY "basrt"
+DECLARE LIBRARY "libdvx"
' Open a resource file for reading. Returns a handle (> 0)
- ' or 0 on failure. Close with ResClose when done.
+ ' or 0 on failure. Close with ResClose when done (whatever a
+ ' program leaves open is closed when it ends).
DECLARE FUNCTION ResOpen(BYVAL path AS STRING) AS LONG
' Close a resource handle.
diff --git a/src/include/basic/security.bas b/src/include/basic/security.bas
index 3ec92e1f..c0546c1b 100644
--- a/src/include/basic/security.bas
+++ b/src/include/basic/security.bas
@@ -23,10 +23,11 @@
' security.bas -- Security for DVX BASIC
'
' The building blocks the encrypted serial link uses, for your own
-' protocols and files: random bytes for keys, a key exchange (two
-' programs agree on a secret over a line anyone can read), a cipher,
-' SHA-256 hashes, HMAC signatures, key stretching (HKDF), and a compare
-' that does not give away where two codes differ.
+' protocols and files: random bytes for keys, key exchanges (two
+' programs agree on a secret over a line anyone can read), ciphers,
+' sealing (encryption that also proves nothing was changed), SHA-256
+' hashes, HMAC and public-key signatures, key stretching (HKDF), and a
+' compare that does not give away where two codes differ.
'
' Keys, data, and results are strings that may hold any bytes, CHR$(0)
' too. A key exchange, cipher, hash, or HMAC in progress is a handle (0
@@ -34,25 +35,39 @@
' and SecHmacEnd$ free theirs). Whatever a program leaves open is freed
' when it ends.
'
-' A secret from SecDhSecret$ is not a key: make keys from it with
-' SecHkdf$. The same key and counter must never encrypt two different
-' messages: move the counter on (SecCipherSetCounter) or make a new key.
+' A secret from SecDhSecret$ or SecX25519$ is not a key: make keys from
+' it with SecHkdf$. The same key and counter must never encrypt two
+' different messages: move the counter on (SecCipherSetCounter) or make
+' a new key. The same goes for a sealing key and nonce: a fresh
+' SecRandom$(SEC_AEAD_NONCE_SIZE) for every message is safe.
'
' Usage: Add this file to your project.
' Sizes in bytes
-CONST SEC_DH_KEY_SIZE = 128 ' a key exchange public key and secret
-CONST SEC_XTEA_KEY_SIZE = 16 ' a cipher key
-CONST SEC_SHA256_SIZE = 32 ' a SHA-256 hash or HMAC
-CONST SEC_HKDF_MAX_OUT = 8160 ' the most SecHkdf$ makes at once
+CONST SEC_DH_KEY_SIZE = 128 ' a key exchange public key and secret
+CONST SEC_XTEA_KEY_SIZE = 16 ' a cipher key
+CONST SEC_SHA256_SIZE = 32 ' a SHA-256 hash or HMAC
+CONST SEC_HKDF_MAX_OUT = 8160 ' the most SecHkdf$ makes at once
+CONST SEC_X25519_KEY_SIZE = 32 ' an X25519 secret key, public key, or shared secret
+CONST SEC_AEAD_KEY_SIZE = 32 ' a sealing key
+CONST SEC_AEAD_NONCE_SIZE = 24 ' a sealing nonce
+CONST SEC_AEAD_TAG_SIZE = 16 ' what sealing adds to a message
+CONST SEC_ED25519_SEED_SIZE = 32 ' the random bytes a signing key is made from
+CONST SEC_ED25519_SECRET_SIZE = 64 ' a signing (secret) key
+CONST SEC_ED25519_PUBLIC_SIZE = 32 ' a signature checking (public) key
+CONST SEC_ED25519_SIG_SIZE = 64 ' a signature
-' Results of SecDhComputeSecret
+' Results of SecDhComputeSecret, SecDhComputeStart, and SecDhStep
CONST SEC_OK = 0
CONST SEC_ERR_PARAM = -1
CONST SEC_ERR_NOT_READY = -2
CONST SEC_ERR_IO = -3
+CONST SEC_DH_PENDING = 1 ' SecDhStep: more of the work to do
-DECLARE LIBRARY "serial"
+' Result of SecX25519Step while there is more to do (then SEC_OK)
+CONST SEC_X25519_PENDING = 1
+
+DECLARE LIBRARY "crypto"
' count random bytes, good for keys and salts (RND is not).
DECLARE FUNCTION SecRandom$(BYVAL count AS LONG) AS BINARY STRING
@@ -70,6 +85,15 @@ DECLARE LIBRARY "serial"
' computer). Returns a handle, or 0.
DECLARE FUNCTION SecDhCreate() AS LONG
+ ' The same without the wait: SecDhCreateStart gives the handle at once,
+ ' and SecDhStep then computes its keys a slice at a time (call it from
+ ' a Timer until it stops saying SEC_DH_PENDING; SEC_OK once they are
+ ' ready). SecDhComputeStart starts the secret the same way (checking
+ ' the other side's key at once), and SecDhStep finishes it.
+ DECLARE FUNCTION SecDhCreateStart() AS LONG
+ DECLARE FUNCTION SecDhComputeStart(BYVAL handle AS LONG, BYVAL remotePublicKey AS BINARY STRING) AS LONG
+ DECLARE FUNCTION SecDhStep(BYVAL handle AS LONG) AS LONG
+
' This side's public key (SEC_DH_KEY_SIZE bytes), to send.
DECLARE FUNCTION SecDhPublicKey$(BYVAL handle AS LONG) AS BINARY STRING
@@ -83,6 +107,31 @@ DECLARE LIBRARY "serial"
DECLARE SUB SecDhFree(BYVAL handle AS LONG)
+ ' ---- Key exchange (X25519): the same idea, much faster
+ ' Each side makes a secret key (SecRandom$(SEC_X25519_KEY_SIZE)),
+ ' sends its SecX25519PublicKey$ to the other, and gives what it got to
+ ' SecX25519$; both then have the same secret. Each call takes about a
+ ' tenth of a second on a slow computer.
+
+ ' The public key of secretKey ("" for a key of another length).
+ DECLARE FUNCTION SecX25519PublicKey$(BYVAL secretKey AS BINARY STRING) AS BINARY STRING
+
+ ' The shared secret (SEC_X25519_KEY_SIZE bytes): "" for a key of
+ ' another length, or a public key that would give everyone the same
+ ' secret (the other side is cheating).
+ DECLARE FUNCTION SecX25519$(BYVAL secretKey AS BINARY STRING, BYVAL otherPublicKey AS BINARY STRING) AS BINARY STRING
+
+ ' The same without the wait: SecX25519Start gives a handle at once
+ ' (with otherPublicKey "" it makes the public key of secretKey), and
+ ' SecX25519Step does a ninth of the work a call (call it from a Timer
+ ' until it stops saying SEC_X25519_PENDING; SEC_OK once it is done).
+ ' SecX25519End$ then gives what SecX25519$ (or SecX25519PublicKey$)
+ ' would have, and frees the handle; SecX25519Free drops one.
+ DECLARE FUNCTION SecX25519Start(BYVAL secretKey AS BINARY STRING, BYVAL otherPublicKey AS BINARY STRING) AS LONG
+ DECLARE FUNCTION SecX25519Step(BYVAL handle AS LONG) AS LONG
+ DECLARE FUNCTION SecX25519End$(BYVAL handle AS LONG) AS BINARY STRING
+ DECLARE SUB SecX25519Free(BYVAL handle AS LONG)
+
' ---- Cipher (XTEA in counter mode: encrypting and decrypting are the
' same call)
@@ -100,6 +149,19 @@ DECLARE LIBRARY "serial"
DECLARE SUB SecCipherFree(BYVAL handle AS LONG)
+ ' ---- Sealing (XChaCha20-Poly1305): encrypt and prove it unchanged
+
+ ' bytes encrypted with key (SEC_AEAD_KEY_SIZE bytes) and nonce
+ ' (SEC_AEAD_NONCE_SIZE), followed by a SEC_AEAD_TAG_SIZE-byte tag
+ ' that also covers extra (sent in the clear, "" for none); "" for a
+ ' key or nonce of another length.
+ DECLARE FUNCTION SecAeadLock$(BYVAL key AS BINARY STRING, BYVAL nonce AS BINARY STRING, BYVAL extra AS BINARY STRING, BYVAL bytes AS BINARY STRING) AS BINARY STRING
+
+ ' What SecAeadLock$ sealed, opened again: "" when anything differs
+ ' from the sealing (the key, nonce, extra, or one sealed byte). A
+ ' sealed message of no bytes also opens to "".
+ DECLARE FUNCTION SecAeadUnlock$(BYVAL key AS BINARY STRING, BYVAL nonce AS BINARY STRING, BYVAL extra AS BINARY STRING, BYVAL sealed AS BINARY STRING) AS BINARY STRING
+
' ---- Hashes and signatures
' The SHA-256 hash of bytes (SEC_SHA256_SIZE bytes).
@@ -125,6 +187,23 @@ DECLARE LIBRARY "serial"
' out of range.
DECLARE FUNCTION SecHkdf$(BYVAL salt AS BINARY STRING, BYVAL secret AS BINARY STRING, BYVAL info AS BINARY STRING, BYVAL length AS LONG) AS BINARY STRING
+ ' Public-key signatures (Ed25519): whoever has the public key can
+ ' check that the secret key's owner signed exactly these bytes.
+
+ ' A secret key (SEC_ED25519_SECRET_SIZE bytes) made from
+ ' SecRandom$(SEC_ED25519_SEED_SIZE); "" for a seed of another length.
+ ' Keep it private.
+ DECLARE FUNCTION SecEd25519KeyPair$(BYVAL seed AS BINARY STRING) AS BINARY STRING
+
+ ' The public key of a secret key, to give out.
+ DECLARE FUNCTION SecEd25519PublicKey$(BYVAL secretKey AS BINARY STRING) AS BINARY STRING
+
+ ' The signature of bytes (SEC_ED25519_SIG_SIZE bytes).
+ DECLARE FUNCTION SecEd25519Sign$(BYVAL secretKey AS BINARY STRING, BYVAL bytes AS BINARY STRING) AS BINARY STRING
+
+ ' True when signature is publicKey's owner's signature of bytes.
+ DECLARE FUNCTION SecEd25519Check(BYVAL signature AS BINARY STRING, BYVAL publicKey AS BINARY STRING, BYVAL bytes AS BINARY STRING) AS LONG
+
' True when a and b hold the same bytes. Takes the same time wherever
' they differ, so a signature check gives nothing away.
DECLARE FUNCTION SecEqual(BYVAL a AS BINARY STRING, BYVAL b AS BINARY STRING) AS LONG
diff --git a/src/include/basic/shell.bas b/src/include/basic/shell.bas
index 02e0d602..f6a37b19 100644
--- a/src/include/basic/shell.bas
+++ b/src/include/basic/shell.bas
@@ -27,10 +27,11 @@
' open documents, run command lines and the Run dialog, the installed
' programs with their names and descriptions, file types (which program
' opens which kind of document), the running programs (find one, bring
-' it to the front, ask it to close, or end it), command lines split into
-' their parts, this program's priority, keys that work in every program,
-' Task Manager, system information, the desktop and the program started
-' after it, Exit DVX, this program's settings folder, and the DVX log
+' it to the front, ask it to close, or end it, and their priority),
+' command lines split into their parts, this program's priority, keys
+' that work in every program, Task Manager, system information, the
+' desktop and the program started after it, Exit DVX, this program's
+' settings folder, and the DVX log
' (DVX.LOG); and Program Manager's program groups (PROGMAN.INI) with
' their items, shortcut keys, and icons.
' Usage: Add this file to your project.
@@ -87,6 +88,13 @@ DECLARE LIBRARY "dvxshell"
' can be read.
DECLARE SUB ShellRunDos(BYVAL program AS STRING, BYVAL args AS STRING, BYVAL folder AS STRING, BYVAL pause AS LONG)
+ ' ShellRunDos, waiting for the DOS program to end, as SHELL does: this
+ ' program is not asked to close, and its events run meanwhile. The
+ ' DOS program's exit code (its ERRORLEVEL), or -1 when it did not run
+ ' (the other programs were kept open, it was not found, or it could
+ ' not start).
+ DECLARE FUNCTION ShellRunDosWait(BYVAL program AS STRING, BYVAL args AS STRING, BYVAL folder AS STRING, BYVAL pause AS LONG) AS LONG
+
' Show the Run dialog. history is the commands to offer, newest
' first, one per line (separated by CHR$(10)). Returns the command
' line the user chose, or "" when the dialog was cancelled; run it
@@ -211,6 +219,13 @@ DECLARE LIBRARY "dvxshell"
' when it was ended.
DECLARE FUNCTION ShellRunningKill(BYVAL appId AS LONG) AS LONG
+ ' A running program's priority, as ShellPriority is this program's
+ ' (-1 when it has no task of its own: some built-in programs share
+ ' DVX's); ShellRunningSetPriority sets it, as Task Manager could. True
+ ' when it was set.
+ DECLARE FUNCTION ShellRunningPriority(BYVAL appId AS LONG) AS LONG
+ DECLARE FUNCTION ShellRunningSetPriority(BYVAL appId AS LONG, BYVAL priority AS LONG) AS LONG
+
' True when a running program is not responding: it has been busy in
' one of its event procedures for 5 seconds or longer.
DECLARE FUNCTION ShellAppIsHung(BYVAL appId AS LONG) AS LONG
@@ -224,6 +239,18 @@ DECLARE LIBRARY "dvxshell"
' from the value you saw last.
DECLARE FUNCTION ShellChangeCount() AS LONG
+ ' Say that what is installed may have changed: call it after your
+ ' program puts programs, fonts, sounds, or the like in DVX's folders
+ ' (installing a package does it by itself). Program Manager then
+ ' looks for new programs when it next comes to the front.
+ DECLARE SUB ShellInstalledChanged()
+
+ ' A number that changes whenever what is installed may have changed
+ ' (a package installed or removed, a DOS program run, a file copy,
+ ' move, or delete finished, or ShellInstalledChanged): check it and
+ ' look again only when it differs from the value you saw last.
+ DECLARE FUNCTION ShellInstalledCount() AS LONG
+
' Open Task Manager (what Ctrl+Esc does).
DECLARE SUB ShellTaskManager()
diff --git a/src/include/basic/sound.bas b/src/include/basic/sound.bas
index 0b086c3c..62b46b0d 100644
--- a/src/include/basic/sound.bas
+++ b/src/include/basic/sound.bas
@@ -46,6 +46,17 @@
' Voice number returned when a sound could not be played
CONST SOUND_FAILED = 0
+' Why a voice ended (SoundEndReason): it played to its end, it was
+' stopped, or the most voices were playing and it was the quietest
+CONST SOUND_END_FINISHED = 0
+CONST SOUND_END_STOPPED = 1
+CONST SOUND_END_DROPPED = 2
+
+' The waveforms of a note queue (NotesOpen, NotesSetTone)
+CONST SOUND_WAVE_SQUARE = 0
+CONST SOUND_WAVE_TRIANGLE = 1
+CONST SOUND_WAVE_NOISE = 2
+
' DVX's event sounds, for SoundPlayEvent and the SoundEvent... functions
CONST SOUND_EVENT_DEFAULT = 0
CONST SOUND_EVENT_START = 1
@@ -121,6 +132,11 @@ CONST FM_ADDITIVE = &H01
' + register; bank 1 only on an OPL3)
CONST FM_BANK_REGS = &H100
+' A sample in the wavetable synthesizer's General MIDI ROM (WtInfo$
+' "rom"): WtStart's address is WT_ADDR_ROM plus its bytes from the ROM's
+' start
+CONST WT_ADDR_ROM = &H80000000
+
DECLARE LIBRARY "audio"
' Play a WAV or VOC file once.
DECLARE FUNCTION PlayWave(BYVAL fileName AS STRING) AS LONG
@@ -137,6 +153,14 @@ DECLARE LIBRARY "audio"
' True while the sound is still playing, else False.
DECLARE FUNCTION SoundPlaying(BYVAL voice AS LONG) AS LONG
+ ' The next of this program's voices that ended since you last asked
+ ' (each once, in the order they ended; the last 64 are kept), 0 when
+ ' none has: call it from a Timer to hear of every sound ending.
+ ' SoundEndReason says why the voice SoundEnded gave last ended
+ ' (SOUND_END_...).
+ DECLARE FUNCTION SoundEnded() AS LONG
+ DECLARE FUNCTION SoundEndReason() AS LONG
+
' Play a tone of frequency hertz (20 to 20000) for milliseconds (0: until
' StopWave) at volume percent.
DECLARE FUNCTION PlayTone(BYVAL frequency AS LONG, BYVAL milliseconds AS LONG, BYVAL volume AS LONG) AS LONG
@@ -152,6 +176,10 @@ DECLARE LIBRARY "audio"
' at 220h, 22050 Hz, 16-bit stereo", or "No sound device".
DECLARE FUNCTION SoundInfo$() AS STRING
+ ' The short name of the sound driver playing ("sb", "speaker", ...;
+ ' "" when none is), the same names SoundDriverName$ lists.
+ DECLARE FUNCTION SoundDriver$() AS STRING
+
' Why the last sound could not be played.
DECLARE FUNCTION SoundError$() AS STRING
@@ -169,16 +197,29 @@ DECLARE LIBRARY "audio"
' 2 ^ RECORD_... set for each) -- numbers.
DECLARE FUNCTION SoundCaps(BYVAL field AS STRING) AS LONG
+ ' What the mixer has done since the device started, by name (any
+ ' case): "renders" (blocks of sound made), "frames" (samples made),
+ ' "underruns" (stream samples that were due but not yet written),
+ ' "clipped" (samples too loud), "dropped" (sounds the voice limit
+ ' dropped or refused), "playing" (voices playing now).
+ DECLARE FUNCTION SoundStats(BYVAL field AS STRING) AS LONG
+
' Play one of DVX's event sounds (SOUND_EVENT_...), the one the
- ' settings choose; with wait True, return once it has played.
+ ' settings choose; with wait True, return once it has played (the
+ ' program's events run meanwhile, as in DoEvents).
DECLARE SUB SoundPlayEvent(BYVAL eventIdx AS LONG, BYVAL wait AS LONG)
' ---- Loaded sounds ----
' Load a WAV or VOC file once, to play it as often as you like.
- ' Returns a sound number, or 0 (SoundError$ says why).
+ ' Returns a sound number, or 0 (SoundError$ says why). The whole file
+ ' is read first: play a long recording with MusicLoad (music.bas).
DECLARE FUNCTION SoundLoad(BYVAL fileName AS STRING) AS LONG
+ ' The same from the bytes of a WAV or VOC file, such as ResGetData$
+ ' or HttpBody$ gives.
+ DECLARE FUNCTION SoundLoadData(BYVAL bytes AS BINARY STRING) AS LONG
+
' Make a sound from raw sound data: 8-bit (0 to 255, 128 is silence)
' or 16-bit (two bytes each, low byte first) samples, mono or stereo
' (left, right, left, ...), at rate hertz. Returns a sound number,
@@ -188,8 +229,12 @@ DECLARE LIBRARY "audio"
' Stop a sound's voices and forget it.
DECLARE SUB SoundFree(BYVAL snd AS LONG)
- ' How long the sound is (milliseconds).
+ ' How long the sound is (milliseconds), and how it was recorded: its
+ ' rate (hertz), bits (8 or 16), and channels (1 or 2).
DECLARE FUNCTION SoundLength(BYVAL snd AS LONG) AS LONG
+ DECLARE FUNCTION SoundRate(BYVAL snd AS LONG) AS LONG
+ DECLARE FUNCTION SoundBits(BYVAL snd AS LONG) AS LONG
+ DECLARE FUNCTION SoundChannels(BYVAL snd AS LONG) AS LONG
' Play a sound on a new voice at volume percent and pan
' (SOUND_PAN_LEFT to SOUND_PAN_RIGHT), once or over and over.
@@ -228,6 +273,11 @@ DECLARE LIBRARY "audio"
' SoundPause, and SoundResume.
DECLARE FUNCTION SoundStreamVoice(BYVAL stream AS LONG) AS LONG
+ ' You will write no more: the stream's voice ends once it has played
+ ' what it holds (SoundEnded tells, as for a sound), with no gap
+ ' counted. Close it afterwards with SoundStreamClose.
+ DECLARE SUB SoundStreamEnd(BYVAL stream AS LONG)
+
' Drop what the stream holds and has not played yet (to jump to
' another place in what you play: what you write next is heard at
' once).
@@ -236,23 +286,60 @@ DECLARE LIBRARY "audio"
' Stop the stream and close it.
DECLARE SUB SoundStreamClose(BYVAL stream AS LONG)
+ ' ---- Note queues: melodies timed exactly ----
+
+ ' Open a queue of tones in a waveform (SOUND_WAVE_...) at volume
+ ' percent. Tones and rests added to it play one after the other,
+ ' each lasting exactly as long as asked, as long as the next is added
+ ' before the one playing ends. Returns a queue number, or 0.
+ DECLARE FUNCTION NotesOpen(BYVAL wave AS LONG, BYVAL volume AS LONG) AS LONG
+
+ ' Add a tone of hertz (20 to 20000; 0 for a rest) lasting
+ ' milliseconds; fractions count. True when it was added, False when
+ ' 256 are waiting (or the hertz are out of range).
+ DECLARE FUNCTION NotesAdd(BYVAL queue AS LONG, BYVAL hertz AS DOUBLE, BYVAL milliseconds AS DOUBLE) AS LONG
+
+ ' The waveform and volume (percent of the queue's) of the tones added
+ ' from now on. True when it worked.
+ DECLARE FUNCTION NotesSetTone(BYVAL queue AS LONG, BYVAL wave AS LONG, BYVAL volume AS LONG) AS LONG
+
+ ' Tones and rests not yet finished (0 once the queue has gone quiet),
+ ' and how many more can be added now.
+ DECLARE FUNCTION NotesQueued(BYVAL queue AS LONG) AS LONG
+ DECLARE FUNCTION NotesSpace(BYVAL queue AS LONG) AS LONG
+
+ ' The queue's voice, for SoundSetVolume and SoundSetPan.
+ DECLARE FUNCTION NotesVoice(BYVAL queue AS LONG) AS LONG
+
+ ' Silence the queue and forget what it holds; close it.
+ DECLARE SUB NotesClear(BYVAL queue AS LONG)
+ DECLARE SUB NotesClose(BYVAL queue AS LONG)
+
' ---- Recording ----
'
' Sound from the card's microphone, line, or CD input into a WAV
- ' file. Most sound cards record or play, not both, so while a
- ' program records nothing plays, and one program records at a time.
+ ' file, or into memory for the program to take as it comes. Most
+ ' sound cards record or play, not both, so while a program records
+ ' nothing plays, and one program records at a time.
' SoundCaps("record") says whether the card can.
' Start recording from input (RECORD_MIC, RECORD_LINE, RECORD_CD)
- ' into the WAV file fileName (made, or replaced) at rate, bits (8 or
- ' 16), and channels (1 or 2); 0 for 22,050 Hz, 16-bit, mono. The
- ' card records at its nearest: RecordRate, RecordBits, and
- ' RecordChannels say what it chose. True when it started; False with
- ' SoundError$ saying why.
+ ' into the WAV file fileName (made, or replaced) -- or, with fileName
+ ' "", into memory for RecordRead$ -- at rate, bits (8 or 16), and
+ ' channels (1 or 2); 0 for 22,050 Hz, 16-bit, mono. The card records
+ ' at its nearest: RecordRate, RecordBits, and RecordChannels say what
+ ' it chose. True when it started; False with SoundError$ saying why.
DECLARE FUNCTION RecordStart(BYVAL fileName AS STRING, BYVAL rate AS LONG, BYVAL bits AS LONG, BYVAL channels AS LONG, BYVAL inputSource AS LONG) AS LONG
- ' Stop recording: the file is complete. True when it was all
- ' written.
+ ' Recording into memory: up to maxBytes of what was recorded and not
+ ' yet taken (whole samples, in the recording's format, as SoundCreate
+ ' and SoundStreamWrite take them), "" when nothing is waiting; and how
+ ' many bytes are waiting. Take them often: about two seconds are
+ ' kept, and what does not fit is lost (RecordLost).
+ DECLARE FUNCTION RecordRead$(BYVAL maxBytes AS LONG) AS BINARY STRING
+ DECLARE FUNCTION RecordQueued() AS LONG
+
+ ' Stop recording: a file is complete. True when it was all written.
DECLARE FUNCTION RecordStop() AS LONG
' True while the program records.
@@ -295,7 +382,8 @@ DECLARE LIBRARY "audio"
' The synthesizer by name (any case): "description" (like "Gravis
' UltraSound wavetable at 220h (26 voices, 1019 KB)", "" when there is
' none), "voices", "free" (voices no program has), "memory" (bytes of
- ' sample memory), "freememory" (the largest free block), and what it
+ ' sample memory), "freememory" (the largest free block), "freetotal"
+ ' (the bytes free in all, in blocks maybe apart), and what it
' does itself: "envelopes", "lfos", "filter", "effects", "8bit",
' "rom" -- numbers and True / False as text.
DECLARE FUNCTION WtInfo$(BYVAL field AS STRING) AS STRING
@@ -309,12 +397,13 @@ DECLARE LIBRARY "audio"
DECLARE SUB WtMemFree(BYVAL address AS LONG)
' Take a voice (0 when none is free) and start it on a sample in the
- ' memory: from address, endFrame samples long, playing from startFrame
- ' (0: its first), its loop from loopStart up to loopEnd (loopMode 0
- ' plays once, 1 loops, 3 loops until released), stored at bits; pitch in cents (0 plays 44,100 samples a
- ' second, 1200 an octave higher), attenuation in centibels (0 loudest,
- ' 60 half as loud, 1440 silent), pan -100 (left) to 100 (right). True
- ' when it started. WtSetPitch, WtSetAttenuation and WtSetPan change
+ ' memory (or, from WT_ADDR_ROM on, in the ROM): from address, endFrame
+ ' samples long, playing from startFrame (0: its first), its loop from
+ ' loopStart up to loopEnd (loopMode 0 plays once, 1 loops, 3 loops
+ ' until released), stored at bits; pitch in cents (0 plays 44,100
+ ' samples a second, 1200 an octave higher), attenuation in centibels
+ ' (0 loudest, 60 half as loud, 1440 silent), pan -100 (left) to 100
+ ' (right), shaped as WtShape set. True when it started. WtSetPitch, WtSetAttenuation and WtSetPan change
' it, WtRelease lets a note looping until released play out, WtStop
' silences it, WtActive is True while it sounds, and WtFree gives the
' voice back.
@@ -328,6 +417,28 @@ DECLARE LIBRARY "audio"
DECLARE FUNCTION WtActive(BYVAL voice AS LONG) AS LONG
DECLARE SUB WtFree(BYVAL voice AS LONG)
+ ' Shape a voice's notes, one value by name (any case), as a SoundFont
+ ' instrument does: kept for the voice's next WtStart, and changed at
+ ' once on a sounding note where the chip can (the filter, the
+ ' effects, the LFO depths). Used where the chip has them (WtInfo$
+ ' "envelopes", "lfos", "filter", "effects"):
+ ' "voldelay", "volattack", "volhold", "voldecay", "volrelease": the
+ ' volume envelope's times, in timecents (1200 times the base-2
+ ' logarithm of seconds; -12000 for none), "volsustain" its sustain
+ ' (centibels below the peak); "moddelay" to "modrelease" the
+ ' modulation envelope's (its sustain in tenths of a percent below
+ ' the peak), "modenvpitch" and "modenvfilter" how far it moves the
+ ' pitch and the filter (cents); "modlfodelay" (timecents),
+ ' "modlfofreq" (absolute cents: 8.176 Hz times 2 ^ (cents / 1200)),
+ ' "modlfopitch", "modlfofilter" (cents), and "modlfovolume"
+ ' (centibels): the modulation LFO; "viblfodelay", "viblfofreq", and
+ ' "viblfopitch": the vibrato LFO; "filterfc" (absolute cents, 13500
+ ' open) and "filterq" (centibels): the low-pass filter; "reverb" and
+ ' "chorus" (tenths of a percent). A new voice's notes are plain:
+ ' no envelope or LFO, the filter open, no effects. True when it
+ ' worked.
+ DECLARE FUNCTION WtShape(BYVAL voice AS LONG, BYVAL field AS STRING, BYVAL value AS LONG) AS LONG
+
' ---- DVX's own sounds and sound settings ----
' How many events DVX plays a sound for (numbered 0 to
diff --git a/src/include/basic/sql.bas b/src/include/basic/sql.bas
index 697338ca..a4680de6 100644
--- a/src/include/basic/sql.bas
+++ b/src/include/basic/sql.bas
@@ -32,7 +32,9 @@
'
' Usage: Add this file to your project.
-DECLARE LIBRARY "basrt"
+' The library named is the one these need (a program that includes this
+' file needs the Databases part); BASIC finds the functions by name.
+DECLARE LIBRARY "dvxsql"
' Open a database file. Returns a handle (>0), or 0 on failure.
DECLARE FUNCTION SQLOpen(BYVAL path AS STRING) AS LONG
@@ -77,4 +79,7 @@ DECLARE LIBRARY "basrt"
' Return the number of rows affected by the last INSERT/UPDATE/DELETE.
DECLARE FUNCTION SQLAffected(BYVAL db AS LONG) AS LONG
+
+ ' Return text with every single quote doubled, to put between quotes.
+ DECLARE FUNCTION SQLEscape$(BYVAL text AS STRING) AS STRING
END DECLARE
diff --git a/src/include/basic/system.bas b/src/include/basic/system.bas
index ec2bc2c8..e97d7323 100644
--- a/src/include/basic/system.bas
+++ b/src/include/basic/system.bas
@@ -255,7 +255,8 @@ DECLARE LIBRARY "dvxshell"
' ---- Libraries ----
' True when the library DECLARE LIBRARY names as libName ("pkgnet",
- ' "dvxsql" ...; any case) is installed and was started with DVX.
+ ' "dvxsql" ...; any case) is installed. DVX loads a library when it is
+ ' first needed, so asking loads it.
' Some parts of DVX are optional (Setup's "What to install" page, and
' Software): a program using one of their libraries asks first, and
' leaves out what needs it, since calling a function of a library that
diff --git a/src/include/basic/xfer.bas b/src/include/basic/xfer.bas
index 75062300..5e1674c4 100644
--- a/src/include/basic/xfer.bas
+++ b/src/include/basic/xfer.bas
@@ -76,11 +76,25 @@ DECLARE LIBRARY "xfer"
' or 0 when the file cannot be opened or the port number is wrong.
DECLARE FUNCTION XferSendFile(BYVAL com AS LONG, BYVAL proto AS LONG, BYVAL path AS STRING) AS LONG
+ ' Send several files as one batch: files has one path per line
+ ' (separate them with CHR$(10); up to XFER_MAX_FILES; XMODEM sends
+ ' only the first). escapeCtl <> 0 escapes every control character
+ ' (ZMODEM, for links that eat them); resumeFiles <> 0 asks a ZMODEM
+ ' receiver to continue files it already has part of.
+ DECLARE FUNCTION XferSendFiles(BYVAL com AS LONG, BYVAL proto AS LONG, BYVAL files AS STRING, BYVAL escapeCtl AS LONG, BYVAL resumeFiles AS LONG) AS LONG
+
' Receive. XMODEM: target is the file to create. YMODEM and ZMODEM:
- ' target is the folder the files go into (the sender names them).
- ' Returns a handle, or 0 when the transfer cannot start.
+ ' target is the folder the files go into (the sender names them). A
+ ' file that is already there is kept and the new one stored under
+ ' another name. Returns a handle, or 0 when the transfer cannot start.
DECLARE FUNCTION XferReceive(BYVAL com AS LONG, BYVAL proto AS LONG, BYVAL target AS STRING) AS LONG
+ ' Receive as XferReceive does, choosing what happens to a file that
+ ' is already there: exists is an XFER_EXIST_ action (XFER_EXIST_ASK
+ ' waits in XFER_ASKING for XferAnswer). escapeCtl <> 0 asks a ZMODEM
+ ' sender to escape every control character.
+ DECLARE FUNCTION XferReceiveFiles(BYVAL com AS LONG, BYVAL proto AS LONG, BYVAL target AS STRING, BYVAL exists AS LONG, BYVAL escapeCtl AS LONG) AS LONG
+
' Move the transfer along. Returns XFER_RUNNING until it ends.
DECLARE FUNCTION XferPoll(BYVAL handle AS LONG) AS LONG
diff --git a/src/installer/Makefile b/src/installer/Makefile
index 3d75978b..104b8138 100644
--- a/src/installer/Makefile
+++ b/src/installer/Makefile
@@ -47,7 +47,7 @@ KPUNCH = ../libs/kpunch
WIDGETS = ../widgets/kpunch
PKGLIB = $(KPUNCH)/pkg
MINIZ = $(PKGLIB)/thirdparty/miniz
-MONO = $(PKGLIB)/thirdparty/monocypher/src
+MONO = $(KPUNCH)/crypto/thirdparty/monocypher/src
LICENSE = ../../LICENSE.txt
OBJDIR = ../../obj/installer
@@ -79,7 +79,7 @@ LDFLAGS = -s -Wl,--gc-sections -lm
# Setup's own sources; the core, fileops, and the text and list helpers
# as they are
-SETUP_SRCS = setup.c setupCore.c pkgRead.c $(PKGLIB)/pkgDb.c $(PKGLIB)/pkgDpk.c $(PKGLIB)/pkgFormat.c $(PKGLIB)/pkgIni.c $(PKGLIB)/pkgManifest.c $(PKGLIB)/pkgOp.c $(PKGLIB)/pkgPlan.c $(PKGLIB)/pkgSign.c $(PKGLIB)/pkgTrust.c $(PKGLIB)/pkgZip.c $(MINIZ)/miniz.c $(MONO)/monocypher.c $(MONO)/optional/monocypher-ed25519.c $(KPUNCH)/serial/security/secHash.c $(KPUNCH)/serial/security/security.c
+SETUP_SRCS = setup.c setupCore.c pkgRead.c $(PKGLIB)/pkgDb.c $(PKGLIB)/pkgDpk.c $(PKGLIB)/pkgFormat.c $(PKGLIB)/pkgIni.c $(PKGLIB)/pkgManifest.c $(PKGLIB)/pkgOp.c $(PKGLIB)/pkgPlan.c $(PKGLIB)/pkgSign.c $(PKGLIB)/pkgTrust.c $(PKGLIB)/pkgZip.c $(MINIZ)/miniz.c $(MONO)/monocypher.c $(MONO)/optional/monocypher-ed25519.c $(KPUNCH)/crypto/secHash.c $(KPUNCH)/crypto/secSign.c $(KPUNCH)/crypto/security.c
CORE_SRCS = $(wildcard $(LIBDVX)/*.c) $(LIBDVX)/platform/dvxPlatformDos.c $(LIBDVX)/platform/dvxPlatformIrq.c $(LIBDVX)/platform/dvxPlatformUtil.c $(LIBDVX)/platform/dvxMemTrack.c $(LIBDVX)/thirdparty/stb_ds_impl.c ../tools/dvxResWrite.c $(wildcard $(KPUNCH)/fileops/*.c) $(KPUNCH)/texthelp/textHelp.c $(KPUNCH)/listhelp/listHelp.c
# The widgets Setup uses: /: (wgtRegister -> wgtRegister);
@@ -93,11 +93,11 @@ WGT_OBJS = $(foreach w,$(SETUP_WGTS),$(DOSOBJ)/wgt/$(notdir $(firstword $(subs
CORE_LIB = $(DOSOBJ)/setupCore.a
# The package tool (host)
-TOOL_SRCS = pkgTool.c pkgBuild.c $(PKGLIB)/pkgFormat.c $(PKGLIB)/pkgCli.c $(PKGLIB)/pkgDb.c $(PKGLIB)/pkgDpk.c $(PKGLIB)/pkgIni.c $(PKGLIB)/pkgManifest.c $(PKGLIB)/pkgOp.c $(PKGLIB)/pkgPlan.c $(PKGLIB)/pkgSign.c $(PKGLIB)/pkgTrust.c $(PKGLIB)/pkgZip.c $(PKGLIB)/pkgZipWrite.c $(MINIZ)/miniz.c $(MONO)/monocypher.c $(MONO)/optional/monocypher-ed25519.c $(KPUNCH)/serial/security/secHash.c $(KPUNCH)/serial/security/security.c $(LIBDVX)/platform/dvxPlatformUtil.c $(LIBDVX)/thirdparty/stb_ds_impl.c
+TOOL_SRCS = pkgTool.c pkgBuild.c $(PKGLIB)/pkgFormat.c $(PKGLIB)/pkgCli.c $(PKGLIB)/pkgDb.c $(PKGLIB)/pkgDpk.c $(PKGLIB)/pkgIni.c $(PKGLIB)/pkgManifest.c $(PKGLIB)/pkgOp.c $(PKGLIB)/pkgPlan.c $(PKGLIB)/pkgSign.c $(PKGLIB)/pkgSignMake.c $(PKGLIB)/pkgTrust.c $(PKGLIB)/pkgZip.c $(PKGLIB)/pkgZipWrite.c $(MINIZ)/miniz.c $(MONO)/monocypher.c $(MONO)/optional/monocypher-ed25519.c $(KPUNCH)/crypto/secHash.c $(KPUNCH)/crypto/secSign.c $(KPUNCH)/crypto/security.c $(LIBDVX)/platform/dvxPlatformUtil.c $(LIBDVX)/thirdparty/stb_ds_impl.c
TOOL_OBJS = $(patsubst %.c,$(HOSTOBJ)/%.o,$(notdir $(TOOL_SRCS)))
TOOL_FLAGS = $(HOST_TOOL_CFLAGS) -I. -I$(PKGLIB) -I$(MINIZ) -I$(MONO) -I$(KPUNCH) -I$(LIBDVX) -I$(LIBDVX)/platform -I$(LIBDVX)/thirdparty
-vpath %.c . $(PKGLIB) $(MINIZ) $(MONO) $(MONO)/optional $(KPUNCH)/serial/security $(LIBDVX)/platform $(LIBDVX)/thirdparty
+vpath %.c . $(PKGLIB) $(MINIZ) $(MONO) $(MONO)/optional $(KPUNCH)/crypto $(LIBDVX)/platform $(LIBDVX)/thirdparty
.PHONY: all clean
diff --git a/src/installer/pkgBuild.c b/src/installer/pkgBuild.c
index 7e310f02..e27f55d4 100644
--- a/src/installer/pkgBuild.c
+++ b/src/installer/pkgBuild.c
@@ -37,8 +37,12 @@
// Names found under the package folder, relative, '/'-separated; folders
// end with '/'
+// The names a folder's tree gives a ZIP, and where each is on disk
+// (relative to the folder: a folder only case apart from one listed
+// before goes under that one's name, see scanFolder)
typedef struct {
char **names;
+ char **paths;
int32_t count;
int32_t cap;
} NameListT;
@@ -46,6 +50,7 @@ typedef struct {
// A name and the tier the rules put it in, for the ZIP's order
typedef struct {
const char *name;
+ const char *path;
int32_t tier;
} TieredNameT;
@@ -53,18 +58,31 @@ typedef struct {
// Prototypes
// ============================================================
-static int byTierName(const void *a, const void *b);
-static int32_t entryTier(const mz_zip_archive_file_stat *st);
-static bool listAdd(NameListT *list, const char *name);
-static void listFree(NameListT *list);
-static bool loadFile(const char *path, uint8_t **data, uint32_t *size);
-bool pkgBuildAppended(const char *exePath, const char *zipPath, const char *outPath, char *err, int32_t errSize);
-bool pkgBuildInfo(const char *zipPath, PkgHeaderT *info, char *err, int32_t errSize);
-int32_t pkgBuildSplit(const char *zipPath, const char *outDir, uint32_t firstRoom, uint32_t diskRoom, char *err, int32_t errSize);
-bool pkgBuildZip(const char *folder, const char *zipPath, const PkgTierRuleT *rules, int32_t ruleCount, char *err, int32_t errSize);
-static bool scanFolder(const char *root, const char *rel, NameListT *list, char *err, int32_t errSize);
-static int32_t tierOf(const char *name, const PkgTierRuleT *rules, int32_t ruleCount);
-static bool writePart(const char *path, const uint8_t *data, uint32_t len, const PkgHeaderT *h, const uint8_t *prefix, uint32_t prefixLen);
+static int byDosName(const void *a, const void *b);
+static int byTierName(const void *a, const void *b);
+static int32_t entryTier(const mz_zip_archive_file_stat *st);
+static bool listAdd(NameListT *list, const char *name, const char *path);
+static const char *listFindCase(const NameListT *list, const char *name);
+static void listFree(NameListT *list);
+static bool loadFile(const char *path, uint8_t **data, uint32_t *size);
+bool pkgBuildAppended(const char *exePath, const char *zipPath, const char *outPath, char *err, int32_t errSize);
+bool pkgBuildInfo(const char *zipPath, PkgHeaderT *info, char *err, int32_t errSize);
+int32_t pkgBuildSplit(const char *zipPath, const char *outDir, uint32_t firstRoom, uint32_t diskRoom, char *err, int32_t errSize);
+bool pkgBuildZip(const char *folder, const char *zipPath, const PkgTierRuleT *rules, int32_t ruleCount, char *err, int32_t errSize);
+static bool scanFolder(const char *root, const char *rel, const char *diskRel, NameListT *list, char *err, int32_t errSize);
+static int32_t tierOf(const char *name, const PkgTierRuleT *rules, int32_t ruleCount);
+static bool writePart(const char *path, const uint8_t *data, uint32_t len, const PkgHeaderT *h, const uint8_t *prefix, uint32_t prefixLen);
+
+
+// Names as DOS compares them (case ignored), a lowercase spelling before
+// an uppercase one (scanFolder: the release tree's own spelling wins).
+static int byDosName(const void *a, const void *b) {
+ const char *x = *(const char *const *)a;
+ const char *y = *(const char *const *)b;
+ int c = strcasecmp(x, y);
+
+ return c ? c : -strcmp(x, y);
+}
static int byTierName(const void *a, const void *b) {
@@ -83,7 +101,7 @@ static int32_t entryTier(const mz_zip_archive_file_stat *st) {
}
-static bool listAdd(NameListT *list, const char *name) {
+static bool listAdd(NameListT *list, const char *name, const char *path) {
if (list->count == list->cap) {
int32_t cap = list->cap + LIST_GROW;
char **names = (char **)realloc(list->names, (size_t)cap * sizeof(char *));
@@ -93,12 +111,23 @@ static bool listAdd(NameListT *list, const char *name) {
}
list->names = names;
+
+ char **paths = (char **)realloc(list->paths, (size_t)cap * sizeof(char *));
+
+ if (!paths) {
+ return false;
+ }
+
+ list->paths = paths;
list->cap = cap;
}
list->names[list->count] = strdup(name);
+ list->paths[list->count] = strdup(path);
- if (!list->names[list->count]) {
+ if (!list->names[list->count] || !list->paths[list->count]) {
+ free(list->names[list->count]);
+ free(list->paths[list->count]);
return false;
}
@@ -107,12 +136,26 @@ static bool listAdd(NameListT *list, const char *name) {
}
+// The name listed that is name but for case (DOS's same name), or NULL.
+static const char *listFindCase(const NameListT *list, const char *name) {
+ for (int32_t i = 0; i < list->count; i++) {
+ if (strcasecmp(list->names[i], name) == 0) {
+ return list->names[i];
+ }
+ }
+
+ return NULL;
+}
+
+
static void listFree(NameListT *list) {
for (int32_t i = 0; i < list->count; i++) {
free(list->names[i]);
+ free(list->paths[i]);
}
free(list->names);
+ free(list->paths);
memset(list, 0, sizeof(*list));
}
@@ -330,7 +373,7 @@ bool pkgBuildZip(const char *folder, const char *zipPath, const PkgTierRuleT *ru
memset(&list, 0, sizeof(list));
memset(&zip, 0, sizeof(zip));
- if (!scanFolder(folder, "", &list, err, errSize)) {
+ if (!scanFolder(folder, "", "", &list, err, errSize)) {
listFree(&list);
return false;
}
@@ -347,6 +390,7 @@ bool pkgBuildZip(const char *folder, const char *zipPath, const PkgTierRuleT *ru
for (int32_t i = 0; i < list.count; i++) {
order[i].name = list.names[i];
+ order[i].path = list.paths[i];
order[i].tier = tierOf(list.names[i], rules, ruleCount);
}
@@ -368,7 +412,7 @@ bool pkgBuildZip(const char *folder, const char *zipPath, const PkgTierRuleT *ru
} else {
char full[PATH_MAX_LEN];
- snprintf(full, sizeof(full), "%s/%s", folder, name);
+ snprintf(full, sizeof(full), "%s/%s", folder, order[i].path);
ok = mz_zip_writer_add_file(&zip, name, full, &comment, commentLen, MZ_UBER_COMPRESSION | MZ_ZIP_FLAG_WRITE_HEADER_SET_SIZE | MZ_ZIP_FLAG_ASCII_FILENAME);
}
@@ -394,11 +438,17 @@ bool pkgBuildZip(const char *folder, const char *zipPath, const PkgTierRuleT *ru
}
-// Add every file and folder under root/rel (rel "" or ending in '/').
-static bool scanFolder(const char *root, const char *rel, NameListT *list, char *err, int32_t errSize) {
+// Add every file and folder under root/diskRel, named rel in the ZIP (both
+// "" or ending in '/'), in DOS name order (byDosName). A folder only case
+// apart from one listed already is that folder on DOS (SYSTEM beside
+// system: the package tool writes DVX's own spelling into a tree copied
+// from Linux): what it holds goes under the spelling listed (the
+// lowercase one when there is one). Two files only case apart are
+// refused, as DOS cannot hold both.
+static bool scanFolder(const char *root, const char *rel, const char *diskRel, NameListT *list, char *err, int32_t errSize) {
char dirPath[PATH_MAX_LEN];
- snprintf(dirPath, sizeof(dirPath), "%s/%s", root, rel);
+ snprintf(dirPath, sizeof(dirPath), "%s/%s", root, diskRel);
DIR *d = opendir(dirPath);
@@ -407,40 +457,63 @@ static bool scanFolder(const char *root, const char *rel, NameListT *list, char
return false;
}
+ NameListT entries;
bool ok = true;
struct dirent *e;
+ memset(&entries, 0, sizeof(entries));
+
while (ok && (e = readdir(d)) != NULL) {
+ if (strcmp(e->d_name, ".") != 0 && strcmp(e->d_name, "..") != 0) {
+ ok = listAdd(&entries, e->d_name, e->d_name);
+ }
+ }
+
+ closedir(d);
+
+ if (!ok) {
+ snprintf(err, errSize, "out of memory reading %s", dirPath);
+ } else if (entries.count > 0) {
+ qsort(entries.names, (size_t)entries.count, sizeof(char *), byDosName);
+ }
+
+ for (int32_t i = 0; ok && i < entries.count; i++) {
+ const char *leaf = entries.names[i];
char name[PKG_NAME_MAX];
+ char path[PATH_MAX_LEN];
char full[PATH_MAX_LEN];
struct stat st;
- if (strcmp(e->d_name, ".") == 0 || strcmp(e->d_name, "..") == 0) {
- continue;
- }
-
- if (snprintf(name, sizeof(name), "%s%s", rel, e->d_name) >= (int)sizeof(name) || !pkgNameIsValid(name)) {
- snprintf(err, errSize, "%s%s is not a DOS 8.3 name", rel, e->d_name);
+ if (snprintf(name, sizeof(name), "%s%s", rel, leaf) >= (int)sizeof(name) || !pkgNameIsValid(name)) {
+ snprintf(err, errSize, "%s%s is not a DOS 8.3 name", rel, leaf);
ok = false;
break;
}
- snprintf(full, sizeof(full), "%s/%s", root, name);
+ snprintf(path, sizeof(path), "%s%s", diskRel, leaf);
+ snprintf(full, sizeof(full), "%s/%s", root, path);
if (stat(full, &st) != 0) {
snprintf(err, errSize, "cannot read %s", full);
ok = false;
} else if (S_ISDIR(st.st_mode)) {
- char sub[PKG_NAME_MAX];
+ char sub[PKG_NAME_MAX];
+ char diskSub[PATH_MAX_LEN];
+ const char *same;
snprintf(sub, sizeof(sub), "%s%c", name, PKG_NAME_SEP);
- ok = listAdd(list, sub) && scanFolder(root, sub, list, err, errSize);
+ snprintf(diskSub, sizeof(diskSub), "%s%c", path, PKG_NAME_SEP);
+ same = listFindCase(list, sub);
+ ok = (same || listAdd(list, sub, diskSub)) && scanFolder(root, same ? same : sub, diskSub, list, err, errSize);
+ } else if (S_ISREG(st.st_mode) && listFindCase(list, name)) {
+ snprintf(err, errSize, "%s and %s differ only in case: DOS cannot hold both", listFindCase(list, name), path);
+ ok = false;
} else if (S_ISREG(st.st_mode)) {
- ok = listAdd(list, name);
+ ok = listAdd(list, name, path);
}
}
- closedir(d);
+ listFree(&entries);
return ok;
}
diff --git a/src/installer/setup.c b/src/installer/setup.c
index 9814436c..b95ebacb 100644
--- a/src/installer/setup.c
+++ b/src/installer/setup.c
@@ -1437,23 +1437,31 @@ static void onTile(WidgetT *w) {
// Step op (status says what it does) in slices like the unpacking, until
-// it is done or has failed, or Cancel stops it (PkgOpRunningE).
+// it is done or has failed. Cancel stops it (PkgOpRunningE): at once
+// while it unpacks; once its files are in, after the rest it must do,
+// still a slice at a time (then it is done).
static PkgOpStateE opRun(PkgOpT *op, const char *status) {
- SliceT slice = { platformClock(), platformClock() };
+ SliceT slice = { platformClock(), platformClock() };
+ bool stopAsked = false;
PkgOpStateE state;
wgtSetText(sSetup.status, status);
- for (state = pkgOpStep(op); state == PkgOpRunningE && !sSetup.cancelRequested; state = pkgOpStep(op)) {
+ for (state = pkgOpStep(op); state == PkgOpRunningE; state = pkgOpStep(op)) {
uint32_t done;
uint32_t total;
+ if (sSetup.cancelRequested && !stopAsked) {
+ pkgOpStop(op);
+ stopAsked = true;
+ }
+
pkgOpProgress(op, &done, &total);
wgtProgressBarSetValue(sSetup.bar, total ? (int32_t)((uint64_t)done * SETUP_PERCENT / total) : SETUP_PERCENT);
sliceIdle(&slice);
}
- return state;
+ return (stopAsked && state == PkgOpFailedE) ? PkgOpRunningE : state;
}
diff --git a/src/installer/setupCore.c b/src/installer/setupCore.c
index 9426bfeb..3355c68e 100644
--- a/src/installer/setupCore.c
+++ b/src/installer/setupCore.c
@@ -420,7 +420,9 @@ bool setupAddonOp(PkgDbT *db, const PkgKeyringT *ring, const SetupAddonT *addon,
*op = NULL;
if (chosen) {
- *op = pkgOpInstall(db, addon->offer.where, ring, true);
+ // Trusted as the installed DVX's keys say: it was found before
+ // they were in
+ *op = pkgOpInstallFile(db, addon->offer.where, ring, true);
return true;
}
diff --git a/src/libs/kpunch/audio/README.md b/src/libs/kpunch/audio/README.md
new file mode 100644
index 00000000..010fc9a6
--- /dev/null
+++ b/src/libs/kpunch/audio/README.md
@@ -0,0 +1,39 @@
+# audio -- Sound
+
+Sound for DVX programs on whatever sound hardware the computer has:
+samples from WAV and VOC files or memory, tones, streams, note queues,
+wavetable voices, recording, and MIDI input, all through one mixer
+shared by every program; what a program opened is stopped and released
+when it ends. BASIC reaches it through SOUND / BEEP and
+src/include/basic/sound.bas.
+
+## Documentation
+
+Full reference is in `audio.dhs` (this directory), compiled into the DVX
+System Reference. Open via the Help Viewer app, or read
+`docs/dvx_system_reference.html` after `make`.
+
+## Source Files
+
+- `audio.h`, `audioInt.h`, `audioDrv.h` -- public, internal, and driver
+ API
+- `audio.c` -- contexts, voices, samples, streams, and the sound device
+- `audioMix.c` -- the mixer: everything audio.lib runs at interrupt time
+- `audioLoad.c` -- WAV and VOC loading
+- `audioSynth.c` -- FM voices shared by every program, the whole FM chip
+ for one, the MIDI port, tones on FM
+- `audioWt.c` -- the wavetable synthesizer: voices and sample memory
+ shared by every program
+- `audioRec.c` -- recording
+- `audioMidiIn.c` -- MIDI input
+- `audioSet.c` -- the [audio] settings in DVX.INI
+- `audioDesk.c` -- desktop sounds: what DVX plays for its own events
+- `audioShell.c` -- audio.lib's hooks into the shell
+- `audioBas.c` -- BASIC entry points (sound.bas)
+- `drivers/` -- the sound card driver modules (sb, wss, gus, guswt, awe,
+ opl, mpu401, lpt, speaker, null)
+- `codec/`, `gus/`, `isa/`, `math/` -- pieces the drivers share
+
+## Build
+
+`make -C src/libs/kpunch/audio` (invoked by the top-level `make`).
diff --git a/src/libs/kpunch/audio/audio.c b/src/libs/kpunch/audio/audio.c
index ff6ca260..90065d03 100644
--- a/src/libs/kpunch/audio/audio.c
+++ b/src/libs/kpunch/audio/audio.c
@@ -34,11 +34,16 @@
// The device is opened when the first context opens and closed with the
// last one. In between, its output is closed after IDLE_CLOSE_US without
// a voice (a desktop with sound effects would otherwise mix silence all
-// the time) and opened again, with the same driver, when a voice starts. Drivers are the driver modules in AUDIO_DRV_DIR (dvxDrv.h);
-// they are loaded, sorted by priority and tried in turn (DVX.INI [audio]
-// driver first), and all but the one that starts are unloaded again. Synth
+// the time) and opened again, with the same driver, when a voice starts.
+// Drivers are the driver modules in AUDIO_DRV_DIR (dvxDrv.h); they are
+// loaded, sorted by priority and tried in turn (DVX.INI [audio] driver
+// first), and all but the one that starts are unloaded again. Synth
// drivers (an FM chip, a MIDI port) come from the same folder: the first
-// of each kind that finds its hardware is kept beside the device.
+// of each kind that finds its hardware is kept beside the device. The
+// modules of a device that closed stay loaded (sParked): the next device
+// tries them first while DVX.INI makes the same driver choice, instead of
+// loading and probing every driver again. The drivers' names are read
+// once.
#include "audioInt.h"
#include "stb_ds.h"
@@ -68,6 +73,13 @@ DVX_ABI_STAMP(audio);
// Frames a pull stream is asked for at a time
#define STREAM_FILL_FRAMES 1024
+// A pump fills pull streams for STREAM_FILL_BUDGET_US at most once each
+// holds STREAM_SAFE_MS of sound (the next pump fills the rest): a burst of
+// a stream's whole ring after a start, a seek or a stall would take the
+// CPU for a few hundred milliseconds
+#define STREAM_FILL_BUDGET_US 15000
+#define STREAM_SAFE_MS 100
+
#define MS_PER_SEC 1000
#define BITS_PER_BYTE 8
#define BITS_8 8
@@ -99,6 +111,8 @@ struct AudioNotesT {
uint64_t usQueued; // time queued since the queue last went quiet ...
uint64_t framesQueued; // ... and the frames it was rounded to
int32_t voice;
+ int32_t level; // the next entries' share of the volume (audioNotesSetTone)
+ uint8_t wave; // ... and their waveform
};
struct AudioSampleT {
@@ -155,8 +169,18 @@ typedef struct {
bool suspended; // output closed for silence (the driver stays chosen)
bool fmTones; // square / triangle tones on FM voices
bool recording; // the card records (audioDevRecordBegin): no output, no new voice
+ char want[AUDIO_NAME_MAX]; // DVX.INI's driver choice it opened under
} AudioDevT;
+// The modules of the last device, kept loaded after it closed (devClose):
+// the DVX.INI driver choice it opened under, its sound driver, and its
+// synth drivers (desc NULL: none).
+typedef struct {
+ char want[AUDIO_NAME_MAX];
+ DvxDriverModT sound;
+ DvxDriverModT synths[AUDIO_SYNTH_KINDS];
+} ParkedT;
+
static const char *sErrorText[AUDIO_ERR_COUNT] = {
"No error",
"Invalid argument",
@@ -176,6 +200,7 @@ static const char *sErrorText[AUDIO_ERR_COUNT] = {
};
static AudioDevT sDev;
+static ParkedT sParked;
static AudioCtxT *sCtxs = NULL;
static AudioSampleT *sSamples = NULL;
static AudioStreamT *sStreams = NULL;
@@ -189,15 +214,19 @@ static uint32_t sDropped = 0;
static bool sPumping = false;
static bool sStreamsDirty = false;
+// The sound drivers' names (audioDriverNames), read once: -1 until then
+static char sDrvNames[AUDIO_DRIVERS_MAX][AUDIO_NAME_MAX];
+static int32_t sDrvNameCount = -1;
+
// ============================================================
// Prototypes
// ============================================================
static int32_t appCurrent(void);
void audioAppEnded(int32_t appId);
-int32_t audioAppEnter(int32_t appId);
AudioCtxT *audioBasicContext(void);
void audioClose(AudioCtxT *ctx);
+void audioDevForget(void);
int32_t audioDevRecordBegin(int32_t source, const AudioDriverT **drv);
void audioDevRecordEnd(void);
int32_t audioDriverNames(char names[][AUDIO_NAME_MAX], int32_t max);
@@ -212,6 +241,7 @@ void audioNotesClear(AudioNotesT *notes);
void audioNotesClose(AudioNotesT *notes);
int32_t audioNotesOpen(AudioCtxT *ctx, AudioWaveE wave, int32_t volume, AudioNotesT **out);
uint32_t audioNotesQueued(const AudioNotesT *notes, uint32_t *tones);
+int32_t audioNotesSetTone(AudioNotesT *notes, AudioWaveE wave, int32_t level);
uint32_t audioNotesSpace(const AudioNotesT *notes);
int32_t audioNotesVoice(const AudioNotesT *notes);
AudioCtxT *audioOpen(void);
@@ -229,6 +259,7 @@ void audioSetVolume(AudioCtxT *ctx, int32_t volume);
void audioShutdown(void);
void audioStopAll(AudioCtxT *ctx);
void audioStreamClose(AudioStreamT *stream);
+void audioStreamEnd(AudioStreamT *stream);
void audioStreamFlush(AudioStreamT *stream);
int32_t audioStreamOpen(AudioCtxT *ctx, const AudioFormatT *fmt, uint32_t bufferMs, AudioFillFnT fill, void *userData, AudioStreamT **out);
uint32_t audioStreamQueued(const AudioStreamT *stream);
@@ -252,6 +283,7 @@ static void devConfig(PrefsHandleT *prefs, AudioDrvConfigT *
static void devEmergencyStop(void);
static void devIdle(void);
static bool devOpen(void);
+static bool devOpenParked(const char *want, const AudioDrvConfigT *cfg);
static void devOpenSynths(DvxDriverModT *mods, const AudioDrvConfigT *cfg);
static bool devResume(void);
static bool devTry(DvxDriverModT *mods, int32_t index, const AudioDrvConfigT *cfg);
@@ -262,11 +294,12 @@ static void gainsUpdate(int32_t slot);
static bool infoUsable(const AudioDrvInfoT *info);
static int32_t lookupVoice(const AudioCtxT *ctx, int32_t voice);
static int32_t makeSample(AudioCtxT *ctx, int16_t *pcm, uint32_t frames, const AudioFormatT *fmt, AudioSampleT **out);
+static void namesNote(DvxDriverModT *mods);
static void reclaimSlots(void);
static void releaseSlot(int32_t slot);
static const AudioDriverT *soundOf(const DvxDriverModT *mod);
static void stepUpdate(int32_t slot);
-static void streamFill(AudioStreamT *s);
+static void streamFill(AudioStreamT *s, uint64_t until);
static const AudioSynthDriverT *synthOf(const DvxDriverModT *mod);
static bool synthTry(const AudioSynthDriverT *syn, const AudioDrvConfigT *cfg);
static int32_t voiceBegin(AudioCtxT *owner, int32_t volume, int32_t pan, uint8_t kind);
@@ -300,19 +333,6 @@ void audioAppEnded(int32_t appId) {
}
-// Make appId the current program and return the one before (for calling
-// a program's callback as that program).
-int32_t audioAppEnter(int32_t appId) {
- int32_t prev = appCurrent();
-
- if (dvxMemAppIdPtr) {
- *dvxMemAppIdPtr = appId;
- }
-
- return prev;
-}
-
-
// The calling BASIC program's context, opened on first use.
AudioCtxT *audioBasicContext(void) {
int32_t appId = appCurrent();
@@ -327,6 +347,7 @@ AudioCtxT *audioBasicContext(void) {
if (ctx) {
ctx->basic = true;
+ audioSetEndFn(ctx, audioBasEnded, NULL);
}
return ctx;
@@ -414,6 +435,17 @@ void audioClose(AudioCtxT *ctx) {
}
+void audioDevForget(void) {
+ dvxDriverUnload(&sParked.sound);
+
+ for (int32_t k = 0; k < AUDIO_SYNTH_KINDS; k++) {
+ dvxDriverUnload(&sParked.synths[k]);
+ }
+
+ memset(&sParked, 0, sizeof(sParked));
+}
+
+
// The card for recording from source: its output closed (as the silence
// timeout does) and new voices refused until audioDevRecordEnd.
int32_t audioDevRecordBegin(int32_t source, const AudioDriverT **drv) {
@@ -454,17 +486,22 @@ void audioDevRecordEnd(void) {
}
+// Read once (devOpen's full load notes them as well): a driver installed
+// meanwhile is used from the next start anyway.
int32_t audioDriverNames(char names[][AUDIO_NAME_MAX], int32_t max) {
- DvxDriverModT *mods = dvxDriversLoad(AUDIO_DRV_DIR, &sDrvClass);
- int32_t count = 0;
+ if (sDrvNameCount < 0) {
+ DvxDriverModT *mods = dvxDriversLoad(AUDIO_DRV_DIR, &sDrvClass);
- for (int32_t i = 0; i < (int32_t)arrlen(mods) && count < max; i++) {
- if (soundOf(&mods[i])) {
- snprintf(names[count++], AUDIO_NAME_MAX, "%s", mods[i].desc->name);
- }
+ namesNote(mods);
+ dvxDriversFree(mods);
+ }
+
+ int32_t count = DVX_MIN(max, sDrvNameCount);
+
+ for (int32_t i = 0; i < count; i++) {
+ snprintf(names[i], AUDIO_NAME_MAX, "%s", sDrvNames[i]);
}
- dvxDriversFree(mods);
return count;
}
@@ -558,9 +595,12 @@ int32_t audioNotesAdd(AudioNotesT *notes, uint32_t milliHz, uint32_t us) {
uint64_t end = (notes->usQueued + us) * rate / PLATFORM_US_PER_SEC;
MixNoteT *e = &q->data[q->head & q->mask];
- e->hz = (mhz + AUDIO_MILLIHZ_PER_HZ / 2) / AUDIO_MILLIHZ_PER_HZ;
+ // A silent tone is a rest for a tone generator
+ e->hz = notes->level > 0 ? (mhz + AUDIO_MILLIHZ_PER_HZ / 2) / AUDIO_MILLIHZ_PER_HZ : 0;
e->phaseStep = (uint32_t)(((uint64_t)mhz << PHASE_SHIFT) / ((uint64_t)rate * AUDIO_MILLIHZ_PER_HZ));
e->frames = (uint32_t)(end - notes->framesQueued);
+ e->level = (uint16_t)notes->level;
+ e->wave = notes->wave;
notes->usQueued += us;
notes->framesQueued = end;
__atomic_signal_fence(__ATOMIC_RELEASE);
@@ -635,6 +675,8 @@ int32_t audioNotesOpen(AudioCtxT *ctx, AudioWaveE wave, int32_t volume, AudioNot
n->ring.data = n->data;
n->ring.mask = AUDIO_NOTES_MAX - 1;
n->owner = ctx;
+ n->level = AUDIO_VOLUME_MAX;
+ n->wave = (uint8_t)wave;
n->next = sNoteQueues;
sNoteQueues = n;
@@ -679,6 +721,17 @@ uint32_t audioNotesQueued(const AudioNotesT *notes, uint32_t *tones) {
}
+int32_t audioNotesSetTone(AudioNotesT *notes, AudioWaveE wave, int32_t level) {
+ if (!notes || (wave != AUDIO_WAVE_SQUARE && wave != AUDIO_WAVE_TRIANGLE && wave != AUDIO_WAVE_NOISE) || level < 0 || level > AUDIO_VOLUME_MAX) {
+ return AUDIO_ERR_ARG;
+ }
+
+ notes->wave = (uint8_t)wave;
+ notes->level = level;
+ return AUDIO_OK;
+}
+
+
uint32_t audioNotesSpace(const AudioNotesT *notes) {
return notes->ring.mask + 1 - (notes->ring.head - notes->ring.tail);
}
@@ -767,15 +820,16 @@ void audioPump(void) {
deliverNotes();
audioRecIdle();
- AudioStreamT *s = sStreams;
+ AudioStreamT *s = sStreams;
+ uint64_t until = platformMicros() + STREAM_FILL_BUDGET_US;
while (s) {
AudioStreamT *next = s->next;
sStreamsDirty = false;
- if (s->fill) {
- streamFill(s);
+ if (s->fill && !s->ring.ended) {
+ streamFill(s, until);
}
s = sStreamsDirty ? sStreams : next;
@@ -911,6 +965,7 @@ void audioShutdown(void) {
audioClose(sCtxs);
}
+ audioDevForget();
arrfree(sNotes);
sNotes = NULL;
}
@@ -961,6 +1016,13 @@ void audioStreamClose(AudioStreamT *stream) {
// With interrupts off: the mixer moves the tail.
+void audioStreamEnd(AudioStreamT *stream) {
+ if (stream) {
+ stream->ring.ended = true;
+ }
+}
+
+
void audioStreamFlush(AudioStreamT *stream) {
if (!stream) {
return;
@@ -1125,7 +1187,7 @@ void audioSuspend(bool suspending) {
return;
}
- PrefsHandleT *prefs = prefsLoad(platformDvxIniPath());
+ PrefsHandleT *prefs = prefsDvxIniShared();
AudioDrvConfigT cfg;
devConfig(prefs, &cfg);
@@ -1140,7 +1202,6 @@ void audioSuspend(bool suspending) {
}
}
- prefsClose(prefs);
}
@@ -1301,10 +1362,10 @@ static void deliverNotes(void) {
}
if (live && ctx->endFn) {
- int32_t prev = audioAppEnter(ctx->appId);
+ int32_t prev = dvxMemSetApp(ctx->appId);
ctx->endFn(ctx, notes[i].voice, notes[i].reason, ctx->endData);
- audioAppEnter(prev);
+ dvxMemSetApp(prev);
}
}
@@ -1312,6 +1373,7 @@ static void deliverNotes(void) {
}
+// The modules stay loaded (sParked) for the next device to try first.
static void devClose(void) {
if (!sDev.mod.desc) {
return;
@@ -1325,13 +1387,16 @@ static void devClose(void) {
}
audioMixLock(false);
- dvxDriverUnload(&sDev.mod);
+ snprintf(sParked.want, sizeof(sParked.want), "%s", sDev.want);
+ sParked.sound = sDev.mod;
memset(&sDev, 0, sizeof(sDev));
dvxLog("Audio: sound device closed");
}
-// Every context is closed, so no program holds a voice or the port.
+// Every context is closed, so no program holds a voice or the port. The
+// synth modules are parked with the device's (the card's own synthesizer
+// has none).
static void devCloseSynths(void) {
audioFmDetach();
audioMidiDetach();
@@ -1342,7 +1407,10 @@ static void devCloseSynths(void) {
for (int32_t k = 0; k < AUDIO_SYNTH_KINDS; k++) {
if (sDev.synths[k].desc) {
synthOf(&sDev.synths[k])->close();
- dvxDriverUnload(&sDev.synths[k]);
+ }
+
+ if (sDev.synths[k].handle) {
+ sParked.synths[k] = sDev.synths[k];
}
}
}
@@ -1415,7 +1483,7 @@ static void devIdle(void) {
static bool devOpen(void) {
- PrefsHandleT *prefs = prefsLoad(platformDvxIniPath());
+ PrefsHandleT *prefs = prefsDvxIniShared();
AudioDrvConfigT cfg;
devConfig(prefs, &cfg);
@@ -1423,12 +1491,20 @@ static bool devOpen(void) {
sMaster = audioPercentToVolume(audioSettingVolume(prefs));
sDev.fmTones = prefsGetBool(prefs, AUDIO_INI_SECTION, INI_FM_TONES, true);
- const char *want = audioSettingDriver(prefs);
+ const char *want = audioSettingDriver(prefs);
+
+ if (devOpenParked(want, &cfg)) {
+ snprintf(sDev.want, sizeof(sDev.want), "%s", want);
+ return true;
+ }
+
DvxDriverModT *mods = dvxDriversLoad(AUDIO_DRV_DIR, &sDrvClass);
int32_t count = (int32_t)arrlen(mods);
int32_t chosen = -1;
int32_t tried = -1;
+ namesNote(mods);
+
if (strcasecmp(want, AUDIO_DRIVER_AUTO) != 0) {
for (int32_t i = 0; i < count; i++) {
if (soundOf(&mods[i]) && strcasecmp(mods[i].desc->name, want) == 0) {
@@ -1450,10 +1526,10 @@ static bool devOpen(void) {
if (chosen >= 0) {
devOpenSynths(mods, &cfg);
+ snprintf(sDev.want, sizeof(sDev.want), "%s", want);
}
dvxDriversFree(mods);
- prefsClose(prefs);
if (chosen < 0) {
dvxLog("Audio: no sound driver could be started");
@@ -1464,6 +1540,41 @@ static bool devOpen(void) {
}
+// The parked modules of the last device, while DVX.INI still makes the
+// same driver choice: its sound driver first (true when it started; its
+// synthesizers come back with it). Every parked module is let go
+// otherwise.
+static bool devOpenParked(const char *want, const AudioDrvConfigT *cfg) {
+ DvxDriverModT *mods = NULL;
+ bool ok = false;
+
+ // A module handed to mods is no longer parked (mods unloads or keeps it)
+ if (sParked.sound.desc && strcasecmp(sParked.want, want) == 0) {
+ arrput(mods, sParked.sound);
+ memset(&sParked.sound, 0, sizeof(sParked.sound));
+ ok = devTry(mods, 0, cfg);
+ dvxDriversFree(mods);
+ mods = NULL;
+ }
+
+ if (ok) {
+ for (int32_t k = 0; k < AUDIO_SYNTH_KINDS; k++) {
+ if (sParked.synths[k].desc) {
+ arrput(mods, sParked.synths[k]);
+ memset(&sParked.synths[k], 0, sizeof(sParked.synths[k]));
+ }
+ }
+
+ devOpenSynths(mods, cfg);
+ dvxDriversFree(mods);
+ dvxLog("Audio: the last device's driver started again");
+ }
+
+ audioDevForget();
+ return ok;
+}
+
+
// The FM chip and the MIDI port: of each kind the first synth driver
// (highest priority first) that finds its hardware. Kept modules are
// taken out of mods so dvxDriversFree leaves them loaded.
@@ -1495,7 +1606,7 @@ static void devOpenSynths(DvxDriverModT *mods, const AudioDrvConfigT *cfg) {
// a new format (DVX.INI changed meanwhile) only needs the mixer reset.
static bool devResume(void) {
const AudioDriverT *drv = soundOf(&sDev.mod);
- PrefsHandleT *prefs = prefsLoad(platformDvxIniPath());
+ PrefsHandleT *prefs = prefsDvxIniShared();
AudioDrvConfigT cfg;
AudioDrvInfoT info;
@@ -1504,8 +1615,6 @@ static bool devResume(void) {
bool opened = drv->open(&cfg, &info);
- prefsClose(prefs);
-
if (!opened || !infoUsable(&info)) {
dvxLog("Audio: driver '%s' could not open the output again", drv->hdr.name);
@@ -1675,6 +1784,22 @@ static int32_t makeSample(AudioCtxT *ctx, int16_t *pcm, uint32_t frames, const A
}
+// The sound drivers' names in mods (a full load), when not read yet.
+static void namesNote(DvxDriverModT *mods) {
+ if (sDrvNameCount >= 0) {
+ return;
+ }
+
+ sDrvNameCount = 0;
+
+ for (int32_t i = 0; i < (int32_t)arrlen(mods) && sDrvNameCount < AUDIO_DRIVERS_MAX; i++) {
+ if (soundOf(&mods[i])) {
+ snprintf(sDrvNames[sDrvNameCount++], AUDIO_NAME_MAX, "%s", mods[i].desc->name);
+ }
+ }
+}
+
+
// Free every ended slot, queueing a notification for owners that want
// one.
static void reclaimSlots(void) {
@@ -1755,16 +1880,18 @@ static void stepUpdate(int32_t slot) {
// Ask the fill function for as much as the ring has room for, as the
-// program owning the stream.
-static void streamFill(AudioStreamT *s) {
+// program owning the stream; once it holds STREAM_SAFE_MS, no longer than
+// until (platformMicros).
+static void streamFill(AudioStreamT *s, uint64_t until) {
uint32_t space = audioStreamSpace(s);
+ uint32_t safe = s->fmt.rate * STREAM_SAFE_MS / MS_PER_SEC;
while (space > 0) {
uint32_t ask = space < STREAM_FILL_FRAMES ? space : STREAM_FILL_FRAMES;
- int32_t prev = audioAppEnter(s->owner->appId);
+ int32_t prev = dvxMemSetApp(s->owner->appId);
uint32_t got = s->fill(s, s->scratch, ask, s->fillData);
- audioAppEnter(prev);
+ dvxMemSetApp(prev);
// The callback closed a stream (maybe this one): stop here.
if (sStreamsDirty || got == 0) {
@@ -1773,7 +1900,7 @@ static void streamFill(AudioStreamT *s) {
audioStreamWrite(s, s->scratch, got < ask ? got : ask);
- if (got < ask) {
+ if (got < ask || s->ring.ended || (audioStreamQueued(s) >= safe && platformMicros() >= until)) {
break;
}
@@ -1782,12 +1909,12 @@ static void streamFill(AudioStreamT *s) {
}
+// The synth driver of a module (NULL: none, or a sound driver).
static const AudioSynthDriverT *synthOf(const DvxDriverModT *mod) {
return mod->kind == DRV_KIND_SYNTH ? (const AudioSynthDriverT *)mod->desc : NULL;
}
-// The synth driver of a module (NULL: none, or a sound driver).
// Open a synthesizer and attach it as its kind; false when it found no
// device.
static bool synthTry(const AudioSynthDriverT *syn, const AudioDrvConfigT *cfg) {
diff --git a/src/libs/kpunch/audio/audio.dhs b/src/libs/kpunch/audio/audio.dhs
index c6f82cc0..c611763c 100644
--- a/src/libs/kpunch/audio/audio.dhs
+++ b/src/libs/kpunch/audio/audio.dhs
@@ -35,12 +35,14 @@
.index audioStreamOpen
.index audioStreamWrite
.index audioStreamFlush
+.index audioStreamEnd
.index audioVoicePause
.index audioWavParse
.index audioDecode
.index SoundPause
.index SoundResume
.index SoundStreamFlush
+.index SoundStreamEnd
.index audioSampleLoad
.index audioGetInfo
.index AudioInfoT
@@ -66,6 +68,7 @@
.index OPL
.index MPU-401
.index audioNotesOpen
+.index audioNotesSetTone
.index audioNotesAdd
.index AudioNotesT
.index audioDeskPlay
@@ -130,7 +133,7 @@ if (voice < 0) {
audioClose(audio); // or let the program end
.endcode
-The first audioOpen starts the sound device and the last audioClose stops it. In between, after AUDIO_IDLE_CLOSE_SEC (3) seconds with nothing playing, the device's output is closed so it costs no CPU time (desktop sounds would otherwise keep it mixing silence), and the next sound opens it again with the same driver before it starts, so nothing of it is lost; audioGetInfo still reports the device. Functions returning int32_t give AUDIO_OK or a voice id (above 0) on success and a negative AUDIO_ERR_* code on failure; audioErrorText says what a code means. audioGetInfo describes the device: the driver's name and description, the mixing rate it achieved, its output bits and channels, the voice limit, and whether it plays sampled sound (hasDac), has a hardware synthesizer (hasSynth), or plays tones on a hardware tone generator (hasTone, the PC speaker). audioGetStats counts mixed blocks and frames, stream underruns, clipped samples, dropped voices, and the voices playing.
+The first audioOpen starts the sound device and the last audioClose stops it; the driver modules it used stay loaded, and the next first audioOpen tries them first (no other driver is loaded or probed) while DVX.INI's driver choice is the same. In between, after AUDIO_IDLE_CLOSE_SEC (10) seconds with nothing playing, the device's output is closed so it costs no CPU time (desktop sounds would otherwise keep it mixing silence), and the next sound opens it again with the same driver before it starts, so nothing of it is lost; audioGetInfo still reports the device. Functions returning int32_t give AUDIO_OK or a voice id (above 0) on success and a negative AUDIO_ERR_* code on failure; audioErrorText says what a code means. audioGetInfo describes the device: the driver's name and description, the mixing rate it achieved, its output bits and channels, the voice limit, and whether it plays sampled sound (hasDac), has a hardware synthesizer (hasSynth), or plays tones on a hardware tone generator (hasTone, the PC speaker). audioGetStats counts mixed blocks and frames, stream underruns, clipped samples, dropped voices, and the voices playing.
None of the functions may be called from an interrupt (a timer-service callback).
@@ -164,7 +167,7 @@ audioNotesAdd(tune, 440 * AUDIO_MILLIHZ_PER_HZ, 250000); // A for 1/4 s
audioNotesAdd(tune, 0, 50000); // a rest
.endcode
-A note queue plays tones and rests one after the other, each lasting exactly as long as asked (its length is counted in output frames from the time queued since the queue last went quiet, so many short notes do not drift). audioNotesAdd(notes, milliHz, us) appends a tone of milliHz thousandths of a hertz (AUDIO_TONE_MIN_HZ to AUDIO_TONE_MAX_HZ, or 0 for a rest) lasting us microseconds; up to AUDIO_NOTES_MAX entries may wait (AUDIO_ERR_BUSY beyond). audioNotesQueued says how many entries have not finished (and how many of them are tones), audioNotesSpace how many more fit, audioNotesClear silences the queue at once. The queue plays on its own voice (audioNotesVoice, for volume and pan) that the voice limit never drops, in the mixer or on the PC speaker's tone generator (never on the FM chip); it is silent while empty. music.lib plays QBasic's PLAY and SOUND through note queues.
+A note queue plays tones and rests one after the other, each lasting exactly as long as asked (its length is counted in output frames from the time queued since the queue last went quiet, so many short notes do not drift). audioNotesAdd(notes, milliHz, us) appends a tone of milliHz thousandths of a hertz (AUDIO_TONE_MIN_HZ to AUDIO_TONE_MAX_HZ, or 0 for a rest) lasting us microseconds; up to AUDIO_NOTES_MAX entries may wait (AUDIO_ERR_BUSY beyond). audioNotesSetTone(notes, wave, level) sets the waveform and loudness (0..AUDIO_VOLUME_MAX, a share of the queue's volume) of the entries added from then on -- each entry plays in its own, so a tune changes them in time -- and a tone generator plays a tone at level 0 as a rest. audioNotesQueued says how many entries have not finished (and how many of them are tones), audioNotesSpace how many more fit, audioNotesClear silences the queue at once. The queue plays on its own voice (audioNotesVoice, for volume and pan) that the voice limit never drops, in the mixer or on the PC speaker's tone generator (never on the FM chip); it is silent while empty. music.lib plays QBasic's PLAY and SOUND through note queues.
.h2 Streams
@@ -175,7 +178,7 @@ AudioStreamT *stream = NULL;
audioStreamOpen(audio, &fmt, 250, fillMusic, &player, &stream);
.endcode
-audioStreamOpen(ctx, fmt, bufferMs, fill, userData, &stream) opens a stream of any rate from AUDIO_RATE_MIN (4000) to AUDIO_RATE_MAX (48000), 8 or 16 bits, mono or stereo, with a buffer of bufferMs milliseconds (0: 250). It plays at once on its own voice (audioStreamVoice), silence until samples arrive. Without fill the program writes samples with audioStreamWrite, which takes as many frames as fit and returns how many; audioStreamSpace and audioStreamQueued tell how much room and how much sound are left. With fill, the library asks for samples whenever the buffer has room: fill(stream, buf, frames, userData) writes up to frames frames in the stream's format and returns how many. Fill functions run from audioPump and the shell's idle time; a program that keeps the desktop busy every frame should call audioPump once a frame. A stream that runs dry plays silence and counts underruns. audioStreamFlush drops what the stream holds and has not played yet (a seek or a stop is heard at once; the two frames the mixer has read ahead still play). audioStreamClose stops and frees it.
+audioStreamOpen(ctx, fmt, bufferMs, fill, userData, &stream) opens a stream of any rate from AUDIO_RATE_MIN (4000) to AUDIO_RATE_MAX (48000), 8 or 16 bits, mono or stereo, with a buffer of bufferMs milliseconds (0: 250). It plays at once on its own voice (audioStreamVoice), silence until samples arrive. Without fill the program writes samples with audioStreamWrite, which takes as many frames as fit and returns how many; audioStreamSpace and audioStreamQueued tell how much room and how much sound are left. With fill, the library asks for samples whenever the buffer has room: fill(stream, buf, frames, userData) writes up to frames frames in the stream's format and returns how many. Fill functions run from audioPump and the shell's idle time; a program that keeps the desktop busy every frame should call audioPump once a frame. Once a stream holds 100 ms of sound, a pump fills it for 15 ms at most (STREAM_FILL_BUDGET_US) and the next pump goes on, so a fill function that takes its time (a module rendered, a file read) does not hold DVX up after a start, a seek, or a stall. A stream that runs dry plays silence and counts underruns. audioStreamEnd says no more frames will come after those written (a fill function may call it with its last ones): the fill function is not asked again, and once the stream has played what it holds its voice ends as a sample's does (AUDIO_END_FINISHED to the end function), with nothing counted as an underrun; the stream stays open until audioStreamClose. Without it a stream that has played its last frame runs on in silence, counting underruns, until it is closed. audioStreamFlush drops what the stream holds and has not played yet (a seek or a stop is heard at once; the two frames the mixer has read ahead still play). audioStreamClose stops and frees it.
audioDrv.h also declares, for audio.lib and the libraries built on it (music.lib), audioDecode (a WAV or VOC file in memory as 16-bit frames), audioConvert (8- or 16-bit frames as 16-bit ones), and audioWavParse(head, headBytes, fileBytes, &fmt, &dataAt, &dataBytes) (a WAV file's format and where its frames are, from its first bytes: the frames are clamped to the file and whole; AUDIO_ERR_FORMAT for anything but a RIFF WAVE file, AUDIO_ERR_UNSUPPORTED, AUDIO_ERR_CORRUPT).
@@ -228,7 +231,7 @@ note.bits = 16;
audioWtStart(audio, v, ¬e); // pitch 0: AUDIO_WT_PITCH_RATE (44,100) frames a second
.endcode
-A wavetable synthesizer (a Gravis UltraSound's GF1, a Sound Blaster AWE32's or AWE64's EMU8000) plays samples from its own memory, and its voices and memory are shared by every program. audioWtGetInfo tells the chip (description, voices, free voices, memory, the largest free block, what it does itself: AUDIO_WT_ENVELOPES, AUDIO_WT_LFOS, AUDIO_WT_FILTER, AUDIO_WT_EFFECTS, AUDIO_WT_8BIT for 8-bit samples, AUDIO_WT_BANK_256K when a 16-bit sample must not cross a 256 KB boundary, AUDIO_WT_ROM for a General MIDI sample ROM; and its level, the centibels a voice at full volume is louder than full-scale digital sound, measured: 0 on a GUS, whose digital sound plays on its own voices, 76 on an AWE). audioWtMemAlloc(ctx, bytes, bits, &addr) takes memory for samples of bits bits (AUDIO_ERR_NO_ROOM when no free block is large enough), audioWtUpload(ctx, addr, frames, count, bits) copies 16-bit frames into it at bits, audioWtMemFree gives it back. audioWtAlloc(ctx) takes a voice (the least recently given back first), audioWtStart(ctx, voice, note) starts it on a note (AudioWtVoiceT: the sample's address, or AUDIO_WT_ADDR_ROM with an address in the ROM, and in frames from there the one it starts from (offset: before the loop or inside it), its loop, and its end; pitch in cents, 0 for AUDIO_WT_PITCH_RATE (44,100) frames a second; attenuation in centibels; pan; the filter, reverb and chorus; both envelopes in timecents; both LFOs; audioWtNoteInit makes a plain one, with no envelope or LFO shaping it, the filter open, no effects, centred, at full volume, for the caller to set the sample on), audioWtUpdate changes what its what bits say (AUDIO_WT_SET_PITCH, _VOLUME, _PAN, _FILTER, _EFFECTS, _LFO), audioWtRelease lets the key up, audioWtStop stops it at once, audioWtActive is false once it is silent, audioWtFree gives it back. The chip does what its flags say; the program does the rest by updating the voice as the note goes on. Closing the context gives back its voices and its memory. The voice calls may be made from a timer callback (music.lib plays SoundFont MIDI and modules that way); the memory calls may not.
+A wavetable synthesizer (a Gravis UltraSound's GF1, a Sound Blaster AWE32's or AWE64's EMU8000) plays samples from its own memory, and its voices and memory are shared by every program. audioWtGetInfo tells the chip (description, voices, free voices, memory, the largest free block, what it does itself: AUDIO_WT_ENVELOPES, AUDIO_WT_LFOS, AUDIO_WT_FILTER, AUDIO_WT_EFFECTS, AUDIO_WT_8BIT for 8-bit samples, AUDIO_WT_BANK_256K when a 16-bit sample must not cross a 256 KB boundary, AUDIO_WT_ROM for a General MIDI sample ROM; and its level, the centibels a voice at full volume is louder than full-scale digital sound, measured: 0 on a GUS, whose digital sound plays on its own voices, 76 on an AWE). audioWtMemAlloc(ctx, bytes, bits, &addr) takes memory for samples of bits bits (AUDIO_ERR_NO_ROOM when no free block is large enough), audioWtUpload(ctx, addr, frames, count, bits) copies 16-bit frames into it at bits, audioWtMemFree gives it back. audioWtAlloc(ctx) takes a voice (the least recently given back first), audioWtStart(ctx, voice, note) starts it on a note (AUDIO_ERR_ARG for a sample in a ROM the chip does not have; AudioWtVoiceT: the sample's address, or AUDIO_WT_ADDR_ROM with an address in the ROM, and in frames from there the one it starts from (offset: before the loop or inside it), its loop, and its end; pitch in cents, 0 for AUDIO_WT_PITCH_RATE (44,100) frames a second; attenuation in centibels; pan; the filter, reverb and chorus; both envelopes in timecents; both LFOs; audioWtNoteInit makes a plain one, with no envelope or LFO shaping it, the filter open, no effects, centred, at full volume, for the caller to set the sample on), audioWtUpdate changes what its what bits say (AUDIO_WT_SET_PITCH, _VOLUME, _PAN, _FILTER, _EFFECTS, _LFO), audioWtRelease lets the key up, audioWtStop stops it at once, audioWtActive is false once it is silent, audioWtFree gives it back. The chip does what its flags say; the program does the rest by updating the voice as the note goes on. Closing the context gives back its voices and its memory. The voice calls may be made from a timer callback (music.lib plays SoundFont MIDI and modules that way); the memory calls may not.
.h2 MIDI Port
@@ -284,7 +287,7 @@ A settings screen reads and writes the keys it offers through the library, so th
.h2 Desktop Sounds
-The library plays the sounds DVX makes for its events (dvxPlaySound in dvxApp.h): when it loads it sets DVX's sound hook to audioDeskPlay(event, wait). The file of each event is read from DVX.INI [sounds] at the first sound: the key is audioDeskKey(event) and the value a WAV or VOC file, empty or missing for none: a name alone is found in the sound folders (the user folder's SOUNDS first, then SHARED\SOUNDS: platformKindFind), anything else is a path; audioDeskReload makes the next sound read the section again (the Control Panel calls it after saving). The sounds play on a context of the library's own, so no program's end cuts one short, at full volume under the master volume. When no sound device starts, the sounds stay silent without trying again until the next reload. With wait the call returns once the sound has played (at most 5 seconds); after the exit sound further events are quiet (DVX is ending). audioDeskLabel(event) names an event for people, audioDeskPlayFile(path) plays any file the same way (to try one out). audioDeskFile(prefs, event) reads an event's file from a copy of DVX.INI ("" for none) and audioDeskSetFile(prefs, event, path) sets it; the library's own reading of [sounds] goes through audioDeskFile too. The default set, made by DVX's own sound generator at build time, is in SHARED\SOUNDS.
+The library plays the sounds DVX makes for its events (dvxPlaySound in dvxApp.h): when it loads it sets DVX's sound hook to audioDeskPlay(event, wait). The file of each event is read from DVX.INI [sounds] at the first sound: the key is audioDeskKey(event) and the value a WAV or VOC file, empty or missing for none: a name alone is found in the sound folders (the user folder's SOUNDS first, then SHARED\SOUNDS: platformKindFind), anything else is a path; audioDeskReload makes the next sound read the section again (the Control Panel calls it after saving). Each event's file is decoded at its first sound and kept until then, so a window opening or a message box does not read the disk. The sounds play on a context of the library's own, so no program's end cuts one short, at full volume under the master volume. When no sound device starts, the sounds stay silent without trying again until the next reload. With wait the call returns once the sound has played (at most 5 seconds), running dvxWaitPump meanwhile (a BASIC program's events go on); after the exit sound further events are quiet (DVX is ending). audioDeskLabel(event) names an event for people, audioDeskPlayFile(path) plays any file the same way (to try one out). audioDeskFile(prefs, event) reads an event's file from a copy of DVX.INI ("" for none) and audioDeskSetFile(prefs, event, path) sets it; the library's own reading of [sounds] goes through audioDeskFile too. The default set, made by DVX's own sound generator at build time, is in SHARED\SOUNDS.
.table
Key Event
@@ -323,7 +326,7 @@ Sound drivers are tried in this order: the one DVX.INI names, then from the high
.h2 Writing a Driver
-A driver is a driver module (see dvxDrv.h in the API reference): .drv in bin/libs/kpunch/audio/drivers, built by the audio Makefile from each drivers// folder, that exports one AudioDriverT named audioDriver, written with AUDIO_DRIVER(name, AUDIO_PRIORITY_*, .open = ..., .play = ..., .close = ...). The descriptor carries DVX_ABI_VERSION; a driver built for another version is refused. audio.lib loads the drivers when the first program opens sound, keeps the one that starts (and the synthesizers that find their hardware) and unloads the rest. open detects the hardware and fills AudioDrvInfoT (description, achieved rate, bits, channels, hasDac, hasSynth, a hardware tone function) without making a sound; play starts output; close stops it and releases everything, after which the mixer is not called again. After a few seconds of silence audio.lib calls close and later open and play again on the same driver, so open must work more than once and must not make a sound (the driver's module stays loaded in between). A driver whose card records also sets AudioDrvInfoT.recSources and gives recordStart(want, source, got) and recordStop: recordStart is called with the output closed and takes the card for recording (its own DMA buffer and interrupt; the format the card records in got), its interrupt then calls audioRecPush(src, bytes) with each block recorded (whole frames; a block that does not fit is dropped and counted), and recordStop gives everything back as close does (sb.drv, wss.drv, and gus.drv use their close for both; the AD1848 codec's routines a Windows Sound System board and an UltraSound MAX share are codec/codecHw.c). While playing, the driver calls audioMixRender(buffer, frames) for each block of output from its own interrupt, or calls audioClockStart to have the timer service drive the mixer (the output is thrown away) when it only plays tones or nothing. Code and data an interrupt touches must be pinned: audioLockSpan pins a module's listed functions and data (a module's code comes before its data, so one span covers them), and the library is built without hot/cold function splitting and without loops turned into library calls. Interrupt code must not call the C library, DVX, or DOS and must not use floating point. audio.lib stops the device from a shutdown hook if DVX ends without closing it.
+A driver is a driver module (see dvxDrv.h in the API reference): .drv in bin/libs/kpunch/audio/drivers, built by the audio Makefile from each drivers// folder, that exports one AudioDriverT named audioDriver, written with AUDIO_DRIVER(name, AUDIO_PRIORITY_*, .open = ..., .play = ..., .close = ...). The descriptor carries DVX_ABI_VERSION; a driver built for another version is refused. audio.lib loads the drivers when the first program opens sound, keeps the one that starts (and the synthesizers that find their hardware) and unloads the rest; when the last program closes sound it closes them but keeps their modules for the next device, which opens them again first (so open and close must work more than once). The drivers' names (audioDriverNames) are read once a session. open detects the hardware and fills AudioDrvInfoT (description, achieved rate, bits, channels, hasDac, hasSynth, a hardware tone function) without making a sound; play starts output; close stops it and releases everything, after which the mixer is not called again. After a few seconds of silence audio.lib calls close and later open and play again on the same driver, so open must work more than once and must not make a sound (the driver's module stays loaded in between). A driver whose card records also sets AudioDrvInfoT.recSources and gives recordStart(want, source, got) and recordStop: recordStart is called with the output closed and takes the card for recording (its own DMA buffer and interrupt; the format the card records in got), its interrupt then calls audioRecPush(src, bytes) with each block recorded (whole frames; a block that does not fit is dropped and counted), and recordStop gives everything back as close does (sb.drv, wss.drv, and gus.drv use their close for both; the AD1848 codec's routines a Windows Sound System board and an UltraSound MAX share are codec/codecHw.c). While playing, the driver calls audioMixRender(buffer, frames) for each block of output from its own interrupt, or calls audioClockStart to have the timer service drive the mixer (the output is thrown away) when it only plays tones or nothing. Code and data an interrupt touches must be pinned: audioLockSpan pins a module's listed functions and data (a module's code comes before its data, so one span covers them), and the library is built without hot/cold function splitting and without loops turned into library calls. Interrupt code must not call the C library, DVX, or DOS and must not use floating point. audio.lib stops the device from a shutdown hook if DVX ends without closing it.
A synthesizer driver exports an AudioSynthDriverT named audioSynth instead, written with AUDIO_SYNTH_DRIVER(name, priority, .kind = AUDIO_SYNTH_FM, AUDIO_SYNTH_MIDI, or AUDIO_SYNTH_WAVETABLE, .open = ..., .close = ..., then .write, .put, or the wavetable functions). open detects the hardware, fills AudioSynthDrvInfoT (description; for FM the voices and whether it is stereo; for a wavetable synthesizer its voices, memory, AUDIO_WT_* flags, and level, 0 when it is as loud as the digital sound or not known) and leaves it silent; close silences it. write(reg, value) sets an FM register (bank x AUDIO_FM_BANK_REGS + index) and must keep the index/data pair together (audioIrqOff / audioIrqRestore); put(byte) sends a MIDI byte, returning false when the port is not ready; a MIDI driver whose port receives also has get(&byte), the next byte the port has (false when none), which the MIDI input's timer calls (mpu401.drv keeps what put finds waiting for get). A wavetable driver has upload(addr, frames, count, bits) (program time; addresses as audioWtMemAlloc gives them) and start, update, release, stop, and active for its voices (numbered from 0, as audio.lib hands them out); it plays a note as AudioWtVoiceT gives it, in its own units. write, put, and the voice functions run at interrupt time too (timer callbacks), so they follow the rules above and are pinned by the driver. A sound driver whose card also has a synthesizer (the GUS) points AudioDrvInfoT.synth at its own AudioSynthDriverT, which is opened before the others.
@@ -333,6 +336,24 @@ src/include/basic/sound.bas (DECLARE LIBRARY "audio"): PlayWave(fileName), PlayW
For a settings program (the Control Panel's Sounds page): SoundEventCount, SoundEventLabel$(eventIdx), SoundEventKey$(eventIdx) (audioDeskLabel / audioDeskKey; events numbered from 0 in DvxSoundE order); SoundEventFile$(prefs, eventIdx) and SoundSetEventFile(prefs, eventIdx, fileName) (audioDeskFile / audioDeskSetFile); SoundEventsReload (audioDeskReload); SoundTestFile(fileName) (audioDeskPlayFile, a name alone found in the sound folders as the events' files are: a voice number, or 0 with SoundError$); SoundFolder$ (SHARED\SOUNDS, the sounds DVX ships; FileOpsKindDir$ and FileOpsKindList give the user's too); SoundMasterVolume and SoundSetMasterVolume(percent) (audioGetMasterVolume / audioSetMasterVolume in percent); SoundDriverCount (reads audioDriverNames, at most AUDIO_DRIVERS_MAX) and SoundDriverName$(index); SoundDriverAuto$ (AUDIO_DRIVER_AUTO); SoundMidiCount and SoundMidiLabel$(index) (audioMidiChoices); SoundMidiInCount and SoundMidiInLabel$(index) (audioMidiInChoices); SoundModulesCount and SoundModulesLabel$(index) (audioModulesChoices); SoundSettingDriver$(prefs), SoundSettingMidi(prefs), SoundSettingMidiIn(prefs), SoundSettingModules(prefs), SoundSettingVolume(prefs), SoundSettingsStore(prefs, driver, midi, modules, volume), and SoundSettingMidiInStore(prefs, midiIn) (the audioSetting functions). prefs is a prefs.bas handle (prefsBasHandle, dvxPrefs.h); a handle that is not open reads the defaults and stores nothing.
-The rest of audio.h, in percent: PlayTriangle (audioTone with AUDIO_WAVE_TRIANGLE); SoundGetVolume (audioGetVolume); SoundCaps(field) (audioGetInfo: "dac", "synth", "tone", "running", "record" True / False; "voices" = voiceLimit, "rate", "bits", "channels", "recordsources"; any case, 0 for another field); SoundPlayEvent(eventIdx, wait) (dvxPlaySound / dvxPlaySoundWait, so the hook audioDeskPlay plays it; CONSTs SOUND_EVENT_* in DvxSoundE order). Samples: SoundLoad(fileName) (audioSampleLoad) and SoundCreate(pcm AS BINARY STRING, rate, bits, channels) (audioSampleCreate; frames = bytes / frame size, a trailing partial frame ignored) return a sound number; SoundFree (audioSampleFree), SoundLength (audioSampleInfo, in milliseconds), SoundPlay(sound, volume, pan, loopIt) (audioPlay; pan -100..100 scaled to AUDIO_PAN_LEFT..AUDIO_PAN_RIGHT). Voices: SoundSetVolume, SoundSetPan, SoundSetPitch(voice, percent) (25..400 percent of AUDIO_PITCH_NORMAL, clamped), and SoundSetFrequency(voice, hz) (the audioVoiceSet functions), SoundPause(voice) and SoundResume(voice) (audioVoicePause; a sound's or a stream's voice); the result is kept for SoundError$. Streams (push only): SoundStreamOpen(rate, bits, channels, bufferMs) (audioStreamOpen without a fill function), SoundStreamWrite(stream, pcm AS BINARY STRING) (audioStreamWrite of the whole frames; returns the bytes taken), SoundStreamSpace / SoundStreamQueued (frames), SoundStreamVoice, SoundStreamFlush (audioStreamFlush), SoundStreamClose. Recording, one per program (audioRecFile): RecordStart(fileName, rate, bits, channels, inputSource) (True / False with SoundError$; 0 for the defaults), RecordStop (audioRecClose; True when the file was all written), Recording, RecordLength (milliseconds recorded, written or waiting), RecordLevel (audioRecPeak), RecordLost, RecordRate, RecordBits, RecordChannels (audioRecFormat), with CONSTs RECORD_MIC, RECORD_LINE, RECORD_CD (AUDIO_REC_*) and SoundCaps "record" and "recordsources". FM: FmInfo$(field) (audioFmGetInfo: "description", "voices", "free", "stereo", "owned" as text), FmAlloc, FmWrite(voice, reg, value) (True / False), FmFree, FmRawAcquire, FmRawWrite(reg, value), FmRawRelease, with CONSTs FM_* for the AUDIO_FM_* values and FM_BANK_REGS (AUDIO_FM_BANK_REGS). Wavetable: WtInfo$(field) (audioWtGetInfo: "description", "voices", "free", "memory", "freememory", "level", and the flags "envelopes", "lfos", "filter", "effects", "8bit", "rom" as text), WtMemAlloc(bytes, bits) (an address, -1 for no room), WtUpload(address, pcm AS BINARY STRING, bits) (16-bit mono samples), WtMemFree, WtAlloc, WtStart(voice, address, startFrame, endFrame, loopStart, loopEnd, loopMode, bits, pitch, attenuation, pan) (a plain note: no envelope or LFO, the filter open; pitch in cents, attenuation in centibels, pan -100..100 scaled to AUDIO_WT_PAN_MAX), WtSetPitch, WtSetAttenuation, WtSetPan (audioWtUpdate of the note kept for the voice; True / False), WtRelease, WtStop, WtActive, WtFree. The MIDI port, declared in music.bas: MidiPortInfo$(field) (audioMidiGetInfo: "description", "present", "owned", "input", "inputowned"), MidiAcquire, MidiPut(byteValue) (False on AUDIO_ERR_BUSY: send it again), MidiRelease; its input: MidiInOpen (True / False with SoundError$), MidiInClose, MidiInMessage (audioMidiInRead: the message packed as status + data1 * 256 + data2 * 65536, AUDIO_MIDI_PACKED_*, or MIDI_NONE, -1), MidiInTime (the ms of the message MidiInMessage gave last), MidiInClock (audioMidiInClock), MidiInLost, with CONSTs MIDI_NOTE_OFF to MIDI_PITCH_BEND, MIDI_KIND_MASK, and MIDI_CHANNEL_MASK. CONSTs SOUND_PAN_*, SOUND_PITCH_* (percent), SOUND_RATE_* (AUDIO_RATE_*), and SOUND_STREAM_MS_* (AUDIO_STREAM_MS_*).
+The rest of audio.h, in percent: PlayTriangle (audioTone with AUDIO_WAVE_TRIANGLE); SoundGetVolume (audioGetVolume); SoundCaps(field) (audioGetInfo: "dac", "synth", "tone", "running", "record" True / False; "voices" = voiceLimit, "rate", "bits", "channels", "recordsources"; any case, 0 for another field); SoundStats(field) (audioGetStats: "renders", "frames", "underruns", "clipped", "dropped", "playing"); SoundDriver$ (AudioInfoT.driver); SoundPlayEvent(eventIdx, wait) (dvxPlaySound / dvxPlaySoundWait, so the hook audioDeskPlay plays it; CONSTs SOUND_EVENT_* in DvxSoundE order).
-Sound and stream numbers are 1..64 per kind, valid only for the program that got them; audioClose of a BASIC context forgets its numbers (audioBasForget) as it frees the samples and streams. BINARY STRING parameters arrive as a pointer and a byte count (two C parameters; see the DECLARE LIBRARY rules in the BASIC reference).
+Samples: SoundLoad(fileName) (audioSampleLoad), SoundLoadData(bytes AS BINARY STRING) (audioSampleLoadMem), and SoundCreate(pcm AS BINARY STRING, rate, bits, channels) (audioSampleCreate; frames = bytes / frame size, a trailing partial frame ignored) return a sound number; SoundFree (audioSampleFree), SoundLength (audioSampleInfo, in milliseconds), SoundRate, SoundBits, and SoundChannels (audioSampleInfo's format), SoundPlay(sound, volume, pan, loopIt) (audioPlay; pan -100..100 scaled to AUDIO_PAN_LEFT..AUDIO_PAN_RIGHT). Voices: SoundSetVolume, SoundSetPan, SoundSetPitch(voice, percent) (25..400 percent of AUDIO_PITCH_NORMAL, clamped), and SoundSetFrequency(voice, hz) (the audioVoiceSet functions), SoundPause(voice) and SoundResume(voice) (audioVoicePause; a sound's or a stream's voice); the result is kept for SoundError$.
+
+Ends: the program's context has audioSetEndFn set to audioBasEnded, which keeps the last BAS_ENDED_MAX (64) ended voices; SoundEnded gives the program's next one (0 for none) and SoundEndReason the AudioEndE of the one it gave last (CONSTs SOUND_END_*).
+
+Note queues: NotesOpen(wave, volume) (audioNotesOpen; CONSTs SOUND_WAVE_* for AudioWaveE), NotesAdd(queue, hertz AS DOUBLE, milliseconds AS DOUBLE) (audioNotesAdd in millihertz and microseconds), NotesSetTone(queue, wave, volume) (audioNotesSetTone, the volume a percent of AUDIO_VOLUME_MAX), NotesQueued, NotesSpace, NotesVoice, NotesClear, NotesClose; numbered 1..64 like sounds.
+
+Streams (push only): SoundStreamOpen(rate, bits, channels, bufferMs) (audioStreamOpen without a fill function), SoundStreamWrite(stream, pcm AS BINARY STRING) (audioStreamWrite of the whole frames; returns the bytes taken), SoundStreamSpace / SoundStreamQueued (frames), SoundStreamVoice, SoundStreamEnd (audioStreamEnd: no more will be written; the voice ends, SoundEnded tells, once what is held has played), SoundStreamFlush (audioStreamFlush), SoundStreamClose.
+
+Recording, one per program (audioRecFile, or audioRecOpen into memory when fileName is ""): RecordStart(fileName, rate, bits, channels, inputSource) (True / False with SoundError$; 0 for the defaults), RecordRead$(maxBytes) AS BINARY STRING (audioRecRead of the whole frames that fit, into a buffer the next call reuses) and RecordQueued (audioRecQueued, in bytes), RecordStop (audioRecClose; True when a file was all written), Recording, RecordLength (milliseconds recorded, written or waiting), RecordLevel (audioRecPeak), RecordLost, RecordRate, RecordBits, RecordChannels (audioRecFormat), with CONSTs RECORD_MIC, RECORD_LINE, RECORD_CD (AUDIO_REC_*) and SoundCaps "record" and "recordsources".
+
+FM: FmInfo$(field) (audioFmGetInfo: "description", "voices", "free", "stereo", "owned" as text), FmAlloc, FmWrite(voice, reg, value) (True / False), FmFree, FmRawAcquire, FmRawWrite(reg, value), FmRawRelease, with CONSTs FM_* for the AUDIO_FM_* values and FM_BANK_REGS (AUDIO_FM_BANK_REGS).
+
+Wavetable: WtInfo$(field) (audioWtGetInfo: "description", "voices", "free", "memory", "freememory" (the largest free block), "freetotal" (freeTotal: all that is free), "level", and the flags "envelopes", "lfos", "filter", "effects", "8bit", "rom" as text), WtMemAlloc(bytes, bits) (an address, -1 for no room), WtUpload(address, pcm AS BINARY STRING, bits) (16-bit mono samples), WtMemFree, WtAlloc (the voice's note made plain: audioWtNoteInit), WtShape(voice, field, value) (an AudioWtVoiceT envelope, LFO, filter, or effects field by name -- "volattack", "modlfofreq", "filterfc", "reverb", ...; int16_t fields clamped -- kept in the voice's note, with audioWtUpdate at once on a sounding note for AUDIO_WT_SET_FILTER, _EFFECTS, and _LFO fields), WtStart(voice, address, startFrame, endFrame, loopStart, loopEnd, loopMode, bits, pitch, attenuation, pan) (the voice's note with the sample and these set; a negative address is AUDIO_WT_ADDR_ROM's, CONST WT_ADDR_ROM; pitch in cents, attenuation in centibels, pan -100..100 scaled to AUDIO_WT_PAN_MAX), WtSetPitch, WtSetAttenuation, WtSetPan (audioWtUpdate of the note kept for the voice; True / False), WtRelease, WtStop, WtActive, WtFree.
+
+The MIDI port, declared in music.bas: MidiPortInfo$(field) (audioMidiGetInfo: "description", "present", "owned", "input", "inputowned"), MidiAcquire, MidiPut(byteValue) (False on AUDIO_ERR_BUSY: send it again), MidiRelease; its input: MidiInOpen (True / False with SoundError$), MidiInClose, MidiInMessage (audioMidiInRead: the message packed as status + data1 * 256 + data2 * 65536, AUDIO_MIDI_PACKED_*, or MIDI_NONE, -1), MidiInTime (the ms of the message MidiInMessage gave last), MidiInClock (audioMidiInClock), MidiInLost, with CONSTs MIDI_NOTE_OFF to MIDI_PITCH_BEND, MIDI_KIND_MASK, and MIDI_CHANNEL_MASK.
+
+CONSTs SOUND_PAN_*, SOUND_PITCH_* (percent), SOUND_RATE_* (AUDIO_RATE_*), and SOUND_STREAM_MS_* (AUDIO_STREAM_MS_*).
+
+Sound, stream, and note queue numbers are 1..64 per kind, valid only for the program that got them; audioClose of a BASIC context forgets its numbers and ended voices (audioBasForget) as it frees the samples, streams, and queues. BINARY STRING parameters arrive as a pointer and a byte count (two C parameters; see the DECLARE LIBRARY rules in the BASIC reference).
diff --git a/src/libs/kpunch/audio/audio.h b/src/libs/kpunch/audio/audio.h
index f7237ccc..cb655b30 100644
--- a/src/libs/kpunch/audio/audio.h
+++ b/src/libs/kpunch/audio/audio.h
@@ -311,6 +311,14 @@ void audioStreamClose(AudioStreamT *stream);
uint32_t audioStreamQueued(const AudioStreamT *stream);
uint32_t audioStreamSpace(const AudioStreamT *stream);
+// No more frames will come after those written (a fill function may call
+// it with its last ones, which are still played): the fill function is
+// not asked again, and once the stream has played what it holds its
+// voice ends as a sample's does (the end function gets
+// AUDIO_END_FINISHED), with nothing counted as an underrun. The stream
+// stays open until audioStreamClose. NULL is ignored.
+void audioStreamEnd(AudioStreamT *stream);
+
// Drop the frames buffered and not yet played (a seek or a stop is heard
// at once; a fill function fills the room again). NULL is ignored.
void audioStreamFlush(AudioStreamT *stream);
@@ -420,6 +428,12 @@ void audioNotesClear(AudioNotesT *notes);
// than rests.
uint32_t audioNotesQueued(const AudioNotesT *notes, uint32_t *tones);
+// The waveform and loudness (0..AUDIO_VOLUME_MAX, a share of the queue's
+// volume) of the entries added from now on; the queue starts in its own
+// waveform at AUDIO_VOLUME_MAX. A tone generator plays a tone at 0 as a
+// rest and ignores the rest.
+int32_t audioNotesSetTone(AudioNotesT *notes, AudioWaveE wave, int32_t level);
+
// Entries that can still be added.
uint32_t audioNotesSpace(const AudioNotesT *notes);
@@ -693,6 +707,7 @@ typedef struct {
uint32_t freeVoices;
uint32_t memory; // bytes of sample memory
uint32_t freeMemory; // the largest free block
+ uint32_t freeTotal; // the bytes free in all (in blocks maybe apart)
uint32_t flags; // AUDIO_WT_*
int32_t level; // centibels a voice at full volume is louder than full-scale digital sound (measured; 0: as loud, or not known)
bool present;
@@ -712,7 +727,8 @@ void audioWtNoteInit(AudioWtVoiceT *v);
int32_t audioWtAlloc(AudioCtxT *ctx);
int32_t audioWtFree(const AudioCtxT *ctx, int32_t voice);
-// audioWtStart starts the voice on a note; Update changes it (what:
+// audioWtStart starts the voice on a note (AUDIO_ERR_ARG for a sample in
+// a ROM the chip does not have); Update changes it (what:
// AUDIO_WT_SET_*); Release lets the key up (the chip's envelopes release,
// a sample that loops while the key is down plays out); Stop stops it at
// once. audioWtActive is false once the voice has fallen silent (its
@@ -841,105 +857,129 @@ void audioSettingMidiInStore(PrefsHandleT *prefs, int32_t midiIn);
// its context. SoundCreate, SoundStreamWrite and WtUpload take BINARY
// STRING PCM (bytes and byte count). The Wt... functions are the
// wavetable synthesizer's in its own units (cents, centibels; pan
-// -100..100; WtMemAlloc -1 for no room), each started note plain (no
-// envelope or LFO, the filter open). The Midi... functions are declared
-// in music.bas.
-int32_t FmAlloc(void);
-void FmFree(int32_t voice);
-const char *FmInfo(const char *field);
-int32_t FmRawAcquire(void);
-void FmRawRelease(void);
-int32_t FmRawWrite(int32_t reg, int32_t value);
-int32_t FmWrite(int32_t voice, int32_t reg, int32_t value);
-int32_t MidiAcquire(void);
-int32_t MidiInClock(void);
-void MidiInClose(void);
-int32_t MidiInLost(void);
-int32_t MidiInMessage(void);
-int32_t MidiInOpen(void);
-int32_t MidiInTime(void);
-const char *MidiPortInfo(const char *field);
-int32_t MidiPut(int32_t byteValue);
-void MidiRelease(void);
-int32_t PlayNoise(int32_t frequency, int32_t milliseconds, int32_t volume);
-int32_t PlayTone(int32_t frequency, int32_t milliseconds, int32_t volume);
-int32_t PlayTriangle(int32_t frequency, int32_t milliseconds, int32_t volume);
-int32_t PlayWave(const char *fileName);
-int32_t PlayWaveLoop(const char *fileName);
-int32_t RecordBits(void);
-int32_t RecordChannels(void);
-int32_t Recording(void);
-int32_t RecordLength(void);
-int32_t RecordLevel(void);
-int32_t RecordLost(void);
-int32_t RecordRate(void);
-int32_t RecordStart(const char *fileName, int32_t rate, int32_t bits, int32_t channels, int32_t source);
-int32_t RecordStop(void);
-int32_t SoundCaps(const char *field);
-int32_t SoundCreate(const uint8_t *pcm, int32_t len, int32_t rate, int32_t bits, int32_t channels);
-const char *SoundDriverAuto(void);
-int32_t SoundDriverCount(void);
-const char *SoundDriverName(int32_t index);
-const char *SoundError(void);
-int32_t SoundEventCount(void);
-const char *SoundEventFile(int32_t prefs, int32_t eventIdx);
-const char *SoundEventKey(int32_t eventIdx);
-const char *SoundEventLabel(int32_t eventIdx);
-void SoundEventsReload(void);
-const char *SoundFolder(void);
-void SoundFree(int32_t sound);
-int32_t SoundGetVolume(void);
-const char *SoundInfo(void);
-int32_t SoundLength(int32_t sound);
-int32_t SoundLoad(const char *fileName);
-int32_t SoundMasterVolume(void);
-int32_t SoundMidiCount(void);
-int32_t SoundMidiInCount(void);
-const char *SoundMidiInLabel(int32_t index);
-const char *SoundMidiLabel(int32_t index);
-int32_t SoundModulesCount(void);
-const char *SoundModulesLabel(int32_t index);
-void SoundPause(int32_t voice);
-int32_t SoundPlay(int32_t sound, int32_t volume, int32_t pan, int32_t loopIt);
-void SoundPlayEvent(int32_t eventIdx, int32_t wait);
-int32_t SoundPlaying(int32_t voice);
-void SoundResume(int32_t voice);
-void SoundSetEventFile(int32_t prefs, int32_t eventIdx, const char *fileName);
-void SoundSetFrequency(int32_t voice, int32_t frequency);
-void SoundSetMasterVolume(int32_t percent);
-void SoundSetPan(int32_t voice, int32_t pan);
-void SoundSetPitch(int32_t voice, int32_t percent);
-const char *SoundSettingDriver(int32_t prefs);
-int32_t SoundSettingMidi(int32_t prefs);
-int32_t SoundSettingMidiIn(int32_t prefs);
-void SoundSettingMidiInStore(int32_t prefs, int32_t midiIn);
-int32_t SoundSettingModules(int32_t prefs);
-void SoundSettingsStore(int32_t prefs, const char *driver, int32_t midi, int32_t modules, int32_t volume);
-int32_t SoundSettingVolume(int32_t prefs);
-void SoundSetVolume(int32_t voice, int32_t volume);
-void SoundStreamClose(int32_t stream);
-void SoundStreamFlush(int32_t stream);
-int32_t SoundStreamOpen(int32_t rate, int32_t bits, int32_t channels, int32_t bufferMs);
-int32_t SoundStreamQueued(int32_t stream);
-int32_t SoundStreamSpace(int32_t stream);
-int32_t SoundStreamVoice(int32_t stream);
-int32_t SoundStreamWrite(int32_t stream, const uint8_t *pcm, int32_t len);
-int32_t SoundTestFile(const char *fileName);
-void SoundVolume(int32_t volume);
-void StopAllSounds(void);
-void StopWave(int32_t voice);
-int32_t WtActive(int32_t voice);
-int32_t WtAlloc(void);
-void WtFree(int32_t voice);
-const char *WtInfo(const char *field);
-int32_t WtMemAlloc(int32_t bytes, int32_t bits);
-void WtMemFree(int32_t address);
-void WtRelease(int32_t voice);
-int32_t WtSetAttenuation(int32_t voice, int32_t attenuation);
-int32_t WtSetPan(int32_t voice, int32_t pan);
-int32_t WtSetPitch(int32_t voice, int32_t pitch);
-int32_t WtStart(int32_t voice, int32_t address, int32_t startFrame, int32_t endFrame, int32_t loopStart, int32_t loopEnd, int32_t loopMode, int32_t bits, int32_t pitch, int32_t attenuation, int32_t pan);
-void WtStop(int32_t voice);
-int32_t WtUpload(int32_t address, const uint8_t *pcm, int32_t len, int32_t bits);
+// -100..100; WtMemAlloc -1 for no room), a voice's notes plain (no
+// envelope or LFO, the filter open) until WtShape shapes them.
+// SoundEnded gives a program's ended voices (audioBasEnded keeps them),
+// RecordStart with no file name records into memory for RecordRead
+// (BINARY STRING: bytes and *outLen), and the Notes... functions are
+// note queues by number. The Midi... functions are declared in
+// music.bas.
+int32_t FmAlloc(void);
+void FmFree(int32_t voice);
+const char *FmInfo(const char *field);
+int32_t FmRawAcquire(void);
+void FmRawRelease(void);
+int32_t FmRawWrite(int32_t reg, int32_t value);
+int32_t FmWrite(int32_t voice, int32_t reg, int32_t value);
+int32_t MidiAcquire(void);
+int32_t MidiInClock(void);
+void MidiInClose(void);
+int32_t MidiInLost(void);
+int32_t MidiInMessage(void);
+int32_t MidiInOpen(void);
+int32_t MidiInTime(void);
+const char *MidiPortInfo(const char *field);
+int32_t MidiPut(int32_t byteValue);
+void MidiRelease(void);
+int32_t NotesAdd(int32_t queue, double hz, double milliseconds);
+void NotesClear(int32_t queue);
+void NotesClose(int32_t queue);
+int32_t NotesOpen(int32_t wave, int32_t volume);
+int32_t NotesQueued(int32_t queue);
+int32_t NotesSetTone(int32_t queue, int32_t wave, int32_t volume);
+int32_t NotesSpace(int32_t queue);
+int32_t NotesVoice(int32_t queue);
+int32_t PlayNoise(int32_t frequency, int32_t milliseconds, int32_t volume);
+int32_t PlayTone(int32_t frequency, int32_t milliseconds, int32_t volume);
+int32_t PlayTriangle(int32_t frequency, int32_t milliseconds, int32_t volume);
+int32_t PlayWave(const char *fileName);
+int32_t PlayWaveLoop(const char *fileName);
+int32_t RecordBits(void);
+int32_t RecordChannels(void);
+int32_t Recording(void);
+int32_t RecordLength(void);
+int32_t RecordLevel(void);
+int32_t RecordLost(void);
+int32_t RecordQueued(void);
+int32_t RecordRate(void);
+const uint8_t *RecordRead(int32_t maxBytes, int32_t *outLen);
+int32_t RecordStart(const char *fileName, int32_t rate, int32_t bits, int32_t channels, int32_t source);
+int32_t RecordStop(void);
+int32_t SoundBits(int32_t sound);
+int32_t SoundCaps(const char *field);
+int32_t SoundChannels(int32_t sound);
+int32_t SoundCreate(const uint8_t *pcm, int32_t len, int32_t rate, int32_t bits, int32_t channels);
+const char *SoundDriver(void);
+const char *SoundDriverAuto(void);
+int32_t SoundDriverCount(void);
+const char *SoundDriverName(int32_t index);
+int32_t SoundEnded(void);
+int32_t SoundEndReason(void);
+const char *SoundError(void);
+int32_t SoundEventCount(void);
+const char *SoundEventFile(int32_t prefs, int32_t eventIdx);
+const char *SoundEventKey(int32_t eventIdx);
+const char *SoundEventLabel(int32_t eventIdx);
+void SoundEventsReload(void);
+const char *SoundFolder(void);
+void SoundFree(int32_t sound);
+int32_t SoundGetVolume(void);
+const char *SoundInfo(void);
+int32_t SoundLength(int32_t sound);
+int32_t SoundLoad(const char *fileName);
+int32_t SoundLoadData(const uint8_t *data, int32_t len);
+int32_t SoundMasterVolume(void);
+int32_t SoundMidiCount(void);
+int32_t SoundMidiInCount(void);
+const char *SoundMidiInLabel(int32_t index);
+const char *SoundMidiLabel(int32_t index);
+int32_t SoundModulesCount(void);
+const char *SoundModulesLabel(int32_t index);
+void SoundPause(int32_t voice);
+int32_t SoundPlay(int32_t sound, int32_t volume, int32_t pan, int32_t loopIt);
+void SoundPlayEvent(int32_t eventIdx, int32_t wait);
+int32_t SoundPlaying(int32_t voice);
+int32_t SoundRate(int32_t sound);
+void SoundResume(int32_t voice);
+void SoundSetEventFile(int32_t prefs, int32_t eventIdx, const char *fileName);
+void SoundSetFrequency(int32_t voice, int32_t frequency);
+void SoundSetMasterVolume(int32_t percent);
+void SoundSetPan(int32_t voice, int32_t pan);
+void SoundSetPitch(int32_t voice, int32_t percent);
+const char *SoundSettingDriver(int32_t prefs);
+int32_t SoundSettingMidi(int32_t prefs);
+int32_t SoundSettingMidiIn(int32_t prefs);
+void SoundSettingMidiInStore(int32_t prefs, int32_t midiIn);
+int32_t SoundSettingModules(int32_t prefs);
+void SoundSettingsStore(int32_t prefs, const char *driver, int32_t midi, int32_t modules, int32_t volume);
+int32_t SoundSettingVolume(int32_t prefs);
+void SoundSetVolume(int32_t voice, int32_t volume);
+int32_t SoundStats(const char *field);
+void SoundStreamClose(int32_t stream);
+void SoundStreamEnd(int32_t stream);
+void SoundStreamFlush(int32_t stream);
+int32_t SoundStreamOpen(int32_t rate, int32_t bits, int32_t channels, int32_t bufferMs);
+int32_t SoundStreamQueued(int32_t stream);
+int32_t SoundStreamSpace(int32_t stream);
+int32_t SoundStreamVoice(int32_t stream);
+int32_t SoundStreamWrite(int32_t stream, const uint8_t *pcm, int32_t len);
+int32_t SoundTestFile(const char *fileName);
+void SoundVolume(int32_t volume);
+void StopAllSounds(void);
+void StopWave(int32_t voice);
+int32_t WtActive(int32_t voice);
+int32_t WtAlloc(void);
+void WtFree(int32_t voice);
+const char *WtInfo(const char *field);
+int32_t WtMemAlloc(int32_t bytes, int32_t bits);
+void WtMemFree(int32_t address);
+void WtRelease(int32_t voice);
+int32_t WtSetAttenuation(int32_t voice, int32_t attenuation);
+int32_t WtSetPan(int32_t voice, int32_t pan);
+int32_t WtSetPitch(int32_t voice, int32_t pitch);
+int32_t WtShape(int32_t voice, const char *field, int32_t value);
+int32_t WtStart(int32_t voice, int32_t address, int32_t startFrame, int32_t endFrame, int32_t loopStart, int32_t loopEnd, int32_t loopMode, int32_t bits, int32_t pitch, int32_t attenuation, int32_t pan);
+void WtStop(int32_t voice);
+int32_t WtUpload(int32_t address, const uint8_t *pcm, int32_t len, int32_t bits);
#endif // AUDIO_LIB_H
diff --git a/src/libs/kpunch/audio/audioBas.c b/src/libs/kpunch/audio/audioBas.c
index 593c839c..4e8ca707 100644
--- a/src/libs/kpunch/audio/audioBas.c
+++ b/src/libs/kpunch/audio/audioBas.c
@@ -39,18 +39,22 @@
#include "audioInt.h"
#include "dvxBas.h"
+#include
#include
+#include
#include
#include
#define PERCENT_MAX 100
-#define BAS_INFO_MAX (AUDIO_DESC_MAX + 48)
#define BAS_SLOTS 64
#define MS_PER_SEC 1000
#define BITS_PER_BYTE 8
#define BYTE_MAX_VALUE 0xFF
#define SAMPLE_BYTES 2 // WtUpload's samples: 16-bit
+#define BAS_ENDED_MAX 64 // ended voices kept for SoundEnded (the oldest go first)
+#define BAS_MILLI 1000 // NotesAdd: hertz to millihertz, milliseconds to microseconds
+
#define BAS_WT_MEM_NONE (-1) // WtMemAlloc: no room
#define BAS_WT_BITS_8 8
#define BAS_WT_BITS_16 16
@@ -63,6 +67,17 @@
#define BAS_PITCH_MIN (AUDIO_PITCH_MIN * PERCENT_MAX / AUDIO_PITCH_NORMAL)
#define BAS_PITCH_MAX (AUDIO_PITCH_MAX * PERCENT_MAX / AUDIO_PITCH_NORMAL)
+// SoundStats fields
+typedef enum {
+ StatRendersE,
+ StatFramesE,
+ StatUnderrunsE,
+ StatClippedE,
+ StatDroppedE,
+ StatPlayingE,
+ StatCountE
+} SoundStatE;
+
// SoundCaps fields
typedef enum {
CapDacE,
@@ -96,6 +111,7 @@ typedef enum {
WtFreeE,
WtMemoryE,
WtFreeMemoryE,
+ WtFreeTotalE,
WtLevelE,
WtEnvelopesE,
WtLfosE,
@@ -124,6 +140,22 @@ typedef struct {
int32_t voice;
} BasWtNoteT;
+// A voice of a BASIC program that ended, until SoundEnded gives it
+typedef struct {
+ const AudioCtxT *ctx;
+ int32_t voice;
+ int32_t reason; // AudioEndE
+} BasEndedT;
+
+// What WtShape sets in a voice's note (AudioWtVoiceT), and what of a
+// sounding note audioWtUpdate changes with it (0: the next WtStart)
+typedef struct {
+ const char *name;
+ size_t offset;
+ uint8_t size; // int16_t or int32_t
+ uint32_t what;
+} BasWtShapeT;
+
// A numbered sound or stream of a BASIC program.
typedef struct {
void *obj; // AudioSampleT / AudioStreamT; NULL: free
@@ -132,18 +164,56 @@ typedef struct {
uint32_t frameBytes; // streams: bytes per frame of its format
} BasSlotT;
+static const char *const sStatNames[StatCountE] = { "renders", "frames", "underruns", "clipped", "dropped", "playing" };
static const char *const sCapNames[CapCountE] = { "dac", "synth", "tone", "voices", "running", "rate", "bits", "channels", "record", "recordsources" };
static const char *const sFmNames[FmFieldCountE] = { "description", "voices", "free", "stereo", "owned" };
static const char *const sMidiNames[MidiFieldCountE] = { "description", "present", "owned", "input", "inputowned" };
-static const char *const sWtNames[WtFieldCountE] = { "description", "voices", "free", "memory", "freememory", "level", "envelopes", "lfos", "filter", "effects", "8bit", "rom" };
+static const char *const sWtNames[WtFieldCountE] = { "description", "voices", "free", "memory", "freememory", "freetotal", "level", "envelopes", "lfos", "filter", "effects", "8bit", "rom" };
static const uint32_t sWtFlags[WtFieldCountE] = { [WtEnvelopesE] = AUDIO_WT_ENVELOPES, [WtLfosE] = AUDIO_WT_LFOS, [WtFilterE] = AUDIO_WT_FILTER, [WtEffectsE] = AUDIO_WT_EFFECTS, [Wt8BitE] = AUDIO_WT_8BIT, [WtRomE] = AUDIO_WT_ROM };
-static char sInfo[BAS_INFO_MAX];
+#define WT_SHAPE(name, field, what) { name, offsetof(AudioWtVoiceT, field), sizeof(((AudioWtVoiceT *)0)->field), what }
+
+static const BasWtShapeT sWtShapes[] = {
+ WT_SHAPE("voldelay", volEnv[AUDIO_WT_ENV_DELAY], 0),
+ WT_SHAPE("volattack", volEnv[AUDIO_WT_ENV_ATTACK], 0),
+ WT_SHAPE("volhold", volEnv[AUDIO_WT_ENV_HOLD], 0),
+ WT_SHAPE("voldecay", volEnv[AUDIO_WT_ENV_DECAY], 0),
+ WT_SHAPE("volsustain", volEnv[AUDIO_WT_ENV_SUSTAIN], 0),
+ WT_SHAPE("volrelease", volEnv[AUDIO_WT_ENV_RELEASE], 0),
+ WT_SHAPE("moddelay", modEnv[AUDIO_WT_ENV_DELAY], 0),
+ WT_SHAPE("modattack", modEnv[AUDIO_WT_ENV_ATTACK], 0),
+ WT_SHAPE("modhold", modEnv[AUDIO_WT_ENV_HOLD], 0),
+ WT_SHAPE("moddecay", modEnv[AUDIO_WT_ENV_DECAY], 0),
+ WT_SHAPE("modsustain", modEnv[AUDIO_WT_ENV_SUSTAIN], 0),
+ WT_SHAPE("modrelease", modEnv[AUDIO_WT_ENV_RELEASE], 0),
+ WT_SHAPE("modenvpitch", modEnvToPitch, 0),
+ WT_SHAPE("modenvfilter", modEnvToFilter, 0),
+ WT_SHAPE("modlfodelay", modLfoDelay, 0),
+ WT_SHAPE("modlfofreq", modLfoFreq, 0),
+ WT_SHAPE("modlfopitch", modLfoToPitch, AUDIO_WT_SET_LFO),
+ WT_SHAPE("modlfofilter", modLfoToFilter, AUDIO_WT_SET_LFO),
+ WT_SHAPE("modlfovolume", modLfoToVolume, AUDIO_WT_SET_LFO),
+ WT_SHAPE("viblfodelay", vibLfoDelay, 0),
+ WT_SHAPE("viblfofreq", vibLfoFreq, 0),
+ WT_SHAPE("viblfopitch", vibLfoToPitch, AUDIO_WT_SET_LFO),
+ WT_SHAPE("filterfc", filterFc, AUDIO_WT_SET_FILTER),
+ WT_SHAPE("filterq", filterQ, AUDIO_WT_SET_FILTER),
+ WT_SHAPE("reverb", reverb, AUDIO_WT_SET_EFFECTS),
+ WT_SHAPE("chorus", chorus, AUDIO_WT_SET_EFFECTS)
+};
+
+static BasTextT sText; // the last string result
static int32_t sLastError = AUDIO_OK;
static char sDrivers[AUDIO_DRIVERS_MAX][AUDIO_NAME_MAX];
static int32_t sDriverCount = -1; // -1: not read yet
static BasSlotT sSamples[BAS_SLOTS];
static BasSlotT sStreams[BAS_SLOTS];
+static BasSlotT sQueues[BAS_SLOTS];
+static BasEndedT sEnded[BAS_ENDED_MAX];
+static int32_t sEndedCount = 0;
+static int32_t sEndReason = AUDIO_END_FINISHED; // of the voice SoundEnded gave last
+static uint8_t *sReadBuf = NULL; // RecordRead$'s bytes (kept, grown)
+static uint32_t sReadCap = 0;
static BasWtNoteT sWtNotes[AUDIO_WT_VOICES_MAX];
static uint32_t sMidiInMs = 0; // the time of the message MidiInMessage gave last
static AudioRecT *sRec = NULL; // the recording a BASIC program runs (one at a time in DVX)
@@ -153,14 +223,17 @@ static AudioCtxT *sRecCtx = NULL; // ... and that program's context
// Prototypes
// ============================================================
+void audioBasEnded(AudioCtxT *ctx, int32_t voice, AudioEndE reason, void *userData);
void audioBasForget(const AudioCtxT *ctx);
static int32_t basAppId(void);
static int32_t basField(const char *field, const char *const *names, int32_t count);
static int32_t basNumber(BasSlotT *slots, void *obj, const AudioCtxT *ctx, uint32_t frameBytes);
static int32_t basOk(int32_t rc);
static int32_t basPercent(int32_t volume);
+static AudioNotesT *basQueue(int32_t queue);
static AudioRecT *basRec(AudioFormatT *fmt);
static int32_t basResult(int32_t rc);
+static bool basSampleFormat(int32_t sound, AudioFormatT *fmt);
static int32_t basScale(int32_t percent, int32_t lo, int32_t hi, int32_t unit);
static BasSlotT *basSlot(BasSlotT *slots, int32_t handle);
static const char *basText(int32_t value);
@@ -170,6 +243,7 @@ static void basVoiceResult(int32_t rc);
static int32_t basWave(const char *fileName, bool loop);
static BasWtNoteT *basWtNote(const AudioCtxT *ctx, int32_t voice);
static int32_t basWtSet(int32_t voice, uint32_t what, int32_t value);
+static BasWtNoteT *basWtTake(const AudioCtxT *ctx, int32_t voice);
int32_t FmAlloc(void);
void FmFree(int32_t voice);
const char *FmInfo(const char *field);
@@ -187,6 +261,14 @@ int32_t MidiInTime(void);
const char *MidiPortInfo(const char *field);
int32_t MidiPut(int32_t byteValue);
void MidiRelease(void);
+int32_t NotesAdd(int32_t queue, double hz, double milliseconds);
+void NotesClear(int32_t queue);
+void NotesClose(int32_t queue);
+int32_t NotesOpen(int32_t wave, int32_t volume);
+int32_t NotesQueued(int32_t queue);
+int32_t NotesSetTone(int32_t queue, int32_t wave, int32_t volume);
+int32_t NotesSpace(int32_t queue);
+int32_t NotesVoice(int32_t queue);
int32_t PlayNoise(int32_t frequency, int32_t milliseconds, int32_t volume);
int32_t PlayTone(int32_t frequency, int32_t milliseconds, int32_t volume);
int32_t PlayTriangle(int32_t frequency, int32_t milliseconds, int32_t volume);
@@ -198,14 +280,21 @@ int32_t Recording(void);
int32_t RecordLength(void);
int32_t RecordLevel(void);
int32_t RecordLost(void);
+int32_t RecordQueued(void);
int32_t RecordRate(void);
+const uint8_t *RecordRead(int32_t maxBytes, int32_t *outLen);
int32_t RecordStart(const char *fileName, int32_t rate, int32_t bits, int32_t channels, int32_t source);
int32_t RecordStop(void);
+int32_t SoundBits(int32_t sound);
int32_t SoundCaps(const char *field);
+int32_t SoundChannels(int32_t sound);
int32_t SoundCreate(const uint8_t *pcm, int32_t len, int32_t rate, int32_t bits, int32_t channels);
+const char *SoundDriver(void);
const char *SoundDriverAuto(void);
int32_t SoundDriverCount(void);
const char *SoundDriverName(int32_t index);
+int32_t SoundEnded(void);
+int32_t SoundEndReason(void);
const char *SoundError(void);
int32_t SoundEventCount(void);
const char *SoundEventFile(int32_t prefs, int32_t eventIdx);
@@ -218,6 +307,7 @@ int32_t SoundGetVolume(void);
const char *SoundInfo(void);
int32_t SoundLength(int32_t sound);
int32_t SoundLoad(const char *fileName);
+int32_t SoundLoadData(const uint8_t *data, int32_t len);
int32_t SoundMasterVolume(void);
int32_t SoundMidiCount(void);
int32_t SoundMidiInCount(void);
@@ -229,6 +319,7 @@ void SoundPause(int32_t voice);
int32_t SoundPlay(int32_t sound, int32_t volume, int32_t pan, int32_t loopIt);
void SoundPlayEvent(int32_t eventIdx, int32_t wait);
int32_t SoundPlaying(int32_t voice);
+int32_t SoundRate(int32_t sound);
void SoundResume(int32_t voice);
void SoundSetEventFile(int32_t prefs, int32_t eventIdx, const char *fileName);
void SoundSetFrequency(int32_t voice, int32_t frequency);
@@ -243,7 +334,9 @@ int32_t SoundSettingModules(int32_t prefs);
void SoundSettingsStore(int32_t prefs, const char *driver, int32_t midi, int32_t modules, int32_t volume);
int32_t SoundSettingVolume(int32_t prefs);
void SoundSetVolume(int32_t voice, int32_t volume);
+int32_t SoundStats(const char *field);
void SoundStreamClose(int32_t stream);
+void SoundStreamEnd(int32_t stream);
void SoundStreamFlush(int32_t stream);
int32_t SoundStreamOpen(int32_t rate, int32_t bits, int32_t channels, int32_t bufferMs);
int32_t SoundStreamQueued(int32_t stream);
@@ -264,11 +357,29 @@ void WtRelease(int32_t voice);
int32_t WtSetAttenuation(int32_t voice, int32_t attenuation);
int32_t WtSetPan(int32_t voice, int32_t pan);
int32_t WtSetPitch(int32_t voice, int32_t pitch);
+int32_t WtShape(int32_t voice, const char *field, int32_t value);
int32_t WtStart(int32_t voice, int32_t address, int32_t startFrame, int32_t endFrame, int32_t loopStart, int32_t loopEnd, int32_t loopMode, int32_t bits, int32_t pitch, int32_t attenuation, int32_t pan);
void WtStop(int32_t voice);
int32_t WtUpload(int32_t address, const uint8_t *pcm, int32_t len, int32_t bits);
+// A voice of ctx ended: kept for SoundEnded (the oldest dropped when
+// BAS_ENDED_MAX are waiting).
+void audioBasEnded(AudioCtxT *ctx, int32_t voice, AudioEndE reason, void *userData) {
+ (void)userData;
+
+ if (sEndedCount == BAS_ENDED_MAX) {
+ memmove(&sEnded[0], &sEnded[1], (size_t)(BAS_ENDED_MAX - 1) * sizeof(sEnded[0]));
+ sEndedCount--;
+ }
+
+ sEnded[sEndedCount].ctx = ctx;
+ sEnded[sEndedCount].voice = voice;
+ sEnded[sEndedCount].reason = (int32_t)reason;
+ sEndedCount++;
+}
+
+
// ctx is closing (it frees its samples and streams itself): forget their
// numbers.
void audioBasForget(const AudioCtxT *ctx) {
@@ -287,6 +398,18 @@ void audioBasForget(const AudioCtxT *ctx) {
sStreams[i].obj = NULL;
sStreams[i].ctx = NULL;
}
+
+ if (sQueues[i].ctx == ctx) {
+ sQueues[i].obj = NULL;
+ sQueues[i].ctx = NULL;
+ }
+ }
+
+ for (int32_t i = sEndedCount - 1; i >= 0; i--) {
+ if (sEnded[i].ctx == ctx) {
+ memmove(&sEnded[i], &sEnded[i + 1], (size_t)(sEndedCount - i - 1) * sizeof(sEnded[0]));
+ sEndedCount--;
+ }
}
for (int32_t i = 0; i < AUDIO_WT_VOICES_MAX; i++) {
@@ -352,6 +475,14 @@ static int32_t basPercent(int32_t volume) {
}
+// The calling program's note queue with this number, else NULL.
+static AudioNotesT *basQueue(int32_t queue) {
+ BasSlotT *s = basSlot(sQueues, queue);
+
+ return s ? (AudioNotesT *)s->obj : NULL;
+}
+
+
// The calling program's recording and its format, or NULL (fmt zeroed).
static AudioRecT *basRec(AudioFormatT *fmt) {
memset(fmt, 0, sizeof(*fmt));
@@ -372,6 +503,23 @@ static int32_t basResult(int32_t rc) {
}
+// The calling program's sound's format, false (fmt zeroed) for no such
+// sound.
+static bool basSampleFormat(int32_t sound, AudioFormatT *fmt) {
+ BasSlotT *s = basSlot(sSamples, sound);
+ uint32_t frames = 0;
+
+ memset(fmt, 0, sizeof(*fmt));
+
+ if (!s) {
+ return false;
+ }
+
+ audioSampleInfo((const AudioSampleT *)s->obj, fmt, &frames);
+ return true;
+}
+
+
// percent clamped to lo..hi, in the library's units (unit per
// PERCENT_MAX).
static int32_t basScale(int32_t percent, int32_t lo, int32_t hi, int32_t unit) {
@@ -395,8 +543,7 @@ static BasSlotT *basSlot(BasSlotT *slots, int32_t handle) {
// A number or a truth value as text, in the buffer the next call reuses.
static const char *basText(int32_t value) {
- snprintf(sInfo, sizeof(sInfo), "%ld", (long)value);
- return sInfo;
+ return basTextFormat(&sText, "%ld", (long)value);
}
@@ -472,6 +619,22 @@ static int32_t basWtSet(int32_t voice, uint32_t what, int32_t value) {
}
+// A note slot for voice of ctx, its note plain; NULL when every slot is
+// taken.
+static BasWtNoteT *basWtTake(const AudioCtxT *ctx, int32_t voice) {
+ for (int32_t i = 0; i < AUDIO_WT_VOICES_MAX; i++) {
+ if (!sWtNotes[i].ctx) {
+ sWtNotes[i].ctx = ctx;
+ sWtNotes[i].voice = voice;
+ audioWtNoteInit(&sWtNotes[i].note);
+ return &sWtNotes[i];
+ }
+ }
+
+ return NULL;
+}
+
+
// A shared two-operator FM voice (FmFree gives it back), or 0.
int32_t FmAlloc(void) {
AudioCtxT *ctx = audioBasicContext();
@@ -498,8 +661,7 @@ const char *FmInfo(const char *field) {
switch ((FmFieldE)basField(field, sFmNames, FmFieldCountE)) {
case FmDescriptionE:
- snprintf(sInfo, sizeof(sInfo), "%s", info.description);
- return sInfo;
+ return basTextSet(&sText, info.description);
case FmVoicesE:
return basText((int32_t)info.voices);
@@ -631,8 +793,7 @@ const char *MidiPortInfo(const char *field) {
switch ((MidiFieldE)basField(field, sMidiNames, MidiFieldCountE)) {
case MidiDescriptionE:
- snprintf(sInfo, sizeof(sInfo), "%s", info.description);
- return sInfo;
+ return basTextSet(&sText, info.description);
case MidiPresentE:
return basText(basTruth(info.present));
@@ -676,6 +837,83 @@ void MidiRelease(void) {
}
+// Append a tone of hz (0: a rest) lasting milliseconds; True when it was
+// added (False when the queue is full or hz is out of range).
+int32_t NotesAdd(int32_t queue, double hz, double milliseconds) {
+ AudioNotesT *notes = basQueue(queue);
+
+ if (!notes || hz < 0 || milliseconds < 0) {
+ return basOk(AUDIO_ERR_ARG);
+ }
+
+ return basOk(audioNotesAdd(notes, (uint32_t)(hz * BAS_MILLI + 0.5), (uint32_t)(milliseconds * BAS_MILLI + 0.5)));
+}
+
+
+void NotesClear(int32_t queue) {
+ audioNotesClear(basQueue(queue));
+}
+
+
+void NotesClose(int32_t queue) {
+ BasSlotT *s = basSlot(sQueues, queue);
+
+ if (s) {
+ audioNotesClose((AudioNotesT *)s->obj);
+ s->obj = NULL;
+ s->ctx = NULL;
+ }
+}
+
+
+// A queue of tones in wave (SOUND_WAVE_*) at volume percent: its number,
+// or 0 (SoundError$ says why).
+int32_t NotesOpen(int32_t wave, int32_t volume) {
+ AudioCtxT *ctx = audioBasicContext();
+ AudioNotesT *notes = NULL;
+
+ if (!ctx) {
+ return basResult(AUDIO_ERR_NO_DEVICE);
+ }
+
+ int32_t rc = audioNotesOpen(ctx, (AudioWaveE)wave, audioPercentToVolume(volume), ¬es);
+
+ return rc < 0 ? basResult(rc) : basNumber(sQueues, notes, ctx, 0);
+}
+
+
+// Entries not yet finished (playing or waiting; 0 once it has gone quiet).
+int32_t NotesQueued(int32_t queue) {
+ AudioNotesT *notes = basQueue(queue);
+
+ return notes ? (int32_t)audioNotesQueued(notes, NULL) : 0;
+}
+
+
+// The waveform and volume (percent of the queue's) of the tones added
+// from now on; True when it worked.
+int32_t NotesSetTone(int32_t queue, int32_t wave, int32_t volume) {
+ AudioNotesT *notes = basQueue(queue);
+
+ return basOk(notes ? audioNotesSetTone(notes, (AudioWaveE)wave, basScale(volume, 0, PERCENT_MAX, AUDIO_VOLUME_MAX)) : AUDIO_ERR_ARG);
+}
+
+
+int32_t NotesSpace(int32_t queue) {
+ AudioNotesT *notes = basQueue(queue);
+
+ return notes ? (int32_t)audioNotesSpace(notes) : 0;
+}
+
+
+// The queue's voice, for SoundSetVolume and SoundSetPan.
+int32_t NotesVoice(int32_t queue) {
+ AudioNotesT *notes = basQueue(queue);
+
+ return notes ? audioNotesVoice(notes) : 0;
+}
+
+
int32_t PlayNoise(int32_t frequency, int32_t milliseconds, int32_t volume) {
return basTone(AUDIO_WAVE_NOISE, frequency, milliseconds, volume);
}
@@ -754,6 +992,15 @@ int32_t RecordLost(void) {
}
+// Bytes recorded into memory and not yet taken by RecordRead$.
+int32_t RecordQueued(void) {
+ AudioFormatT fmt;
+ AudioRecT *rec = basRec(&fmt);
+
+ return rec ? (int32_t)(audioRecQueued(rec) * fmt.channels * (fmt.bits / BITS_PER_BYTE)) : 0;
+}
+
+
// The rate the card records at (the nearest it has to the one asked).
int32_t RecordRate(void) {
AudioFormatT fmt;
@@ -763,6 +1010,38 @@ int32_t RecordRate(void) {
}
+// Up to maxBytes of what a recording into memory has taken (whole frames,
+// in its format), oldest first; "" when nothing is waiting.
+const uint8_t *RecordRead(int32_t maxBytes, int32_t *outLen) {
+ AudioFormatT fmt;
+ AudioRecT *rec = basRec(&fmt);
+ uint32_t frameBytes = fmt.channels * (fmt.bits / BITS_PER_BYTE);
+ uint32_t frames = (rec && frameBytes > 0 && maxBytes > 0) ? (uint32_t)maxBytes / frameBytes : 0;
+
+ *outLen = 0;
+ frames = rec ? DVX_MIN(frames, audioRecQueued(rec)) : 0;
+
+ if (frames * frameBytes > sReadCap) {
+ uint8_t *grown = (uint8_t *)realloc(sReadBuf, frames * frameBytes);
+
+ if (!grown) {
+ sLastError = AUDIO_ERR_NO_MEMORY;
+ return (const uint8_t *)"";
+ }
+
+ sReadBuf = grown;
+ sReadCap = frames * frameBytes;
+ }
+
+ if (frames == 0) {
+ return (const uint8_t *)"";
+ }
+
+ *outLen = (int32_t)(audioRecRead(rec, sReadBuf, frames) * frameBytes);
+ return sReadBuf;
+}
+
+
// Record from source (RECORD_MIC, RECORD_LINE, RECORD_CD) into a WAV file
// in rate, bits and channels (0: the default). True when it started;
// False with SoundError$ saying why.
@@ -783,7 +1062,8 @@ int32_t RecordStart(const char *fileName, int32_t rate, int32_t bits, int32_t ch
fmt.bits = (uint32_t)bits;
fmt.channels = (uint32_t)channels;
- int32_t rc = audioRecFile(ctx, fileName, &fmt, source, &rec);
+ // No file name: into memory, for RecordRead$
+ int32_t rc = (fileName && fileName[0]) ? audioRecFile(ctx, fileName, &fmt, source, &rec) : audioRecOpen(ctx, &fmt, source, 0, &rec);
if (rc == AUDIO_OK) {
sRec = rec;
@@ -809,6 +1089,16 @@ int32_t RecordStop(void) {
}
+// A loaded sound's recorded bits (8 or 16), channels (1 or 2) and rate
+// (hertz); 0 for no such sound.
+int32_t SoundBits(int32_t sound) {
+ AudioFormatT fmt;
+
+ basSampleFormat(sound, &fmt);
+ return (int32_t)fmt.bits;
+}
+
+
// What the sound device can do (opens the program's context, so the
// device is started): "dac", "synth", "tone", "running", "record" True /
// False; "voices", "rate", "bits", "channels" numbers; "recordsources"
@@ -857,6 +1147,14 @@ int32_t SoundCaps(const char *field) {
}
+int32_t SoundChannels(int32_t sound) {
+ AudioFormatT fmt;
+
+ basSampleFormat(sound, &fmt);
+ return (int32_t)fmt.channels;
+}
+
+
// A sound made from raw PCM bytes (8-bit unsigned or 16-bit signed
// little-endian, stereo interleaved left, right); a trailing partial
// frame is ignored.
@@ -888,6 +1186,17 @@ int32_t SoundCreate(const uint8_t *pcm, int32_t len, int32_t rate, int32_t bits,
}
+// The short name of the sound driver running ("sb", "speaker" ...; ""
+// when none runs).
+const char *SoundDriver(void) {
+ AudioInfoT info;
+
+ audioBasicContext();
+ audioGetInfo(&info);
+ return basTextSet(&sText, info.driver);
+}
+
+
// The driver name DVX.INI uses for "find the best one".
const char *SoundDriverAuto(void) {
return AUDIO_DRIVER_AUTO;
@@ -912,6 +1221,33 @@ const char *SoundDriverName(int32_t index) {
}
+// The next voice of the calling program that ended since it was last
+// asked (each once, in the order they ended), 0 when none has.
+int32_t SoundEnded(void) {
+ const AudioCtxT *ctx = audioBasicContext();
+
+ for (int32_t i = 0; ctx && i < sEndedCount; i++) {
+ if (sEnded[i].ctx == ctx) {
+ int32_t voice = sEnded[i].voice;
+
+ sEndReason = sEnded[i].reason;
+ memmove(&sEnded[i], &sEnded[i + 1], (size_t)(sEndedCount - i - 1) * sizeof(sEnded[0]));
+ sEndedCount--;
+ return voice;
+ }
+ }
+
+ return 0;
+}
+
+
+// Why the voice SoundEnded gave last ended: SOUND_END_FINISHED,
+// SOUND_END_STOPPED or SOUND_END_DROPPED.
+int32_t SoundEndReason(void) {
+ return sEndReason;
+}
+
+
const char *SoundError(void) {
return audioErrorText(sLastError);
}
@@ -926,10 +1262,10 @@ int32_t SoundEventCount(void) {
// name alone (a file in the sound folders) or a path, in native
// separators.
const char *SoundEventFile(int32_t prefs, int32_t eventIdx) {
- static char file[DVX_MAX_PATH];
+ char file[DVX_MAX_PATH];
snprintf(file, sizeof(file), "%s", audioDeskFile(prefsBasHandle(prefs), (DvxSoundE)eventIdx));
- return platformPathNative(file);
+ return basTextSet(&sText, platformPathNative(file));
}
@@ -958,9 +1294,9 @@ void SoundEventsReload(void) {
// The folder of DVX's own sound files (SHARED\SOUNDS; the user's are in
// FileOpsKindDir$(FILEOPS_KIND_SOUNDS, True)).
const char *SoundFolder(void) {
- static char dir[DVX_MAX_PATH];
+ char dir[DVX_MAX_PATH];
- return platformKindDir(dir, sizeof(dir), DvxKindSoundsE, false) ? dir : "";
+ return basTextSet(&sText, platformKindDir(dir, sizeof(dir), DvxKindSoundsE, false) ? dir : "");
}
@@ -992,12 +1328,10 @@ const char *SoundInfo(void) {
audioGetInfo(&info);
if (!info.running) {
- snprintf(sInfo, sizeof(sInfo), "No sound device");
- } else {
- snprintf(sInfo, sizeof(sInfo), "%s, %lu Hz, %lu-bit %s", info.description, (unsigned long)info.rate, (unsigned long)info.bits, info.channels > 1 ? "stereo" : "mono");
+ return basTextSet(&sText, "No sound device");
}
- return sInfo;
+ return basTextFormat(&sText, "%s, %lu Hz, %lu-bit %s", info.description, (unsigned long)info.rate, (unsigned long)info.bits, info.channels > 1 ? "stereo" : "mono");
}
@@ -1035,6 +1369,22 @@ int32_t SoundLoad(const char *fileName) {
}
+// A WAV or VOC sound from the bytes of its file (a resource, an HTTP
+// reply): its number, or 0 (SoundError$ says why).
+int32_t SoundLoadData(const uint8_t *data, int32_t len) {
+ AudioCtxT *ctx = audioBasicContext();
+ AudioSampleT *sample = NULL;
+
+ if (!ctx) {
+ return basResult(AUDIO_ERR_NO_DEVICE);
+ }
+
+ int32_t rc = audioSampleLoadMem(ctx, data, len > 0 ? (uint32_t)len : 0, &sample);
+
+ return rc < 0 ? basResult(rc) : basNumber(sSamples, sample, ctx, 0);
+}
+
+
// The master volume of all sound, percent.
int32_t SoundMasterVolume(void) {
return basPercent(audioGetMasterVolume());
@@ -1114,6 +1464,14 @@ int32_t SoundPlaying(int32_t voice) {
}
+int32_t SoundRate(int32_t sound) {
+ AudioFormatT fmt;
+
+ basSampleFormat(sound, &fmt);
+ return (int32_t)fmt.rate;
+}
+
+
void SoundResume(int32_t voice) {
AudioCtxT *ctx = audioBasicContext();
@@ -1212,6 +1570,42 @@ void SoundSetVolume(int32_t voice, int32_t volume) {
}
+// The mixer's counters since the device started, by name (any case):
+// "renders", "frames", "underruns", "clipped", "dropped", "playing"; 0
+// for another name.
+int32_t SoundStats(const char *field) {
+ AudioStatsT stats;
+
+ audioBasicContext();
+ audioGetStats(&stats);
+
+ switch ((SoundStatE)basField(field, sStatNames, StatCountE)) {
+ case StatRendersE:
+ return (int32_t)stats.renders;
+
+ case StatFramesE:
+ return (int32_t)stats.frames;
+
+ case StatUnderrunsE:
+ return (int32_t)stats.underruns;
+
+ case StatClippedE:
+ return (int32_t)stats.clipped;
+
+ case StatDroppedE:
+ return (int32_t)stats.dropped;
+
+ case StatPlayingE:
+ return (int32_t)stats.playing;
+
+ case StatCountE:
+ break;
+ }
+
+ return 0;
+}
+
+
void SoundStreamClose(int32_t stream) {
BasSlotT *s = basSlot(sStreams, stream);
@@ -1224,6 +1618,17 @@ void SoundStreamClose(int32_t stream) {
// What the stream holds and has not played yet dropped.
+// No more will be written: the stream's voice ends once it has played
+// what it holds (audioStreamEnd).
+void SoundStreamEnd(int32_t stream) {
+ BasSlotT *s = basSlot(sStreams, stream);
+
+ if (s) {
+ audioStreamEnd((AudioStreamT *)s->obj);
+ }
+}
+
+
void SoundStreamFlush(int32_t stream) {
BasSlotT *s = basSlot(sStreams, stream);
@@ -1344,10 +1749,17 @@ int32_t WtActive(int32_t voice) {
}
+// A voice, its note plain (no envelope or LFO shapes it) until WtShape
+// sets one.
int32_t WtAlloc(void) {
- AudioCtxT *ctx = audioBasicContext();
+ AudioCtxT *ctx = audioBasicContext();
+ int32_t voice = ctx ? audioWtAlloc(ctx) : AUDIO_ERR_NO_DEVICE;
- return basResult(ctx ? audioWtAlloc(ctx) : AUDIO_ERR_NO_DEVICE);
+ if (voice > 0) {
+ basWtTake(ctx, voice);
+ }
+
+ return basResult(voice);
}
@@ -1373,8 +1785,7 @@ const char *WtInfo(const char *field) {
switch ((WtFieldE)f) {
case WtDescriptionE:
- snprintf(sInfo, sizeof(sInfo), "%s", info.description);
- return sInfo;
+ return basTextSet(&sText, info.description);
case WtVoicesE:
return basText((int32_t)info.voices);
@@ -1388,6 +1799,9 @@ const char *WtInfo(const char *field) {
case WtFreeMemoryE:
return basText((int32_t)info.freeMemory);
+ case WtFreeTotalE:
+ return basText((int32_t)info.freeTotal);
+
case WtLevelE:
return basText(info.level);
@@ -1441,6 +1855,40 @@ int32_t WtSetPitch(int32_t voice, int32_t pitch) {
}
+// One value of the shape of a voice's notes, by name (any case; see
+// sound.bas): kept for its next WtStart, and changed at once on a note
+// sounding where the chip can (filter, effects, LFO depths). True when
+// it worked.
+int32_t WtShape(int32_t voice, const char *field, int32_t value) {
+ AudioCtxT *ctx = audioBasicContext();
+ BasWtNoteT *w = basWtNote(ctx, voice);
+ int32_t f = -1;
+
+ for (int32_t i = 0; field && i < DVX_ARRAY_LEN(sWtShapes); i++) {
+ if (strcasecmp(field, sWtShapes[i].name) == 0) {
+ f = i;
+ }
+ }
+
+ if (!w || f < 0) {
+ return basOk(w ? AUDIO_ERR_ARG : AUDIO_ERR_VOICE);
+ }
+
+ const BasWtShapeT *sh = &sWtShapes[f];
+ uint8_t *at = (uint8_t *)&w->note + sh->offset;
+
+ if (sh->size == sizeof(int16_t)) {
+ int16_t v16 = (int16_t)DVX_MAX(INT16_MIN, DVX_MIN(value, INT16_MAX));
+
+ memcpy(at, &v16, sizeof(v16));
+ } else {
+ memcpy(at, &value, sizeof(value));
+ }
+
+ return basOk((sh->what && audioWtActive(ctx, voice)) ? audioWtUpdate(ctx, voice, &w->note, sh->what) : AUDIO_OK);
+}
+
+
// A plain note (audioWtNoteInit) on a sample in the memory, kept for the
// Set functions.
int32_t WtStart(int32_t voice, int32_t address, int32_t startFrame, int32_t endFrame, int32_t loopStart, int32_t loopEnd, int32_t loopMode, int32_t bits, int32_t pitch, int32_t attenuation, int32_t pan) {
@@ -1451,21 +1899,20 @@ int32_t WtStart(int32_t voice, int32_t address, int32_t startFrame, int32_t endF
return basOk(AUDIO_ERR_NO_DEVICE);
}
- if (address < 0 || startFrame < 0 || startFrame > endFrame || loopStart < 0 || loopEnd < loopStart || loopEnd > endFrame || (loopMode != AUDIO_WT_LOOP_NONE && loopMode != AUDIO_WT_LOOP_ON && loopMode != AUDIO_WT_LOOP_RELEASE) || (bits != BAS_WT_BITS_8 && bits != BAS_WT_BITS_16)) {
+ // A negative address is in the ROM (WT_ADDR_ROM + bytes from its start)
+ if (startFrame < 0 || startFrame > endFrame || loopStart < 0 || loopEnd < loopStart || loopEnd > endFrame || (loopMode != AUDIO_WT_LOOP_NONE && loopMode != AUDIO_WT_LOOP_ON && loopMode != AUDIO_WT_LOOP_RELEASE) || (bits != BAS_WT_BITS_8 && bits != BAS_WT_BITS_16)) {
return basOk(AUDIO_ERR_ARG);
}
- for (int32_t i = 0; !w && i < AUDIO_WT_VOICES_MAX; i++) {
- w = sWtNotes[i].ctx ? NULL : &sWtNotes[i];
- }
+ w = w ? w : basWtTake(ctx, voice);
if (!w) {
return basOk(AUDIO_ERR_NO_VOICE);
}
+ // The sample and the note's own values; the shape (WtShape) stays
AudioWtVoiceT *n = &w->note;
- audioWtNoteInit(n);
n->addr = (uint32_t)address;
n->offset = (uint32_t)startFrame;
n->end = (uint32_t)endFrame;
@@ -1477,11 +1924,7 @@ int32_t WtStart(int32_t voice, int32_t address, int32_t startFrame, int32_t endF
n->attenuation = attenuation < 0 ? 0 : (attenuation > AUDIO_WT_ATTEN_MAX ? AUDIO_WT_ATTEN_MAX : attenuation);
n->pan = basScale(pan, BAS_PAN_MIN, BAS_PAN_MAX, AUDIO_WT_PAN_MAX);
- int32_t rc = audioWtStart(ctx, voice, n);
-
- w->ctx = rc == AUDIO_OK ? ctx : NULL;
- w->voice = voice;
- return basOk(rc);
+ return basOk(audioWtStart(ctx, voice, n));
}
diff --git a/src/libs/kpunch/audio/audioDesk.c b/src/libs/kpunch/audio/audioDesk.c
index c599f625..413da9c9 100644
--- a/src/libs/kpunch/audio/audioDesk.c
+++ b/src/libs/kpunch/audio/audioDesk.c
@@ -24,15 +24,17 @@
//
// DVX calls audioDeskPlay (the sound hook, set by audioDeskInit) for an
// event; the event's file comes from DVX.INI [sounds], read at the first
-// sound and again after audioDeskReload. The sounds play on a context
-// opened for no program, so no program's ending cuts one short. Once the
-// exit sound has played DVX is ending: later events (windows closing as
-// the programs are ended) stay quiet.
+// sound and again after audioDeskReload, and is decoded once (kept until
+// then: a window opening or a message box does not read the disk). The
+// sounds play on a context opened for no program, so no program's ending
+// cuts one short. Once the exit sound has played DVX is ending: later
+// events (windows closing as the programs are ended) stay quiet.
#include "audioInt.h"
#include "dvxPrefs.h"
#include
+#include
// Longest a caller waits for a sound to play (dvxPlaySoundWait)
#define DESK_WAIT_MAX_US (5 * PLATFORM_US_PER_SEC)
@@ -54,11 +56,12 @@ static const DeskEventT sEvents[SoundCountE] = {
[SoundWindowCloseE] = { "windowClose", "Close window" }
};
-static AudioCtxT *sDeskCtx = NULL;
-static char sFiles[SoundCountE][DVX_MAX_PATH];
-static bool sLoaded = false;
-static bool sEnding = false;
-static bool sNoDevice = false; // no sound device started: not tried again until a reload
+static AudioCtxT *sDeskCtx = NULL;
+static char sFiles[SoundCountE][DVX_MAX_PATH];
+static AudioSampleT *sSounds[SoundCountE]; // decoded (NULL: not yet, or it did not load)
+static bool sLoaded = false;
+static bool sEnding = false;
+static bool sNoDevice = false; // no sound device started: not tried again until a reload
// ============================================================
// Prototypes
@@ -73,8 +76,11 @@ int32_t audioDeskPlayFile(const char *path);
void audioDeskReload(void);
void audioDeskSetFile(PrefsHandleT *prefs, DvxSoundE event, const char *path);
void audioDeskShutdown(void);
-static bool eventValid(DvxSoundE event);
-static void filesLoad(void);
+static bool deskOpen(void);
+static int32_t deskPlay(DvxSoundE event);
+static bool eventValid(DvxSoundE event);
+static void filesLoad(void);
+static void soundsFree(void);
const char *audioDeskFile(PrefsHandleT *prefs, DvxSoundE event) {
@@ -109,7 +115,7 @@ void audioDeskPlay(DvxSoundE event, bool wait) {
return;
}
- int32_t voice = audioDeskPlayFile(sFiles[event]);
+ int32_t voice = deskPlay(event);
sEnding = event == SoundExitE;
@@ -121,24 +127,21 @@ void audioDeskPlay(DvxSoundE event, bool wait) {
while (audioVoiceIsPlaying(sDeskCtx, voice) && platformMicros() - start < DESK_WAIT_MAX_US) {
audioPump();
- platformYield();
+
+ if (!dvxWaitPump()) {
+ return;
+ }
}
}
// Everything is done as no program, so the memory a sound takes is the
-// library's own. When no sound device starts (audioOpen logs why) the
-// sounds stay silent without trying again at every event.
+// library's own.
int32_t audioDeskPlayFile(const char *path) {
- int32_t prev = audioAppEnter(0);
+ int32_t prev = dvxMemSetApp(DVX_MEM_SYSTEM);
int32_t rc = AUDIO_ERR_NO_DEVICE;
- if (!sDeskCtx && !sNoDevice) {
- sDeskCtx = audioOpen();
- sNoDevice = !sDeskCtx;
- }
-
- if (sDeskCtx) {
+ if (deskOpen()) {
rc = audioPlayFile(sDeskCtx, path, AUDIO_VOLUME_MAX, AUDIO_PAN_CENTER, false);
if (rc < 0) {
@@ -146,12 +149,14 @@ int32_t audioDeskPlayFile(const char *path) {
}
}
- audioAppEnter(prev);
+ dvxMemSetApp(prev);
return rc;
}
+// The decoded sounds go too: the next event reads its file again.
void audioDeskReload(void) {
+ soundsFree();
sLoaded = false;
sNoDevice = false;
}
@@ -168,12 +173,49 @@ void audioDeskSetFile(PrefsHandleT *prefs, DvxSoundE event, const char *path) {
// take the hook back.
void audioDeskShutdown(void) {
dvxSetSoundHook(NULL);
+ memset(sSounds, 0, sizeof(sSounds));
sDeskCtx = NULL;
sLoaded = false;
sNoDevice = false;
}
+// The desktop's context, opened at the first sound (as no program: the
+// caller entered app 0). When no sound device starts (audioOpen logs
+// why) the sounds stay silent without trying again at every event.
+static bool deskOpen(void) {
+ if (!sDeskCtx && !sNoDevice) {
+ sDeskCtx = audioOpen();
+ sNoDevice = !sDeskCtx;
+ }
+
+ return sDeskCtx != NULL;
+}
+
+
+// The event's sound, decoded at its first play and kept, played as no
+// program. The voice, or an error (logged).
+static int32_t deskPlay(DvxSoundE event) {
+ int32_t prev = dvxMemSetApp(DVX_MEM_SYSTEM);
+ int32_t rc = AUDIO_ERR_NO_DEVICE;
+
+ if (deskOpen()) {
+ rc = sSounds[event] ? AUDIO_OK : audioSampleLoad(sDeskCtx, sFiles[event], &sSounds[event]);
+
+ if (rc == AUDIO_OK) {
+ rc = audioPlay(sSounds[event], AUDIO_VOLUME_MAX, AUDIO_PAN_CENTER, false);
+ }
+
+ if (rc < 0) {
+ dvxLog("Audio: desktop sound %s: %s", sFiles[event], audioErrorText(rc));
+ }
+ }
+
+ dvxMemSetApp(prev);
+ return rc;
+}
+
+
static bool eventValid(DvxSoundE event) {
return (int32_t)event >= 0 && event < SoundCountE;
}
@@ -184,7 +226,7 @@ static void filesLoad(void) {
return;
}
- PrefsHandleT *prefs = prefsLoad(platformDvxIniPath());
+ PrefsHandleT *prefs = prefsDvxIniShared();
// A name is found in the sound folders; one not found is kept as it
// is, so playing it logs what is missing
@@ -196,6 +238,14 @@ static void filesLoad(void) {
}
}
- prefsClose(prefs);
sLoaded = true;
}
+
+
+// Let the decoded sounds go (their voices stop).
+static void soundsFree(void) {
+ for (int32_t i = 0; i < SoundCountE; i++) {
+ audioSampleFree(sSounds[i]);
+ sSounds[i] = NULL;
+ }
+}
diff --git a/src/libs/kpunch/audio/audioDrv.h b/src/libs/kpunch/audio/audioDrv.h
index 790a83fe..845e0397 100644
--- a/src/libs/kpunch/audio/audioDrv.h
+++ b/src/libs/kpunch/audio/audioDrv.h
@@ -281,6 +281,34 @@ void audioConvert(const void *src, int16_t *dst, uint32_t count, uint32_t bit
int32_t audioDecode(const uint8_t *data, uint32_t size, int16_t **pcm, uint32_t *frames, AudioFormatT *fmt);
int32_t audioWavParse(const uint8_t *head, uint32_t headBytes, uint32_t fileBytes, AudioFormatT *fmt, uint32_t *dataAt, uint32_t *dataBytes);
+// A VOC file's sound a part at a time, for a caller that plays it as it
+// reads it (audioDecode walks it the same way): read gives len bytes of
+// the file from at (returns how many it read). audioVocBegin checks the
+// file's header (AUDIO_ERR_FORMAT: not a VOC file). Each audioVocNext
+// gives the next part: *count samples of the sound from byte *at
+// (AudioVocDataE), *count samples of silence, or the end; fmt is the
+// format the parts so far set (haveFmt), the same for all of them.
+typedef enum {
+ AudioVocDataE,
+ AudioVocSilenceE,
+ AudioVocEndE
+} AudioVocPartE;
+
+typedef struct {
+ uint32_t (*read)(void *ctx, uint32_t at, uint8_t *buf, uint32_t len);
+ void *ctx;
+ uint32_t size; // the file's bytes
+ uint32_t pos; // the next block's header
+ AudioFormatT fmt;
+ uint32_t extRate; // an extended block's format, for the next sound block
+ uint32_t extChans;
+ bool extPending;
+ bool haveFmt;
+} AudioVocT;
+
+int32_t audioVocBegin(AudioVocT *v);
+int32_t audioVocNext(AudioVocT *v, AudioVocPartE *kind, uint32_t *at, uint32_t *count);
+
// Interrupts off / back as they were, around the few instructions that
// must not be split by an interrupt (an FM chip's index/data pair, a
// table an interrupt also changes). Inline, so code that runs at
diff --git a/src/libs/kpunch/audio/audioInt.h b/src/libs/kpunch/audio/audioInt.h
index 5d51a835..bb15d0a0 100644
--- a/src/libs/kpunch/audio/audioInt.h
+++ b/src/libs/kpunch/audio/audioInt.h
@@ -38,8 +38,9 @@
// Frames mixed per pass (the accumulator's length)
// Silence after which the device's output is closed until the next voice
-// (audio.c devIdle)
-#define AUDIO_IDLE_CLOSE_SEC 3
+// (audio.c devIdle): opening it again costs a sound 25-60 ms (the card set
+// up, its first interrupt awaited), so sounds a few seconds apart keep it
+#define AUDIO_IDLE_CLOSE_SEC 10
#define MIX_CHUNK_FRAMES 256
#define MIX_ACC_CHANNELS 2
@@ -84,16 +85,20 @@ typedef struct {
volatile uint32_t head; // written by the program side
volatile uint32_t tail; // advanced by the mixer
volatile bool fed; // samples have arrived at least once
+ volatile bool ended; // no more will come (audioStreamEnd): empty is the end
} AudioRingT;
// A note queue (audioNotesOpen): tones and rests played one after the
// other. An entry is a tone of phaseStep (hz for a tone generator) for
-// frames output frames; hz 0 is a rest. head counts entries written,
+// frames output frames, in wave at level (a share of the voice's volume,
+// 0..AUDIO_VOLUME_MAX); hz 0 is a rest. head counts entries written,
// tail entries the mixer has started.
typedef struct {
uint32_t phaseStep;
uint32_t hz;
uint32_t frames;
+ uint16_t level;
+ uint8_t wave;
} MixNoteT;
typedef struct {
@@ -119,6 +124,7 @@ typedef struct {
volatile int32_t gainL; // 0..AUDIO_VOLUME_MAX: volume x pan x context x master
volatile int32_t gainR;
uint32_t phase; // tones: waveform position (32-bit turn)
+ uint32_t level; // note queues: the playing entry's share of the volume (0..AUDIO_VOLUME_MAX)
volatile uint32_t phaseStep; // tones: phase added per output frame
uint32_t lfsr; // noise shift register
volatile uint32_t remain; // timed tones, note queues: output frames left
@@ -170,6 +176,11 @@ typedef struct {
extern AudioRecRingT *volatile audioRecRing;
+// Let go of the last device's parked driver modules (audio.c): the next
+// device detects from scratch, as at DVX's start (the library's unload,
+// and tests that give DVX.INI new settings).
+void audioDevForget(void);
+
// Mixer (audioMix.c). Prime loads a voice's first two source frames
// before it is marked playing; Lock pins the mixer's code and data;
// Reset empties it for a device with this format.
@@ -217,11 +228,12 @@ bool audioWtLock(bool lock);
// The library's end of a program's life, of the shell's idle time, of a
// DOS program run with DVX stepped aside (audioSuspend, a platform suspend
// hook) and of its own unloading (audio.c; audioShell.c hooks them up),
-// the calling BASIC program's context, and making appId the current
-// program (returns the one before). audioBasForget (audioBas.c) drops the
-// BASIC numbers of a closing BASIC context's sounds and streams.
+// and the calling BASIC program's context (a program's callback is called
+// as that program: dvxMemSetApp). audioBasForget (audioBas.c) drops the
+// BASIC numbers of a closing BASIC context's sounds and streams, and
+// audioBasEnded keeps a BASIC context's ended voices for SoundEnded.
void audioAppEnded(int32_t appId);
-int32_t audioAppEnter(int32_t appId);
+void audioBasEnded(AudioCtxT *ctx, int32_t voice, AudioEndE reason, void *userData);
void audioBasForget(const AudioCtxT *ctx);
AudioCtxT *audioBasicContext(void);
void audioIdle(void *unused);
diff --git a/src/libs/kpunch/audio/audioLoad.c b/src/libs/kpunch/audio/audioLoad.c
index 3992a98b..08127edd 100644
--- a/src/libs/kpunch/audio/audioLoad.c
+++ b/src/libs/kpunch/audio/audioLoad.c
@@ -42,6 +42,7 @@
// format.
#include "audioInt.h"
+#include "dvxEndian.h"
#include
#include
@@ -88,6 +89,7 @@
#define VOC_TC16_BASE 65536
#define VOC_TC16_CLOCK 256000000
#define VOC_MODE_STEREO 1
+#define VOC_BODY_MAX VOC_NEW_HEADER_BYTES // a block's bytes read for its fields
// Sample formats
#define BITS_8 8
@@ -97,17 +99,12 @@
#define U8_SCALE 256 // 8-bit sample to 16-bit
#define CHANNELS_MAX 2
#define BYTE_MASK 0xFF
-#define SHIFT_8 8
-#define SHIFT_16 16
-#define SHIFT_24 24
// VOC output while it is assembled
typedef struct {
int16_t *pcm;
uint32_t count; // samples stored
uint32_t cap; // samples allocated
- AudioFormatT fmt;
- bool haveFmt; // a sound or silence block set the format
} VocOutT;
// ============================================================
@@ -116,15 +113,15 @@ typedef struct {
void audioConvert(const void *src, int16_t *dst, uint32_t count, uint32_t bits);
int32_t audioDecode(const uint8_t *data, uint32_t size, int16_t **pcm, uint32_t *frames, AudioFormatT *fmt);
+int32_t audioVocBegin(AudioVocT *v);
+int32_t audioVocNext(AudioVocT *v, AudioVocPartE *kind, uint32_t *at, uint32_t *count);
int32_t audioWavParse(const uint8_t *head, uint32_t headBytes, uint32_t fileBytes, AudioFormatT *fmt, uint32_t *dataAt, uint32_t *dataBytes);
static int32_t checkFormat(const AudioFormatT *fmt);
static int32_t decodeVoc(const uint8_t *data, uint32_t size, int16_t **pcm, uint32_t *frames, AudioFormatT *fmt);
static int32_t decodeWav(const uint8_t *data, uint32_t size, int16_t **pcm, uint32_t *frames, AudioFormatT *fmt);
-static uint32_t rd16(const uint8_t *p);
-static uint32_t rd24(const uint8_t *p);
-static uint32_t rd32(const uint8_t *p);
-static int32_t vocAppend(VocOutT *out, const uint8_t *src, uint32_t bytes, uint32_t silence);
-static int32_t vocSetFormat(VocOutT *out, uint32_t rate, uint32_t bits, uint32_t channels);
+static int32_t vocAppend(VocOutT *out, const uint8_t *src, uint32_t count, uint32_t bits);
+static uint32_t vocMemRead(void *ctx, uint32_t at, uint8_t *buf, uint32_t len);
+static int32_t vocSetFormat(AudioVocT *v, uint32_t rate, uint32_t bits, uint32_t channels);
void audioConvert(const void *src, int16_t *dst, uint32_t count, uint32_t bits) {
@@ -139,7 +136,7 @@ void audioConvert(const void *src, int16_t *dst, uint32_t count, uint32_t bits)
}
for (uint32_t i = 0; i < count; i++) {
- dst[i] = (int16_t)rd16(&s[i * 2]);
+ dst[i] = (int16_t)dvxGetLe16(&s[i * 2]);
}
}
@@ -160,6 +157,161 @@ int32_t audioDecode(const uint8_t *data, uint32_t size, int16_t **pcm, uint32_t
}
+int32_t audioVocBegin(AudioVocT *v) {
+ uint8_t head[VOC_HEADER_MIN_BYTES];
+
+ if (v->size < VOC_HEADER_MIN_BYTES || v->read(v->ctx, 0, head, VOC_HEADER_MIN_BYTES) != VOC_HEADER_MIN_BYTES || memcmp(head, VOC_SIGNATURE, VOC_SIGNATURE_BYTES) != 0) {
+ return v->size < VOC_HEADER_MIN_BYTES ? AUDIO_ERR_CORRUPT : AUDIO_ERR_FORMAT;
+ }
+
+ uint32_t headerBytes = dvxGetLe16(head + VOC_HEADER_SIZE_OFFSET);
+ uint32_t version = dvxGetLe16(head + VOC_VERSION_OFFSET);
+
+ if (dvxGetLe16(head + VOC_CHECK_OFFSET) != ((~version + VOC_CHECK_MAGIC) & 0xFFFF) || headerBytes < VOC_HEADER_MIN_BYTES || headerBytes > v->size) {
+ return AUDIO_ERR_CORRUPT;
+ }
+
+ v->pos = headerBytes;
+ v->haveFmt = false;
+ v->extPending = false;
+ return AUDIO_OK;
+}
+
+
+// A missing terminator block ends the file where the data ends. Blocks
+// that hold no sound (an extended block's format, markers, text, repeats)
+// are passed over.
+int32_t audioVocNext(AudioVocT *v, AudioVocPartE *kind, uint32_t *at, uint32_t *count) {
+ *kind = AudioVocEndE;
+ *at = 0;
+ *count = 0;
+
+ for (;;) {
+ uint8_t hdr[VOC_BLOCK_HEADER_BYTES];
+ uint8_t body[VOC_BODY_MAX];
+
+ if (v->pos >= v->size) {
+ return AUDIO_OK;
+ }
+
+ if (v->read(v->ctx, v->pos, hdr, 1) != 1) {
+ return AUDIO_ERR_CORRUPT;
+ }
+
+ if (hdr[0] == VOC_BLOCK_END) {
+ return AUDIO_OK;
+ }
+
+ if (v->size - v->pos < VOC_BLOCK_HEADER_BYTES || v->read(v->ctx, v->pos, hdr, VOC_BLOCK_HEADER_BYTES) != VOC_BLOCK_HEADER_BYTES) {
+ return AUDIO_ERR_CORRUPT;
+ }
+
+ uint32_t type = hdr[0];
+ uint32_t len = dvxGetLe24(hdr + 1);
+ uint32_t start = v->pos + VOC_BLOCK_HEADER_BYTES;
+
+ if (len > v->size - start) {
+ return AUDIO_ERR_CORRUPT;
+ }
+
+ uint32_t fields = len < VOC_BODY_MAX ? len : VOC_BODY_MAX;
+
+ if (v->read(v->ctx, start, body, fields) != fields) {
+ return AUDIO_ERR_CORRUPT;
+ }
+
+ v->pos = start + len;
+
+ switch (type) {
+ case VOC_BLOCK_SOUND: {
+ if (len < VOC_SOUND_HEADER_BYTES) {
+ return AUDIO_ERR_CORRUPT;
+ }
+
+ uint32_t rate = VOC_TC_CLOCK / (VOC_TC_BASE - body[0]);
+ uint32_t chans = 1;
+
+ if (v->extPending) {
+ rate = v->extRate;
+ chans = v->extChans;
+ v->extPending = false;
+ }
+
+ if (body[1] != VOC_CODEC_PCM8) {
+ return AUDIO_ERR_UNSUPPORTED;
+ }
+
+ int32_t rc = vocSetFormat(v, rate, BITS_8, chans);
+
+ *kind = AudioVocDataE;
+ *at = start + VOC_SOUND_HEADER_BYTES;
+ *count = len - VOC_SOUND_HEADER_BYTES;
+ return rc;
+ }
+
+ case VOC_BLOCK_CONTINUE:
+ if (!v->haveFmt) {
+ return AUDIO_ERR_CORRUPT;
+ }
+
+ *kind = AudioVocDataE;
+ *at = start;
+ *count = len / (v->fmt.bits / BITS_PER_BYTE);
+ return AUDIO_OK;
+
+ case VOC_BLOCK_SILENCE: {
+ if (len < VOC_SILENCE_BYTES) {
+ return AUDIO_ERR_CORRUPT;
+ }
+
+ int32_t rc = v->haveFmt ? AUDIO_OK : vocSetFormat(v, VOC_TC_CLOCK / (VOC_TC_BASE - body[2]), BITS_8, 1);
+
+ *kind = AudioVocSilenceE;
+ *count = (dvxGetLe16(body) + 1) * v->fmt.channels;
+ return rc;
+ }
+
+ case VOC_BLOCK_EXTENDED:
+ if (len < VOC_EXTENDED_BYTES) {
+ return AUDIO_ERR_CORRUPT;
+ }
+
+ if (body[3] > VOC_MODE_STEREO || body[2] != VOC_CODEC_PCM8) {
+ return AUDIO_ERR_UNSUPPORTED;
+ }
+
+ v->extChans = (uint32_t)body[3] + 1;
+ v->extRate = VOC_TC16_CLOCK / (VOC_TC16_BASE - dvxGetLe16(body)) / v->extChans;
+ v->extPending = true;
+ break;
+
+ case VOC_BLOCK_NEW_SOUND: {
+ if (len < VOC_NEW_HEADER_BYTES) {
+ return AUDIO_ERR_CORRUPT;
+ }
+
+ uint32_t codec = dvxGetLe16(body + 6);
+ uint32_t bits = body[4];
+
+ if (!((codec == VOC_CODEC_PCM8 && bits == BITS_8) || (codec == VOC_CODEC_PCM16 && bits == BITS_16))) {
+ return AUDIO_ERR_UNSUPPORTED;
+ }
+
+ int32_t rc = vocSetFormat(v, dvxGetLe32(body), bits, body[5]);
+
+ *kind = AudioVocDataE;
+ *at = start + VOC_NEW_HEADER_BYTES;
+ *count = (len - VOC_NEW_HEADER_BYTES) / (bits / BITS_PER_BYTE);
+ return rc;
+ }
+
+ default:
+ break;
+ }
+ }
+}
+
+
// The chunks walked from the start (a chunk that claims more than the
// file has is damaged, but for the data chunk: a recorder that stopped
// early leaves one, and what is there plays). The 'fmt ' chunk must lie
@@ -179,7 +331,7 @@ int32_t audioWavParse(const uint8_t *head, uint32_t headBytes, uint32_t fileByte
while (pos + CHUNK_HEADER_BYTES <= headBytes && !(fmtChunk && haveData)) {
const uint8_t *id = head + pos;
- uint32_t len = rd32(head + pos + CHUNK_SIZE_OFFSET);
+ uint32_t len = dvxGetLe32(head + pos + CHUNK_SIZE_OFFSET);
uint32_t avail = fileBytes - pos - CHUNK_HEADER_BYTES;
if (memcmp(id, "data", CHUNK_ID_BYTES) == 0) {
@@ -208,15 +360,15 @@ int32_t audioWavParse(const uint8_t *head, uint32_t headBytes, uint32_t fileByte
return AUDIO_ERR_CORRUPT;
}
- uint32_t tag = rd16(fmtChunk);
+ uint32_t tag = dvxGetLe16(fmtChunk);
if (tag == WAV_TAG_EXTENSIBLE && fmtBytes >= WAV_EXT_FMT_BYTES) {
- tag = rd16(fmtChunk + WAV_EXT_SUBFORMAT);
+ tag = dvxGetLe16(fmtChunk + WAV_EXT_SUBFORMAT);
}
- fmt->channels = rd16(fmtChunk + WAV_FMT_CHANNELS);
- fmt->rate = rd32(fmtChunk + WAV_FMT_RATE);
- fmt->bits = rd16(fmtChunk + WAV_FMT_BITS);
+ fmt->channels = dvxGetLe16(fmtChunk + WAV_FMT_CHANNELS);
+ fmt->rate = dvxGetLe32(fmtChunk + WAV_FMT_RATE);
+ fmt->bits = dvxGetLe16(fmtChunk + WAV_FMT_BITS);
if (tag != WAV_TAG_PCM || checkFormat(fmt) != AUDIO_OK) {
return AUDIO_ERR_UNSUPPORTED;
@@ -224,7 +376,7 @@ int32_t audioWavParse(const uint8_t *head, uint32_t headBytes, uint32_t fileByte
uint32_t frameBytes = fmt->channels * fmt->bits / BITS_PER_BYTE;
- if (rd16(fmtChunk + WAV_FMT_BLOCK_ALIGN) != frameBytes || *dataBytes < frameBytes) {
+ if (dvxGetLe16(fmtChunk + WAV_FMT_BLOCK_ALIGN) != frameBytes || *dataBytes < frameBytes) {
return AUDIO_ERR_CORRUPT;
}
@@ -243,139 +395,25 @@ static int32_t checkFormat(const AudioFormatT *fmt) {
static int32_t decodeVoc(const uint8_t *data, uint32_t size, int16_t **pcm, uint32_t *frames, AudioFormatT *fmt) {
- if (size < VOC_HEADER_MIN_BYTES) {
- return AUDIO_ERR_CORRUPT;
- }
-
- uint32_t headerBytes = rd16(data + VOC_HEADER_SIZE_OFFSET);
- uint32_t version = rd16(data + VOC_VERSION_OFFSET);
-
- if (rd16(data + VOC_CHECK_OFFSET) != ((~version + VOC_CHECK_MAGIC) & 0xFFFF) || headerBytes < VOC_HEADER_MIN_BYTES || headerBytes > size) {
- return AUDIO_ERR_CORRUPT;
- }
-
- VocOutT out;
- uint32_t pos = headerBytes;
- uint32_t extRate = 0;
- uint32_t extChans = 0;
- bool extPending = false;
- int32_t rc = AUDIO_OK;
+ AudioVocT v;
+ VocOutT out;
+ AudioVocPartE kind = AudioVocDataE;
+ uint32_t at;
+ uint32_t count;
+ memset(&v, 0, sizeof(v));
memset(&out, 0, sizeof(out));
+ v.read = vocMemRead;
+ v.ctx = (void *)data;
+ v.size = size;
- // A missing terminator block ends the file where the data ends.
- while (rc == AUDIO_OK && pos < size && data[pos] != VOC_BLOCK_END) {
- if (size - pos < VOC_BLOCK_HEADER_BYTES) {
- rc = AUDIO_ERR_CORRUPT;
- break;
- }
+ int32_t rc = audioVocBegin(&v);
- uint32_t type = data[pos];
- uint32_t len = rd24(data + pos + 1);
- const uint8_t *body = data + pos + VOC_BLOCK_HEADER_BYTES;
-
- if (len > size - pos - VOC_BLOCK_HEADER_BYTES) {
- rc = AUDIO_ERR_CORRUPT;
- break;
- }
-
- switch (type) {
- case VOC_BLOCK_SOUND: {
- if (len < VOC_SOUND_HEADER_BYTES) {
- rc = AUDIO_ERR_CORRUPT;
- break;
- }
-
- uint32_t rate = VOC_TC_CLOCK / (VOC_TC_BASE - body[0]);
- uint32_t chans = 1;
-
- if (extPending) {
- rate = extRate;
- chans = extChans;
- extPending = false;
- }
-
- if (body[1] != VOC_CODEC_PCM8) {
- rc = AUDIO_ERR_UNSUPPORTED;
- break;
- }
-
- rc = vocSetFormat(&out, rate, BITS_8, chans);
-
- if (rc == AUDIO_OK) {
- rc = vocAppend(&out, body + VOC_SOUND_HEADER_BYTES, len - VOC_SOUND_HEADER_BYTES, 0);
- }
-
- break;
- }
-
- case VOC_BLOCK_CONTINUE:
- rc = out.haveFmt ? vocAppend(&out, body, len, 0) : AUDIO_ERR_CORRUPT;
- break;
-
- case VOC_BLOCK_SILENCE:
- if (len < VOC_SILENCE_BYTES) {
- rc = AUDIO_ERR_CORRUPT;
- break;
- }
-
- if (!out.haveFmt) {
- rc = vocSetFormat(&out, VOC_TC_CLOCK / (VOC_TC_BASE - body[2]), BITS_8, 1);
- }
-
- if (rc == AUDIO_OK) {
- rc = vocAppend(&out, NULL, 0, rd16(body) + 1);
- }
-
- break;
-
- case VOC_BLOCK_EXTENDED:
- if (len < VOC_EXTENDED_BYTES || body[3] > VOC_MODE_STEREO) {
- rc = len < VOC_EXTENDED_BYTES ? AUDIO_ERR_CORRUPT : AUDIO_ERR_UNSUPPORTED;
- break;
- }
-
- extChans = (uint32_t)body[3] + 1;
- extRate = VOC_TC16_CLOCK / (VOC_TC16_BASE - rd16(body)) / extChans;
- extPending = true;
-
- if (body[2] != VOC_CODEC_PCM8) {
- rc = AUDIO_ERR_UNSUPPORTED;
- }
-
- break;
-
- case VOC_BLOCK_NEW_SOUND: {
- if (len < VOC_NEW_HEADER_BYTES) {
- rc = AUDIO_ERR_CORRUPT;
- break;
- }
-
- uint32_t codec = rd16(body + 6);
- uint32_t bits = body[4];
-
- if (!((codec == VOC_CODEC_PCM8 && bits == BITS_8) || (codec == VOC_CODEC_PCM16 && bits == BITS_16))) {
- rc = AUDIO_ERR_UNSUPPORTED;
- break;
- }
-
- rc = vocSetFormat(&out, rd32(body), bits, body[5]);
-
- if (rc == AUDIO_OK) {
- rc = vocAppend(&out, body + VOC_NEW_HEADER_BYTES, len - VOC_NEW_HEADER_BYTES, 0);
- }
-
- break;
- }
-
- default:
- break;
- }
-
- pos += VOC_BLOCK_HEADER_BYTES + len;
+ while (rc == AUDIO_OK && (rc = audioVocNext(&v, &kind, &at, &count)) == AUDIO_OK && kind != AudioVocEndE) {
+ rc = vocAppend(&out, kind == AudioVocDataE ? data + at : NULL, count, v.fmt.bits);
}
- if (rc == AUDIO_OK && (!out.haveFmt || out.count < out.fmt.channels)) {
+ if (rc == AUDIO_OK && (!v.haveFmt || out.count < v.fmt.channels)) {
rc = AUDIO_ERR_CORRUPT;
}
@@ -385,8 +423,8 @@ static int32_t decodeVoc(const uint8_t *data, uint32_t size, int16_t **pcm, uint
}
*pcm = out.pcm;
- *frames = out.count / out.fmt.channels;
- *fmt = out.fmt;
+ *frames = out.count / v.fmt.channels;
+ *fmt = v.fmt;
return AUDIO_OK;
}
@@ -414,37 +452,18 @@ static int32_t decodeWav(const uint8_t *data, uint32_t size, int16_t **pcm, uint
}
-static uint32_t rd16(const uint8_t *p) {
- return (uint32_t)p[0] | ((uint32_t)p[1] << SHIFT_8);
-}
-
-
-static uint32_t rd24(const uint8_t *p) {
- return rd16(p) | ((uint32_t)p[2] << SHIFT_16);
-}
-
-
-static uint32_t rd32(const uint8_t *p) {
- return rd24(p) | ((uint32_t)p[3] << SHIFT_24);
-}
-
-
-// Append bytes of sound in the current format, or silence frames of
-// silence (src NULL). A stereo frame may continue in the next block, so
-// only the total is cut to whole frames (decodeVoc).
-static int32_t vocAppend(VocOutT *out, const uint8_t *src, uint32_t bytes, uint32_t silence) {
- uint32_t add = src ? bytes / (out->fmt.bits / BITS_PER_BYTE) : silence * out->fmt.channels;
-
+// count samples of src (bits each) added to out; silence when src is NULL.
+static int32_t vocAppend(VocOutT *out, const uint8_t *src, uint32_t count, uint32_t bits) {
// An empty block adds nothing; before the first sound out->pcm is
// still NULL, and neither NULL + 0 nor memset(NULL, ..) is defined.
- if (add == 0) {
+ if (count == 0) {
return AUDIO_OK;
}
- if (out->count + add > out->cap) {
- uint32_t cap = out->cap ? out->cap : add;
+ if (out->count + count > out->cap) {
+ uint32_t cap = out->cap ? out->cap : count;
- while (cap < out->count + add) {
+ while (cap < out->count + count) {
cap *= 2;
}
@@ -459,30 +478,38 @@ static int32_t vocAppend(VocOutT *out, const uint8_t *src, uint32_t bytes, uint3
}
if (src) {
- audioConvert(src, out->pcm + out->count, add, out->fmt.bits);
+ audioConvert(src, out->pcm + out->count, count, bits);
} else {
- memset(out->pcm + out->count, 0, add * sizeof(int16_t));
+ memset(out->pcm + out->count, 0, count * sizeof(int16_t));
}
- out->count += add;
+ out->count += count;
return AUDIO_OK;
}
+// audioVoc's read for a VOC image in memory (ctx: its bytes; the walker
+// reads only within v->size).
+static uint32_t vocMemRead(void *ctx, uint32_t at, uint8_t *buf, uint32_t len) {
+ memcpy(buf, (const uint8_t *)ctx + at, len);
+ return len;
+}
+
+
// The first sound or silence block sets the format; a later one that
// differs is refused.
-static int32_t vocSetFormat(VocOutT *out, uint32_t rate, uint32_t bits, uint32_t channels) {
+static int32_t vocSetFormat(AudioVocT *v, uint32_t rate, uint32_t bits, uint32_t channels) {
AudioFormatT fmt = { rate, bits, channels };
if (checkFormat(&fmt) != AUDIO_OK) {
return AUDIO_ERR_UNSUPPORTED;
}
- if (!out->haveFmt) {
- out->fmt = fmt;
- out->haveFmt = true;
+ if (!v->haveFmt) {
+ v->fmt = fmt;
+ v->haveFmt = true;
return AUDIO_OK;
}
- return (fmt.rate == out->fmt.rate && fmt.bits == out->fmt.bits && fmt.channels == out->fmt.channels) ? AUDIO_OK : AUDIO_ERR_UNSUPPORTED;
+ return (fmt.rate == v->fmt.rate && fmt.bits == v->fmt.bits && fmt.channels == v->fmt.channels) ? AUDIO_OK : AUDIO_ERR_UNSUPPORTED;
}
diff --git a/src/libs/kpunch/audio/audioMix.c b/src/libs/kpunch/audio/audioMix.c
index 70b5b8da..20922a73 100644
--- a/src/libs/kpunch/audio/audioMix.c
+++ b/src/libs/kpunch/audio/audioMix.c
@@ -311,8 +311,9 @@ static void endVoice(MixVoiceT *v, AudioEndE reason) {
// Fetch the source frame after next into next. False when a sample that
-// does not loop has no more frames; a stream with nothing buffered gives
-// silence (counted as an underrun once it has had samples). A source has
+// does not loop has no more frames, or an ended stream (audioStreamEnd)
+// has played what it held; a stream with nothing buffered gives silence
+// (counted as an underrun once it has had samples). A source has
// one or two channels (audio.c checks): a frame's index is a shift, not
// a multiply.
static bool loadNext(MixVoiceT *v) {
@@ -320,6 +321,10 @@ static bool loadNext(MixVoiceT *v) {
AudioRingT *r = v->ring;
if (r->head == r->tail) {
+ if (r->ended) {
+ return false;
+ }
+
v->nextL = 0;
v->nextR = 0;
@@ -358,10 +363,8 @@ static bool loadNext(MixVoiceT *v) {
// A note queue plays its entries back to back, each for exactly its
// frames; with none left it is silent (toneHz 0) until more arrive.
static void mixNotes(MixVoiceT *v, int32_t *acc, uint32_t n) {
- MixNoteRingT *q = v->notes;
- int32_t gainL = v->gainL;
- int32_t gainR = v->gainR;
- uint32_t i = 0;
+ MixNoteRingT *q = v->notes;
+ uint32_t i = 0;
while (i < n) {
if (v->remain == 0) {
@@ -374,11 +377,16 @@ static void mixNotes(MixVoiceT *v, int32_t *acc, uint32_t n) {
v->phaseStep = e->phaseStep;
v->toneHz = e->hz;
v->remain = e->frames;
+ v->wave = e->wave;
+ v->level = e->level;
q->tail++;
continue;
}
- uint32_t k = v->remain < n - i ? v->remain : n - i;
+ // The entry's level of the voice's gain
+ int32_t gainL = (int32_t)(((uint32_t)v->gainL * v->level) >> MIX_GAIN_SHIFT);
+ int32_t gainR = (int32_t)(((uint32_t)v->gainR * v->level) >> MIX_GAIN_SHIFT);
+ uint32_t k = v->remain < n - i ? v->remain : n - i;
for (uint32_t j = 0; j < k && v->toneHz; j++) {
int32_t smp = toneSample(v);
diff --git a/src/libs/kpunch/audio/audioRec.c b/src/libs/kpunch/audio/audioRec.c
index 45a26ec0..2116f131 100644
--- a/src/libs/kpunch/audio/audioRec.c
+++ b/src/libs/kpunch/audio/audioRec.c
@@ -35,6 +35,7 @@
// voices that were held play on.
#include "audioInt.h"
+#include "dvxEndian.h"
#include
#include
@@ -55,10 +56,6 @@
#define PERCENT 100
#define U8_BIAS 128
#define U8_SCALE 256 // an 8-bit sample's step in 16-bit terms
-#define BYTE_SHIFT 8
-#define BYTE_MASK 0xFF
-#define WORD_SHIFT 16
-#define WORD_MASK 0xFFFF
#define WORD_BYTES 2
// WAV: RIFF, the fmt chunk (PCM), the data chunk's header
@@ -111,8 +108,6 @@ uint32_t audioRecRead(AudioRecT *rec, void *pcm, uint32_t frames);
void audioRecSuspend(void);
void audioWavHeader(uint8_t *out, const AudioFormatT *fmt, uint32_t dataBytes);
static uint32_t frameBytesOf(const AudioFormatT *fmt);
-static void put16(uint8_t *p, uint32_t v);
-static void put32(uint8_t *p, uint32_t v);
static void putTag(uint8_t *p, const char *tag);
static AudioRecT *recAlloc(uint32_t bytes);
static int32_t recBegin(AudioCtxT *ctx, AudioFormatT *fmt, int32_t source, uint32_t bufferMs, const char *path, AudioRecT **out);
@@ -241,18 +236,18 @@ void audioWavHeader(uint8_t *out, const AudioFormatT *fmt, uint32_t dataBytes) {
uint32_t align = frameBytesOf(fmt);
putTag(out, "RIFF");
- put32(out + WAV_AT_RIFF_SIZE, dataBytes + WAV_RIFF_EXTRA);
+ dvxPutLe32(out + WAV_AT_RIFF_SIZE, dataBytes + WAV_RIFF_EXTRA);
putTag(out + WAV_AT_WAVE, "WAVE");
putTag(out + WAV_AT_FMT, "fmt ");
- put32(out + WAV_AT_FMT_SIZE, WAV_FMT_BYTES);
- put16(out + WAV_AT_FORMAT, WAV_FORMAT_PCM);
- put16(out + WAV_AT_CHANNELS, fmt->channels);
- put32(out + WAV_AT_RATE, fmt->rate);
- put32(out + WAV_AT_BYTE_RATE, fmt->rate * align);
- put16(out + WAV_AT_ALIGN, align);
- put16(out + WAV_AT_BITS, fmt->bits);
+ dvxPutLe32(out + WAV_AT_FMT_SIZE, WAV_FMT_BYTES);
+ dvxPutLe16(out + WAV_AT_FORMAT, WAV_FORMAT_PCM);
+ dvxPutLe16(out + WAV_AT_CHANNELS, fmt->channels);
+ dvxPutLe32(out + WAV_AT_RATE, fmt->rate);
+ dvxPutLe32(out + WAV_AT_BYTE_RATE, fmt->rate * align);
+ dvxPutLe16(out + WAV_AT_ALIGN, align);
+ dvxPutLe16(out + WAV_AT_BITS, fmt->bits);
putTag(out + WAV_AT_DATA, "data");
- put32(out + WAV_AT_DATA_SIZE, dataBytes);
+ dvxPutLe32(out + WAV_AT_DATA_SIZE, dataBytes);
}
@@ -261,18 +256,6 @@ static uint32_t frameBytesOf(const AudioFormatT *fmt) {
}
-static void put16(uint8_t *p, uint32_t v) {
- p[0] = (uint8_t)(v & BYTE_MASK);
- p[1] = (uint8_t)((v >> BYTE_SHIFT) & BYTE_MASK);
-}
-
-
-static void put32(uint8_t *p, uint32_t v) {
- put16(p, v & WORD_MASK);
- put16(p + WORD_BYTES, v >> WORD_SHIFT);
-}
-
-
// A chunk's four-character tag (no terminator).
static void putTag(uint8_t *p, const char *tag) {
for (int32_t i = 0; i < FOURCC_BYTES; i++) {
@@ -436,7 +419,7 @@ static void recPeak(AudioRecT *rec, const uint8_t *p, uint32_t bytes) {
if (rec->fmt.bits == BITS_16) {
for (uint32_t i = 0; i + 1 < bytes; i += WORD_BYTES) {
- int32_t v = (int16_t)(p[i] | (p[i + 1] << BYTE_SHIFT));
+ int32_t v = (int16_t)dvxGetLe16(p + i);
v = v < 0 ? -v : v;
peak = v > peak ? v : peak;
diff --git a/src/libs/kpunch/audio/audioWt.c b/src/libs/kpunch/audio/audioWt.c
index ef812a9e..f70a3732 100644
--- a/src/libs/kpunch/audio/audioWt.c
+++ b/src/libs/kpunch/audio/audioWt.c
@@ -37,13 +37,14 @@
// aligned, and a 16-bit one inside a 256 KB boundary where the chip asks.
#include "audioInt.h"
+#include "stb_ds.h"
#include
#include
#define WT_ID_STRIDE 64
#define WT_GEN_MAX (INT32_MAX / WT_ID_STRIDE - 1)
-#define WT_BLOCKS_MAX 1024
+#define WT_BLOCKS_MAX 1024 // blocks of memory given out at once
#define WT_ALIGN 32 // bytes a block starts on
#define WT_BANK_BYTES 0x40000U // AUDIO_WT_BANK_256K
#define BITS_8 8
@@ -75,8 +76,7 @@ typedef struct {
} WtChipT;
static WtChipT sWt;
-static WtBlockT sBlocks[WT_BLOCKS_MAX]; // in use, by address
-static uint32_t sBlockCount;
+static WtBlockT *sBlocks = NULL; // stb_ds: in use, by address
// ============================================================
// Prototypes
@@ -101,7 +101,6 @@ int32_t audioWtUpload(AudioCtxT *ctx, uint32_t addr, const int16_t *frame
static int32_t blockOf(const AudioCtxT *ctx, uint32_t addr, uint32_t bytes);
static void giveBack(uint32_t voice);
static int32_t lookup(const AudioCtxT *ctx, int32_t voice);
-static void removeBlock(uint32_t i);
// Interrupt-safe.
@@ -151,7 +150,7 @@ int32_t audioWtAlloc(AudioCtxT *ctx) {
void audioWtAttach(const AudioSynthDriverT *drv, const AudioSynthDrvInfoT *info) {
memset(&sWt, 0, sizeof(sWt));
- sBlockCount = 0;
+ arrfree(sBlocks);
sWt.drv = drv;
sWt.count = info->voices < AUDIO_WT_VOICES_MAX ? info->voices : AUDIO_WT_VOICES_MAX;
sWt.memory = info->memory;
@@ -173,9 +172,9 @@ void audioWtCtxClosed(AudioCtxT *ctx) {
audioIrqRestore(flags);
- for (uint32_t i = sBlockCount; i > 0; i--) {
- if (sBlocks[i - 1].owner == ctx) {
- removeBlock(i - 1);
+ for (int32_t i = (int32_t)arrlen(sBlocks) - 1; i >= 0; i--) {
+ if (sBlocks[i].owner == ctx) {
+ arrdel(sBlocks, i);
}
}
}
@@ -183,7 +182,7 @@ void audioWtCtxClosed(AudioCtxT *ctx) {
void audioWtDetach(void) {
memset(&sWt, 0, sizeof(sWt));
- sBlockCount = 0;
+ arrfree(sBlocks);
}
@@ -203,6 +202,7 @@ int32_t audioWtFree(const AudioCtxT *ctx, int32_t voice) {
void audioWtGetInfo(AudioWtInfoT *info) {
uint32_t largest = 0;
+ uint32_t total = 0;
uint32_t at = 0;
memset(info, 0, sizeof(*info));
@@ -212,17 +212,21 @@ void audioWtGetInfo(AudioWtInfoT *info) {
}
// The largest gap between the blocks in use
- for (uint32_t i = 0; i <= sBlockCount; i++) {
- uint32_t next = i < sBlockCount ? sBlocks[i].addr : sWt.memory;
+ uint32_t count = (uint32_t)arrlen(sBlocks);
+
+ for (uint32_t i = 0; i <= count; i++) {
+ uint32_t next = i < count ? sBlocks[i].addr : sWt.memory;
largest = next - at > largest ? next - at : largest;
- at = i < sBlockCount ? sBlocks[i].addr + sBlocks[i].bytes : at;
+ total += next - at;
+ at = i < count ? sBlocks[i].addr + sBlocks[i].bytes : at;
}
snprintf(info->description, sizeof(info->description), "%s", sWt.description);
info->voices = sWt.count;
info->memory = sWt.memory;
info->freeMemory = largest;
+ info->freeTotal = total;
info->flags = sWt.flags;
info->level = sWt.level;
info->present = true;
@@ -265,14 +269,16 @@ int32_t audioWtMemAlloc(AudioCtxT *ctx, uint32_t bytes, uint32_t bits, uint32_t
return AUDIO_ERR_NO_SYNTH;
}
- if (sBlockCount >= WT_BLOCKS_MAX || (bankRule && bytes > WT_BANK_BYTES)) {
+ uint32_t count = (uint32_t)arrlen(sBlocks);
+
+ if (count >= WT_BLOCKS_MAX || (bankRule && bytes > WT_BANK_BYTES)) {
return AUDIO_ERR_NO_ROOM;
}
uint32_t gapStart = 0;
- for (uint32_t i = 0; i <= sBlockCount; i++) {
- uint32_t gapEnd = i < sBlockCount ? sBlocks[i].addr : sWt.memory;
+ for (uint32_t i = 0; i <= count; i++) {
+ uint32_t gapEnd = i < count ? sBlocks[i].addr : sWt.memory;
uint32_t start = (gapStart + WT_ALIGN - 1) / WT_ALIGN * WT_ALIGN;
if (bankRule && start / WT_BANK_BYTES != (start + bytes - 1) / WT_BANK_BYTES) {
@@ -280,16 +286,14 @@ int32_t audioWtMemAlloc(AudioCtxT *ctx, uint32_t bytes, uint32_t bits, uint32_t
}
if (start <= gapEnd && bytes <= gapEnd - start) {
- memmove(&sBlocks[i + 1], &sBlocks[i], (sBlockCount - i) * sizeof(WtBlockT));
- sBlocks[i].owner = ctx;
- sBlocks[i].addr = start;
- sBlocks[i].bytes = bytes;
- sBlockCount++;
+ WtBlockT block = { ctx, start, bytes };
+
+ arrins(sBlocks, i, block);
*addr = start;
return AUDIO_OK;
}
- gapStart = i < sBlockCount ? sBlocks[i].addr + sBlocks[i].bytes : gapStart;
+ gapStart = i < count ? sBlocks[i].addr + sBlocks[i].bytes : gapStart;
}
return AUDIO_ERR_NO_ROOM;
@@ -297,9 +301,9 @@ int32_t audioWtMemAlloc(AudioCtxT *ctx, uint32_t bytes, uint32_t bits, uint32_t
int32_t audioWtMemFree(AudioCtxT *ctx, uint32_t addr) {
- for (uint32_t i = 0; i < sBlockCount; i++) {
+ for (int32_t i = 0; i < (int32_t)arrlen(sBlocks); i++) {
if (sBlocks[i].owner == ctx && sBlocks[i].addr == addr) {
- removeBlock(i);
+ arrdel(sBlocks, i);
return AUDIO_OK;
}
}
@@ -342,6 +346,11 @@ int32_t audioWtStart(const AudioCtxT *ctx, int32_t voice, const AudioWtVoiceT *v
return AUDIO_ERR_VOICE;
}
+ // A sample in a ROM the chip does not have
+ if ((v->addr & AUDIO_WT_ADDR_ROM) && !(sWt.flags & AUDIO_WT_ROM)) {
+ return AUDIO_ERR_ARG;
+ }
+
sWt.drv->start((uint32_t)i, v);
return AUDIO_OK;
}
@@ -389,11 +398,11 @@ int32_t audioWtUpload(AudioCtxT *ctx, uint32_t addr, const int16_t *frames, uint
// The block of ctx holding addr .. addr + bytes - 1 (its index), -1 when
// none does.
static int32_t blockOf(const AudioCtxT *ctx, uint32_t addr, uint32_t bytes) {
- for (uint32_t i = 0; i < sBlockCount; i++) {
+ for (int32_t i = 0; i < (int32_t)arrlen(sBlocks); i++) {
const WtBlockT *b = &sBlocks[i];
if (b->owner == ctx && addr >= b->addr && bytes <= b->bytes && addr - b->addr <= b->bytes - bytes) {
- return (int32_t)i;
+ return i;
}
}
@@ -422,9 +431,3 @@ static int32_t lookup(const AudioCtxT *ctx, int32_t voice) {
return i;
}
-
-
-static void removeBlock(uint32_t i) {
- memmove(&sBlocks[i], &sBlocks[i + 1], (sBlockCount - i - 1) * sizeof(WtBlockT));
- sBlockCount--;
-}
diff --git a/src/libs/kpunch/conn/Makefile b/src/libs/kpunch/conn/Makefile
index 1822bbb9..55e57ade 100644
--- a/src/libs/kpunch/conn/Makefile
+++ b/src/libs/kpunch/conn/Makefile
@@ -26,12 +26,12 @@
# behind one interface (see conn.h) -- and their BASIC face (connBas.c,
# src/include/basic/conn.bas), which runs transfers with the transfer
# library and writes to Terminal controls. It sits on the serial library
-# (rs232, secLink) and the networking library (BSD sockets: its SDK
+# (rs232), the secure link library, and the networking library (BSD sockets: its SDK
# headers come first on the include path).
include ../../../common.mk
-CFLAGS = $(DOS_CFLAGS) -I. -I../dvxnet/include -I../libdvx -I../libdvx/platform -I../libdvx/thirdparty -I../../../widgets/kpunch
+CFLAGS = $(DOS_CFLAGS) -I. -I.. -I../dvxnet/include -I../libdvx -I../libdvx/platform -I../libdvx/thirdparty -I../../../widgets/kpunch
OBJDIR = ../../../../obj/conn
LIBSDIR = ../../../../bin/libs
diff --git a/src/libs/kpunch/conn/conn.c b/src/libs/kpunch/conn/conn.c
index 585b7608..132d5812 100644
--- a/src/libs/kpunch/conn/conn.c
+++ b/src/libs/kpunch/conn/conn.c
@@ -24,18 +24,24 @@
// per connection type; the common code owns the receive and send buffers,
// the status text and the break timer. Telnet and the secure link over
// the network use the standard BSD socket API (the networking library on
-// DOS), always non-blocking, so connPoll never waits.
+// DOS), always non-blocking, so connPoll never waits. The networking
+// library is an optional part: its calls are looked up when a network
+// connection starts (netFind), and without them such a connection ends at
+// once saying so, while serial, modem, and secure links over a COM port
+// work as ever.
#include "conn.h"
#include "connTelnet.h"
+#include "dvxPlat.h"
#include "dvxTypes.h"
+#include "dvxnet/dvxNet.h"
#include "../serial/rs232/rs232.h"
-#include "../serial/seclink/secLink.h"
+#include "../seclink/secLink.h"
#include
+#include
#include
#include
-#include
#include
#include
#include
@@ -63,6 +69,7 @@ DVX_ABI_STAMP(conn);
#define DEFAULT_ROWS 25
#define MS_PER_SEC 1000
#define COM_NUMBER_BASE 1 // RS232_COM1 is shown as COM1
+#define NO_NET_TEXT "Networking is not installed"
// Receive room the secure link needs before it is polled: one poll can
// deliver everything the UART buffered (secLink has already acknowledged
@@ -121,12 +128,28 @@ struct ConnS {
bool portOpen;
bool withPass; // secure link: a passphrase authenticates it
bool tcpConnecting; // secure link over the network: TCP not up yet
+ bool looking; // Telnet / secure link over the network: the host being looked up
+ NetLookupT lookup;
int32_t sock; // Telnet / secure link over the network
int32_t cols; // window size for Telnet (NAWS)
int32_t rows;
TelnetT telnet;
};
+// The networking library's calls (see netFind)
+typedef struct {
+ __typeof__(&connect) connect;
+ __typeof__(&fcntl) fcntl;
+ __typeof__(&getsockopt) getsockopt;
+ __typeof__(&netLookupCancel) lookupCancel;
+ __typeof__(&netLookupPoll) lookupPoll;
+ __typeof__(&netLookupStart) lookupStart;
+ __typeof__(&poll) poll;
+ __typeof__(&recv) recv;
+ __typeof__(&send) send;
+ __typeof__(&socket) socket;
+} NetCallsT;
+
// Modem result lines, matched at the start of a line (any case)
typedef enum {
ResultOkE,
@@ -162,6 +185,7 @@ static void modemLine(ConnT *c, uint32_t nowMs);
static int32_t modemOpen(ConnT *c, uint32_t nowMs);
static void modemPoll(ConnT *c, uint32_t nowMs);
static void modemRetry(ConnT *c, const char *reason, uint32_t nowMs);
+static bool netFind(void);
static void rs232Text(int32_t rc, char *out, int32_t outSize);
static void rxPut(ConnT *c, const uint8_t *data, int32_t len);
static int32_t rxSpace(const ConnT *c);
@@ -181,9 +205,10 @@ static int32_t serialSend(ConnT *c, const uint8_t *data, int32_t len);
static void setStatus(ConnT *c, const char *fmt, ...);
static int32_t sockRead(void *ctx, uint8_t *buf, int32_t len);
static int32_t sockWrite(void *ctx, const uint8_t *data, int32_t len);
-static int32_t tcpBegin(ConnT *c, uint32_t nowMs);
+static int32_t tcpBegin(ConnT *c);
static int32_t tcpCheck(ConnT *c, uint32_t nowMs);
static void tcpClose(ConnT *c);
+static bool tcpConnect(ConnT *c, uint32_t addr, uint32_t nowMs);
static void tcpFailed(ConnT *c, int32_t err);
static void telnetClose(ConnT *c);
static int32_t telnetOpen(ConnT *c, uint32_t nowMs);
@@ -194,6 +219,9 @@ static bool timeReached(uint32_t nowMs, uint32_t deadline);
static void txQueue(ConnT *c, const uint8_t *data, int32_t len);
static void txQueueText(ConnT *c, const char *text);
+static NetCallsT sNet;
+static bool sNetFound = false; // every call of sNet was found
+
static const ConnOpsT sOps[ConnTypeCountE] = {
[ConnSerialE] = { true, serialOpen, serialPoll, serialSend, serialClose, serialPending },
[ConnModemE] = { true, modemOpen, modemPoll, serialSend, serialClose, serialPending },
@@ -267,8 +295,8 @@ const char *connErrorText(int32_t err) {
case CONN_ERR_PORT:
return "The port could not be opened";
- case CONN_ERR_HOST:
- return "The host could not be reached";
+ case CONN_ERR_NO_NET:
+ return NO_NET_TEXT;
default:
return "The connection could not be opened";
@@ -398,7 +426,7 @@ uint32_t connStatusSerial(const ConnT *c) {
bool connTypeAvailable(int32_t type) {
- return type >= 0 && type < ConnTypeCountE;
+ return type >= 0 && type < ConnTypeCountE && (type != ConnTelnetE || netFind());
}
@@ -440,7 +468,7 @@ static void fail(ConnT *c, const char *fmt, ...) {
// Send what the Telnet filter queued, as far as the socket takes it.
static void flushNet(ConnT *c) {
while (c->telnet.outLen > 0) {
- ssize_t n = send(c->sock, c->telnet.out, (size_t)c->telnet.outLen, MSG_DONTWAIT);
+ ssize_t n = sNet.send(c->sock, c->telnet.out, (size_t)c->telnet.outLen, MSG_DONTWAIT);
if (n <= 0) {
return;
@@ -653,6 +681,32 @@ static void modemRetry(ConnT *c, const char *reason, uint32_t nowMs) {
}
+// The networking library's calls, looked up by name (it is an optional
+// part, so conn.lib does not import them); true once every one is found.
+// A call not found yet is looked for again at the next network
+// connection. fcntl and poll are the C library's too, but a lookup finds
+// a loaded module's name before dvx.exe's, so these are the networking
+// library's own (the ones that know its sockets).
+static bool netFind(void) {
+ if (sNetFound) {
+ return true;
+ }
+
+ sNet.connect = dlsym(NULL, DVX_SYM("connect"));
+ sNet.fcntl = dlsym(NULL, DVX_SYM("fcntl"));
+ sNet.getsockopt = dlsym(NULL, DVX_SYM("getsockopt"));
+ sNet.lookupCancel = dlsym(NULL, DVX_SYM("netLookupCancel"));
+ sNet.lookupPoll = dlsym(NULL, DVX_SYM("netLookupPoll"));
+ sNet.lookupStart = dlsym(NULL, DVX_SYM("netLookupStart"));
+ sNet.poll = dlsym(NULL, DVX_SYM("poll"));
+ sNet.recv = dlsym(NULL, DVX_SYM("recv"));
+ sNet.send = dlsym(NULL, DVX_SYM("send"));
+ sNet.socket = dlsym(NULL, DVX_SYM("socket"));
+ sNetFound = sNet.connect && sNet.fcntl && sNet.getsockopt && sNet.lookupCancel && sNet.lookupPoll && sNet.lookupStart && sNet.poll && sNet.recv && sNet.send && sNet.socket;
+ return sNetFound;
+}
+
+
// Plain words for an rs232 error code.
static void rs232Text(int32_t rc, char *out, int32_t outSize) {
const char *text;
@@ -718,7 +772,7 @@ static void secureClose(ConnT *c) {
// starts once the TCP connection is up; see securePoll).
static int32_t secureOpen(ConnT *c, uint32_t nowMs) {
if (c->cfg.host[0] != '\0') {
- int32_t rc = tcpBegin(c, nowMs);
+ int32_t rc = tcpBegin(c);
if (rc == CONN_OK) {
c->tcpConnecting = true;
@@ -749,7 +803,7 @@ static int32_t securePending(const ConnT *c) {
static void securePoll(ConnT *c, uint32_t nowMs) {
if (c->tcpConnecting) {
- PktStreamT stream = { sockRead, sockWrite, c };
+ PktStreamT stream = { sockRead, sockWrite, c, NULL };
if (tcpCheck(c, nowMs) <= 0) {
return;
@@ -776,10 +830,10 @@ static void securePoll(ConnT *c, uint32_t nowMs) {
setStatus(c, "Secure link established%s", c->withPass ? " (passphrase checked)" : "");
} else if (rc == SECLINK_ERR_AUTH) {
fail(c, "The passphrase does not match the host's");
+ } else if (rc == SECLINK_ERR_TIMEOUT || (rc == SECLINK_PENDING && timeReached(nowMs, c->deadline))) {
+ fail(c, "The host did not answer the secure link");
} else if (rc != SECLINK_PENDING) {
fail(c, "The secure link could not be set up");
- } else if (timeReached(nowMs, c->deadline)) {
- fail(c, "The host did not answer the secure link");
}
return;
@@ -926,7 +980,7 @@ static void setStatus(ConnT *c, const char *fmt, ...) {
// Socket as a secLink byte stream (see PktStreamT).
static int32_t sockRead(void *ctx, uint8_t *buf, int32_t len) {
- ssize_t n = recv(((ConnT *)ctx)->sock, buf, (size_t)len, MSG_DONTWAIT);
+ ssize_t n = sNet.recv(((ConnT *)ctx)->sock, buf, (size_t)len, MSG_DONTWAIT);
if (n == 0) {
return -1;
@@ -941,7 +995,7 @@ static int32_t sockRead(void *ctx, uint8_t *buf, int32_t len) {
static int32_t sockWrite(void *ctx, const uint8_t *data, int32_t len) {
- ssize_t n = send(((ConnT *)ctx)->sock, data, (size_t)len, MSG_DONTWAIT);
+ ssize_t n = sNet.send(((ConnT *)ctx)->sock, data, (size_t)len, MSG_DONTWAIT);
if (n < 0) {
return (errno == EWOULDBLOCK) ? 0 : -1;
@@ -951,64 +1005,58 @@ static int32_t sockWrite(void *ctx, const uint8_t *data, int32_t len) {
}
-// Look the host up and start a non-blocking TCP connection to it.
-static int32_t tcpBegin(ConnT *c, uint32_t nowMs) {
- struct addrinfo hints;
- struct addrinfo *res = NULL;
- char port[PORT_TEXT_MAX];
- int rc;
-
+// Start looking the host up (tcpCheck connects once it is found).
+static int32_t tcpBegin(ConnT *c) {
if (c->cfg.host[0] == '\0') {
fail(c, "No host name is set");
return CONN_ERR_PARAM;
}
- memset(&hints, 0, sizeof(hints));
- hints.ai_family = AF_INET;
- hints.ai_socktype = SOCK_STREAM;
- snprintf(port, sizeof(port), "%d", (int)c->cfg.tcpPort);
-
- if (getaddrinfo(c->cfg.host, port, &hints, &res) != 0 || !res) {
- fail(c, "The host %s was not found", c->cfg.host);
- return CONN_ERR_HOST;
+ if (!netFind()) {
+ fail(c, NO_NET_TEXT);
+ return CONN_ERR_NO_NET;
}
- c->sock = socket(AF_INET, SOCK_STREAM, 0);
-
- if (c->sock < 0) {
- freeaddrinfo(res);
- tcpFailed(c, errno);
- return CONN_ERR_HOST;
- }
-
- fcntl(c->sock, F_SETFL, O_NONBLOCK);
- rc = connect(c->sock, res->ai_addr, res->ai_addrlen);
- freeaddrinfo(res);
-
- if (rc != 0 && errno != EINPROGRESS) {
- tcpFailed(c, errno);
- return CONN_ERR_HOST;
- }
-
- c->state = ConnOpeningE;
- c->deadline = nowMs + TCP_CONNECT_MS;
+ sNet.lookupStart(&c->lookup, c->cfg.host);
+ c->looking = true;
+ c->state = ConnOpeningE;
return CONN_OK;
}
// Is the TCP connection up? 1 yes, 0 not yet, -1 it failed (and the
-// connection has ended with the reason).
+// connection has ended with the reason). First the host is looked up (it
+// ends by itself: DHCP's first address and the DNS server each have their
+// limit), then a non-blocking connection is started to it.
static int32_t tcpCheck(ConnT *c, uint32_t nowMs) {
struct pollfd p;
int err = 0;
socklen_t len = sizeof(err);
+ if (c->looking) {
+ uint32_t addr;
+ int32_t rc = sNet.lookupPoll(&c->lookup, &addr);
+
+ if (rc == NET_LOOKUP_BUSY) {
+ return 0;
+ }
+
+ c->looking = false;
+
+ if (rc != 0) {
+ fail(c, "The host %s was not found", c->cfg.host);
+ return -1;
+ }
+
+ return tcpConnect(c, addr, nowMs) ? 0 : -1;
+ }
+
p.fd = c->sock;
p.events = POLLOUT;
p.revents = 0;
- if (poll(&p, 1, 0) > 0) {
- getsockopt(c->sock, SOL_SOCKET, SO_ERROR, &err, &len);
+ if (sNet.poll(&p, 1, 0) > 0) {
+ sNet.getsockopt(c->sock, SOL_SOCKET, SO_ERROR, &err, &len);
if (err == 0 && !(p.revents & (POLLERR | POLLHUP))) {
return 1;
@@ -1027,7 +1075,15 @@ static int32_t tcpCheck(ConnT *c, uint32_t nowMs) {
}
+// The socket goes, and a lookup under way (either is only there when the
+// networking library is).
static void tcpClose(ConnT *c) {
+ if (c->looking) {
+ sNet.lookupCancel(&c->lookup);
+ }
+
+ c->looking = false;
+
if (c->sock == NO_SOCKET) {
return;
}
@@ -1037,6 +1093,36 @@ static void tcpClose(ConnT *c) {
}
+// A socket, and a non-blocking connection to the host (at addr, network
+// order) started. False (the connection ended with the reason) when it
+// cannot be.
+static bool tcpConnect(ConnT *c, uint32_t addr, uint32_t nowMs) {
+ struct sockaddr_in to;
+
+ memset(&to, 0, sizeof(to));
+ to.sin_family = AF_INET;
+ to.sin_port = htons((uint16_t)c->cfg.tcpPort);
+ to.sin_addr.s_addr = addr;
+ c->sock = sNet.socket(AF_INET, SOCK_STREAM, 0);
+
+ if (c->sock < 0) {
+ c->sock = NO_SOCKET;
+ tcpFailed(c, errno);
+ return false;
+ }
+
+ sNet.fcntl(c->sock, F_SETFL, O_NONBLOCK);
+
+ if (sNet.connect(c->sock, (struct sockaddr *)&to, sizeof(to)) != 0 && errno != EINPROGRESS) {
+ tcpFailed(c, errno);
+ return false;
+ }
+
+ c->deadline = nowMs + TCP_CONNECT_MS;
+ return true;
+}
+
+
// End the connection with a plain-words reason for a socket error.
static void tcpFailed(ConnT *c, int32_t err) {
switch (err) {
@@ -1069,8 +1155,9 @@ static void telnetClose(ConnT *c) {
static int32_t telnetOpen(ConnT *c, uint32_t nowMs) {
int32_t rc;
+ (void)nowMs;
telnetInit(&c->telnet, c->cols, c->rows);
- rc = tcpBegin(c, nowMs);
+ rc = tcpBegin(c);
if (rc == CONN_OK) {
setStatus(c, "Connecting to %s port %d", c->cfg.host, (int)c->cfg.tcpPort);
@@ -1109,7 +1196,7 @@ static void telnetPoll(ConnT *c, uint32_t nowMs) {
break;
}
- n = recv(c->sock, in, (size_t)((room < READ_CHUNK) ? room : READ_CHUNK), MSG_DONTWAIT);
+ n = sNet.recv(c->sock, in, (size_t)((room < READ_CHUNK) ? room : READ_CHUNK), MSG_DONTWAIT);
if (n == 0) {
fail(c, "%s closed the connection", c->cfg.host);
diff --git a/src/libs/kpunch/conn/conn.dep b/src/libs/kpunch/conn/conn.dep
index 215a451c..7e5825c5 100644
--- a/src/libs/kpunch/conn/conn.dep
+++ b/src/libs/kpunch/conn/conn.dep
@@ -1,5 +1,5 @@
-dvxnet
serial
+seclink
xfer
libtasks
libdvx
diff --git a/src/libs/kpunch/conn/conn.dhs b/src/libs/kpunch/conn/conn.dhs
index 6de169b7..0865fc39 100644
--- a/src/libs/kpunch/conn/conn.dhs
+++ b/src/libs/kpunch/conn/conn.dhs
@@ -77,7 +77,7 @@ The Telnet protocol itself is a separate filter (connTelnet.c) that only moves b
ConnT *connCreate(void) Allocate a closed connection.
void connDestroy(c) Close and free.
void connConfigDefaults(cfg) COM1, 9600 8N1, no flow control, ATZ, ATDT.
- int32_t connOpen(c, cfg, nowMs) Start connecting (cfg is copied). CONN_OK, CONN_ERR_PARAM, CONN_ERR_PORT, or CONN_ERR_HOST (the host was not found or refused at once); connStatus explains. Later failures (refused, no answer within 30 seconds, connection lost, host closed it) end the connection with the reason in connStatus.
+ int32_t connOpen(c, cfg, nowMs) Start connecting (cfg is copied). CONN_OK, CONN_ERR_PARAM, CONN_ERR_PORT, or CONN_ERR_NO_NET (Telnet, or a secure link over the network, without the networking library); connStatus explains. A network host is looked up and connected to while connPoll runs (never waiting inside one call), so later failures (the host not found, refused, no answer within 30 seconds, connection lost, host closed it) end the connection with the reason in connStatus.
void connPoll(c, nowMs) Dial, key exchange, receive, send, timers.
int32_t connRead(c, buf, max) Take received bytes (0 when none).
int32_t connWrite(c, data, len) Queue bytes to send; returns the count accepted (0 while not open or when the 8 KB send buffer is full).
@@ -88,7 +88,7 @@ The Telnet protocol itself is a separate filter (connTelnet.c) that only moves b
const char *connStatus(c) What is happening, or why the connection ended, in plain words.
uint32_t connStatusSerial(c) Changes whenever the status text changes.
void connSetWindowSize(c, cols, rows) The terminal size for Telnet's NAWS (sent now if the host asked for it).
- bool connTypeAvailable(type) True for every ConnTypeE value.
+ bool connTypeAvailable(type) True for every ConnTypeE value but ConnTelnetE when the networking library is not installed (an optional part of DVX; connections over the network then end at once with CONN_ERR_NO_NET). conn.lib does not import the networking library: it looks its calls up when a network connection starts.
bool connCanBreak(type) True for serial and modem.
const char *connErrorText(err) Text for a connOpen result.
.endtable
@@ -142,6 +142,7 @@ Add conn.bas to a BASIC project (and xfer.bas for transfers): connBas.c gives BA
ConnState, ConnStatus$, ConnStatusSerial connState, connStatus, connStatusSerial.
ConnWrite(h, bytes$), ConnRead$(h), ConnPending(h) Bytes out and in without a Terminal: connWrite / connRead with BINARY STRING data, so every byte value passes.
ConnCanBreak(type), ConnSendBreak(h) connCanBreak, connSendBreak.
+ ConnTypeAvailable(type) connTypeAvailable: False for CONN_TELNET when Networking is not installed (a secure link over the network needs it too).
ConnClose h, ConnFree h Close (a modem hangs up); close and free the handle.
ConnAttach(h, ctrl$), ConnDetach h Show the connection on a Terminal control, and send what is typed there.
ConnSetEcho h, on; ConnSetAnswerback h, text$ Local echo (Enter as CR LF); the reply to ENQ.
diff --git a/src/libs/kpunch/conn/conn.h b/src/libs/kpunch/conn/conn.h
index 13c4bee5..71377f26 100644
--- a/src/libs/kpunch/conn/conn.h
+++ b/src/libs/kpunch/conn/conn.h
@@ -71,7 +71,7 @@
#define CONN_OK 0
#define CONN_ERR_PARAM (-1)
#define CONN_ERR_PORT (-3)
-#define CONN_ERR_HOST (-4) // host not found or not reachable
+#define CONN_ERR_NO_NET (-4) // Telnet or a secure link over the network, without the networking library
// Secure link: the channel that carries terminal data
#define CONN_SECURE_CHANNEL 0
@@ -158,7 +158,9 @@ const char *connStatus(const ConnT *c);
// Changes whenever the status text changes (so a caller can redraw it)
uint32_t connStatusSerial(const ConnT *c);
-// True for every ConnTypeE value (all types are available).
+// True for a ConnTypeE value that can be used here: every one but Telnet,
+// which needs the networking library (an optional part of DVX; a secure
+// link over the network does too, and then ends with CONN_ERR_NO_NET).
bool connTypeAvailable(int32_t type);
// Queue bytes to send. Returns the count accepted (0 while not open or
diff --git a/src/libs/kpunch/conn/connBas.c b/src/libs/kpunch/conn/connBas.c
index a3c28650..8d1b139a 100644
--- a/src/libs/kpunch/conn/connBas.c
+++ b/src/libs/kpunch/conn/connBas.c
@@ -63,11 +63,11 @@
#include
#define BAS_COM_FIRST 1 // BASIC numbers COM ports from 1
-#define READ_MAX 4096 // received bytes moved to the screen per ConnPoll
+#define READ_MAX 4096 // received bytes ConnRead$ gives at most
+#define SHOW_PIECE 256 // received bytes moved to the screen at a time
+#define SHOW_BUDGET_MS 20 // ... until a ConnPoll has spent this long on them
#define ANSWERBACK_MAX 64
#define FILES_MAX (XFER_MAX_FILES * XFER_PATH_MAX)
-#define FILE_LIST_SEP '\n'
-#define FILE_LIST_CR '\r'
#define ENQ 0x05
#define TERM_BIND_SYM "basFormRtTermBind"
#define TERM_FORGET_SYM "basFormRtTermForget"
@@ -135,6 +135,7 @@ void ConnSetXferWindow(int32_t handle, int32_t bytes);
int32_t ConnState(int32_t handle);
const char *ConnStatus(int32_t handle);
int32_t ConnStatusSerial(int32_t handle);
+int32_t ConnTypeAvailable(int32_t type);
int32_t ConnWrite(int32_t handle, const uint8_t *data, int32_t len);
int32_t ConnZmodemRequest(int32_t handle);
static void echoLocal(BasConnT *b, const uint8_t *data, int32_t len);
@@ -531,6 +532,13 @@ int32_t ConnStatusSerial(int32_t handle) {
}
+// True when connections of type can be made here (Telnet needs
+// Networking).
+int32_t ConnTypeAvailable(int32_t type) {
+ return connTypeAvailable(type) ? BAS_TRUE : BAS_FALSE;
+}
+
+
// Queue bytes (a BINARY STRING) to send. Returns the bytes accepted (0
// while not open).
int32_t ConnWrite(int32_t handle, const uint8_t *data, int32_t len) {
@@ -599,34 +607,42 @@ static XferT *runningTransfer(BasConnT *b) {
// with the answerback and watch for a ZMODEM sender starting (the rest of
// what was read with its start frame is not shown).
static void showIncoming(BasConnT *b) {
- int32_t n = connRead(b->conn, sReadBuf, (int32_t)sizeof(sReadBuf));
- int32_t out = 0;
+ uint32_t start = nowMs();
+ bool seen = false; // a ZMODEM start in this poll: the rest is the transfer's
- if (n <= 0) {
- return;
- }
+ // A piece at a time, for a while: a flood (bare line feeds scroll the
+ // whole screen each) waits in the connection for the next poll
+ do {
+ int32_t n = connRead(b->conn, sReadBuf, SHOW_PIECE);
+ int32_t out = 0;
- if (b->capture) {
- fwrite(sReadBuf, 1, (size_t)n, b->capture);
- }
-
- for (int32_t i = 0; i < n; i++) {
- uint8_t ch = sReadBuf[i];
-
- if (xferDetectFeed(&b->detect, ch)) {
- b->zmodemSeen = true;
- break;
+ if (n <= 0) {
+ return;
}
- if (ch == ENQ) {
- connWrite(b->conn, (const uint8_t *)b->answerback, (int32_t)strlen(b->answerback));
- continue;
+ if (b->capture) {
+ fwrite(sReadBuf, 1, (size_t)n, b->capture);
}
- sReadBuf[out++] = ch;
- }
+ for (int32_t i = 0; i < n && !seen; i++) {
+ uint8_t ch = sReadBuf[i];
- wgtAnsiTermWrite(b->term, sReadBuf, out);
+ if (xferDetectFeed(&b->detect, ch)) {
+ b->zmodemSeen = true;
+ seen = true;
+ break;
+ }
+
+ if (ch == ENQ) {
+ connWrite(b->conn, (const uint8_t *)b->answerback, (int32_t)strlen(b->answerback));
+ continue;
+ }
+
+ sReadBuf[out++] = ch;
+ }
+
+ wgtAnsiTermWrite(b->term, sReadBuf, out);
+ } while (!seen && nowMs() - start < SHOW_BUDGET_MS);
}
@@ -668,36 +684,9 @@ static int32_t startTransfer(int32_t handle, bool sending, int32_t proto, const
if (sending) {
static char list[FILES_MAX];
const char *paths[XFER_MAX_FILES] = { NULL };
- int32_t count = 0;
- // One path per line (a line feed at the very end starts none)
snprintf(list, sizeof(list), "%s", files ? files : "");
-
- for (char *p = list; *p && count < XFER_MAX_FILES; ) {
- char *end = strchr(p, FILE_LIST_SEP);
-
- if (end) {
- *end = '\0';
- }
-
- size_t len = strlen(p);
-
- if (len > 0 && p[len - 1] == FILE_LIST_CR) {
- p[len - 1] = '\0';
- }
-
- if (*p) {
- paths[count++] = p;
- }
-
- if (!end) {
- break;
- }
-
- p = end + 1;
- }
-
- ok = xferStartSend(x, (XferProtoE)proto, &io, &opt, paths, count, nowMs());
+ ok = xferStartSend(x, (XferProtoE)proto, &io, &opt, paths, xferSplitList(list, paths), nowMs());
} else {
ok = xferStartReceive(x, (XferProtoE)proto, &io, &opt, target ? target : "", nowMs());
}
diff --git a/src/libs/kpunch/crypto/Makefile b/src/libs/kpunch/crypto/Makefile
new file mode 100644
index 00000000..c6c79e59
--- /dev/null
+++ b/src/libs/kpunch/crypto/Makefile
@@ -0,0 +1,80 @@
+# 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.
+
+# Cryptography Library Makefile for DJGPP cross-compilation
+#
+# crypto.lib: Diffie-Hellman, XTEA-CTR, the random number generator
+# (security.c), SHA-256, HMAC and HKDF (secHash.c), X25519 and
+# XChaCha20-Poly1305 (secCurve.c), Ed25519 (secSign.c), and their BASIC
+# entry points (secBas.c, security.bas). Monocypher (thirdparty) is
+# built in; the library keeps only what its own objects reach of it
+# (DXE_GC) and exports its own functions only. The package library, the
+# network, the shell's seed file, and the secure link use it.
+
+include ../../../common.mk
+
+MONO = thirdparty/monocypher/src
+CFLAGS = $(DOS_CFLAGS) -I. -I$(MONO) -I../libdvx -I../libdvx/platform -I../libtasks
+
+OBJDIR = ../../../../obj/crypto
+LIBSDIR = ../../../../bin/libs
+
+SRCS = $(wildcard *.c)
+OWN_OBJS = $(patsubst %.c,$(OBJDIR)/%.o,$(SRCS))
+MONO_OBJS = $(OBJDIR)/monocypher.o $(OBJDIR)/monocypher-ed25519.o
+OBJS = $(OWN_OBJS) $(MONO_OBJS)
+GC_OBJ = $(OBJDIR)/cryptoGc.o
+TARGETDIR = $(LIBSDIR)/kpunch/crypto
+TARGET = $(TARGETDIR)/crypto.lib
+
+vpath %.c . $(MONO) $(MONO)/optional
+
+.PHONY: all clean
+
+all: $(TARGET) $(TARGETDIR)/crypto.dep
+
+$(TARGETDIR)/crypto.dep: crypto.dep | $(TARGETDIR)
+ sed 's/$$/\r/' $< > $@
+
+$(GC_OBJ): $(OBJS)
+ $(DXE_GC) $@ $(OWN_OBJS) -- $(OBJS)
+
+$(TARGET): $(GC_OBJ) | $(TARGETDIR)
+ $(DXE3GEN) -o $(TARGETDIR)/crypto.dxe -U $(GC_OBJ)
+ mv $(TARGETDIR)/crypto.dxe $@
+
+$(OBJDIR)/%.o: %.c $(DVX_MAKEFILES) | $(OBJDIR)
+ $(CC) $(CFLAGS) -c -o $@ $<
+
+$(MONO_OBJS): $(OBJDIR)/%.o: %.c $(DVX_MAKEFILES) | $(OBJDIR)
+ $(CC) $(CFLAGS) $(DXE_GC_CFLAGS) -c -o $@ $<
+
+$(OBJDIR):
+ mkdir -p $(OBJDIR)
+
+$(TARGETDIR):
+ mkdir -p $(TARGETDIR)
+
+-include $(OBJS:.o=.d)
+
+clean:
+ rm -rf $(OBJDIR) $(TARGETDIR)
diff --git a/src/libs/kpunch/crypto/README.md b/src/libs/kpunch/crypto/README.md
new file mode 100644
index 00000000..45ac5ad4
--- /dev/null
+++ b/src/libs/kpunch/crypto/README.md
@@ -0,0 +1,42 @@
+# crypto -- Cryptography
+
+Compact cryptographic primitives: the X25519 key exchange (RFC 7748),
+XChaCha20-Poly1305 authenticated encryption, Ed25519 signatures (RFC
+8032), 1024-bit Diffie-Hellman key exchange (RFC 2409 Group 2), XTEA
+block cipher in CTR mode, SHA-256 / HMAC-SHA256 / HKDF-SHA256 for key
+derivation and message authentication, and a SHA-256 DRBG with an
+entropy pool and seed file. X25519, XChaCha20-Poly1305, and Ed25519
+are Monocypher's (thirdparty/monocypher, BSD licence, see its
+DVX-VERSION.md); the library keeps only the parts of it these functions
+use. The secure link, the package library, the network, and the
+shell's random seed use it; BASIC reaches it through
+src/include/basic/security.bas.
+
+## Documentation
+
+Full reference is in `crypto.dhs` (this directory), compiled into the
+DVX System Reference -- open via the Help Viewer app, or read
+`docs/dvx_system_reference.html` after `make`.
+
+Topics covered: DH handshake API, XTEA-CTR encrypt/decrypt, hashing,
+DRBG entropy pool and seed file, remote-key validation range, the BASIC
+functions.
+
+## Source Files
+
+- `security.h` -- public API (DH, XTEA-CTR, DRBG)
+- `security.c` -- implementation (bignum, Montgomery ladder, XTEA, DRBG)
+- `secHash.h` -- SHA-256, HMAC-SHA256, HKDF-SHA256 API
+- `secHash.c` -- hash implementation
+- `secCurve.h` -- X25519, XChaCha20-Poly1305, and Ed25519 API
+- `secCurve.c` -- X25519 and XChaCha20-Poly1305 over Monocypher
+- `secSign.c` -- Ed25519 over Monocypher (apart, so the proxy can
+ leave it out)
+- `secBas.c` -- BASIC entry points (security.bas)
+- `thirdparty/monocypher/` -- Monocypher 4.0.3 with one small DVX patch
+ (X25519 a slice at a time; its DVX-VERSION.md)
+
+## Build
+
+Built into `crypto.lib` by `make -C src/libs/kpunch/crypto` (invoked by
+the top-level `make`).
diff --git a/src/libs/kpunch/crypto/crypto.dep b/src/libs/kpunch/crypto/crypto.dep
new file mode 100644
index 00000000..d613d36a
--- /dev/null
+++ b/src/libs/kpunch/crypto/crypto.dep
@@ -0,0 +1 @@
+libdvx
diff --git a/src/libs/kpunch/crypto/crypto.dhs b/src/libs/kpunch/crypto/crypto.dhs
new file mode 100644
index 00000000..96106622
--- /dev/null
+++ b/src/libs/kpunch/crypto/crypto.dhs
@@ -0,0 +1,261 @@
+# 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.
+
+.section Technical Reference (for Developers)|Libraries
+.topic lib.crypto
+.title Cryptography Library
+.toc 0 Cryptography Library
+.index Security
+.index Diffie-Hellman
+.index XTEA
+.index XTEA-CTR
+.index SHA-256
+.index HMAC
+.index HKDF
+.index DRBG
+.index X25519
+.index XChaCha20-Poly1305
+.index Ed25519
+.index Monocypher
+.index secX25519
+.index secAeadLock
+.index secAeadUnlock
+.index secEd25519Check
+.index secEd25519Sign
+.index secDhCreate
+.index secCipherCreate
+.index secRngBytes
+.index secRngAddEntropy
+.index secRngLoadSeed
+.index security.bas
+
+.h1 Cryptography Library
+
+Cryptographic primitives for DVX programs and libraries (the secure link, the package library, the network, and the shell's random seed use them): the X25519 key exchange (RFC 7748), XChaCha20-Poly1305 authenticated encryption, Ed25519 signatures (RFC 8032), 1024-bit Diffie-Hellman key exchange (RFC 2409 Group 2), XTEA block cipher in CTR mode, SHA-256, HMAC-SHA256, and HKDF-SHA256, and a SHA-256 based random number generator. X25519, XChaCha20-Poly1305, and Ed25519 are Monocypher's (thirdparty/monocypher, BSD licence); the library keeps only the parts of it these functions use.
+
+XTEA requires no lookup tables and compiles to approximately 20 instructions per round, making it suitable for 486-class hardware where AES S-box tables would thrash the 8KB cache. SHA-256 needs only 32-bit arithmetic and a 256-byte constant table.
+
+Loaded as: bin/libs/kpunch/crypto/crypto.lib
+
+Headers: crypto/security.h, crypto/secHash.h, crypto/secCurve.h
+
+.h2 Constants
+
+.table
+ Constant Value Description
+ -------- ----- -----------
+ SEC_DH_KEY_SIZE 128 Size of a DH public key or shared secret in bytes (1024 bits).
+ SEC_XTEA_KEY_SIZE 16 Size of an XTEA key in bytes (128 bits).
+ SEC_RNG_SEED_SIZE 32 Bytes stored in a seed file.
+ SEC_SHA256_SIZE 32 SHA-256 digest size.
+ SEC_SHA256_BLOCK_SIZE 64 SHA-256 block size.
+ SEC_HKDF_MAX_OUT 8160 Largest HKDF output (255 digests).
+ SEC_X25519_KEY_SIZE 32 An X25519 secret key, public key, or shared secret.
+ SEC_AEAD_KEY_SIZE 32 An XChaCha20-Poly1305 key.
+ SEC_AEAD_NONCE_SIZE 24 An XChaCha20-Poly1305 nonce.
+ SEC_AEAD_TAG_SIZE 16 The Poly1305 tag.
+ SEC_ED25519_SEED_SIZE 32 The random bytes an Ed25519 key pair is made from.
+ SEC_ED25519_SECRET_SIZE 64 An Ed25519 secret key (the seed, then the public key).
+ SEC_ED25519_PUBLIC_SIZE 32 An Ed25519 public key.
+ SEC_ED25519_SIG_SIZE 64 An Ed25519 signature.
+ SEC_X25519_JOB_WORDS 72 Size of a SecX25519JobT in 32-bit words.
+ SEC_X25519_PENDING 1 SecX25519Step (BASIC): more of the work to do.
+.endtable
+
+.h2 Error Codes
+
+.table
+ Constant Value Description
+ -------- ----- -----------
+ SEC_SUCCESS 0 Operation succeeded.
+ SEC_ERR_PARAM -1 Invalid parameter.
+ SEC_ERR_NOT_READY -2 DH context not ready (keys not generated or secret not computed).
+ SEC_ERR_IO -3 Seed file could not be read or written.
+.endtable
+
+.h2 RNG Functions
+
+A SHA-256 hash DRBG over a 32-byte entropy pool. secRngAddEntropy hashes new bytes into the pool and never discards what is already there, so it is safe to call at any time, before or after the first output. Each output block is a hash of the pool and a counter; after every request the pool is re-hashed so earlier output cannot be recomputed from a later state.
+
+The first secRngBytes call gathers hardware entropy automatically. On DOS that is 256 samples of the PIT counter, each taken after a busy wait sized by the previous sample, with the full time stamp counter on CPUs that have one (Pentium and later), else with the BIOS tick count. On a CPU without a time stamp counter the samples depend mostly on the PIT phase and time of day, which an observer can narrow down, so applications should feed user-input timings to secRngAddEntropy and keep a seed file: call secRngLoadSeed at startup and secRngSaveSeed at shutdown. Host builds read /dev/urandom.
+
+Under the DVX shell this is automatic: the shell loads the seed file SECLINK.RND in the user folder at startup, feeds the timing of every key press and mouse event to secRngAddEntropy, and saves a fresh seed at shutdown. Applications only need their own calls when they run without the shell.
+
+.code
+void secRngAddEntropy(const uint8_t *data, int32_t len);
+void secRngBytes(uint8_t *buf, int32_t len);
+int32_t secRngGatherEntropy(uint8_t *buf, int32_t len);
+int secRngLoadSeed(const char *path);
+int secRngSaveSeed(const char *path);
+void secSecureZero(void *ptr, int len);
+.endcode
+
+.table
+ Function Description
+ -------- -----------
+ secRngAddEntropy Hash bytes into the pool (key timings, mouse positions, anything unpredictable).
+ secRngBytes Generate pseudorandom bytes.
+ secRngGatherEntropy Fill buf with condensed hardware entropy (one fresh gather per 32 bytes). Returns the byte count (0 if a host build cannot read its random device).
+ secRngLoadSeed Mix a seed file into the pool, then immediately rewrite it with fresh output so a seed is never used twice. SEC_SUCCESS or SEC_ERR_IO.
+ secRngSaveSeed Write SEC_RNG_SEED_SIZE bytes of fresh output for the next run. SEC_SUCCESS or SEC_ERR_IO.
+ secSecureZero Zero memory in a way the compiler cannot optimize away (for key material).
+.endtable
+
+.h2 Hash Functions
+
+.code
+void secSha256(const uint8_t *data, int32_t len, uint8_t digest[32]);
+void secSha256Init(SecSha256T *ctx);
+void secSha256Update(SecSha256T *ctx, const uint8_t *data, int32_t len);
+void secSha256Final(SecSha256T *ctx, uint8_t digest[32]);
+void secHmacSha256(const uint8_t *key, int32_t keyLen, const uint8_t *data, int32_t len, uint8_t mac[32]);
+void secHmacInit(SecHmacT *h, const uint8_t *key, int32_t keyLen);
+void secHmacUpdate(SecHmacT *h, const uint8_t *data, int32_t len);
+void secHmacFinal(SecHmacT *h, uint8_t mac[32]);
+int secHkdf(const uint8_t *salt, int32_t saltLen, const uint8_t *ikm, int32_t ikmLen, const uint8_t *info, int32_t infoLen, uint8_t *okm, int32_t okmLen);
+bool secConstTimeEqual(const void *a, const void *b, int32_t len);
+.endcode
+
+SHA-256 follows FIPS 180-4, HMAC RFC 2104, and HKDF RFC 5869 (extract and expand; a NULL or empty salt means 32 zero bytes). secHkdf returns SEC_ERR_PARAM for okmLen outside 1..SEC_HKDF_MAX_OUT. secConstTimeEqual compares without revealing where the inputs differ; use it for every MAC check.
+
+.h2 Diffie-Hellman Key Exchange
+
+1024-bit DH with 256-bit private exponents using the RFC 2409 Group 2 safe prime. The modular exponentiation is a Montgomery ladder with branch-free operand swaps, so its timing does not depend on the private key.
+
+.code
+SecDhT *secDhCreate(void);
+int secDhGenerateKeys(SecDhT *dh);
+int secDhGetPublicKey(SecDhT *dh, uint8_t *buf, int *len);
+int secDhComputeSecret(SecDhT *dh, const uint8_t *remotePub, int len);
+int secDhGetSecret(SecDhT *dh, uint8_t *buf, int *len);
+void secDhDestroy(SecDhT *dh);
+.endcode
+
+.table
+ Function Description
+ -------- -----------
+ secDhCreate Allocate a new DH context.
+ secDhGenerateKeys Generate a private exponent and public key from the DRBG.
+ secDhGetPublicKey Copy the local public key into buf. *len receives the key size.
+ secDhComputeSecret Compute the shared secret from the remote public key. Keys outside [2, p-2] are rejected with SEC_ERR_PARAM (0, 1, and p-1 would force a trivially known secret).
+ secDhGetSecret Copy the shared secret (SEC_DH_KEY_SIZE big-endian bytes) into buf. It is input for a key derivation function such as secHkdf, never a key by itself.
+ secDhDestroy Securely zero and free the DH context.
+.endtable
+
+Typical usage order: secDhCreate, secDhGenerateKeys, exchange public keys, secDhComputeSecret, secDhGetSecret, secHkdf, secDhDestroy.
+
+.h2 XTEA-CTR Cipher
+
+XTEA in counter mode. Encrypt and decrypt are the same operation (XOR with keystream). The counter must never repeat with the same key.
+
+.code
+SecCipherT *secCipherCreate(const uint8_t *key);
+void secCipherCrypt(SecCipherT *c, uint8_t *data, int len);
+void secCipherSetCounter(SecCipherT *c, uint64_t counter);
+void secCipherDestroy(SecCipherT *c);
+.endcode
+
+.table
+ Function Description
+ -------- -----------
+ secCipherCreate Allocate a cipher context with a 128-bit key. Returns NULL on allocation failure or NULL key. Initial counter is zero.
+ secCipherCrypt Encrypt or decrypt data in place. The 64-bit counter increments by one every 8 bytes (one XTEA block).
+ secCipherSetCounter Position the 64-bit block counter (secLink derives it from the frame sequence number).
+ secCipherDestroy Securely zero the cipher context (key and counter state) and free its memory.
+.endtable
+
+secCipherCreate returns NULL if the key pointer is NULL or allocation fails. secCipherCrypt is a no-op when any of c/data is NULL or len <= 0.
+
+.h2 X25519 Key Exchange
+
+Elliptic-curve Diffie-Hellman on Curve25519 (crypto/secCurve.h). Each side makes a secret key from 32 random bytes (secRngBytes), sends its public key, and combines its secret key with the other side's public key; both get the same 32-byte secret. One call takes about a sixth of a second on a 486 (fixtures/bncurve on the emulated 486DX2-66: 165 ms), so a program that must stay responsive does it a slice at a time with a SecX25519JobT (the secure link does).
+
+.code
+void secX25519PublicKey(uint8_t *publicKey, const uint8_t *secretKey);
+bool secX25519(uint8_t *shared, const uint8_t *secretKey, const uint8_t *peerPublic);
+
+void secX25519JobStart(SecX25519JobT *job, const uint8_t *secretKey, const uint8_t *peerPublic);
+bool secX25519JobStep(SecX25519JobT *job);
+bool secX25519JobResult(SecX25519JobT *job, uint8_t *out);
+void secX25519JobWipe(SecX25519JobT *job);
+.endcode
+
+secX25519 is false when the peer's key gives an all-zero secret (one of the few keys that give everyone the same secret: a cheating peer). The secret is input for a key derivation function such as secHkdf, never a key by itself.
+
+A job is the same computation in nine slices of about 20 ms each on a 486 (eight of the ladder, then the inversion): secX25519JobStart begins it (peerPublic NULL makes the public key of secretKey), secX25519JobStep does a slice and is true once there is nothing left, secX25519JobResult writes the result (false as secX25519 is, or before the job is done) and wipes the job, and secX25519JobWipe drops one. A SecX25519JobT is the caller's (no allocation); the slicing is a small DVX patch to Monocypher (thirdparty/monocypher/DVX-VERSION.md).
+
+.h2 XChaCha20-Poly1305
+
+Authenticated encryption: the data is encrypted and a 16-byte tag proves it, and some associated data sent in the clear, unchanged.
+
+.code
+void secAeadLock(const uint8_t *key, const uint8_t *nonce, const uint8_t *ad, int32_t adLen, uint8_t *data, int32_t len, uint8_t *tag);
+bool secAeadUnlock(const uint8_t *key, const uint8_t *nonce, const uint8_t *ad, int32_t adLen, uint8_t *data, int32_t len, const uint8_t *tag);
+.endcode
+
+Both work in place. secAeadUnlock decrypts only when the tag matches; otherwise it returns false and leaves data as it was. A key and nonce pair must never seal two different messages: a counter in the nonce (the secure link uses its frame number) or a fresh random nonce (24 bytes is enough to pick at random) keeps them apart.
+
+.h2 Ed25519 Signatures
+
+Public-key signatures (RFC 8032, with SHA-512), as the package library uses for add-on packages.
+
+.code
+void secEd25519KeyPair(uint8_t *secretKey, uint8_t *publicKey, uint8_t *seed);
+void secEd25519Sign(uint8_t *sig, const uint8_t *secretKey, const uint8_t *msg, int32_t len);
+bool secEd25519Check(const uint8_t *sig, const uint8_t *publicKey, const uint8_t *msg, int32_t len);
+.endcode
+
+secEd25519KeyPair makes the 64-byte secret key (the seed followed by the public key) and the public key from 32 random bytes, and wipes the seed. secSign.c holds these three and secCurve.c the rest, so a program that links the sources in (the host proxy) can leave the signatures out.
+
+.h2 BASIC
+
+src/include/basic/security.bas (DECLARE LIBRARY "crypto") declares the BASIC functions (crypto/secBas.c). Keys, data, and results are BINARY STRING (pointer and length in, a result buffer and length out), so every byte value passes; the result buffer is wiped before it is reused. A handle (1, 2, ...; 0 = none) is a key exchange, a cipher, or an incremental SHA-256 / HMAC; each records the program that made it and the shell's app-cleanup hook frees (and wipes) what a program leaves open.
+
+.table
+ BASIC C it calls
+ ----- ----------
+ SecRandom$(count), SecAddEntropy bytes secRngBytes; secRngAddEntropy.
+ SecDhCreate() secDhCreate + secDhGenerateKeys.
+ SecDhPublicKey$(h), SecDhSecret$(h) secDhGetPublicKey; secDhGetSecret ("" before SecDhComputeSecret).
+ SecDhComputeSecret(h, remotePublicKey) secDhComputeSecret: SEC_OK or a SEC_ERR_ code.
+ SecDhFree h secDhDestroy.
+ SecCipherCreate(key) secCipherCreate; 0 unless key is SEC_XTEA_KEY_SIZE bytes.
+ SecCipherCrypt$(h, bytes) secCipherCrypt on a copy.
+ SecCipherSetCounter h, counter secCipherSetCounter (a DOUBLE: exact up to 2^53).
+ SecCipherFree h secCipherDestroy.
+ SecSha256$(bytes); SecSha256Open, SecSha256Add, SecSha256End$ secSha256; secSha256Init / Update / Final (End frees the handle).
+ SecHmac$(key, bytes); SecHmacOpen, SecHmacAdd, SecHmacEnd$ secHmacSha256; secHmacInit / Update / Final.
+ SecHkdf$(salt, secret, info, length) secHkdf ("" for a length outside 1..SEC_HKDF_MAX_OUT).
+ SecEqual(a, b) secConstTimeEqual (False for different lengths).
+ SecX25519PublicKey$(secretKey) secX25519PublicKey ("" unless the key is SEC_X25519_KEY_SIZE bytes).
+ SecX25519$(secretKey, otherPublicKey) secX25519 ("" for a wrong size or a refused key).
+ SecX25519Start(secretKey, otherPublicKey) secX25519JobStart in a handle ("" otherPublicKey: the public key); 0 for wrong sizes.
+ SecX25519Step(h); SecX25519End$(h); SecX25519Free h secX25519JobStep (SEC_X25519_PENDING, then SEC_OK); secX25519JobResult (frees the handle); secX25519JobWipe.
+ SecAeadLock$(key, nonce, extra, bytes) secAeadLock on a copy: the ciphertext, then the tag.
+ SecAeadUnlock$(key, nonce, extra, sealed) secAeadUnlock on a copy ("" when the tag does not match).
+ SecEd25519KeyPair$(seed) secEd25519KeyPair: the secret key (the program's seed is not wiped).
+ SecEd25519PublicKey$(secretKey) The public key (the secret key's last SEC_ED25519_PUBLIC_SIZE bytes).
+ SecEd25519Sign$(secretKey, bytes) secEd25519Sign.
+ SecEd25519Check(signature, publicKey, bytes) secEd25519Check (False for wrong sizes).
+.endtable
+
+The SEC_ CONSTs of security.bas are compared with security.h, secHash.h, and secCurve.h by a formrt test, which also calls every function for real.
diff --git a/src/libs/kpunch/serial/security/secBas.c b/src/libs/kpunch/crypto/secBas.c
similarity index 55%
rename from src/libs/kpunch/serial/security/secBas.c
rename to src/libs/kpunch/crypto/secBas.c
index d01ae9da..2550ef4b 100644
--- a/src/libs/kpunch/serial/security/secBas.c
+++ b/src/libs/kpunch/crypto/secBas.c
@@ -23,9 +23,10 @@
// secBas.c -- DVX BASIC entry points of the security library
// (src/include/basic/security.bas)
//
-// Random bytes, the Diffie-Hellman key exchange, the XTEA-CTR cipher,
-// SHA-256, HMAC-SHA256, HKDF and a constant-time compare, over the same
-// functions secLink and C programs use. Called through DECLARE LIBRARY:
+// Random bytes, the Diffie-Hellman and X25519 key exchanges, the XTEA-CTR
+// cipher, XChaCha20-Poly1305, Ed25519 signatures, SHA-256, HMAC-SHA256,
+// HKDF and a constant-time compare, over the same functions secLink, the
+// package library and C programs use. Called through DECLARE LIBRARY:
// keys, data and results are BINARY STRING (pointer + length in, a result
// buffer + length out; claudeDocs/round2/binaryStrings.md), so every byte
// value passes. A result stays valid until the next call that returns
@@ -36,133 +37,112 @@
// or HMAC in progress. Each handle records the program that made it; the
// shell's app-cleanup hook frees (and wipes) what a program leaves open.
+#include "secCurve.h"
#include "security.h"
#include "secHash.h"
-#include "dvxMem.h"
#include "dvxPlat.h"
#include "dvxBas.h"
-#include
#include
#include
#include
-#define SHELL_CLEANUP_SYM "shellRegisterAppCleanup"
+// ABI stamp checked by the loader (see DVX_ABI_VERSION).
+DVX_ABI_STAMP(crypto);
typedef enum {
- BasSecFreeE,
BasSecDhE,
BasSecCipherE,
BasSecSha256E,
- BasSecHmacE
+ BasSecHmacE,
+ BasSecX25519E
} BasSecKindE;
-// One handle: what it holds and the program it belongs to
+// What one handle holds (the handle table records its program)
typedef struct {
- void *obj; // SecDhT, SecCipherT, SecSha256T or SecHmacT
- BasSecKindE kind; // BasSecFreeE: the handle is free
- int32_t appId;
+ void *obj; // SecDhT, SecCipherT, SecSha256T, SecHmacT or SecX25519JobT
+ BasSecKindE kind;
} BasSecT;
-// The shell's app-cleanup registry (shellApp.h), looked up at run time:
-// this library loads before the shell, and the host tests may have none.
-typedef void (*BasCleanupFnT)(int32_t appId);
-typedef void (*BasCleanupRegFnT)(BasCleanupFnT fn);
-
-static BasSecT *sHandles = NULL; // handle = index + 1
-static int32_t sHandleCount = 0;
-static bool sCleanupHooked = false;
-static uint8_t *sOut = NULL; // the last result
-static int32_t sOutCap = 0;
+static uint8_t *sOut = NULL; // the last result
+static int32_t sOutCap = 0;
// ============================================================
// Prototypes
// ============================================================
-static void appEnded(int32_t appId);
static int32_t handleNew(BasSecKindE kind, void *obj);
static void *handleObj(int32_t handle, BasSecKindE kind);
static void objFree(BasSecKindE kind, void *obj);
static const uint8_t *result(int32_t len, int32_t *outLen);
void SecAddEntropy(const uint8_t *data, int32_t len);
+const uint8_t *SecAeadLock(const uint8_t *key, int32_t keyLen, const uint8_t *nonce, int32_t nonceLen, const uint8_t *ad, int32_t adLen, const uint8_t *plain, int32_t len, int32_t *outLen);
+const uint8_t *SecAeadUnlock(const uint8_t *key, int32_t keyLen, const uint8_t *nonce, int32_t nonceLen, const uint8_t *ad, int32_t adLen, const uint8_t *sealed, int32_t len, int32_t *outLen);
int32_t SecCipherCreate(const uint8_t *key, int32_t len);
const uint8_t *SecCipherCrypt(int32_t handle, const uint8_t *data, int32_t len, int32_t *outLen);
void SecCipherFree(int32_t handle);
void SecCipherSetCounter(int32_t handle, double counter);
int32_t SecDhComputeSecret(int32_t handle, const uint8_t *remotePub, int32_t len);
+int32_t SecDhComputeStart(int32_t handle, const uint8_t *remotePub, int32_t len);
int32_t SecDhCreate(void);
+int32_t SecDhCreateStart(void);
void SecDhFree(int32_t handle);
const uint8_t *SecDhPublicKey(int32_t handle, int32_t *outLen);
const uint8_t *SecDhSecret(int32_t handle, int32_t *outLen);
+int32_t SecDhStep(int32_t handle);
+int32_t SecEd25519Check(const uint8_t *sig, int32_t sigLen, const uint8_t *publicKey, int32_t publicLen, const uint8_t *msg, int32_t len);
+const uint8_t *SecEd25519KeyPair(const uint8_t *seed, int32_t len, int32_t *outLen);
+const uint8_t *SecEd25519PublicKey(const uint8_t *secretKey, int32_t len, int32_t *outLen);
+const uint8_t *SecEd25519Sign(const uint8_t *secretKey, int32_t secretLen, const uint8_t *msg, int32_t len, int32_t *outLen);
int32_t SecEqual(const uint8_t *a, int32_t aLen, const uint8_t *b, int32_t bLen);
-static void secFree(int32_t handle);
const uint8_t *SecHkdf(const uint8_t *salt, int32_t saltLen, const uint8_t *ikm, int32_t ikmLen, const uint8_t *info, int32_t infoLen, int32_t length, int32_t *outLen);
const uint8_t *SecHmac(const uint8_t *key, int32_t keyLen, const uint8_t *data, int32_t len, int32_t *outLen);
void SecHmacAdd(int32_t handle, const uint8_t *data, int32_t len);
const uint8_t *SecHmacEnd(int32_t handle, int32_t *outLen);
int32_t SecHmacOpen(const uint8_t *key, int32_t len);
const uint8_t *SecRandom(int32_t count, int32_t *outLen);
+static void secRelease(void *obj);
const uint8_t *SecSha256(const uint8_t *data, int32_t len, int32_t *outLen);
void SecSha256Add(int32_t handle, const uint8_t *data, int32_t len);
const uint8_t *SecSha256End(int32_t handle, int32_t *outLen);
int32_t SecSha256Open(void);
+const uint8_t *SecX25519(const uint8_t *secretKey, int32_t secretLen, const uint8_t *peerPublic, int32_t peerLen, int32_t *outLen);
+const uint8_t *SecX25519End(int32_t handle, int32_t *outLen);
+void SecX25519Free(int32_t handle);
+const uint8_t *SecX25519PublicKey(const uint8_t *secretKey, int32_t len, int32_t *outLen);
+int32_t SecX25519Start(const uint8_t *secretKey, int32_t secretLen, const uint8_t *peerPublic, int32_t peerLen);
+int32_t SecX25519Step(int32_t handle);
+static BasHandlesT sHandles = BAS_HANDLES(secRelease, 0); // BasSecT
-// Shell app-cleanup hook: free what a program that ended left open.
-static void appEnded(int32_t appId) {
- for (int32_t i = 0; i < sHandleCount; i++) {
- if (sHandles[i].kind != BasSecFreeE && sHandles[i].appId == appId) {
- secFree(i + 1);
- }
- }
-}
// A handle for obj (owned by it from now on), or 0 (obj freed) when out of
// memory.
static int32_t handleNew(BasSecKindE kind, void *obj) {
- int32_t idx = 0;
+ BasSecT *h = (BasSecT *)malloc(sizeof(*h));
+ int32_t handle = BAS_NO_HANDLE;
- if (!sCleanupHooked) {
- BasCleanupRegFnT reg = (BasCleanupRegFnT)dlsym(NULL, DVX_SYM(SHELL_CLEANUP_SYM));
-
- sCleanupHooked = true;
-
- if (reg) {
- reg(appEnded);
- }
+ if (h) {
+ h->obj = obj;
+ h->kind = kind;
+ handle = basHandleAdd(&sHandles, h);
}
- while (idx < sHandleCount && sHandles[idx].kind != BasSecFreeE) {
- idx++;
+ if (handle == BAS_NO_HANDLE) {
+ objFree(kind, obj);
+ free(h);
}
- if (idx == sHandleCount) {
- BasSecT *grown = (BasSecT *)realloc(sHandles, (size_t)(sHandleCount + 1) * sizeof(BasSecT));
-
- if (!grown) {
- objFree(kind, obj);
- return BAS_NO_HANDLE;
- }
-
- sHandles = grown;
- sHandleCount++;
- }
-
- sHandles[idx].obj = obj;
- sHandles[idx].kind = kind;
- sHandles[idx].appId = dvxMemAppIdPtr ? *dvxMemAppIdPtr : 0;
- return idx + 1;
+ return handle;
}
// What handle holds when it is a kind handle, else NULL.
static void *handleObj(int32_t handle, BasSecKindE kind) {
- if (handle <= BAS_NO_HANDLE || handle > sHandleCount || sHandles[handle - 1].kind != kind) {
- return NULL;
- }
+ const BasSecT *h = (const BasSecT *)basHandleGet(&sHandles, handle);
- return sHandles[handle - 1].obj;
+ return (h && h->kind == kind) ? h->obj : NULL;
}
@@ -187,7 +167,9 @@ static void objFree(BasSecKindE kind, void *obj) {
free(obj);
break;
- case BasSecFreeE:
+ case BasSecX25519E:
+ secX25519JobWipe((SecX25519JobT *)obj);
+ free(obj);
break;
}
}
@@ -228,6 +210,45 @@ void SecAddEntropy(const uint8_t *data, int32_t len) {
}
+// XChaCha20-Poly1305: plain encrypted with key (SEC_AEAD_KEY_SIZE bytes)
+// and nonce (SEC_AEAD_NONCE_SIZE), the tag over it and ad after it; ""
+// for a key or nonce of another length.
+const uint8_t *SecAeadLock(const uint8_t *key, int32_t keyLen, const uint8_t *nonce, int32_t nonceLen, const uint8_t *ad, int32_t adLen, const uint8_t *plain, int32_t len, int32_t *outLen) {
+ *outLen = 0;
+
+ if (keyLen != SEC_AEAD_KEY_SIZE || nonceLen != SEC_AEAD_NONCE_SIZE || len < 0 || !result(len + SEC_AEAD_TAG_SIZE, outLen)) {
+ return NULL;
+ }
+
+ memcpy(sOut, plain, (size_t)len);
+ secAeadLock(key, nonce, ad, adLen, sOut, len, sOut + len);
+ return sOut;
+}
+
+
+// What SecAeadLock sealed, decrypted: "" when the tag does not match (the
+// key, nonce, ad, or sealed bytes differ from the sealing) or a size is
+// wrong.
+const uint8_t *SecAeadUnlock(const uint8_t *key, int32_t keyLen, const uint8_t *nonce, int32_t nonceLen, const uint8_t *ad, int32_t adLen, const uint8_t *sealed, int32_t len, int32_t *outLen) {
+ int32_t plainLen = len - SEC_AEAD_TAG_SIZE;
+
+ *outLen = 0;
+
+ if (keyLen != SEC_AEAD_KEY_SIZE || nonceLen != SEC_AEAD_NONCE_SIZE || plainLen < 0 || !result(plainLen, outLen)) {
+ return NULL;
+ }
+
+ memcpy(sOut, sealed, (size_t)plainLen);
+
+ if (!secAeadUnlock(key, nonce, ad, adLen, sOut, plainLen, sealed + plainLen)) {
+ *outLen = 0;
+ return NULL;
+ }
+
+ return sOut;
+}
+
+
// An XTEA-CTR cipher with key (SEC_XTEA_KEY_SIZE bytes), counter 0. 0
// for a key of another length.
int32_t SecCipherCreate(const uint8_t *key, int32_t len) {
@@ -266,7 +287,7 @@ const uint8_t *SecCipherCrypt(int32_t handle, const uint8_t *data, int32_t len,
void SecCipherFree(int32_t handle) {
if (handleObj(handle, BasSecCipherE)) {
- secFree(handle);
+ basHandleFree(&sHandles, handle);
}
}
@@ -295,6 +316,19 @@ int32_t SecDhComputeSecret(int32_t handle, const uint8_t *remotePub, int32_t len
}
+// The same a slice at a time: SEC_DH_PENDING (SecDhStep computes it), or
+// SEC_ERR_PARAM for a bad handle or an unacceptable key (at once).
+int32_t SecDhComputeStart(int32_t handle, const uint8_t *remotePub, int32_t len) {
+ SecDhT *dh = (SecDhT *)handleObj(handle, BasSecDhE);
+
+ if (!dh || !remotePub) {
+ return SEC_ERR_PARAM;
+ }
+
+ return secDhComputeStart(dh, remotePub, len);
+}
+
+
// A key exchange with fresh keys (secDhCreate + secDhGenerateKeys; can take
// a few seconds on a slow computer). 0 when it could not be made.
int32_t SecDhCreate(void) {
@@ -313,9 +347,23 @@ int32_t SecDhCreate(void) {
}
+// A key exchange whose keys SecDhStep computes a slice at a time. 0 when
+// it could not be made.
+int32_t SecDhCreateStart(void) {
+ SecDhT *dh = secDhCreate();
+
+ if (!dh) {
+ return BAS_NO_HANDLE;
+ }
+
+ secDhGenerateStart(dh);
+ return handleNew(BasSecDhE, dh);
+}
+
+
void SecDhFree(int32_t handle) {
if (handleObj(handle, BasSecDhE)) {
- secFree(handle);
+ basHandleFree(&sHandles, handle);
}
}
@@ -366,6 +414,71 @@ const uint8_t *SecDhSecret(int32_t handle, int32_t *outLen) {
}
+// A slice of the keys or secret being computed: SEC_DH_PENDING while more
+// is left, SEC_OK once ready, SEC_ERR_NOT_READY when nothing is.
+int32_t SecDhStep(int32_t handle) {
+ SecDhT *dh = (SecDhT *)handleObj(handle, BasSecDhE);
+
+ return dh ? secDhStep(dh) : SEC_ERR_PARAM;
+}
+
+
+// True when sig is publicKey's Ed25519 signature of msg.
+int32_t SecEd25519Check(const uint8_t *sig, int32_t sigLen, const uint8_t *publicKey, int32_t publicLen, const uint8_t *msg, int32_t len) {
+ if (sigLen != SEC_ED25519_SIG_SIZE || publicLen != SEC_ED25519_PUBLIC_SIZE) {
+ return BAS_FALSE;
+ }
+
+ return secEd25519Check(sig, publicKey, msg, len) ? BAS_TRUE : BAS_FALSE;
+}
+
+
+// An Ed25519 secret key (SEC_ED25519_SECRET_SIZE bytes, the public key in
+// its last SEC_ED25519_PUBLIC_SIZE) from seed (SEC_ED25519_SEED_SIZE
+// random bytes); "" for a seed of another length.
+const uint8_t *SecEd25519KeyPair(const uint8_t *seed, int32_t len, int32_t *outLen) {
+ uint8_t copy[SEC_ED25519_SEED_SIZE];
+
+ *outLen = 0;
+
+ if (len != SEC_ED25519_SEED_SIZE || !result(SEC_ED25519_SECRET_SIZE, outLen)) {
+ return NULL;
+ }
+
+ // The key pair wipes the seed it is given; the program's stays
+ memcpy(copy, seed, sizeof(copy));
+ secEd25519KeyPair(sOut, sOut + SEC_ED25519_SEED_SIZE, copy);
+ return sOut;
+}
+
+
+// The public key of an Ed25519 secret key; "" for a key of another length.
+const uint8_t *SecEd25519PublicKey(const uint8_t *secretKey, int32_t len, int32_t *outLen) {
+ *outLen = 0;
+
+ if (len != SEC_ED25519_SECRET_SIZE || !result(SEC_ED25519_PUBLIC_SIZE, outLen)) {
+ return NULL;
+ }
+
+ memcpy(sOut, secretKey + SEC_ED25519_SEED_SIZE, SEC_ED25519_PUBLIC_SIZE);
+ return sOut;
+}
+
+
+// The Ed25519 signature (SEC_ED25519_SIG_SIZE bytes) of msg; "" for a
+// secret key of another length.
+const uint8_t *SecEd25519Sign(const uint8_t *secretKey, int32_t secretLen, const uint8_t *msg, int32_t len, int32_t *outLen) {
+ *outLen = 0;
+
+ if (secretLen != SEC_ED25519_SECRET_SIZE || !result(SEC_ED25519_SIG_SIZE, outLen)) {
+ return NULL;
+ }
+
+ secEd25519Sign(sOut, secretKey, msg, len);
+ return sOut;
+}
+
+
// True when a and b hold the same bytes, compared in a time that does not
// depend on where they differ (for MACs and passwords).
int32_t SecEqual(const uint8_t *a, int32_t aLen, const uint8_t *b, int32_t bLen) {
@@ -377,16 +490,6 @@ int32_t SecEqual(const uint8_t *a, int32_t aLen, const uint8_t *b, int32_t bLen)
}
-// Free (and wipe) what handle holds.
-static void secFree(int32_t handle) {
- BasSecT *h = &sHandles[handle - 1];
-
- objFree(h->kind, h->obj);
- h->obj = NULL;
- h->kind = BasSecFreeE;
-}
-
-
// HKDF-SHA256: length bytes (1 to SEC_HKDF_MAX_OUT) of key material from
// ikm, with salt ("" = none) and info; "" for a length out of range.
const uint8_t *SecHkdf(const uint8_t *salt, int32_t saltLen, const uint8_t *ikm, int32_t ikmLen, const uint8_t *info, int32_t infoLen, int32_t length, int32_t *outLen) {
@@ -445,7 +548,7 @@ const uint8_t *SecHmacEnd(int32_t handle, int32_t *outLen) {
secHmacFinal(h, sOut);
}
- secFree(handle);
+ basHandleFree(&sHandles, handle);
return out;
}
@@ -479,6 +582,15 @@ const uint8_t *SecRandom(int32_t count, int32_t *outLen) {
}
+// sHandles' freeFn: free (and wipe) what a handle held.
+static void secRelease(void *obj) {
+ BasSecT *h = (BasSecT *)obj;
+
+ objFree(h->kind, h->obj);
+ free(h);
+}
+
+
// SHA-256 of data (SEC_SHA256_SIZE bytes).
const uint8_t *SecSha256(const uint8_t *data, int32_t len, int32_t *outLen) {
const uint8_t *out = result(SEC_SHA256_SIZE, outLen);
@@ -518,7 +630,7 @@ const uint8_t *SecSha256End(int32_t handle, int32_t *outLen) {
secSha256Final(ctx, sOut);
}
- secFree(handle);
+ basHandleFree(&sHandles, handle);
return out;
}
@@ -534,3 +646,105 @@ int32_t SecSha256Open(void) {
secSha256Init(ctx);
return handleNew(BasSecSha256E, ctx);
}
+
+
+// The X25519 secret both sides share: this side's secret key and the
+// other side's public key (SEC_X25519_KEY_SIZE bytes each); "" for a key
+// of another length, or a public key that gives everyone the same secret.
+const uint8_t *SecX25519(const uint8_t *secretKey, int32_t secretLen, const uint8_t *peerPublic, int32_t peerLen, int32_t *outLen) {
+ *outLen = 0;
+
+ if (secretLen != SEC_X25519_KEY_SIZE || peerLen != SEC_X25519_KEY_SIZE || !result(SEC_X25519_KEY_SIZE, outLen)) {
+ return NULL;
+ }
+
+ if (!secX25519(sOut, secretKey, peerPublic)) {
+ *outLen = 0;
+ return NULL;
+ }
+
+ return sOut;
+}
+
+
+// What a SecX25519Start computed, once SecX25519Step said SEC_OK: ""
+// before then, for a bad handle, or for a refused public key. Frees the
+// handle.
+const uint8_t *SecX25519End(int32_t handle, int32_t *outLen) {
+ SecX25519JobT *job = (SecX25519JobT *)handleObj(handle, BasSecX25519E);
+ uint8_t out[SEC_X25519_KEY_SIZE];
+ bool ok;
+
+ *outLen = 0;
+
+ if (!job) {
+ return NULL;
+ }
+
+ ok = secX25519JobResult(job, out);
+ basHandleFree(&sHandles, handle);
+
+ if (!ok || !result(SEC_X25519_KEY_SIZE, outLen)) {
+ secSecureZero(out, sizeof(out));
+ return NULL;
+ }
+
+ memcpy(sOut, out, SEC_X25519_KEY_SIZE);
+ secSecureZero(out, sizeof(out));
+ return sOut;
+}
+
+
+void SecX25519Free(int32_t handle) {
+ if (handleObj(handle, BasSecX25519E)) {
+ basHandleFree(&sHandles, handle);
+ }
+}
+
+
+// The X25519 public key of a secret key (SEC_X25519_KEY_SIZE random
+// bytes); "" for a key of another length.
+const uint8_t *SecX25519PublicKey(const uint8_t *secretKey, int32_t len, int32_t *outLen) {
+ *outLen = 0;
+
+ if (len != SEC_X25519_KEY_SIZE || !result(SEC_X25519_KEY_SIZE, outLen)) {
+ return NULL;
+ }
+
+ secX25519PublicKey(sOut, secretKey);
+ return sOut;
+}
+
+
+// SecX25519$ (or, for an empty peerPublic, SecX25519PublicKey$) a slice
+// at a time: a handle that SecX25519Step works on and SecX25519End$
+// finishes; 0 for a key of another length.
+int32_t SecX25519Start(const uint8_t *secretKey, int32_t secretLen, const uint8_t *peerPublic, int32_t peerLen) {
+ SecX25519JobT *job;
+
+ if (secretLen != SEC_X25519_KEY_SIZE || (peerLen != 0 && peerLen != SEC_X25519_KEY_SIZE)) {
+ return BAS_NO_HANDLE;
+ }
+
+ job = (SecX25519JobT *)malloc(sizeof(*job));
+
+ if (!job) {
+ return BAS_NO_HANDLE;
+ }
+
+ secX25519JobStart(job, secretKey, peerLen ? peerPublic : NULL);
+ return handleNew(BasSecX25519E, job);
+}
+
+
+// A slice of a SecX25519Start: SEC_X25519_PENDING while there is more,
+// then SEC_SUCCESS; SEC_ERR_PARAM for a bad handle.
+int32_t SecX25519Step(int32_t handle) {
+ SecX25519JobT *job = (SecX25519JobT *)handleObj(handle, BasSecX25519E);
+
+ if (!job) {
+ return SEC_ERR_PARAM;
+ }
+
+ return secX25519JobStep(job) ? SEC_SUCCESS : SEC_X25519_PENDING;
+}
diff --git a/src/libs/kpunch/crypto/secCurve.c b/src/libs/kpunch/crypto/secCurve.c
new file mode 100644
index 00000000..97b62810
--- /dev/null
+++ b/src/libs/kpunch/crypto/secCurve.c
@@ -0,0 +1,141 @@
+// 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.
+
+// secCurve.c -- the X25519 key exchange and XChaCha20-Poly1305 (see
+// secCurve.h), over Monocypher.
+
+#include "secCurve.h"
+#include "monocypher.h"
+
+#include
+#include
+
+// Ladder bits a slice does: eight slices, then the inversion (about as
+// much work as one)
+#define JOB_SLICE_BITS 32
+
+// A job is a SecX25519JobT on the outside; it holds the ladder state and
+// whether the ladder is through
+typedef struct {
+ crypto_x25519_ctx ctx;
+ uint8_t out[SEC_X25519_KEY_SIZE];
+ bool laddered;
+ bool done;
+} JobT;
+
+typedef char secCurveJobFits[(sizeof(SecX25519JobT) >= sizeof(JobT)) ? 1 : -1];
+
+static const uint8_t sBasePoint[SEC_X25519_KEY_SIZE] = { 9 };
+
+// ============================================================
+// Prototypes
+// ============================================================
+
+static bool allZero(const uint8_t *key);
+void secAeadLock(const uint8_t *key, const uint8_t *nonce, const uint8_t *ad, int32_t adLen, uint8_t *data, int32_t len, uint8_t *tag);
+bool secAeadUnlock(const uint8_t *key, const uint8_t *nonce, const uint8_t *ad, int32_t adLen, uint8_t *data, int32_t len, const uint8_t *tag);
+bool secX25519(uint8_t *shared, const uint8_t *secretKey, const uint8_t *peerPublic);
+bool secX25519JobResult(SecX25519JobT *job, uint8_t *out);
+void secX25519JobStart(SecX25519JobT *job, const uint8_t *secretKey, const uint8_t *peerPublic);
+bool secX25519JobStep(SecX25519JobT *job);
+void secX25519JobWipe(SecX25519JobT *job);
+void secX25519PublicKey(uint8_t *publicKey, const uint8_t *secretKey);
+
+
+// The all-zero test ORs every byte so its time does not depend on where
+// a byte is set.
+static bool allZero(const uint8_t *key) {
+ uint8_t any = 0;
+
+ for (int32_t i = 0; i < SEC_X25519_KEY_SIZE; i++) {
+ any |= key[i];
+ }
+
+ return any == 0;
+}
+
+
+// Monocypher encrypts and decrypts in place when the two buffers are one.
+void secAeadLock(const uint8_t *key, const uint8_t *nonce, const uint8_t *ad, int32_t adLen, uint8_t *data, int32_t len, uint8_t *tag) {
+ crypto_aead_lock(data, tag, key, nonce, ad, (size_t)adLen, data, (size_t)len);
+}
+
+
+bool secAeadUnlock(const uint8_t *key, const uint8_t *nonce, const uint8_t *ad, int32_t adLen, uint8_t *data, int32_t len, const uint8_t *tag) {
+ return crypto_aead_unlock(data, tag, key, nonce, ad, (size_t)adLen, data, (size_t)len) == 0;
+}
+
+
+bool secX25519(uint8_t *shared, const uint8_t *secretKey, const uint8_t *peerPublic) {
+ crypto_x25519(shared, secretKey, peerPublic);
+ return !allZero(shared);
+}
+
+
+bool secX25519JobResult(SecX25519JobT *job, uint8_t *out) {
+ JobT *j = (JobT *)job;
+ bool ok = j->done && !allZero(j->out);
+
+ if (j->done) {
+ memcpy(out, j->out, SEC_X25519_KEY_SIZE);
+ }
+
+ secX25519JobWipe(job);
+ return ok;
+}
+
+
+void secX25519JobStart(SecX25519JobT *job, const uint8_t *secretKey, const uint8_t *peerPublic) {
+ JobT *j = (JobT *)job;
+
+ secX25519JobWipe(job);
+ crypto_x25519_start(&j->ctx, secretKey, peerPublic ? peerPublic : sBasePoint);
+}
+
+
+// The ladder a slice at a time, then the inversion as a slice of its own.
+bool secX25519JobStep(SecX25519JobT *job) {
+ JobT *j = (JobT *)job;
+
+ if (j->done) {
+ return true;
+ }
+
+ if (!j->laddered) {
+ j->laddered = crypto_x25519_step(&j->ctx, JOB_SLICE_BITS) != 0;
+ return false;
+ }
+
+ crypto_x25519_finish(&j->ctx, j->out);
+ j->done = true;
+ return true;
+}
+
+
+void secX25519JobWipe(SecX25519JobT *job) {
+ crypto_wipe(job, sizeof(*job));
+}
+
+
+void secX25519PublicKey(uint8_t *publicKey, const uint8_t *secretKey) {
+ crypto_x25519_public_key(publicKey, secretKey);
+}
diff --git a/src/libs/kpunch/crypto/secCurve.h b/src/libs/kpunch/crypto/secCurve.h
new file mode 100644
index 00000000..491025f5
--- /dev/null
+++ b/src/libs/kpunch/crypto/secCurve.h
@@ -0,0 +1,88 @@
+// 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.
+
+// Curve25519 and XChaCha20-Poly1305 for the security library, on
+// Monocypher (thirdparty/monocypher): the X25519 key exchange (RFC 7748),
+// XChaCha20-Poly1305 authenticated encryption, and Ed25519 signatures
+// (RFC 8032). secCurve.c holds the key exchange and the encryption (what
+// the secure link needs), secSign.c the signatures (the package library).
+//
+// An X25519 takes about a sixth of a second on a 486, so a caller that
+// must stay responsive does it a slice at a time (SecX25519JobT; secLink
+// does).
+
+#ifndef SECCURVE_H
+#define SECCURVE_H
+
+#include
+#include
+
+#define SEC_X25519_KEY_SIZE 32 // secret, public, and shared
+#define SEC_AEAD_KEY_SIZE 32
+#define SEC_AEAD_NONCE_SIZE 24
+#define SEC_AEAD_TAG_SIZE 16
+#define SEC_ED25519_SEED_SIZE 32
+#define SEC_ED25519_SECRET_SIZE 64 // the seed, then the public key
+#define SEC_ED25519_PUBLIC_SIZE 32
+#define SEC_ED25519_SIG_SIZE 64
+#define SEC_X25519_JOB_WORDS 72 // room for the ladder state and its result
+#define SEC_X25519_PENDING 1 // SecX25519Step (BASIC): more of the work to do
+
+// An X25519 under way (secX25519Job*): opaque, the caller's to keep
+typedef struct {
+ uint32_t state[SEC_X25519_JOB_WORDS];
+} SecX25519JobT;
+
+// XChaCha20-Poly1305. secAeadLock encrypts data (len bytes, in place)
+// and writes the tag over it and ad (adLen bytes, sent in the clear);
+// secAeadUnlock checks the tag and decrypts in place only when it
+// matches, else leaves data as it was and returns false. A key and
+// nonce pair must never lock two different messages; a random nonce
+// (secRngBytes) is safe.
+void secAeadLock(const uint8_t *key, const uint8_t *nonce, const uint8_t *ad, int32_t adLen, uint8_t *data, int32_t len, uint8_t *tag);
+bool secAeadUnlock(const uint8_t *key, const uint8_t *nonce, const uint8_t *ad, int32_t adLen, uint8_t *data, int32_t len, const uint8_t *tag);
+
+// Ed25519 (RFC 8032, SHA-512). secEd25519KeyPair makes the secret and
+// public key from 32 random bytes; it wipes seed. secEd25519Check is
+// true when sig is the key's signature of msg.
+bool secEd25519Check(const uint8_t *sig, const uint8_t *publicKey, const uint8_t *msg, int32_t len);
+void secEd25519KeyPair(uint8_t *secretKey, uint8_t *publicKey, uint8_t *seed);
+void secEd25519Sign(uint8_t *sig, const uint8_t *secretKey, const uint8_t *msg, int32_t len);
+
+// X25519. secretKey is 32 random bytes (secRngBytes). secX25519 is
+// false when the peer's key is one of the few that give everyone the same
+// secret (an all-zero result): a peer that sends one is cheating. Make
+// keys from the shared secret with a KDF (secHkdf); never use it as one.
+bool secX25519(uint8_t *shared, const uint8_t *secretKey, const uint8_t *peerPublic);
+void secX25519PublicKey(uint8_t *publicKey, const uint8_t *secretKey);
+
+// The same a slice at a time, each about a ninth of the work (about 20 ms
+// on a 486): secX25519JobStart begins one (peerPublic NULL: the public
+// key of secretKey), secX25519JobStep does a slice and is true once
+// there is nothing left, and secX25519JobResult writes the result (false
+// as secX25519 is) and wipes the job. secX25519JobWipe drops one.
+bool secX25519JobResult(SecX25519JobT *job, uint8_t *out);
+void secX25519JobStart(SecX25519JobT *job, const uint8_t *secretKey, const uint8_t *peerPublic);
+bool secX25519JobStep(SecX25519JobT *job);
+void secX25519JobWipe(SecX25519JobT *job);
+
+#endif
diff --git a/src/libs/kpunch/serial/security/secHash.c b/src/libs/kpunch/crypto/secHash.c
similarity index 96%
rename from src/libs/kpunch/serial/security/secHash.c
rename to src/libs/kpunch/crypto/secHash.c
index 07e17e64..d122a860 100644
--- a/src/libs/kpunch/serial/security/secHash.c
+++ b/src/libs/kpunch/crypto/secHash.c
@@ -31,6 +31,7 @@
#include
#include "secHash.h"
#include "security.h"
+#include "../libdvx/dvxEndian.h"
// ========================================================================
@@ -40,6 +41,7 @@
#define SHA256_ROUNDS 64
#define SHA256_STATE_WORDS 8
#define SHA256_BLOCK_WORDS 16
+#define SHA256_WORD_BYTES 4 // the message and digest are big-endian words
// Padding: a 0x80 marker byte, zeros, then the 64-bit bit length
#define SHA256_PAD_MARKER 0x80
@@ -242,10 +244,7 @@ void secSha256Final(SecSha256T *ctx, uint8_t digest[SEC_SHA256_SIZE]) {
sha256Block(ctx->state, ctx->buf);
for (int32_t i = 0; i < SHA256_STATE_WORDS; i++) {
- digest[i * 4] = (uint8_t)(ctx->state[i] >> 24);
- digest[i * 4 + 1] = (uint8_t)(ctx->state[i] >> 16);
- digest[i * 4 + 2] = (uint8_t)(ctx->state[i] >> 8);
- digest[i * 4 + 3] = (uint8_t)(ctx->state[i]);
+ dvxPutBe32(digest + i * SHA256_WORD_BYTES, ctx->state[i]);
}
secSecureZero(ctx, sizeof(*ctx));
@@ -299,7 +298,7 @@ static void sha256Block(uint32_t state[SHA256_STATE_WORDS], const uint8_t *block
uint32_t h = state[7];
for (int32_t i = 0; i < SHA256_BLOCK_WORDS; i++) {
- w[i] = ((uint32_t)block[i * 4] << 24) | ((uint32_t)block[i * 4 + 1] << 16) | ((uint32_t)block[i * 4 + 2] << 8) | (uint32_t)block[i * 4 + 3];
+ w[i] = dvxGetBe32(block + i * SHA256_WORD_BYTES);
}
for (int32_t i = SHA256_BLOCK_WORDS; i < SHA256_ROUNDS; i++) {
diff --git a/src/libs/kpunch/serial/security/secHash.h b/src/libs/kpunch/crypto/secHash.h
similarity index 100%
rename from src/libs/kpunch/serial/security/secHash.h
rename to src/libs/kpunch/crypto/secHash.h
diff --git a/src/libs/kpunch/crypto/secSign.c b/src/libs/kpunch/crypto/secSign.c
new file mode 100644
index 00000000..bf29c649
--- /dev/null
+++ b/src/libs/kpunch/crypto/secSign.c
@@ -0,0 +1,53 @@
+// 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.
+
+// secSign.c -- Ed25519 signatures (see secCurve.h), over Monocypher's
+// RFC 8032 flavour (SHA-512, optional/monocypher-ed25519.c).
+
+#include "secCurve.h"
+#include "monocypher.h"
+#include "optional/monocypher-ed25519.h"
+
+#include
+
+// ============================================================
+// Prototypes
+// ============================================================
+
+bool secEd25519Check(const uint8_t *sig, const uint8_t *publicKey, const uint8_t *msg, int32_t len);
+void secEd25519KeyPair(uint8_t *secretKey, uint8_t *publicKey, uint8_t *seed);
+void secEd25519Sign(uint8_t *sig, const uint8_t *secretKey, const uint8_t *msg, int32_t len);
+
+
+bool secEd25519Check(const uint8_t *sig, const uint8_t *publicKey, const uint8_t *msg, int32_t len) {
+ return crypto_ed25519_check(sig, publicKey, msg, (size_t)len) == 0;
+}
+
+
+void secEd25519KeyPair(uint8_t *secretKey, uint8_t *publicKey, uint8_t *seed) {
+ crypto_ed25519_key_pair(secretKey, publicKey, seed);
+}
+
+
+void secEd25519Sign(uint8_t *sig, const uint8_t *secretKey, const uint8_t *msg, int32_t len) {
+ crypto_ed25519_sign(sig, secretKey, msg, (size_t)len);
+}
diff --git a/src/libs/kpunch/serial/security/security.c b/src/libs/kpunch/crypto/security.c
similarity index 85%
rename from src/libs/kpunch/serial/security/security.c
rename to src/libs/kpunch/crypto/security.c
index 4c524923..ac25708c 100644
--- a/src/libs/kpunch/serial/security/security.c
+++ b/src/libs/kpunch/crypto/security.c
@@ -42,13 +42,12 @@
#include
#include
#include
-#ifdef __DJGPP__
-#include
-#include
-#include
-#endif
#include "security.h"
#include "secHash.h"
+#include "../libdvx/dvxEndian.h"
+#ifdef __DJGPP__
+#include "dvxPlat.h"
+#endif
// ========================================================================
@@ -59,6 +58,7 @@
#define BN_WORDS (BN_BITS / 32)
#define BN_BYTES (BN_BITS / 8)
#define BN_WORD_BITS 32
+#define BN_WORD_BYTES (BN_WORD_BITS / 8)
// DH wire size (SEC_DH_KEY_SIZE) and bignum byte width (BN_BYTES) must agree;
// they describe the same 1024-bit value and are used interchangeably.
typedef char secDhKeySizeMatch[(BN_BYTES == SEC_DH_KEY_SIZE) ? 1 : -1];
@@ -79,6 +79,9 @@ typedef char secDhKeySizeMatch[(BN_BYTES == SEC_DH_KEY_SIZE) ? 1 : -1];
// Smallest valid remote public key (keys must lie in [2, p-2])
#define DH_MIN_PUBLIC 2
+// Ladder steps (exponent bits) secDhStep does: a sixteenth of the work
+#define DH_STEP_BITS 16
+
#define XTEA_ROUNDS 32
#define XTEA_DELTA 0x9E3779B9
#define XTEA_BLOCK_SIZE 8 // XTEA operates on 8-byte (two-uint32_t) blocks
@@ -93,21 +96,11 @@ typedef char secDhKeySizeMatch[(BN_BYTES == SEC_DH_KEY_SIZE) ? 1 : -1];
#define RNG_COUNTER_BYTES 8
#ifdef __DJGPP__
-// Hardware entropy sources for the DRBG pool
-#define PIT_CMD_PORT 0x43 // PIT mode/command register
-#define PIT_CH0_PORT 0x40 // PIT channel 0 data port
-#define PIT_LATCH_CMD 0x00 // Latch counter command for channel 0
-#define BIOS_TICK_ADDR 0x46C // BIOS tick count linear address (18.2 Hz)
-
-// PIT samples hashed per 32 bytes of gathered entropy. Each sample is
+// Entropy stamps hashed per 32 bytes of gathered entropy. Each sample is
// taken after a busy wait whose length depends on the previous reading,
// so sampling instants accumulate interrupt, bus and cache jitter.
#define RNG_GATHER_SAMPLES 256
#define RNG_SPIN_MASK 0x3F
-
-#define EFLAGS_ID 0x00200000u // toggleable only when CPUID exists
-#define CPUID_LEAF_FEATURES 1
-#define CPUID_EDX_TSC 0x00000010u // time stamp counter present
#else
// Operating system random device used by host builds
#define RNG_OS_DEVICE "/dev/urandom"
@@ -122,10 +115,21 @@ typedef struct {
uint32_t w[BN_WORDS];
} BigNumT;
+// What a SecDhT is computing a step at a time
+typedef enum {
+ DhIdleE,
+ DhKeysE, // the public key: g^private
+ DhSecretE // the shared secret: remote^private
+} DhOpE;
+
struct SecDhS {
BigNumT privateKey;
BigNumT publicKey;
BigNumT sharedSecret;
+ BigNumT r0; // the ladder under way (Montgomery form)
+ BigNumT r1;
+ int32_t bit; // its next exponent bit (counting down)
+ DhOpE op;
bool hasKeys;
bool hasSecret;
};
@@ -141,14 +145,6 @@ typedef struct {
bool gathered;
} RngStateT;
-#ifdef __DJGPP__
-typedef struct {
- uint32_t tscLo;
- uint32_t tscHi;
- uint16_t pit;
-} EntropySampleT;
-#endif
-
// ========================================================================
// Static globals
@@ -195,7 +191,8 @@ static void bnClear(BigNumT *a);
static int bnCmp(const BigNumT *a, const BigNumT *b);
static void bnCondSwap(BigNumT *a, BigNumT *b, uint32_t swap);
static void bnFromBytes(BigNumT *a, const uint8_t *buf);
-static void bnModExp(BigNumT *result, const BigNumT *base, const BigNumT *exp);
+static void bnModExpStart(SecDhT *dh, const BigNumT *base);
+static bool bnModExpStep(SecDhT *dh, int32_t bits, BigNumT *result);
static void bnMontMul(BigNumT *result, const BigNumT *a, const BigNumT *b);
static void bnSet(BigNumT *a, uint32_t val);
static int bnShiftLeft1(BigNumT *a);
@@ -203,9 +200,6 @@ static int bnSub(BigNumT *result, const BigNumT *a, const BigNumT *b);
static void bnToBytes(uint8_t *buf, const BigNumT *a);
static uint32_t computeM0Inv(uint32_t m0);
static void computeR2(BigNumT *r2, const BigNumT *m);
-#ifdef __DJGPP__
-static bool cpuHasTsc(void);
-#endif
static void dhInit(void);
static void gatherBlock(uint8_t digest[SEC_SHA256_SIZE]);
static void rngHashCounter(uint8_t out[SEC_SHA256_SIZE], uint8_t tag);
@@ -214,11 +208,14 @@ void secCipherCrypt(SecCipherT *c, uint8_t *data, int len);
void secCipherDestroy(SecCipherT *c);
void secCipherSetCounter(SecCipherT *c, uint64_t counter);
int secDhComputeSecret(SecDhT *dh, const uint8_t *remotePub, int len);
+int secDhComputeStart(SecDhT *dh, const uint8_t *remotePub, int len);
SecDhT *secDhCreate(void);
void secDhDestroy(SecDhT *dh);
int secDhGenerateKeys(SecDhT *dh);
+int secDhGenerateStart(SecDhT *dh);
int secDhGetPublicKey(SecDhT *dh, uint8_t *buf, int *len);
int secDhGetSecret(SecDhT *dh, uint8_t *buf, int *len);
+int secDhStep(SecDhT *dh);
void secRngAddEntropy(const uint8_t *data, int32_t len);
void secRngBytes(uint8_t *buf, int32_t len);
int32_t secRngGatherEntropy(uint8_t *buf, int32_t len);
@@ -272,14 +269,13 @@ static void bnCondSwap(BigNumT *a, BigNumT *b, uint32_t swap) {
static void bnFromBytes(BigNumT *a, const uint8_t *buf) {
for (int i = 0; i < BN_WORDS; i++) {
- int j = (BN_WORDS - 1 - i) * 4;
-
- a->w[i] = ((uint32_t)buf[j] << 24) | ((uint32_t)buf[j + 1] << 16) | ((uint32_t)buf[j + 2] << 8) | (uint32_t)buf[j + 3];
+ a->w[i] = dvxGetBe32(buf + (BN_WORDS - 1 - i) * BN_WORD_BYTES);
}
}
-// Modular exponentiation mod the DH prime using Montgomery multiplication.
+// Modular exponentiation mod the DH prime using Montgomery multiplication,
+// to the private key's power, a slice of its bits at a time.
//
// Montgomery multiplication replaces expensive modular reduction (division
// by a 1024-bit number) with cheaper additions and right-shifts, at the
@@ -290,33 +286,45 @@ static void bnFromBytes(BigNumT *a, const uint8_t *buf) {
// Montgomery ladder over a fixed DH_PRIVATE_BITS exponent bits: every bit
// costs exactly one multiply and one square, and the operands are chosen
// with a branch-free conditional swap, so neither the operation count nor
-// the memory access pattern depends on the exponent. Invariant: r1 = r0 *
-// base. Exponents are private keys, always below 2^DH_PRIVATE_BITS.
-static void bnModExp(BigNumT *result, const BigNumT *base, const BigNumT *exp) {
- BigNumT r0;
- BigNumT r1;
+// the memory access pattern depends on the exponent (nor does where the
+// slices end: a fixed number of bits each). Invariant: r1 = r0 * base.
+// Private keys are always below 2^DH_PRIVATE_BITS.
+static void bnModExpStart(SecDhT *dh, const BigNumT *base) {
BigNumT one;
bnSet(&one, 1);
// r0 = 1 and r1 = base, both in Montgomery form
- bnMontMul(&r0, &one, &sDhR2);
- bnMontMul(&r1, base, &sDhR2);
+ bnMontMul(&dh->r0, &one, &sDhR2);
+ bnMontMul(&dh->r1, base, &sDhR2);
+ dh->bit = DH_PRIVATE_BITS - 1;
+}
- for (int i = DH_PRIVATE_BITS - 1; i >= 0; i--) {
- uint32_t bit = (uint32_t)bnBit(exp, i);
- bnCondSwap(&r0, &r1, bit);
- bnMontMul(&r1, &r0, &r1);
- bnMontMul(&r0, &r0, &r0);
- bnCondSwap(&r0, &r1, bit);
+// Up to bits more of the ladder; true once it is done, the power in result
+// (back from Montgomery form) and the ladder's state wiped.
+static bool bnModExpStep(SecDhT *dh, int32_t bits, BigNumT *result) {
+ BigNumT one;
+
+ for (; bits > 0 && dh->bit >= 0; bits--, dh->bit--) {
+ uint32_t bit = (uint32_t)bnBit(&dh->privateKey, dh->bit);
+
+ bnCondSwap(&dh->r0, &dh->r1, bit);
+ bnMontMul(&dh->r1, &dh->r0, &dh->r1);
+ bnMontMul(&dh->r0, &dh->r0, &dh->r0);
+ bnCondSwap(&dh->r0, &dh->r1, bit);
+ }
+
+ if (dh->bit >= 0) {
+ return false;
}
// Convert back from Montgomery form: result = r0 * 1 * R^(-1) mod m
- bnMontMul(result, &r0, &one);
-
- secSecureZero(&r0, sizeof(r0));
- secSecureZero(&r1, sizeof(r1));
+ bnSet(&one, 1);
+ bnMontMul(result, &dh->r0, &one);
+ secSecureZero(&dh->r0, sizeof(dh->r0));
+ secSecureZero(&dh->r1, sizeof(dh->r1));
+ return true;
}
@@ -420,12 +428,7 @@ static int bnSub(BigNumT *result, const BigNumT *a, const BigNumT *b) {
static void bnToBytes(uint8_t *buf, const BigNumT *a) {
for (int i = 0; i < BN_WORDS; i++) {
- int j = (BN_WORDS - 1 - i) * 4;
- uint32_t w = a->w[i];
- buf[j] = (uint8_t)(w >> 24);
- buf[j + 1] = (uint8_t)(w >> 16);
- buf[j + 2] = (uint8_t)(w >> 8);
- buf[j + 3] = (uint8_t)(w);
+ dvxPutBe32(buf + (BN_WORDS - 1 - i) * BN_WORD_BYTES, a->w[i]);
}
}
@@ -463,47 +466,6 @@ static void computeR2(BigNumT *r2, const BigNumT *m) {
}
-#ifdef __DJGPP__
-// RDTSC exists when CPUID exists (EFLAGS.ID toggles) and leaf 1 reports
-// the TSC feature. A 386 or an early 486 has neither.
-static bool cpuHasTsc(void) {
- uint32_t before;
- uint32_t after;
- uint32_t eax;
- uint32_t ebx;
- uint32_t ecx;
- uint32_t edx;
-
- __asm__ __volatile__ (
- "pushfl\n\t"
- "popl %%eax\n\t"
- "movl %%eax, %0\n\t"
- "xorl %2, %%eax\n\t"
- "pushl %%eax\n\t"
- "popfl\n\t"
- "pushfl\n\t"
- "popl %%eax\n\t"
- "movl %%eax, %1\n\t"
- "pushl %0\n\t"
- "popfl"
- : "=&r"(before), "=&r"(after)
- : "i"(EFLAGS_ID)
- : "eax", "cc"
- );
-
- if (((before ^ after) & EFLAGS_ID) == 0) {
- return false;
- }
-
- __asm__ __volatile__ ("cpuid" : "=a"(eax), "=b"(ebx), "=c"(ecx), "=d"(edx) : "a"(CPUID_LEAF_FEATURES));
-
- return (edx & CPUID_EDX_TSC) != 0;
-}
-
-
-#endif
-
-
static void dhInit(void) {
if (sDhInited) {
return;
@@ -516,51 +478,30 @@ static void dhInit(void) {
#ifdef __DJGPP__
-// Condense RNG_GATHER_SAMPLES hardware samples into one digest. Each
-// sample is a latched PIT channel 0 reading (1.19 MHz) plus, on CPUs that
-// have one, the full time stamp counter. The busy wait before each sample
-// is sized by the previous PIT reading, so every sampling instant depends
-// on all the jitter before it; the BIOS tick count anchors the digest to
-// the time of day. The TSC alone contributes the unknown uptime at CPU
-// clock resolution, far beyond what an attacker can enumerate.
+// Condense RNG_GATHER_SAMPLES platform entropy stamps into one digest.
+// Each is the latched PIT channel 0 counter (1.19 MHz, its own crystal)
+// with the time stamp counter on CPUs that have one, else with the BIOS
+// tick count. The busy wait before each sample is sized by the previous
+// one, so every sampling instant depends on all the jitter before it.
+// The TSC alone contributes the unknown uptime at CPU clock resolution,
+// far beyond what an attacker can enumerate.
static void gatherBlock(uint8_t digest[SEC_SHA256_SIZE]) {
- SecSha256T ctx;
- EntropySampleT sample;
- uint32_t ticks;
- bool hasTsc;
- uint16_t prev;
+ SecSha256T ctx;
+ uint64_t sample = 0;
secSha256Init(&ctx);
- ticks = _farpeekl(_dos_ds, BIOS_TICK_ADDR);
- secSha256Update(&ctx, (const uint8_t *)&ticks, sizeof(ticks));
-
- hasTsc = cpuHasTsc();
- prev = 0;
- memset(&sample, 0, sizeof(sample));
-
for (int32_t i = 0; i < RNG_GATHER_SAMPLES; i++) {
- volatile uint32_t spin = (uint32_t)(prev & RNG_SPIN_MASK) + 1;
+ volatile uint32_t spin = (uint32_t)(sample & RNG_SPIN_MASK) + 1;
while (spin > 0) {
spin--;
}
- outportb(PIT_CMD_PORT, PIT_LATCH_CMD);
- sample.pit = inportb(PIT_CH0_PORT);
- sample.pit |= (uint16_t)(inportb(PIT_CH0_PORT) << 8);
-
- if (hasTsc) {
- __asm__ __volatile__ ("rdtsc" : "=a"(sample.tscLo), "=d"(sample.tscHi));
- }
-
+ sample = platformEntropyStamp();
secSha256Update(&ctx, (const uint8_t *)&sample, sizeof(sample));
- prev = sample.pit;
}
- ticks = _farpeekl(_dos_ds, BIOS_TICK_ADDR);
- secSha256Update(&ctx, (const uint8_t *)&ticks, sizeof(ticks));
-
secSha256Final(&ctx, digest);
secSecureZero(&sample, sizeof(sample));
}
@@ -659,12 +600,23 @@ void secCipherSetCounter(SecCipherT *c, uint64_t counter) {
}
-// Compute the shared secret from the remote side's public key.
+int secDhComputeSecret(SecDhT *dh, const uint8_t *remotePub, int len) {
+ int rc = secDhComputeStart(dh, remotePub, len);
+
+ while (rc == SEC_DH_PENDING) {
+ rc = secDhStep(dh);
+ }
+
+ return rc;
+}
+
+
+// Start computing the shared secret from the remote side's public key.
// Validates that the remote key is in [2, p-2] to prevent small-subgroup
// attacks (keys of 0, 1, or p-1 would produce trivially guessable secrets).
// The shared secret is remote^private mod p, which both sides compute
// independently and arrive at the same value (the DH property).
-int secDhComputeSecret(SecDhT *dh, const uint8_t *remotePub, int len) {
+int secDhComputeStart(SecDhT *dh, const uint8_t *remotePub, int len) {
BigNumT remote;
BigNumT low;
BigNumT high;
@@ -693,12 +645,13 @@ int secDhComputeSecret(SecDhT *dh, const uint8_t *remotePub, int len) {
return SEC_ERR_PARAM;
}
- // shared = remote^private mod p
- bnModExp(&dh->sharedSecret, &remote, &dh->privateKey);
- dh->hasSecret = true;
+ // shared = remote^private mod p, by secDhStep
+ bnModExpStart(dh, &remote);
+ dh->op = DhSecretE;
+ dh->hasSecret = false;
secSecureZero(&remote, sizeof(remote));
- return SEC_SUCCESS;
+ return SEC_DH_PENDING;
}
@@ -718,10 +671,21 @@ void secDhDestroy(SecDhT *dh) {
}
-// Generate a DH keypair: random 256-bit private key, then compute
+int secDhGenerateKeys(SecDhT *dh) {
+ int rc = secDhGenerateStart(dh);
+
+ while (rc == SEC_DH_PENDING) {
+ rc = secDhStep(dh);
+ }
+
+ return rc;
+}
+
+
+// Start a DH keypair: random 256-bit private key now, then (secDhStep)
// public = g^private mod p. The private key is only 256 bits (not 1024)
// to keep exponentiation fast on 486-class hardware.
-int secDhGenerateKeys(SecDhT *dh) {
+int secDhGenerateStart(SecDhT *dh) {
if (!dh) {
return SEC_ERR_PARAM;
}
@@ -737,13 +701,12 @@ int secDhGenerateKeys(SecDhT *dh) {
// trivial).
dh->privateKey.w[DH_PRIVATE_WORDS - 1] |= DH_PRIVATE_TOP_BIT;
- // public = g^private mod p
- bnModExp(&dh->publicKey, &sDhGenerator, &dh->privateKey);
-
- dh->hasKeys = true;
+ bnModExpStart(dh, &sDhGenerator);
+ dh->op = DhKeysE;
+ dh->hasKeys = false;
dh->hasSecret = false;
- return SEC_SUCCESS;
+ return SEC_DH_PENDING;
}
@@ -786,6 +749,37 @@ int secDhGetSecret(SecDhT *dh, uint8_t *buf, int *len) {
// Hash the new bytes into the pool: pool = SHA-256(pool || tag || data).
// Nothing already in the pool is lost, so this is safe to call at any
// time and with any data, including data an attacker may know.
+int secDhStep(SecDhT *dh) {
+ if (!dh) {
+ return SEC_ERR_PARAM;
+ }
+
+ switch (dh->op) {
+ case DhKeysE:
+ if (!bnModExpStep(dh, DH_STEP_BITS, &dh->publicKey)) {
+ return SEC_DH_PENDING;
+ }
+
+ dh->hasKeys = true;
+ break;
+
+ case DhSecretE:
+ if (!bnModExpStep(dh, DH_STEP_BITS, &dh->sharedSecret)) {
+ return SEC_DH_PENDING;
+ }
+
+ dh->hasSecret = true;
+ break;
+
+ case DhIdleE:
+ return SEC_ERR_NOT_READY;
+ }
+
+ dh->op = DhIdleE;
+ return SEC_SUCCESS;
+}
+
+
void secRngAddEntropy(const uint8_t *data, int32_t len) {
SecSha256T ctx;
uint8_t tag = RNG_TAG_ADD;
diff --git a/src/libs/kpunch/serial/security/security.h b/src/libs/kpunch/crypto/security.h
similarity index 85%
rename from src/libs/kpunch/serial/security/security.h
rename to src/libs/kpunch/crypto/security.h
index 1b7cb17a..4e760ae6 100644
--- a/src/libs/kpunch/serial/security/security.h
+++ b/src/libs/kpunch/crypto/security.h
@@ -44,9 +44,9 @@
// RNG:
// SHA-256 DRBG over an entropy pool. Adding entropy never discards what
// is already in the pool. The first output auto-gathers hardware
-// entropy (on DOS: hundreds of jittered PIT samples, the CPU time stamp
-// counter when present, and the BIOS tick; elsewhere the OS random
-// device). Callers should also feed user-input timings with
+// entropy (on DOS: hundreds of jittered PIT samples, each with the CPU
+// time stamp counter when present, else the BIOS tick; elsewhere the OS
+// random device). Callers should also feed user-input timings with
// secRngAddEntropy and carry entropy across runs with a seed file
// (secRngLoadSeed at startup, secRngSaveSeed at shutdown).
@@ -66,6 +66,7 @@
#define SEC_ERR_PARAM (-1)
#define SEC_ERR_NOT_READY (-2)
#define SEC_ERR_IO (-3)
+#define SEC_DH_PENDING 1 // secDhStep: more of the exponentiation to do
// Opaque types
typedef struct SecDhS SecDhT;
@@ -92,12 +93,22 @@ void secSecureZero(void *ptr, int len);
// public key must lie in [2, p-2]. secDhGetSecret returns the shared
// secret as SEC_DH_KEY_SIZE big-endian bytes for a KDF (never use it as a
// key directly).
+//
+// An exponentiation takes about a second on a 486, so it can also be done
+// a slice at a time: secDhGenerateStart or secDhComputeStart starts one
+// (checking what it is given at once), and secDhStep does a slice of it
+// (an eighth to a sixteenth of the work) until it says SEC_SUCCESS.
+// secDhGenerateKeys and secDhComputeSecret do the whole of it at once.
+// Starting another drops one under way.
int secDhComputeSecret(SecDhT *dh, const uint8_t *remotePub, int len);
+int secDhComputeStart(SecDhT *dh, const uint8_t *remotePub, int len);
SecDhT *secDhCreate(void);
void secDhDestroy(SecDhT *dh);
int secDhGenerateKeys(SecDhT *dh);
+int secDhGenerateStart(SecDhT *dh);
int secDhGetPublicKey(SecDhT *dh, uint8_t *buf, int *len);
int secDhGetSecret(SecDhT *dh, uint8_t *buf, int *len);
+int secDhStep(SecDhT *dh);
// XTEA cipher in CTR mode. CTR mode turns a block cipher into a stream
// cipher: encrypt an incrementing counter with the key to produce a
diff --git a/src/libs/kpunch/pkg/thirdparty/monocypher/AUTHORS.md b/src/libs/kpunch/crypto/thirdparty/monocypher/AUTHORS.md
similarity index 100%
rename from src/libs/kpunch/pkg/thirdparty/monocypher/AUTHORS.md
rename to src/libs/kpunch/crypto/thirdparty/monocypher/AUTHORS.md
diff --git a/src/libs/kpunch/crypto/thirdparty/monocypher/DVX-VERSION.md b/src/libs/kpunch/crypto/thirdparty/monocypher/DVX-VERSION.md
new file mode 100644
index 00000000..704d39cf
--- /dev/null
+++ b/src/libs/kpunch/crypto/thirdparty/monocypher/DVX-VERSION.md
@@ -0,0 +1,54 @@
+# Monocypher for DVX
+
+| Item | Value |
+|---|---|
+| Project | Monocypher, a small, auditable crypto library, https://monocypher.org |
+| Version | 4.0.3 (2026-06-15; fixes a timing leak in EdDSA/Ed25519 signing) |
+| Download | https://monocypher.org/download/monocypher-4.0.3.tar.gz |
+| Tarball SHA-512 | 40904ada5c7ee4f7741733e38b69a30a4b0561cbffba5ffe7c2dce16136d540251ec0d9056ff606510d3b5b708fb8a40db7e0870d4a0b2dc17ba2bfb880f8965 (matches the .sha512 published beside the download) |
+| Files kept | src/monocypher.c, src/monocypher.h, src/optional/monocypher-ed25519.c, src/optional/monocypher-ed25519.h, LICENCE.md, AUTHORS.md -- byte for byte as in the tarball, in the same folders, but for the patch below. Not kept: doc/, tests/, the makefile, README, CHANGELOG. |
+| Licence | Dual: 2-clause BSD or CC-0 (LICENCE.md); DVX takes the BSD licence. Keep the notice with the source. |
+| Patches | One: dvx-x25519-step.patch (this folder), 2026-10-20 |
+
+## Patches
+
+`dvx-x25519-step.patch` adds `crypto_x25519_ctx` and
+`crypto_x25519_start`, `crypto_x25519_step`, and `crypto_x25519_finish`
+(monocypher.h, and after `crypto_x25519_public_key` in monocypher.c): the
+Montgomery ladder of `scalarmult()` with its state kept in the caller's
+context, so it can run a number of bits at a time. The steps are
+scalarmult()'s, in the same order (the result is crypto_x25519()'s; the
+X25519 vectors in src/test/serial check both). Why: one X25519 takes
+165 ms on a 486DX2-66, too long to hold up every other program;
+secCurve.c's SecX25519JobT does it in nine slices of about 20 ms. Nothing
+else in Monocypher is changed.
+
+To reapply after an update: from this folder, `patch -p1 <
+dvx-x25519-step.patch` (or by hand: the patch only adds lines), then
+rebuild and run the tests below.
+
+## Where it is used
+
+- `crypto.lib` (src/libs/kpunch/crypto): the one copy in DVX.
+ secCurve.c wraps X25519 and XChaCha20-Poly1305 (the secure link's
+ protocol 3, security.bas), secSign.c wraps Ed25519 (the SHA-512 flavour
+ in optional/), which checks and makes the signatures of add-on packages
+ for the package library and its writer module. The library keeps only
+ what those wrappers reach (DXE_GC) and exports the wrappers alone.
+- Programs that link the sources in rather than load crypto.lib: the
+ package tool (`bin/host/dvxpkg`, and DVXPKG.EXE in DVX), SETUP.EXE, the
+ host proxy (`bin/host/secproxy`, secCurve.c only), and the host tests.
+- Host tests: RFC 7748 and the XChaCha20-Poly1305 draft's vectors
+ (src/test/serial), an RFC 8032 vector (src/test/pkg).
+
+It compiles with the normal DVX warning flags for DJGPP and the host; no
+flags are relaxed for it.
+
+## Updating
+
+1. Download the new tarball, check its SHA-512 against the .sha512 file
+ on the download page, record the new version and hash here.
+2. Replace the six files above, nothing else, and reapply the patch.
+3. Rebuild (`make`), run `make -C src/test/serial` (the X25519 and
+ XChaCha20-Poly1305 vectors and the secure link), `make -C src/test/pkg`
+ (the RFC 8032 vector must still match), and `make -C src/test/setup`.
diff --git a/src/libs/kpunch/pkg/thirdparty/monocypher/LICENCE.md b/src/libs/kpunch/crypto/thirdparty/monocypher/LICENCE.md
similarity index 100%
rename from src/libs/kpunch/pkg/thirdparty/monocypher/LICENCE.md
rename to src/libs/kpunch/crypto/thirdparty/monocypher/LICENCE.md
diff --git a/src/libs/kpunch/crypto/thirdparty/monocypher/dvx-x25519-step.patch b/src/libs/kpunch/crypto/thirdparty/monocypher/dvx-x25519-step.patch
new file mode 100644
index 00000000..33ce0b6d
--- /dev/null
+++ b/src/libs/kpunch/crypto/thirdparty/monocypher/dvx-x25519-step.patch
@@ -0,0 +1,95 @@
+--- a/src/monocypher.h
++++ b/src/monocypher.h
+@@ -195,6 +195,23 @@
+ const uint8_t your_secret_key [32],
+ const uint8_t their_public_key [32]);
+
++// DVX patch (DVX-VERSION.md): crypto_x25519() a slice at a time, for
++// computers slow enough that one X-25519 holds up everything else.
++// start, then step until it returns 1, then finish (which wipes ctx).
++typedef struct {
++ int32_t x1[10], x2[10], z2[10], x3[10], z3[10];
++ uint8_t scalar[32];
++ int swap;
++ int pos;
++} crypto_x25519_ctx;
++
++void crypto_x25519_start (crypto_x25519_ctx *ctx,
++ const uint8_t your_secret_key [32],
++ const uint8_t their_public_key [32]);
++int crypto_x25519_step (crypto_x25519_ctx *ctx, int nb_bits);
++void crypto_x25519_finish(crypto_x25519_ctx *ctx,
++ uint8_t raw_shared_secret[32]);
++
+ // Conversion to EdDSA
+ void crypto_x25519_to_eddsa(uint8_t eddsa[32], const uint8_t x25519[32]);
+
+--- a/src/monocypher.c
++++ b/src/monocypher.c
+@@ -1593,6 +1593,66 @@
+ crypto_x25519(public_key, secret_key, base_point);
+ }
+
++// DVX patch (DVX-VERSION.md): the ladder of scalarmult() kept in ctx, so
++// it can run nb_bits at a time. The same steps as scalarmult(), in the
++// same order, so the result is crypto_x25519()'s.
++void crypto_x25519_start(crypto_x25519_ctx *ctx,
++ const u8 your_secret_key [32],
++ const u8 their_public_key[32])
++{
++ crypto_eddsa_trim_scalar(ctx->scalar, your_secret_key);
++ fe_frombytes(ctx->x1, their_public_key);
++ fe_1(ctx->x2); fe_0(ctx->z2);
++ fe_copy(ctx->x3, ctx->x1); fe_1(ctx->z3);
++ ctx->swap = 0;
++ ctx->pos = 254;
++}
++
++int crypto_x25519_step(crypto_x25519_ctx *ctx, int nb_bits)
++{
++ fe t0, t1;
++ for (; nb_bits > 0 && ctx->pos >= 0; nb_bits--, ctx->pos--) {
++ int b = scalar_bit(ctx->scalar, ctx->pos);
++ ctx->swap ^= b;
++ fe_cswap(ctx->x2, ctx->x3, ctx->swap);
++ fe_cswap(ctx->z2, ctx->z3, ctx->swap);
++ ctx->swap = b;
++
++ fe_sub(t0, ctx->x3, ctx->z3);
++ fe_sub(t1, ctx->x2, ctx->z2);
++ fe_add(ctx->x2, ctx->x2, ctx->z2);
++ fe_add(ctx->z2, ctx->x3, ctx->z3);
++ fe_mul(ctx->z3, t0, ctx->x2);
++ fe_mul(ctx->z2, ctx->z2, t1);
++ fe_sq (t0, t1);
++ fe_sq (t1, ctx->x2);
++ fe_add(ctx->x3, ctx->z3, ctx->z2);
++ fe_sub(ctx->z2, ctx->z3, ctx->z2);
++ fe_mul(ctx->x2, t1, t0);
++ fe_sub(t1, t1, t0);
++ fe_sq (ctx->z2, ctx->z2);
++ fe_mul_small(ctx->z3, t1, 121666);
++ fe_sq (ctx->x3, ctx->x3);
++ fe_add(t0, t0, ctx->z3);
++ fe_mul(ctx->z3, ctx->x1, ctx->z2);
++ fe_mul(ctx->z2, t1, t0);
++ }
++ WIPE_BUFFER(t0);
++ WIPE_BUFFER(t1);
++ return ctx->pos < 0;
++}
++
++void crypto_x25519_finish(crypto_x25519_ctx *ctx,
++ u8 raw_shared_secret[32])
++{
++ fe_cswap(ctx->x2, ctx->x3, ctx->swap);
++ fe_cswap(ctx->z2, ctx->z3, ctx->swap);
++ fe_invert(ctx->z2, ctx->z2);
++ fe_mul(ctx->x2, ctx->x2, ctx->z2);
++ fe_tobytes(raw_shared_secret, ctx->x2);
++ crypto_wipe(ctx, sizeof(*ctx));
++}
++
+ ///////////////////////////
+ /// Arithmetic modulo L ///
+ ///////////////////////////
diff --git a/src/libs/kpunch/pkg/thirdparty/monocypher/src/monocypher.c b/src/libs/kpunch/crypto/thirdparty/monocypher/src/monocypher.c
similarity index 98%
rename from src/libs/kpunch/pkg/thirdparty/monocypher/src/monocypher.c
rename to src/libs/kpunch/crypto/thirdparty/monocypher/src/monocypher.c
index fe5bbdc8..527b9114 100644
--- a/src/libs/kpunch/pkg/thirdparty/monocypher/src/monocypher.c
+++ b/src/libs/kpunch/crypto/thirdparty/monocypher/src/monocypher.c
@@ -1593,6 +1593,66 @@ void crypto_x25519_public_key(u8 public_key[32],
crypto_x25519(public_key, secret_key, base_point);
}
+// DVX patch (DVX-VERSION.md): the ladder of scalarmult() kept in ctx, so
+// it can run nb_bits at a time. The same steps as scalarmult(), in the
+// same order, so the result is crypto_x25519()'s.
+void crypto_x25519_start(crypto_x25519_ctx *ctx,
+ const u8 your_secret_key [32],
+ const u8 their_public_key[32])
+{
+ crypto_eddsa_trim_scalar(ctx->scalar, your_secret_key);
+ fe_frombytes(ctx->x1, their_public_key);
+ fe_1(ctx->x2); fe_0(ctx->z2);
+ fe_copy(ctx->x3, ctx->x1); fe_1(ctx->z3);
+ ctx->swap = 0;
+ ctx->pos = 254;
+}
+
+int crypto_x25519_step(crypto_x25519_ctx *ctx, int nb_bits)
+{
+ fe t0, t1;
+ for (; nb_bits > 0 && ctx->pos >= 0; nb_bits--, ctx->pos--) {
+ int b = scalar_bit(ctx->scalar, ctx->pos);
+ ctx->swap ^= b;
+ fe_cswap(ctx->x2, ctx->x3, ctx->swap);
+ fe_cswap(ctx->z2, ctx->z3, ctx->swap);
+ ctx->swap = b;
+
+ fe_sub(t0, ctx->x3, ctx->z3);
+ fe_sub(t1, ctx->x2, ctx->z2);
+ fe_add(ctx->x2, ctx->x2, ctx->z2);
+ fe_add(ctx->z2, ctx->x3, ctx->z3);
+ fe_mul(ctx->z3, t0, ctx->x2);
+ fe_mul(ctx->z2, ctx->z2, t1);
+ fe_sq (t0, t1);
+ fe_sq (t1, ctx->x2);
+ fe_add(ctx->x3, ctx->z3, ctx->z2);
+ fe_sub(ctx->z2, ctx->z3, ctx->z2);
+ fe_mul(ctx->x2, t1, t0);
+ fe_sub(t1, t1, t0);
+ fe_sq (ctx->z2, ctx->z2);
+ fe_mul_small(ctx->z3, t1, 121666);
+ fe_sq (ctx->x3, ctx->x3);
+ fe_add(t0, t0, ctx->z3);
+ fe_mul(ctx->z3, ctx->x1, ctx->z2);
+ fe_mul(ctx->z2, t1, t0);
+ }
+ WIPE_BUFFER(t0);
+ WIPE_BUFFER(t1);
+ return ctx->pos < 0;
+}
+
+void crypto_x25519_finish(crypto_x25519_ctx *ctx,
+ u8 raw_shared_secret[32])
+{
+ fe_cswap(ctx->x2, ctx->x3, ctx->swap);
+ fe_cswap(ctx->z2, ctx->z3, ctx->swap);
+ fe_invert(ctx->z2, ctx->z2);
+ fe_mul(ctx->x2, ctx->x2, ctx->z2);
+ fe_tobytes(raw_shared_secret, ctx->x2);
+ crypto_wipe(ctx, sizeof(*ctx));
+}
+
///////////////////////////
/// Arithmetic modulo L ///
///////////////////////////
diff --git a/src/libs/kpunch/pkg/thirdparty/monocypher/src/monocypher.h b/src/libs/kpunch/crypto/thirdparty/monocypher/src/monocypher.h
similarity index 94%
rename from src/libs/kpunch/pkg/thirdparty/monocypher/src/monocypher.h
rename to src/libs/kpunch/crypto/thirdparty/monocypher/src/monocypher.h
index 291b8b9e..203346cc 100644
--- a/src/libs/kpunch/pkg/thirdparty/monocypher/src/monocypher.h
+++ b/src/libs/kpunch/crypto/thirdparty/monocypher/src/monocypher.h
@@ -195,6 +195,23 @@ void crypto_x25519(uint8_t raw_shared_secret[32],
const uint8_t your_secret_key [32],
const uint8_t their_public_key [32]);
+// DVX patch (DVX-VERSION.md): crypto_x25519() a slice at a time, for
+// computers slow enough that one X-25519 holds up everything else.
+// start, then step until it returns 1, then finish (which wipes ctx).
+typedef struct {
+ int32_t x1[10], x2[10], z2[10], x3[10], z3[10];
+ uint8_t scalar[32];
+ int swap;
+ int pos;
+} crypto_x25519_ctx;
+
+void crypto_x25519_start (crypto_x25519_ctx *ctx,
+ const uint8_t your_secret_key [32],
+ const uint8_t their_public_key [32]);
+int crypto_x25519_step (crypto_x25519_ctx *ctx, int nb_bits);
+void crypto_x25519_finish(crypto_x25519_ctx *ctx,
+ uint8_t raw_shared_secret[32]);
+
// Conversion to EdDSA
void crypto_x25519_to_eddsa(uint8_t eddsa[32], const uint8_t x25519[32]);
diff --git a/src/libs/kpunch/pkg/thirdparty/monocypher/src/optional/monocypher-ed25519.c b/src/libs/kpunch/crypto/thirdparty/monocypher/src/optional/monocypher-ed25519.c
similarity index 100%
rename from src/libs/kpunch/pkg/thirdparty/monocypher/src/optional/monocypher-ed25519.c
rename to src/libs/kpunch/crypto/thirdparty/monocypher/src/optional/monocypher-ed25519.c
diff --git a/src/libs/kpunch/pkg/thirdparty/monocypher/src/optional/monocypher-ed25519.h b/src/libs/kpunch/crypto/thirdparty/monocypher/src/optional/monocypher-ed25519.h
similarity index 100%
rename from src/libs/kpunch/pkg/thirdparty/monocypher/src/optional/monocypher-ed25519.h
rename to src/libs/kpunch/crypto/thirdparty/monocypher/src/optional/monocypher-ed25519.h
diff --git a/src/libs/kpunch/dvxnet/Makefile b/src/libs/kpunch/dvxnet/Makefile
index 06443dd2..947e361a 100644
--- a/src/libs/kpunch/dvxnet/Makefile
+++ b/src/libs/kpunch/dvxnet/Makefile
@@ -40,7 +40,13 @@ LIBSDIR = ../../../../bin/libs
SRCS = dvxNet.c netBas.c netDos.c netOrder.c netProto.c netResolve.c netSock.c netStack.c pktDrvDos.c pktRing.c
LWIP_SRCS = $(wildcard $(LWIP_DIR)/core/*.c) $(wildcard $(LWIP_DIR)/core/ipv4/*.c) $(LWIP_DIR)/netif/ethernet.c
-OBJS = $(patsubst %.c,$(OBJDIR)/%.o,$(SRCS)) $(patsubst $(LWIP_DIR)/%.c,$(OBJDIR)/lwip/%.o,$(LWIP_SRCS))
+OWN_OBJS = $(patsubst %.c,$(OBJDIR)/%.o,$(SRCS))
+OBJS = $(OWN_OBJS) $(patsubst $(LWIP_DIR)/%.c,$(OBJDIR)/lwip/%.o,$(LWIP_SRCS))
+
+# What the library's own objects (whole: the packet driver's interrupt
+# code is laid out by file) reach of lwIP is all dvxnet.lib keeps, and it
+# exports its own functions only (DXE_GC)
+GC_OBJ = $(OBJDIR)/dvxnetGc.o
TARGETDIR = $(LIBSDIR)/kpunch/dvxnet
TARGET = $(TARGETDIR)/dvxnet.lib
@@ -51,8 +57,11 @@ all: $(TARGET) $(TARGETDIR)/dvxnet.dep
$(TARGETDIR)/dvxnet.dep: dvxnet.dep | $(TARGETDIR)
sed 's/$$/\r/' $< > $@
-$(TARGET): $(OBJS) | $(TARGETDIR)
- $(DXE3GEN) -o $(TARGETDIR)/dvxnet.dxe -U $(OBJS)
+$(GC_OBJ): $(OBJS)
+ $(DXE_GC) $@ $(OWN_OBJS) -- $(OBJS)
+
+$(TARGET): $(GC_OBJ) | $(TARGETDIR)
+ $(DXE3GEN) -o $(TARGETDIR)/dvxnet.dxe -U $(GC_OBJ)
mv $(TARGETDIR)/dvxnet.dxe $@
$(OBJDIR)/%.o: %.c $(DVX_MAKEFILES) | $(OBJDIR)
@@ -60,7 +69,7 @@ $(OBJDIR)/%.o: %.c $(DVX_MAKEFILES) | $(OBJDIR)
$(OBJDIR)/lwip/%.o: $(LWIP_DIR)/%.c $(DVX_MAKEFILES)
@mkdir -p $(dir $@)
- $(CC) $(CFLAGS) -c -o $@ $<
+ $(CC) $(CFLAGS) $(DXE_GC_CFLAGS) -c -o $@ $<
$(OBJDIR):
mkdir -p $(OBJDIR)
diff --git a/src/libs/kpunch/dvxnet/dvxNet.h b/src/libs/kpunch/dvxnet/dvxNet.h
index 558b67a7..5094b33e 100644
--- a/src/libs/kpunch/dvxnet/dvxNet.h
+++ b/src/libs/kpunch/dvxnet/dvxNet.h
@@ -50,6 +50,11 @@
#define NET_MAC_TEXT_MAX 18 // "00:11:22:33:44:55" + NUL
#define NET_DRIVER_NAME_MAX 32
#define NET_MESSAGE_MAX 96
+#define NET_LOOKUP_NAME_MAX 256 // a DNS name and NUL
+
+// netLookupPoll while a lookup goes on (else 0, or an EAI_* code of
+// , all at or below 0)
+#define NET_LOOKUP_BUSY 1
// Packet driver software interrupt range searched when vector is 0
#define NET_VECTOR_AUTO 0
@@ -102,6 +107,20 @@ typedef struct {
char message[NET_MESSAGE_MAX]; // plain-words state, e.g. "Waiting for a DHCP server"
} NetStatusT;
+// A name lookup that does not wait, as getaddrinfo does: netLookupStart,
+// then netLookupPoll (from a Timer, or each frame) until it stops saying
+// NET_LOOKUP_BUSY. It waits for DHCP's first address too, so a
+// connection started after it goes at once.
+typedef struct {
+ char name[NET_LOOKUP_NAME_MAX];
+ int32_t owner; // the app it is for
+ int32_t slot; // the query sent (0: none)
+ int32_t result; // NET_LOOKUP_BUSY, or how it ended
+ uint32_t addr; // network order, once found
+ uint32_t since; // when its present wait began (ms)
+ bool awaiting; // waiting for DHCP's first address
+} NetLookupT;
+
// Close every socket the app with this id still has open, and free the
// BASIC network settings handles it left (the shell does this when an
// app ends or is ended; callable by anyone).
@@ -141,6 +160,20 @@ const char *netErrorText(int32_t err);
// Current state, addresses and counters.
void netGetStatus(NetStatusT *status);
+// Give up a lookup that has not ended (its query is dropped); one that
+// has is left as it is.
+void netLookupCancel(NetLookupT *lk);
+
+// One look at a lookup: NET_LOOKUP_BUSY, 0 with *addr (network order),
+// or an EAI_* code (; gai_strerror says it in words). Once
+// ended it says the same again.
+int32_t netLookupPoll(NetLookupT *lk, uint32_t *addr);
+
+// Start looking up name, a host name or a dotted address (which needs no
+// lookup, only the wait for DHCP's first address). The stack starts
+// itself.
+void netLookupStart(NetLookupT *lk, const char *name);
+
// Let the stack do its work: take received frames, run timers, send.
// The shell calls this every idle frame and every waiting socket call
// calls it, so programs rarely need to.
@@ -161,4 +194,12 @@ const char *netStateText(int32_t state);
// Stop the stack and close every socket.
void netStop(void);
+// A net.bas TCP handle (TcpConnect, TcpAccept) as a byte stream that
+// never waits, for another library's BASIC functions (comm.bas's
+// CommOpenTcp runs the secure packet link over it): the bytes read (0 =
+// none now) or taken (0 = no room now); negative once the connection has
+// closed or failed.
+int32_t netBasStreamRead(int32_t handle, uint8_t *buf, int32_t len);
+int32_t netBasStreamWrite(int32_t handle, const uint8_t *data, int32_t len);
+
#endif
diff --git a/src/libs/kpunch/dvxnet/dvxnet.dep b/src/libs/kpunch/dvxnet/dvxnet.dep
index a9c88ea7..105be3c8 100644
--- a/src/libs/kpunch/dvxnet/dvxnet.dep
+++ b/src/libs/kpunch/dvxnet/dvxnet.dep
@@ -1,3 +1,4 @@
dvxshell
libtasks
libdvx
+crypto
diff --git a/src/libs/kpunch/dvxnet/dvxnet.dhs b/src/libs/kpunch/dvxnet/dvxnet.dhs
index 6da7e123..e9e6115a 100644
--- a/src/libs/kpunch/dvxnet/dvxnet.dhs
+++ b/src/libs/kpunch/dvxnet/dvxnet.dhs
@@ -161,6 +161,7 @@ Sockets are real descriptors below FD_SETSIZE (256), at most 32 at a time. dup()
.index Cooperative multitasking
.index tsYield
.index netPoll
+.index netLookupStart
.h1 Waiting, Polling, and Cooperative Multitasking
@@ -168,7 +169,26 @@ DVX runs programs cooperatively, so a call that has to wait never stops the syst
The network also runs from the shell's idle time, so connections progress (and data arrives) while a program is busy with other things; every socket call runs it too. A program that polls non-blocking sockets in its own loop needs nothing else.
-Right after the network starts, DHCP needs a moment to get an address. connect, sendto, and name lookups wait for it (up to 20 seconds) instead of failing, so a program started together with DVX can connect at once.
+Right after the network starts, DHCP needs a moment to get an address. Blocking connect and sendto calls, and name lookups, wait for it (up to 20 seconds) instead of failing, so a program started together with DVX can connect at once. A non-blocking socket does not wait.
+
+A program that must not wait at all, not even inside one call, looks a name up with netLookupStart and netLookupPoll (dvxNet.h) instead of getaddrinfo: each poll only looks, and the lookup waits for DHCP's first address too, so a non-blocking connect to the address it finds goes at once. DVX's own web requests (http) and Terminal connect this way.
+
+.code
+NetLookupT lk;
+uint32_t addr;
+int32_t rc;
+
+netLookupStart(&lk, "bbs.example.org");
+
+// Each frame, or from a timer:
+rc = netLookupPoll(&lk, &addr);
+
+if (rc == 0) {
+ // addr (network order): connect a non-blocking socket to it
+} else if (rc != NET_LOOKUP_BUSY) {
+ // gai_strerror(rc) says why
+}
+.endcode
.h2 Sockets Belong to Programs
@@ -195,6 +215,9 @@ The settings live in DVX.INI, section [network]: dhcp (true/false), address, net
void netStop(void) Close every socket and stop (the packet driver is released).
int32_t netRestart(void) Stop, then start with the saved settings (after they changed).
void netPoll(void) Take received frames, run the timers, send. Called from the shell's idle time and by every socket call.
+ void netLookupStart(NetLookupT *lk, const char *name) Start looking a name (or a dotted address) up without waiting.
+ int32_t netLookupPoll(NetLookupT *lk, uint32_t *addr) NET_LOOKUP_BUSY, 0 with the address (network order), or an EAI_* code; never waits.
+ void netLookupCancel(NetLookupT *lk) Give up a lookup that has not ended.
void netGetStatus(NetStatusT *status) State (NetStoppedE, NetNoDriverE, NetConfiguringE, NetUpE), plain-words message, addresses, name servers, host name, packet driver name, interrupt and hardware address, packet and byte counters, frames lost while DVX was busy, open sockets.
const char *netStateText(int32_t state) Plain words for a state.
const char *netErrorText(int32_t err) Plain words for an errno value, network codes included.
@@ -229,7 +252,7 @@ Name servers set in the settings are used even with DHCP; left empty, the DHCP s
Settings dvxNet.c DVX.INI, status, errors in plain words.
.endtable
-lwIP runs without threads (NO_SYS), configured only by port/lwipopts.h: TCP MSS 1460 with a 4-segment window, 16 TCP connections, 48 KB of stack memory, DHCP, DNS (2 servers), ARP, ICMP (ping answers), IP fragments, loopback. Nothing runs lwIP at interrupt time.
+lwIP runs without threads (NO_SYS), configured only by port/lwipopts.h: TCP MSS 1460 with a 4-segment window, 16 TCP connections, 24 KB of stack memory for data being sent and 20 receive buffers, DHCP, DNS (2 servers), ARP, ICMP (ping answers), IP fragments, loopback. Nothing runs lwIP at interrupt time.
The receive ring holds 16 frames. A burst larger than that while DVX is busy loses frames (counted as "lost" in the status); TCP sends them again.
@@ -242,7 +265,7 @@ On the host the same sources are built into the "net" test suite: a fake packet
.h1 Networking from BASIC
-net.bas declares simple functions over the socket API (netBas.c): HostLookup$, HostName$, NetAddress$, NetError$, TcpConnect, TcpListen, TcpAccept, TcpSend, TcpPending, TcpRecv$, TcpConnected, TcpShutdown, TcpPeer$, TcpLocalPort, TcpSetNoDelay, TcpSetKeepAlive, TcpClose, UdpOpen, UdpSend, UdpRecv$, UdpFrom$, UdpClose. A handle is the socket's descriptor + 1, so 0 means failure. Only TcpConnect (up to 20 seconds) and TcpSend wait; everything else returns at once, so a program polls from a Timer. TcpSend, TcpRecv$, UdpSend, and UdpRecv$ pass their data as BINARY STRING (pointer and length), so every byte value goes through. See the BASIC language reference, Include Libraries.
+net.bas declares simple functions over the socket API (netBas.c): HostLookup$, HostLookupStart / Poll / Result$ / Close, HostName$, NetAddress$, NetError$, TcpConnect, TcpConnectStart / Poll, TcpListen, TcpAccept, TcpSend, TcpSendSome, TcpPending, TcpRecv$, TcpPeek$ (recv with MSG_PEEK), TcpConnected, TcpShutdown, TcpPeer$, TcpLocalPort, TcpSetNoDelay, TcpSetKeepAlive, TcpClose, UdpOpen, UdpSend, UdpRecv$, UdpFrom$, UdpClose. A handle is the socket's descriptor + 1, so 0 means failure. Only TcpConnect (up to 20 seconds), HostLookup$, and TcpSend wait; everything else returns at once, so a program polls from a Timer. The other socket options need no binding: TcpListen sets SO_REUSEADDR and UdpOpen SO_BROADCAST, the stack takes SO_LINGER and the buffer sizes without effect (closing finishes in the background; the buffers are fixed), and no BASIC call waits on a receive, so SO_RCVTIMEO does nothing for it. TcpSend, TcpRecv$, UdpSend, and UdpRecv$ pass their data as BINARY STRING (pointer and length), so every byte value goes through. See the BASIC language reference, Include Libraries.
.table
BASIC C it calls
@@ -269,6 +292,8 @@ The network settings and status (what the Control Panel's Network page shows) ar
NetConfigFree(cfg) Free the handle.
NetVectorCount / NetVector(i) The packet driver interrupt choices: 0 (find it), then NET_VECTOR_FIRST..NET_VECTOR_LAST.
NetRestart() netRestart; True for NET_OK.
+ netBasStreamRead / netBasStreamWrite A net.bas TCP handle as a byte stream that never waits, for another library's BASIC functions (comm.bas's CommOpenTcp runs the secure packet link over it): bytes read or taken (0 = none now), negative once the connection has closed or failed.
+ NetState() netGetStatus's state (NetStateE; BASIC's NET_STATE_STOPPED, _NO_DRIVER, _CONFIGURING, _UP).
NetStatus$(field) netGetStatus, one field as text: message, up, link, dhcp, address, netmask, gateway, dns1, dns2, hostname, driver, mac, vector, sockets, rxpackets, rxbytes, rxdropped, txpackets, txbytes.
.endtable
diff --git a/src/libs/kpunch/dvxnet/netBas.c b/src/libs/kpunch/dvxnet/netBas.c
index a9f08b8b..c5d8c1de 100644
--- a/src/libs/kpunch/dvxnet/netBas.c
+++ b/src/libs/kpunch/dvxnet/netBas.c
@@ -24,8 +24,12 @@
// (src/include/basic/net.bas), written over the ordinary BSD socket API.
//
// A BASIC handle is the socket's descriptor + 1, so 0 means "failed".
-// Everything but TcpConnect and TcpSend returns at once, so a program
-// polls from a Timer (TcpPending, TcpRecv$, TcpAccept, UdpRecv$). What
+// Everything but HostLookup$, TcpConnect, and TcpSend returns at once, so
+// a program polls from a Timer (TcpPending, TcpRecv$, TcpAccept,
+// UdpRecv$); those three have forms that do not wait either
+// (HostLookupStart, TcpConnectStart, TcpSendSome), whose lookups and
+// connections under way are kept here, for the program that started
+// them. What
// is sent and received is a BINARY STRING (pointer + length both ways,
// claudeDocs/round2/binaryStrings.md), so every byte value passes.
// Sockets belong to the program and are closed for it when it ends.
@@ -41,6 +45,7 @@
#include "netInt.h"
#include "dvxTypes.h"
#include "dvxBas.h"
+#include "thirdparty/stb_ds_wrap.h"
#include
#include
@@ -48,6 +53,7 @@
#include
#include
#include
+#include
#include
#include
#include
@@ -64,6 +70,9 @@
#define BAS_ENDPOINT_MAX (INET_ADDRSTRLEN + 8)
#define BAS_PORT_MAX 65535
#define BAS_CONFIG_MAX 16
+#define BAS_WAIT_MAX 16 // lookups and connections under way at once
+#define BAS_NO_FD (-1)
+#define MS_PER_SEC 1000
#define BAS_FIELD_MAX NET_MESSAGE_MAX
#define BAS_DNS_PREFIX "dns"
#define BAS_DNS_FIRST 1 // "dns1" is the first name server
@@ -75,12 +84,22 @@
// One settings handle
typedef struct {
- bool used;
- int32_t appId;
NetConfigT cfg; // being edited
NetConfigT loaded; // as NetConfigLoad read it
} BasNetConfigT;
+// A lookup (HostLookupStart) or a connection (TcpConnectStart) under way
+typedef struct {
+ int32_t handle; // its lookup handle
+ NetLookupT lookup;
+ int32_t fd; // a connection's socket; BAS_NO_FD for a lookup
+ BasNetWaitE result;
+ uint16_t port; // a connection's
+ bool connecting; // the connection is started (the lookup is done)
+ uint32_t addr; // found (network order)
+ uint32_t deadline; // when the connection gives up (netStackNow)
+} BasNetWaitT;
+
// A text field of NetConfigT / NetStatusT by name
typedef struct {
const char *name;
@@ -105,8 +124,7 @@ static const BasTextFieldT sStatusFields[] = {
{ "message", offsetof(NetStatusT, message), NET_MESSAGE_MAX }
};
-static BasNetConfigT sConfigs[BAS_CONFIG_MAX];
-static char sField[BAS_FIELD_MAX];
+static char sField[BAS_FIELD_MAX];
static uint8_t sRecv[BAS_RECV_MAX];
static char sLookup[NI_MAXHOST];
@@ -126,9 +144,16 @@ static int32_t dnsIndex(const char *field);
static const char *endpointText(const struct sockaddr_in *addr);
static int32_t handleFd(int32_t handle);
const char *HostLookup(const char *name);
+void HostLookupClose(int32_t lookup);
+int32_t HostLookupPoll(int32_t lookup);
+const char *HostLookupResult(int32_t lookup);
+int32_t HostLookupStart(const char *name);
const char *HostName(const char *address);
+static BasNetWaitT *lookupOf(int32_t lookup);
const char *NetAddress(void);
void netBasAppEnded(int32_t appId);
+int32_t netBasStreamRead(int32_t handle, uint8_t *buf, int32_t len);
+int32_t netBasStreamWrite(int32_t handle, const uint8_t *data, int32_t len);
int32_t NetConfigChanged(int32_t config);
void NetConfigFree(int32_t config);
const char *NetConfigGet(int32_t config, const char *field);
@@ -138,24 +163,30 @@ int32_t NetConfigSet(int32_t config, const char *field, const char
int32_t NetConfigStore(int32_t config, int32_t prefs);
const char *NetError(void);
int32_t NetRestart(void);
+int32_t NetState(void);
const char *NetStatus(const char *field);
void NetStop(void);
int32_t NetVector(int32_t index);
int32_t NetVectorCount(void);
static int32_t newSocket(int32_t type);
static int32_t noteError(void);
+static const uint8_t *recvSome(int32_t handle, int flags, int32_t *outLen);
static bool resolve(const char *host, int32_t port, struct sockaddr_in *out);
static int32_t setFlag(int32_t handle, int level, int name, int32_t on);
int32_t TcpAccept(int32_t handle);
void TcpClose(int32_t handle);
int32_t TcpConnect(const char *host, int32_t port);
int32_t TcpConnected(int32_t handle);
+int32_t TcpConnectPoll(int32_t handle);
+int32_t TcpConnectStart(const char *host, int32_t port);
int32_t TcpListen(int32_t port);
int32_t TcpLocalPort(int32_t handle);
+const uint8_t *TcpPeek(int32_t handle, int32_t *outLen);
const char *TcpPeer(int32_t handle);
int32_t TcpPending(int32_t handle);
const uint8_t *TcpRecv(int32_t handle, int32_t *outLen);
int32_t TcpSend(int32_t handle, const uint8_t *data, int32_t len);
+int32_t TcpSendSome(int32_t handle, const uint8_t *data, int32_t len);
int32_t TcpSetKeepAlive(int32_t handle, int32_t on);
int32_t TcpSetNoDelay(int32_t handle, int32_t on);
int32_t TcpShutdown(int32_t handle);
@@ -165,6 +196,15 @@ const char *UdpFrom(void);
int32_t UdpOpen(int32_t port);
const uint8_t *UdpRecv(int32_t handle, int32_t *outLen);
int32_t UdpSend(int32_t handle, const char *host, int32_t port, const uint8_t *data, int32_t len);
+static void waitEnd(BasNetWaitT *w, BasNetWaitE result, int32_t err);
+static void waitFree(BasNetWaitT *w);
+static BasNetWaitT *waitNew(void);
+static BasNetWaitT *waitOfFd(int32_t fd);
+static void waitRelease(void *obj);
+static void waitStep(BasNetWaitT *w);
+
+static BasHandlesT sConfigs = BAS_HANDLES(free, BAS_CONFIG_MAX); // BasNetConfigT
+static BasHandlesT sWaits = BAS_HANDLES(waitRelease, BAS_WAIT_MAX); // BasNetWaitT
// A yes / no field as text: "-1" (True) or "0" (False), so VAL gives
@@ -176,11 +216,7 @@ static const char *basBoolText(bool value) {
// The settings handle's entry, or NULL when it is not open.
static BasNetConfigT *configOf(int32_t handle) {
- if (handle < 1 || handle > BAS_CONFIG_MAX || !sConfigs[handle - 1].used) {
- return NULL;
- }
-
- return &sConfigs[handle - 1];
+ return (BasNetConfigT *)basHandleGet(&sConfigs, handle);
}
@@ -232,6 +268,57 @@ const char *HostLookup(const char *name) {
}
+// Give up (or free) a lookup.
+void HostLookupClose(int32_t lookup) {
+ waitFree(lookupOf(lookup));
+}
+
+
+// BasNetBusyE while the lookup goes on; BasNetDoneE once the address is
+// found (HostLookupResult$); BasNetFailedE (NetError$ says why).
+int32_t HostLookupPoll(int32_t lookup) {
+ BasNetWaitT *w = lookupOf(lookup);
+
+ if (!w) {
+ sLastError = EINVAL;
+ return BasNetFailedE;
+ }
+
+ waitStep(w);
+ return w->result;
+}
+
+
+// The address a finished lookup found ("192.0.2.10"), "" before that.
+const char *HostLookupResult(int32_t lookup) {
+ const BasNetWaitT *w = lookupOf(lookup);
+ struct in_addr in;
+
+ sLookup[0] = '\0';
+
+ if (w && w->result == BasNetDoneE) {
+ in.s_addr = w->addr;
+ inet_ntop(AF_INET, &in, sLookup, sizeof(sLookup));
+ }
+
+ return sLookup;
+}
+
+
+// Start looking name up without waiting: a lookup handle (poll it with
+// HostLookupPoll), or 0 when too many are under way.
+int32_t HostLookupStart(const char *name) {
+ BasNetWaitT *w = waitNew();
+
+ if (!w) {
+ return 0;
+ }
+
+ netLookupStart(&w->lookup, name ? name : "");
+ return w->handle;
+}
+
+
// The name of an address ("192.0.2.10"), through getnameinfo: the
// address itself when no name is known for it, "" when address is not
// one (NetError$ says why).
@@ -259,6 +346,14 @@ const char *HostName(const char *address) {
}
+// A lookup handle's entry (not a connection's), or NULL.
+static BasNetWaitT *lookupOf(int32_t lookup) {
+ BasNetWaitT *w = (BasNetWaitT *)basHandleGet(&sWaits, lookup);
+
+ return (w && w->fd == BAS_NO_FD) ? w : NULL;
+}
+
+
const char *NetAddress(void) {
NetStatusT st;
@@ -269,13 +364,38 @@ const char *NetAddress(void) {
}
-// Free what an app that ended left open (netCloseAppSockets).
+// Free what an app that ended left open (netCloseAppSockets closes its
+// sockets).
void netBasAppEnded(int32_t appId) {
- for (int32_t i = 0; i < BAS_CONFIG_MAX; i++) {
- if (sConfigs[i].used && sConfigs[i].appId == appId) {
- sConfigs[i].used = false;
- }
+ basHandlesAppEnded(&sConfigs, appId);
+ basHandlesAppEnded(&sWaits, appId);
+}
+
+
+int32_t netBasStreamRead(int32_t handle, uint8_t *buf, int32_t len) {
+ ssize_t n = recv(handleFd(handle), buf, (size_t)len, MSG_DONTWAIT);
+
+ // 0 bytes from a read that asked for some: the other side closed it
+ if (n == 0) {
+ return -1;
}
+
+ if (n < 0) {
+ return (errno == EWOULDBLOCK) ? 0 : -1;
+ }
+
+ return (int32_t)n;
+}
+
+
+int32_t netBasStreamWrite(int32_t handle, const uint8_t *data, int32_t len) {
+ ssize_t n = send(handleFd(handle), data, (size_t)len, MSG_DONTWAIT);
+
+ if (n < 0) {
+ return (errno == EWOULDBLOCK) ? 0 : -1;
+ }
+
+ return (int32_t)n;
}
@@ -288,11 +408,7 @@ int32_t NetConfigChanged(int32_t config) {
void NetConfigFree(int32_t config) {
- BasNetConfigT *c = configOf(config);
-
- if (c) {
- c->used = false;
- }
+ basHandleFree(&sConfigs, config);
}
@@ -325,17 +441,22 @@ const char *NetConfigGet(int32_t config, const char *field) {
// A handle on the [network] settings of a prefs.bas handle (the
// defaults for 0); 0 when every handle is in use.
int32_t NetConfigLoad(int32_t prefs) {
- for (int32_t i = 0; i < BAS_CONFIG_MAX; i++) {
- if (!sConfigs[i].used) {
- netConfigFromPrefs(prefsBasHandle(prefs), &sConfigs[i].cfg);
- sConfigs[i].loaded = sConfigs[i].cfg;
- sConfigs[i].appId = netSockAppId();
- sConfigs[i].used = true;
- return i + 1;
- }
+ BasNetConfigT *c = (BasNetConfigT *)malloc(sizeof(*c));
+
+ if (!c) {
+ return BAS_NO_HANDLE;
}
- return 0;
+ netConfigFromPrefs(prefsBasHandle(prefs), &c->cfg);
+ c->loaded = c->cfg;
+
+ int32_t handle = basHandleAdd(&sConfigs, c);
+
+ if (handle == BAS_NO_HANDLE) {
+ free(c);
+ }
+
+ return handle;
}
@@ -417,6 +538,15 @@ int32_t NetRestart(void) {
}
+// The network's state as a number (NetStateE: BASIC's NET_STATE_*).
+int32_t NetState(void) {
+ NetStatusT st;
+
+ netGetStatus(&st);
+ return st.state;
+}
+
+
// The network's state as text: "message" (in plain words), "up", "link",
// "dhcp" ("-1" / "0"), "address", "netmask", "gateway", "dns1", "dns2",
// "hostname", "driver", "mac", "vector", "sockets", "rxpackets",
@@ -513,6 +643,24 @@ static int32_t noteError(void) {
}
+// Up to BAS_RECV_MAX bytes of what has arrived, never waiting (flags:
+// MSG_PEEK leaves them there). *outLen 0 when nothing has.
+static const uint8_t *recvSome(int32_t handle, int flags, int32_t *outLen) {
+ ssize_t n = recv(handleFd(handle), sRecv, BAS_RECV_MAX, flags | MSG_DONTWAIT);
+
+ if (n < 0) {
+ if (errno != EWOULDBLOCK) {
+ noteError();
+ }
+
+ n = 0;
+ }
+
+ *outLen = (int32_t)n;
+ return sRecv;
+}
+
+
static bool resolve(const char *host, int32_t port, struct sockaddr_in *out) {
struct addrinfo hints;
struct addrinfo *res = NULL;
@@ -572,7 +720,9 @@ int32_t TcpAccept(int32_t handle) {
}
+// Close it (a connection still being made stops).
void TcpClose(int32_t handle) {
+ waitFree(waitOfFd(handleFd(handle)));
close(handleFd(handle));
}
@@ -624,6 +774,66 @@ int32_t TcpConnected(int32_t handle) {
}
+// BasNetBusyE while the connection TcpConnectStart started is being made;
+// BasNetDoneE once it is (the handle then works as TcpConnect's);
+// BasNetFailedE (NetError$ says why; close it).
+int32_t TcpConnectPoll(int32_t handle) {
+ BasNetWaitT *w = waitOfFd(handleFd(handle));
+
+ if (!w) {
+ return TcpConnected(handle) ? BasNetDoneE : BasNetFailedE;
+ }
+
+ waitStep(w);
+
+ BasNetWaitE result = w->result;
+
+ if (result != BasNetBusyE) {
+ waitFree(w);
+ }
+
+ return result;
+}
+
+
+// Start connecting to host on port without waiting: the connection's
+// handle at once (poll it with TcpConnectPoll), or 0 when it cannot
+// start (NetError$).
+int32_t TcpConnectStart(const char *host, int32_t port) {
+ BasNetWaitT *w;
+ int32_t handle;
+ int on = 1;
+
+ if (!host || port <= 0 || port > BAS_PORT_MAX) {
+ sLastError = EINVAL;
+ return 0;
+ }
+
+ w = waitNew();
+
+ if (!w) {
+ return 0;
+ }
+
+ handle = newSocket(SOCK_STREAM);
+
+ if (handle == 0 || ioctl(handleFd(handle), FIONBIO, &on) != 0) {
+ if (handle != 0) {
+ noteError();
+ close(handleFd(handle));
+ }
+
+ waitFree(w);
+ return 0;
+ }
+
+ w->fd = handleFd(handle);
+ w->port = (uint16_t)port;
+ netLookupStart(&w->lookup, host);
+ return handle;
+}
+
+
int32_t TcpListen(int32_t port) {
struct sockaddr_in addr;
int on = 1;
@@ -671,6 +881,14 @@ int32_t TcpLocalPort(int32_t handle) {
}
+// What has arrived (up to BAS_RECV_MAX bytes), left waiting for the next
+// TcpRecv$: a program can look for the end of a line or a message's
+// length before it takes any of it. "" when nothing has.
+const uint8_t *TcpPeek(int32_t handle, int32_t *outLen) {
+ return recvSome(handle, MSG_PEEK, outLen);
+}
+
+
// "address:port" of the other side of a connection, "" when it is not
// connected.
const char *TcpPeer(int32_t handle) {
@@ -701,18 +919,7 @@ int32_t TcpPending(int32_t handle) {
// What has arrived (up to BAS_RECV_MAX bytes), "" when nothing has.
const uint8_t *TcpRecv(int32_t handle, int32_t *outLen) {
- ssize_t n = recv(handleFd(handle), sRecv, BAS_RECV_MAX, MSG_DONTWAIT);
-
- if (n < 0) {
- if (errno != EWOULDBLOCK) {
- noteError();
- }
-
- n = 0;
- }
-
- *outLen = (int32_t)n;
- return sRecv;
+ return recvSome(handle, 0, outLen);
}
@@ -730,6 +937,24 @@ int32_t TcpSend(int32_t handle, const uint8_t *data, int32_t len) {
}
+// Send what the connection takes now, never waiting: the bytes taken (0
+// while its buffer is full), -1 on failure.
+int32_t TcpSendSome(int32_t handle, const uint8_t *data, int32_t len) {
+ ssize_t n = send(handleFd(handle), data, (size_t)len, MSG_DONTWAIT);
+
+ if (n >= 0) {
+ return (int32_t)n;
+ }
+
+ if (errno == EWOULDBLOCK) {
+ return 0;
+ }
+
+ noteError();
+ return -1;
+}
+
+
// Keep-alive probes on an idle connection (SO_KEEPALIVE): True when set.
int32_t TcpSetKeepAlive(int32_t handle, int32_t on) {
return setFlag(handle, SOL_SOCKET, SO_KEEPALIVE, on);
@@ -860,3 +1085,141 @@ int32_t UdpSend(int32_t handle, const char *host, int32_t port, const uint8_t *d
return (int32_t)n;
}
+
+
+// The wait is over: result, with the error NetError$ gives (0: none).
+static void waitEnd(BasNetWaitT *w, BasNetWaitE result, int32_t err) {
+ w->result = result;
+
+ if (err) {
+ sLastError = err;
+ }
+}
+
+
+// Free a wait and its handle. NULL is ignored.
+static void waitFree(BasNetWaitT *w) {
+ if (w) {
+ basHandleFree(&sWaits, w->handle);
+ }
+}
+
+
+// A new wait, with its handle, for the calling program; NULL (noted)
+// when too many are under way.
+static BasNetWaitT *waitNew(void) {
+ BasNetWaitT *w = (BasNetWaitT *)calloc(1, sizeof(*w));
+
+ if (w) {
+ w->fd = BAS_NO_FD;
+ w->result = BasNetBusyE;
+ w->handle = basHandleAdd(&sWaits, w);
+
+ if (w->handle == BAS_NO_HANDLE) {
+ free(w);
+ w = NULL;
+ }
+ }
+
+ if (!w) {
+ sLastError = EMFILE;
+ }
+
+ return w;
+}
+
+
+// The connection under way on socket fd, or NULL.
+static BasNetWaitT *waitOfFd(int32_t fd) {
+ for (int32_t i = 0; fd != BAS_NO_FD && i < (int32_t)arrlen(sWaits.objs); i++) {
+ BasNetWaitT *w = (BasNetWaitT *)sWaits.objs[i];
+
+ if (w && w->fd == fd) {
+ return w;
+ }
+ }
+
+ return NULL;
+}
+
+
+// sWaits' freeFn: a lookup under way is given up.
+static void waitRelease(void *obj) {
+ BasNetWaitT *w = (BasNetWaitT *)obj;
+
+ netLookupCancel(&w->lookup);
+ free(w);
+}
+
+
+// One look at a wait: its lookup, then (a connection) the connecting
+// within BAS_CONNECT_SECS; never waiting. Done, a connection's socket
+// works as a blocking one again, as TcpConnect's does.
+static void waitStep(BasNetWaitT *w) {
+ struct sockaddr_in to;
+ struct pollfd p;
+ int err = 0;
+ int off = 0;
+ socklen_t len = sizeof(err);
+
+ if (w->result != BasNetBusyE) {
+ return;
+ }
+
+ if (!w->connecting) {
+ int32_t rc = netLookupPoll(&w->lookup, &w->addr);
+
+ if (rc == NET_LOOKUP_BUSY) {
+ return;
+ }
+
+ if (rc != 0) {
+ waitEnd(w, BasNetFailedE, EHOSTUNREACH);
+ return;
+ }
+
+ if (w->fd == BAS_NO_FD) {
+ waitEnd(w, BasNetDoneE, 0);
+ return;
+ }
+
+ memset(&to, 0, sizeof(to));
+ to.sin_family = AF_INET;
+ to.sin_port = htons(w->port);
+ to.sin_addr.s_addr = w->addr;
+ w->connecting = true;
+ w->deadline = netStackNow() + BAS_CONNECT_SECS * MS_PER_SEC;
+
+ if (connect(w->fd, (struct sockaddr *)&to, sizeof(to)) == 0) {
+ ioctl(w->fd, FIONBIO, &off);
+ waitEnd(w, BasNetDoneE, 0);
+ return;
+ }
+
+ if (errno != EINPROGRESS) {
+ waitEnd(w, BasNetFailedE, errno);
+ return;
+ }
+ }
+
+ p.fd = w->fd;
+ p.events = POLLOUT;
+ p.revents = 0;
+
+ if (poll(&p, 1, 0) > 0) {
+ getsockopt(w->fd, SOL_SOCKET, SO_ERROR, &err, &len);
+
+ if (err == 0 && !(p.revents & (POLLERR | POLLHUP))) {
+ ioctl(w->fd, FIONBIO, &off);
+ waitEnd(w, BasNetDoneE, 0);
+ } else {
+ waitEnd(w, BasNetFailedE, err ? err : ECONNREFUSED);
+ }
+
+ return;
+ }
+
+ if ((int32_t)(netStackNow() - w->deadline) >= 0) {
+ waitEnd(w, BasNetFailedE, ETIMEDOUT);
+ }
+}
diff --git a/src/libs/kpunch/dvxnet/netInt.h b/src/libs/kpunch/dvxnet/netInt.h
index 4ac73263..f0e04ad0 100644
--- a/src/libs/kpunch/dvxnet/netInt.h
+++ b/src/libs/kpunch/dvxnet/netInt.h
@@ -69,6 +69,10 @@ typedef enum {
// Largest UDP payload accepted for sending
#define PROTO_UDP_MAX 8192
+// Datagrams a UDP socket holds unread (more are dropped); each holds a
+// receive buffer (PBUF_POOL_SIZE, lwipopts.h)
+#define PROTO_DGRAM_QUEUE 16
+
// Protocol object (defined in netProto.c)
typedef struct NetProtoS NetProtoT;
@@ -77,6 +81,9 @@ typedef struct {
uint16_t port; // host byte order
} NetEndpointT;
+// The longest wait for DHCP's first address
+#define NET_DHCP_WAIT_MS 20000
+
// ---- netStack.c ----
// Start the stack with cfg (NET_OK or NET_ERR_*). Already running: NET_OK.
@@ -105,6 +112,10 @@ void netStackWaitStep(void);
// DVX can connect at once. Returns at once otherwise.
void netStackAwaitAddress(void);
+// True while DHCP is still getting the first address (netLookupPoll
+// waits for it a look at a time).
+bool netStackAwaitingAddress(void);
+
// Milliseconds since an arbitrary start (lwIP's sys_now).
uint32_t netStackNow(void);
@@ -182,7 +193,16 @@ void protoResolveDropOwner(int32_t owner);
// ---- netBas.c ----
-// Free the network settings handles (NetConfigLoad) an app left.
+// HostLookupPoll's and TcpConnectPoll's results: net.bas's NET_BUSY,
+// NET_DONE, and NET_FAILED, which must not change
+typedef enum {
+ BasNetBusyE,
+ BasNetDoneE,
+ BasNetFailedE
+} BasNetWaitE;
+
+// Free the network settings handles (NetConfigLoad) and the lookups and
+// connections under way (HostLookupStart, TcpConnectStart) an app left.
void netBasAppEnded(int32_t appId);
// ---- netSock.c ----
diff --git a/src/libs/kpunch/dvxnet/netProto.c b/src/libs/kpunch/dvxnet/netProto.c
index 7b74086f..362ce8ae 100644
--- a/src/libs/kpunch/dvxnet/netProto.c
+++ b/src/libs/kpunch/dvxnet/netProto.c
@@ -46,7 +46,6 @@
#include
#include
-#define PROTO_DGRAM_QUEUE 16
#define PROTO_BACKLOG_MAX 16
#define PROTO_RESOLVE_SLOTS 8
#define PROTO_TCP_CHUNK_MAX 0xFFFF // tcp_write / tcp_recved take a u16_t
diff --git a/src/libs/kpunch/dvxnet/netResolve.c b/src/libs/kpunch/dvxnet/netResolve.c
index 425705f0..02966204 100644
--- a/src/libs/kpunch/dvxnet/netResolve.c
+++ b/src/libs/kpunch/dvxnet/netResolve.c
@@ -22,9 +22,10 @@
// netResolve.c -- name lookups and address text (see netInt.h):
// getaddrinfo & co., gethostbyname/gethostbyaddr, gethostname and the
-// inet_* conversions. Names go to lwIP's DNS resolver; the caller waits
-// in netStackWaitStep, so other programs run meanwhile. There is no
-// reverse lookup: an address's "name" is its dotted text.
+// inet_* conversions, and lookups that do not wait (netLookupStart).
+// Names go to lwIP's DNS resolver; getaddrinfo's caller waits in
+// netStackWaitStep, so other programs run meanwhile. There is no reverse
+// lookup: an address's "name" is its dotted text.
#include "netInt.h"
@@ -102,7 +103,23 @@ static char sNtoa[INET_ADDRSTRLEN];
// ============================================================
static struct hostent *fillHost(const char *name, uint32_t addr);
+void freeaddrinfo(struct addrinfo *res);
+const char *gai_strerror(int err);
+int getaddrinfo(const char *node, const char *service, const struct addrinfo *hints, struct addrinfo **res);
+struct hostent *gethostbyaddr(const void *addr, socklen_t len, int type);
+struct hostent *gethostbyname(const char *name);
+int gethostname(char *name, NetNameLenT len);
+int getnameinfo(const struct sockaddr *addr, socklen_t addrLen, char *host, socklen_t hostLen, char *serv, socklen_t servLen, int flags);
+in_addr_t inet_addr(const char *text);
+int inet_aton(const char *text, struct in_addr *addr);
+char *inet_ntoa(struct in_addr addr);
+const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
+int inet_pton(int af, const char *src, void *dst);
+static int32_t lookupEnd(NetLookupT *lk, int32_t result, uint32_t *addr);
static int32_t lookupName(const char *name, uint32_t *addr);
+void netLookupCancel(NetLookupT *lk);
+int32_t netLookupPoll(NetLookupT *lk, uint32_t *addr);
+void netLookupStart(NetLookupT *lk, const char *name);
static bool parsePort(const char *service, bool numericOnly, uint16_t *port);
@@ -473,46 +490,122 @@ int inet_pton(int af, const char *src, void *dst) {
}
-// Name lookup: 0 with *addr (network order), or an EAI_* code.
+// The lookup is over, with result (0: found, *addr given).
+static int32_t lookupEnd(NetLookupT *lk, int32_t result, uint32_t *addr) {
+ lk->slot = 0;
+ lk->result = result;
+
+ if (result == 0) {
+ *addr = lk->addr;
+ }
+
+ return result;
+}
+
+
+// Name lookup, waiting (other tasks run meanwhile): 0 with *addr
+// (network order), or an EAI_* code. A dotted address is no lookup and
+// no wait.
static int32_t lookupName(const char *name, uint32_t *addr) {
- int32_t slot;
- int32_t rc;
- uint32_t start;
+ NetLookupT lk;
+ int32_t rc;
if (netParseAddress(name, addr)) {
return 0;
}
- netStart();
-
- if (!netStackRunning()) {
- return EAI_FAIL;
- }
-
- // The DNS servers usually come from DHCP.
- netStackAwaitAddress();
- slot = protoResolveStart(name, netSockAppId(), addr);
-
- if (slot == 0) {
- return 0;
- }
-
- if (slot < 0) {
- return (slot == -ENOMEM) ? EAI_MEMORY : EAI_NONAME;
- }
-
- start = netStackNow();
-
- while ((rc = protoResolvePoll(slot, addr)) == -EINPROGRESS) {
- if (netStackNow() - start >= RESOLVE_TIMEOUT_MS) {
- protoResolveAbandon(slot);
- return EAI_AGAIN;
- }
+ netLookupStart(&lk, name);
+ while ((rc = netLookupPoll(&lk, addr)) == NET_LOOKUP_BUSY) {
netStackWaitStep();
}
- return (rc == 0) ? 0 : EAI_NONAME;
+ return rc;
+}
+
+
+void netLookupCancel(NetLookupT *lk) {
+ if (lk->result != NET_LOOKUP_BUSY) {
+ return;
+ }
+
+ if (lk->slot > 0) {
+ protoResolveAbandon(lk->slot);
+ }
+
+ lk->slot = 0;
+ lk->result = EAI_AGAIN;
+}
+
+
+// First DHCP's address (the DNS servers usually come with it), then the
+// query, each waited for a look at a time.
+int32_t netLookupPoll(NetLookupT *lk, uint32_t *addr) {
+ int32_t rc;
+
+ if (lk->result != NET_LOOKUP_BUSY) {
+ return lookupEnd(lk, lk->result, addr);
+ }
+
+ netStackPoll();
+
+ if (lk->awaiting) {
+ if (netStackAwaitingAddress() && netStackNow() - lk->since < NET_DHCP_WAIT_MS) {
+ return NET_LOOKUP_BUSY;
+ }
+
+ lk->awaiting = false;
+
+ if (netParseAddress(lk->name, &lk->addr)) {
+ return lookupEnd(lk, 0, addr);
+ }
+
+ lk->slot = protoResolveStart(lk->name, lk->owner, &lk->addr);
+ lk->since = netStackNow();
+
+ if (lk->slot == 0) {
+ return lookupEnd(lk, 0, addr);
+ }
+
+ if (lk->slot < 0) {
+ return lookupEnd(lk, (lk->slot == -ENOMEM) ? EAI_MEMORY : EAI_NONAME, addr);
+ }
+ }
+
+ rc = protoResolvePoll(lk->slot, &lk->addr);
+
+ if (rc != -EINPROGRESS) {
+ return lookupEnd(lk, (rc == 0) ? 0 : EAI_NONAME, addr);
+ }
+
+ if (netStackNow() - lk->since >= RESOLVE_TIMEOUT_MS) {
+ protoResolveAbandon(lk->slot);
+ return lookupEnd(lk, EAI_AGAIN, addr);
+ }
+
+ return NET_LOOKUP_BUSY;
+}
+
+
+void netLookupStart(NetLookupT *lk, const char *name) {
+ memset(lk, 0, sizeof(*lk));
+ lk->owner = netSockAppId();
+ lk->result = NET_LOOKUP_BUSY;
+
+ if (!name || !name[0] || snprintf(lk->name, sizeof(lk->name), "%s", name) >= (int)sizeof(lk->name)) {
+ lk->result = EAI_NONAME;
+ return;
+ }
+
+ netStart();
+
+ if (!netStackRunning()) {
+ lk->result = EAI_FAIL;
+ return;
+ }
+
+ lk->awaiting = true;
+ lk->since = netStackNow();
}
diff --git a/src/libs/kpunch/dvxnet/netSock.c b/src/libs/kpunch/dvxnet/netSock.c
index 85b8f454..1a261b0f 100644
--- a/src/libs/kpunch/dvxnet/netSock.c
+++ b/src/libs/kpunch/dvxnet/netSock.c
@@ -163,7 +163,12 @@ int connect(int fd, const struct sockaddr *addr, socklen_t addrLen) {
return fail(err);
}
- netStackAwaitAddress();
+ // A non-blocking one does not wait for DHCP's first address either
+ // (netLookupPoll does)
+ if (!s->nonBlocking) {
+ netStackAwaitAddress();
+ }
+
rc = protoConnect(s->proto, &ep);
if (rc != -EINPROGRESS) {
@@ -816,7 +821,11 @@ static ssize_t sendCommon(int fd, const void *buf, size_t len, int flags, const
if (s->type == SOCK_DGRAM) {
int32_t n;
- netStackAwaitAddress();
+ // Only a send that may wait waits for DHCP's first address
+ if (!s->nonBlocking && !(flags & MSG_DONTWAIT)) {
+ netStackAwaitAddress();
+ }
+
n = protoSend(s->proto, (const uint8_t *)buf, (int32_t)len, to ? &ep : NULL);
return (n < 0) ? fail(-n) : (ssize_t)n;
diff --git a/src/libs/kpunch/dvxnet/netStack.c b/src/libs/kpunch/dvxnet/netStack.c
index 9a5aa210..08631394 100644
--- a/src/libs/kpunch/dvxnet/netStack.c
+++ b/src/libs/kpunch/dvxnet/netStack.c
@@ -35,6 +35,7 @@
#include "pktDrv.h"
#include "dvxPlat.h"
#include "taskSwch.h"
+#include "../crypto/security.h"
#include "lwip/dhcp.h"
#include "lwip/dns.h"
@@ -56,10 +57,6 @@
#define DIAG_MAX 160
#define IF_NAME0 'e'
#define IF_NAME1 'n'
-#define NET_DHCP_WAIT_MS 20000
-#define RAND_SHIFT_A 13 // xorshift32 (Marsaglia) shifts
-#define RAND_SHIFT_B 17
-#define RAND_SHIFT_C 5
static bool sLwipReady = false;
static bool sRunning = false;
@@ -74,7 +71,6 @@ static uint32_t sTxPackets = 0;
static uint32_t sRxBytes = 0;
static uint32_t sTxBytes = 0;
static uint32_t sRxNoBuf = 0;
-static uint32_t sRandState = 0;
static uint32_t sClockLast = 0;
static uint32_t sClockMs = 0;
static uint64_t sClockRem = 0;
@@ -156,12 +152,17 @@ void netStackAssert(const char *msg, const char *file, int32_t line) {
void netStackAwaitAddress(void) {
uint32_t start = netStackNow();
- while (sRunning && sHaveDriver && sCfg.dhcp && ip4_addr_isany_val(*netif_ip4_addr(&sNetif)) && netStackNow() - start < NET_DHCP_WAIT_MS) {
+ while (netStackAwaitingAddress() && netStackNow() - start < NET_DHCP_WAIT_MS) {
netStackWaitStep();
}
}
+bool netStackAwaitingAddress(void) {
+ return sRunning && sHaveDriver && sCfg.dhcp && ip4_addr_isany_val(*netif_ip4_addr(&sNetif));
+}
+
+
void netStackDiag(const char *fmt, ...) {
char text[DIAG_MAX];
va_list ap;
@@ -229,20 +230,14 @@ void netStackPoll(void) {
}
-// xorshift32 stirred with the platform's entropy stamp: DNS query ids,
-// DHCP transaction ids and local ports need to be hard to guess, not
-// cryptographic.
+// DNS query ids, DHCP transaction ids, local ports, and TCP initial
+// sequence numbers, from the security library's generator: values an
+// observer could predict from earlier ones would let it forge DNS answers
+// or inject into a connection.
uint32_t netStackRand(void) {
- uint32_t x = sRandState ^ (uint32_t)platformEntropyStamp();
+ uint32_t x;
- if (x == 0) {
- x = 1;
- }
-
- x ^= x << RAND_SHIFT_A;
- x ^= x >> RAND_SHIFT_B;
- x ^= x << RAND_SHIFT_C;
- sRandState = x;
+ secRngBytes((uint8_t *)&x, (int32_t)sizeof(x));
return x;
}
diff --git a/src/libs/kpunch/dvxnet/port/lwipopts.h b/src/libs/kpunch/dvxnet/port/lwipopts.h
index 4041233e..2f9a8964 100644
--- a/src/libs/kpunch/dvxnet/port/lwipopts.h
+++ b/src/libs/kpunch/dvxnet/port/lwipopts.h
@@ -53,16 +53,34 @@
#define MEMP_MEM_MALLOC 0
// Pointer size: 4 on DOS, 8 for the 64-bit host test build.
#define MEM_ALIGNMENT __SIZEOF_POINTER__
-#define MEM_SIZE (48 * 1024)
-#define MEMP_NUM_PBUF 32
+
+// Sized against measured peaks (2026-10-20): the host suite (a 256 KB
+// TCP transfer with both ends on loopback, a 64 KB one and a 40-datagram
+// burst over the fake wire) and the DOS network scripts (a DHCP lease, a
+// lookup, an echo, a telnet session, and the package server's downloads
+// and upload). The heap holds sent data until it is acknowledged (host
+// peak 12,688 bytes, DOS 872); the receive pool holds each frame until
+// the program reads it (host 15, DOS 1). A full heap makes a send wait,
+// and a full pool drops a frame (TCP sends it again).
+// Four connections' full send buffers
+#define MEM_SIZE (24 * 1024)
+// Our sends always copy, so only lwIP's own references (peak 0)
+#define MEMP_NUM_PBUF 8
+// DHCP, DNS (a port per lookup in flight, up to DNS_TABLE_SIZE), and
+// programs' sockets (peak 2)
#define MEMP_NUM_UDP_PCB 12
+// Closed connections wait in TIME_WAIT, reclaimed when the pool runs out
+// (peak 13, the package server's requests)
#define MEMP_NUM_TCP_PCB 16
#define MEMP_NUM_TCP_PCB_LISTEN 8
-#define MEMP_NUM_TCP_SEG 48
+// Two connections' full send queues (peak 4)
+#define MEMP_NUM_TCP_SEG (2 * TCP_SND_QUEUELEN)
+// An 8 KB datagram (PROTO_UDP_MAX) is six fragments
#define MEMP_NUM_REASSDATA 4
#define MEMP_NUM_FRAG_PBUF 8
-#define MEMP_NUM_ARP_QUEUE 16
-#define PBUF_POOL_SIZE 32
+// A full datagram queue (PROTO_DGRAM_QUEUE, netProto.c) and a full TCP
+// window
+#define PBUF_POOL_SIZE 20
// ---- Protocols ----
#define LWIP_IPV4 1
@@ -115,8 +133,25 @@
#define CHECKSUM_CHECK_UDP 1
#define CHECKSUM_CHECK_TCP 1
-// ---- Statistics and debugging off (DVX keeps its own traffic counters) ----
+// ---- Statistics off (DVX keeps its own traffic counters). The host test
+// suite builds with NET_POOL_STATS to count each pool's peak use and
+// failed allocations against the sizes above. Debugging stays off by
+// leaving LWIP_DEBUG undefined: lwIP tests it with #ifdef, so defining it
+// as 0 would turn it on. ----
+#ifdef NET_POOL_STATS
+#define LWIP_STATS 1
+#define MEM_STATS 1
+#define MEMP_STATS 1
+#define LINK_STATS 0
+#define ETHARP_STATS 0
+#define IP_STATS 0
+#define IPFRAG_STATS 0
+#define ICMP_STATS 0
+#define UDP_STATS 0
+#define TCP_STATS 0
+#define SYS_STATS 0
+#else
#define LWIP_STATS 0
-#define LWIP_DEBUG 0
+#endif
#endif
diff --git a/src/libs/kpunch/dvxshell/Makefile b/src/libs/kpunch/dvxshell/Makefile
index a1364623..af082367 100644
--- a/src/libs/kpunch/dvxshell/Makefile
+++ b/src/libs/kpunch/dvxshell/Makefile
@@ -37,7 +37,7 @@ WPAPERDIR = $(SHAREDDIR)/wpaper
BANKDIR = $(SHAREDDIR)/banks
KEYSDIR = $(SYSTEMDIR)/KEYS
-SRCS = shellMain.c shellApp.c shellBas.c shellSysBas.c shellInfo.c shellAssoc.c shellProg.c shellRun.c shellSaver.c pmModel.c pmModelBas.c
+SRCS = shellMain.c shellApp.c shellBas.c shellSysBas.c shellInfo.c shellAssoc.c shellProg.c shellProgCache.c shellRun.c shellSaver.c pmModel.c pmModelBas.c
OBJS = $(patsubst %.c,$(OBJDIR)/%.o,$(SRCS))
TARGETDIR = $(LIBSDIR)/kpunch/dvxshell
TARGET = $(TARGETDIR)/dvxshell.lib
diff --git a/src/libs/kpunch/dvxshell/README.md b/src/libs/kpunch/dvxshell/README.md
index fb69f99b..97ab9ea2 100644
--- a/src/libs/kpunch/dvxshell/README.md
+++ b/src/libs/kpunch/dvxshell/README.md
@@ -20,16 +20,27 @@ program discovery, running programs and documents, and file associations.
- `shellApp.h` -- application contract (AppDescriptorT, DxeAppContextT)
- `shellInf.h` -- shell-internal interfaces exposed to apps
+- `pmModel.h` -- program groups and items (PROGMAN.INI), one model for
+ every desktop
+- `shellSaver.h` -- the screen saver
- `shellAssoc.h` -- file associations (SYSTEM/ASSOC.INI, with the
user's changes in USER/ASSOC.INI laid over it)
- `shellProg.h` -- installed programs: discovery, names, icons
+- `shellProgCache.h` -- the program cache (PROGRAMS.CAC in the user
+ folder): names, descriptions, and pictures kept between runs
- `shellRun.h` -- running programs, documents, and command lines; the Run dialog
- `shellApp.c` -- app registry, load, unload, lifecycle
- `shellAssoc.c` -- file association store
- `shellInfo.c` -- system info service
- `shellMain.c` -- shell entry point, main loop, crash handler
- `shellProg.c` -- installed program discovery and icons
+- `shellProgCache.c` -- the program cache, checked by file stamps and
+ saved a chunk at a time in the idle time
- `shellRun.c` -- command lines, shellRunProgram, Run dialog
+- `shellSaver.c` -- the screen saver: wait, start, and stop
+- `pmModel.c` -- program groups model and PROGMAN.INI
+- `shellBas.c`, `shellSysBas.c`, `pmModelBas.c` -- BASIC entry points
+ (shell.bas, system.bas, and the groups model)
## Build
diff --git a/src/libs/kpunch/dvxshell/dvxshell.dhs b/src/libs/kpunch/dvxshell/dvxshell.dhs
index 099754c6..85ccf917 100644
--- a/src/libs/kpunch/dvxshell/dvxshell.dhs
+++ b/src/libs/kpunch/dvxshell/dvxshell.dhs
@@ -228,6 +228,8 @@ Per-app slot in the shell's app table. Slot 0 is reserved for the shell itself;
taskCodeDepth int32_t * Per task id: SHELL_APP_CALL calls into this app still open on that task's stack.
killPending bool A force kill was deferred; shellReapApps finishes it.
faulted bool It crashed: its force kill runs no kill hooks (shellRegisterAppKill).
+ workTaskId uint32_t The task shellAppTaskStart started for it, while that runs; else 0.
+ workFn, workArg void (*)(void *), void * What that task runs.
.endtable
.topic shell.lifecycle
@@ -306,7 +308,7 @@ Gracefully shut down a single app. Calls the app's shutdownFn (if present), dest
void shellForceKillApp(AppContextT *ctx, ShellAppT *app);
.endcode
-Forcibly kill an app. Used by the Task Manager "End Task" or as part of crash recovery (which marks the app faulted first). The app's shutdownFn is NOT called: a force kill targets a hung or faulting app whose own code cannot be trusted to run. Instead the libraries' kill hooks (shellRegisterAppKill) end what they run for the app, unless it faulted, then the shell drops every idle and desktop-update callback the app registered, destroys all of the app's windows, kills its task (for main-loop apps), and finally closes the DXE handle.
+Forcibly kill an app. Used by the Task Manager "End Task" or as part of crash recovery (which marks the app faulted first). The app's shutdownFn is NOT called: a force kill targets a hung or faulting app whose own code cannot be trusted to run. Instead the libraries' kill hooks (shellRegisterAppKill) end what they run for the app, unless it faulted, then the shell drops every idle and desktop-update callback the app registered, destroys all of the app's windows, kills its tasks (a main-loop app's, and a work task shellAppTaskStart started), and finally closes the DXE handle.
Cleanup order: callbacks and windows first (removes them from the compositor), then the task (frees its stack; any dispatch frames still open on it are dropped from the context's dispatch depth), then the widget classes and system-wide hotkeys the app registered (its shutdownFn never got the chance to unregister them), then the DXE handle (unmaps the code). Closing the DXE before destroying windows would cause callbacks into unmapped code.
@@ -507,9 +509,17 @@ void shellDataPath(const DxeAppContextT *ctx, const char *filename, char *outPat
Build the full path of a file the program ships in the DATA folder beside its .app ("" when it does not fit), e.g. "APPS\ACME\MYGAME\DATA\LEVEL1.DAT". Read-only.
+.h2 Program Cache
+
+Program names, descriptions, and icons come from the programs' own files (their name, description, and icon32 resources), and reading them takes several file opens per program, tens of milliseconds each on a slow disk, every time DVX starts. The shell keeps what it read in PROGRAMS.CAC in the user folder, with each file's size and time (platformFileStamp), and answers from it while a file is unchanged: shellProgramInfo, shellItemIcon, shellItemIcons, shellItemIconFile, and shellProgramIcon all go through it, so Program Manager, File Manager, and BASIC's Shell calls start without opening any program that has not changed.
+
+A file is checked against its stamp the first time it is asked about after what is installed last changed (platformInstalledChangeCount), and whenever shellProgramScan reads names (the folder is fresh in DOS's buffers then); a program that changed is read again, and a file that is gone loses its entry. A picture is kept as the image file's bytes (a DVX file's icon resource, or a picture file of up to 32 KB) and decoded for the display mode each time; bytes that turn out to be no image are noted as none.
+
+The file is read on first use. A change is saved three seconds after the last one, 4 KB at a time in the shell's idle time, so no frame waits for the whole write, and what is left at shutdown is saved then; the memory goes after a minute unused. It is DVX's memory (dvxMemSetApp), whichever program's call made it. Nothing is saved when DVX cannot save (platformIsReadOnly). Deleting PROGRAMS.CAC is harmless: it is made again. A file rewritten at the same size within two seconds keeps its stamp (DOS keeps a file's time to two seconds), so a program changed that quickly shows its old name or icon until it changes again.
+
.h2 Random Seed File
-When the serial library is loaded, the shell feeds keyboard and mouse event timings to its random-number generator (dvxSetInputEntropyHook) and carries generator state across runs in SECLINK.RND in the user folder: the file is read and immediately rewritten at startup, and rewritten again at shutdown. The library is found at run time; without it no seed file is written.
+When the cryptography library is loaded, the shell feeds keyboard and mouse event timings to its random-number generator (dvxSetInputEntropyHook) and carries generator state across runs in SECLINK.RND in the user folder: the file is read and immediately rewritten at startup, and rewritten again at shutdown. The library is found at run time; without it no seed file is written.
.topic shell.desktop
.title Desktop Callbacks
@@ -522,6 +532,7 @@ When the serial library is loaded, the shell feeds keyboard and mouse event timi
.index shellUnregisterAppCleanup
.index shellRegisterAppKill
.index shellUnregisterAppKill
+.index shellAppTaskStart
.index ShellAppCleanupFnT
.index shellDesktopUpdate
.index shellDesktopChangeCount
@@ -530,7 +541,7 @@ When the serial library is loaded, the shell feeds keyboard and mouse event timi
.h1 Desktop Callbacks
-The shell wires several AppContextT event hooks (onCtrlEsc, onF1, onTitleChange, idleCallback) during startup and exposes them to other DXEs via function-pointer extern and registration API. Desktop managers register a callback to refresh their display when apps come and go. The Task Manager DXE registers its Ctrl+Esc handler via the shellCtrlEscFn extern pointer (see Task Manager documentation); shellOpenTaskManager is the one way to open it.
+The shell wires several AppContextT event hooks (onCtrlEsc, onF1, onTitleChange, idleCallback, busyCallback, inputHere) during startup and exposes them to other DXEs via function-pointer extern and registration API. Desktop managers register a callback to refresh their display when apps come and go. The Task Manager DXE registers its Ctrl+Esc handler via the shellCtrlEscFn extern pointer (see Task Manager documentation); shellOpenTaskManager is the one way to open it.
.h2 Shell Event Hooks
@@ -538,6 +549,8 @@ The shell wires several AppContextT event hooks (onCtrlEsc, onF1, onTitleChange,
AppContextT field Handler Purpose
----------------- ------- -------
idleCallback shellIdleDispatch Runs one frame of Run Minimized (shellMinimizeIdle) and every registered idle handler (see shellRegisterIdle), then yields to cooperative tasks, when dvxUpdate has no input events and no dirty rects.
+ busyCallback shellBusyDispatch After a frame that painted: yields to cooperative tasks, and runs shellIdleDispatch when the idle handlers have not run for 20 ms, so a program that keeps the screen changing never starves the others, audio, or the network.
+ inputHere shellInputHere Input, events, and widget timers are taken on the shell's own task (or on any task once the shell's has not taken them for 250 ms), so callbacks run on the shell's task, never on another program's stack.
onCtrlEsc ctrlEscHandler shellOpenTaskManager.
onF1 f1HelpHandler Opens the focused app's helpFile/helpTopic (or the system help) in the help viewer, reusing a running viewer that sets onNewArgs.
onTitleChange titleChangeHandler Runs shellDesktopUpdate so Program Manager window lists refresh.
@@ -565,7 +578,7 @@ Remove a previously registered callback. Call this before app shutdown to avoid
void shellRegisterIdle(void (*fn)(void *ctx), void *ctx);
.endcode
-Register an idle handler invoked every idle frame by the shell main loop, regardless of which app is foreground. Background pollers (serial and secLink) register here so their connections keep pumping while another app is in front. Registration is idempotent on the (fn, ctx) pair. The registering module MUST unregister before it is unloaded -- app DXEs do this from their _appShutdown hook on a graceful exit. On a force kill (which does not run _appShutdown) the shell drops the app's registrations itself.
+Register an idle handler invoked every idle frame by the shell main loop (and at least every 20 ms while frames keep painting), regardless of which app is foreground. Background pollers (serial and secLink) register here so their connections keep pumping while another app is in front. Registration is idempotent on the (fn, ctx) pair. The registering module MUST unregister before it is unloaded -- app DXEs do this from their _appShutdown hook on a graceful exit. On a force kill (which does not run _appShutdown) the shell drops the app's registrations itself.
.h2 shellUnregisterIdle
@@ -595,6 +608,14 @@ void shellUnregisterAppKill(ShellAppCleanupFnT fn);
Register a function the shell calls with an app's id when the app is ended by force (End Task, a screen saver that would not close, DVX exiting) while none of the app's code is running and its windows are still there -- before the windows go and before the cleanup hooks. A library ends what it runs for the app the way the app's own end would: the BASIC runtime (basrt) frees a standalone program's VM, forms, screen, and module this way, closing its serial ports and its forms' windows, so End Task on a BASIC program leaks nothing. Not called for an app that faulted (its state cannot be trusted), nor for one whose kill had to wait until its code returned (End Task from inside the app's own dialog: its windows went at once). Registration rules as for cleanup hooks.
+.h2 shellAppTaskStart
+
+.code
+int32_t shellAppTaskStart(AppContextT *ctx, const char *name, uint32_t stackSize, void (*fn)(void *arg), void *arg);
+.endcode
+
+Run fn(arg) on a cooperative task of its own (stackSize bytes; 0 for the default) that belongs to the running program (ctx->currentAppId), for a long piece of a callback-only program's work: the DVX BASIC IDE runs each program it starts this way. A callback-only program's code runs inside the shell's main loop, which only reaps programs that ended, carries out close and Exit requests and DOS program starts, and starts the screen saver between frames; work done in a callback holds all of that until it returns. On its own task the work leaves the main loop free. The program is the running one on the task (windows it makes are the program's), it is not reaped while the task runs (even with all its windows closed), and ending it by force (End Task) ends the task. One at a time per program. Returns the task's ID, or -1 when one runs already, no program is running, or no task can be made.
+
.h2 shellDesktopUpdate
.code
diff --git a/src/libs/kpunch/dvxshell/pmModel.h b/src/libs/kpunch/dvxshell/pmModel.h
index 1d09142a..d3ba604e 100644
--- a/src/libs/kpunch/dvxshell/pmModel.h
+++ b/src/libs/kpunch/dvxshell/pmModel.h
@@ -54,7 +54,7 @@
#define PM_NAME_MAX SHELL_APP_NAME_MAX
#define PM_ARGS_MAX 256
-#define PM_HOTKEY_TEXT 24 // "Ctrl+Alt+Shift+F12" plus NUL, rounded up
+#define PM_HOTKEY_TEXT DVX_ACCEL_TEXT_MAX
#define PM_GROUP_MAX 64 // user groups kept from PROGMAN.INI
#define PM_ITEM_MAX 256 // items kept per group
#define PM_HISTORY_MAX 10 // File > Run... commands remembered
diff --git a/src/libs/kpunch/dvxshell/shellApp.c b/src/libs/kpunch/dvxshell/shellApp.c
index f3150b0a..76e12c34 100644
--- a/src/libs/kpunch/dvxshell/shellApp.c
+++ b/src/libs/kpunch/dvxshell/shellApp.c
@@ -32,7 +32,6 @@
#include "stb_ds_wrap.h"
#include
-#include
#include
#include
#include
@@ -41,9 +40,6 @@
#include
#include "dvxMem.h"
-// File copy chunk size for multi-instance temp copies
-#define COPY_BUF_SIZE 32768
-
// dispatchDepth while a callback dispatched by a top-level dvxUpdate runs
// (e.g. the Task Manager's End Task button). Anything deeper means some
// other callback -- possibly the kill target's own modal loop -- is still
@@ -84,12 +80,13 @@ static int32_t allocSlot(void);
static int32_t appCodeDepth(const ShellAppT *app);
static bool appCodeMayBeLive(const AppContextT *ctx, const ShellAppT *app);
static bool appCounted(const ShellAppT *app);
+static uint32_t appOwnTask(const ShellAppT *app);
static int32_t *appTaskCodeSlot(ShellAppT *app, uint32_t taskId);
static int32_t appTaskDispatchShare(const ShellAppT *app);
static void appTaskWrapper(void *arg);
static WindowT *appTopWindow(const AppContextT *ctx, int32_t appId);
+static void appWorkTaskWrapper(void *arg);
static void cleanupTempFile(ShellAppT *app);
-static int32_t copyFile(const char *src, const char *dst);
static void defaultHelpFile(DxeAppContextT *dxeCtx);
static void deferForceKill(AppContextT *ctx, ShellAppT *app);
static void deliverNewArgs(AppContextT *ctx, ShellAppT *app, const char *args);
@@ -108,8 +105,11 @@ void shellAppCallEnter(ShellAppT *app);
void shellAppCallLeave(ShellAppT *app);
void shellAppCodeForgetTask(uint32_t taskId);
ShellAppT *shellAppFindByPath(const char *path);
+int32_t shellAppGetPriority(int32_t appId);
void shellAppInit(void);
+bool shellAppSetPriority(int32_t appId, int32_t priority);
int32_t shellAppSlotCount(void);
+int32_t shellAppTaskStart(AppContextT *ctx, const char *name, uint32_t stackSize, void (*fn)(void *arg), void *arg);
#ifdef DVX_HOST_TEST
void shellAppTestReset(void);
#endif
@@ -191,7 +191,7 @@ static bool appCodeMayBeLive(const AppContextT *ctx, const ShellAppT *app) {
return true;
}
- if (app->hasMainLoop && app->mainTaskId > 0 && tsCurrentId() == app->mainTaskId) {
+ if ((app->hasMainLoop && app->mainTaskId > 0 && tsCurrentId() == app->mainTaskId) || (app->workTaskId > 0 && tsCurrentId() == app->workTaskId)) {
return true;
}
@@ -206,6 +206,18 @@ static bool appCounted(const ShellAppT *app) {
}
+// The task that is the program's own, for its priority: its main loop's,
+// else its shellAppTaskStart task while one runs; 0 for none (a callback
+// program runs on the shell's task).
+static uint32_t appOwnTask(const ShellAppT *app) {
+ if (app->hasMainLoop && app->mainTaskId != TS_MAIN_TASK_ID) {
+ return app->mainTaskId;
+ }
+
+ return app->workTaskId;
+}
+
+
// Grows the app's per-task call table on demand (zero-filled).
static int32_t *appTaskCodeSlot(ShellAppT *app, uint32_t taskId) {
while (arrlen(app->taskCodeDepth) <= (ptrdiff_t)taskId) {
@@ -216,14 +228,20 @@ static int32_t *appTaskCodeSlot(ShellAppT *app, uint32_t taskId) {
}
-// Dispatch frames open on the app's own task, which tsKill discards along
-// with the task (0 for a callback-only app).
+// Dispatch frames open on the app's own tasks (its main loop's, its work
+// task's), which tsKill discards along with them.
static int32_t appTaskDispatchShare(const ShellAppT *app) {
- if (!app->hasMainLoop || app->mainTaskId == 0) {
- return 0;
+ int32_t share = 0;
+
+ if (app->hasMainLoop && app->mainTaskId > 0) {
+ share += shellTaskDepthShare(app->mainTaskId);
}
- return shellTaskDepthShare(app->mainTaskId);
+ if (app->workTaskId > 0) {
+ share += shellTaskDepthShare(app->workTaskId);
+ }
+
+ return share;
}
@@ -273,6 +291,19 @@ static WindowT *appTopWindow(const AppContextT *ctx, int32_t appId) {
}
+// Task entry of shellAppTaskStart's work: the program is the running one
+// for the task's life (as appTaskWrapper does), and workTaskId clears as
+// the task ends (its slot may be handed out again).
+static void appWorkTaskWrapper(void *arg) {
+ ShellAppT *app = sApps[(int32_t)(intptr_t)arg];
+
+ app->dxeCtx->shellCtx->currentAppId = app->appId;
+ app->workFn(app->workArg);
+ app->dxeCtx->shellCtx->currentAppId = 0;
+ app->workTaskId = 0;
+}
+
+
// Delete the temp file for a multi-instance app copy, if one exists.
static void cleanupTempFile(ShellAppT *app) {
if (app->tempPath[0] != '\0') {
@@ -282,46 +313,6 @@ static void cleanupTempFile(ShellAppT *app) {
}
-// Binary file copy. Returns 0 on success, -1 on failure.
-static int32_t copyFile(const char *src, const char *dst) {
- FILE *in = fopen(src, "rb");
-
- if (!in) {
- dvxLog("copyFile: failed to open source '%s' (errno=%d)", src, errno);
- return -1;
- }
-
- FILE *out = fopen(dst, "wb");
-
- if (!out) {
- dvxLog("copyFile: failed to create dest '%s' (errno=%d)", dst, errno);
- fclose(in);
- return -1;
- }
-
- // Static rather than on-stack: shellLoadApp runs on app task stacks as
- // small as TS_DEFAULT_STACK_SIZE (32KB), which a 32KB stack frame would
- // overflow. The cooperative switcher is single-threaded, so a shared
- // copy buffer is safe.
- static char buf[COPY_BUF_SIZE];
- size_t n;
-
- while ((n = fread(buf, 1, sizeof(buf), in)) > 0) {
- if (fwrite(buf, 1, n, out) != n) {
- dvxLog("copyFile: write failed '%s' -> '%s' (errno=%d)", src, dst, errno);
- fclose(in);
- fclose(out);
- remove(dst);
- return -1;
- }
- }
-
- fclose(in);
- fclose(out);
- return 0;
-}
-
-
// Force kill while the app's code may still be on a stack (see
// appCodeMayBeLive). Its windows are queued for destruction -- hidden at
// once, so modal loops over them see them go, but their memory stays valid
@@ -488,9 +479,18 @@ static void forceKillNow(AppContextT *ctx, ShellAppT *app) {
}
-// Kill an app's cooperative task if it has one and it is still alive.
-// Shared by shellForceKillApp and shellReapApp.
+// Kill an app's cooperative tasks (its main loop's, its work task) that
+// are still alive. Shared by shellForceKillApp and shellReapApp.
static void killAppTask(ShellAppT *app) {
+ if (app->workTaskId > 0) {
+ if (tsGetState(app->workTaskId) != TaskStateTerminated) {
+ shellTaskDepthForget(app->workTaskId);
+ tsKill(app->workTaskId);
+ }
+
+ app->workTaskId = 0;
+ }
+
if (app->hasMainLoop && app->mainTaskId > 0) {
if (tsGetState(app->mainTaskId) != TaskStateTerminated) {
shellTaskDepthForget(app->mainTaskId);
@@ -646,6 +646,15 @@ ShellAppT *shellAppFindByPath(const char *path) {
}
+int32_t shellAppGetPriority(int32_t appId) {
+ const ShellAppT *app = shellGetApp(appId);
+ uint32_t task = (app && appCounted(app)) ? appOwnTask(app) : 0;
+ int32_t pri = task ? tsGetPriority(task) : TS_ERR_PARAM;
+
+ return pri >= TS_OK ? pri : SHELL_NO_PRIORITY;
+}
+
+
void shellAppInit(void) {
// Seed slot 0 (reserved for the shell). Slots are heap-allocated so
// their addresses stay stable as the sApps index array grows.
@@ -654,11 +663,37 @@ void shellAppInit(void) {
}
+bool shellAppSetPriority(int32_t appId, int32_t priority) {
+ const ShellAppT *app = shellGetApp(appId);
+ uint32_t task = (app && appCounted(app)) ? appOwnTask(app) : 0;
+
+ return task && tsSetPriority(task, priority) == TS_OK;
+}
+
+
int32_t shellAppSlotCount(void) {
return arrlen(sApps);
}
+int32_t shellAppTaskStart(AppContextT *ctx, const char *name, uint32_t stackSize, void (*fn)(void *arg), void *arg) {
+ int32_t appId = ctx->currentAppId;
+ ShellAppT *app = (appId > 0 && appId < arrlen(sApps)) ? sApps[appId] : NULL;
+
+ if (!app || app->state != AppStateRunningE || app->workTaskId != 0 || !fn) {
+ return -1;
+ }
+
+ app->workFn = fn;
+ app->workArg = arg;
+
+ int32_t taskId = tsCreate(name, appWorkTaskWrapper, (void *)(intptr_t)appId, stackSize, TS_PRIORITY_NORMAL);
+
+ app->workTaskId = (taskId > 0) ? (uint32_t)taskId : 0;
+ return taskId > 0 ? taskId : -1;
+}
+
+
#ifdef DVX_HOST_TEST
// Test seam: release every slot (the caller has already terminated the
// apps) so the next shellAppInit starts from an empty table.
@@ -799,8 +834,10 @@ static int32_t shellLoadAppInternal(AppContextT *ctx, const char *path, const ch
return refuseSecondCopy(ctx, path);
}
- if (copyFile(path, tempPath) != 0) {
+ if (!platformCopyFile(path, tempPath)) {
char msg[SHELL_MSG_MAX];
+
+ dvxLog("Shell: cannot copy %s to %s for another copy of it", path, tempPath);
snprintf(msg, sizeof(msg), "Failed to create instance copy of %s.", platformPathBaseName(path));
dvxErrorBox(ctx, NULL, msg);
return -1;
@@ -812,8 +849,8 @@ static int32_t shellLoadAppInternal(AppContextT *ctx, const char *path, const ch
// Show hourglass during the load (dlopen + symbol resolution + appMain)
dvxSetBusy(ctx, true);
- // Load the DXE
- void *handle = dlopen(loadPath, RTLD_NOW | RTLD_GLOBAL);
+ // Load the DXE (and any library it needs that is not loaded yet)
+ void *handle = platformModuleOpen(loadPath, RTLD_NOW | RTLD_GLOBAL);
if (!handle) {
char msg[SHELL_MSG_MAX];
@@ -1103,8 +1140,9 @@ bool shellReapApps(AppContextT *ctx) {
// Callback-only apps terminate when their last window closes.
// They have no main loop to set AppStateTerminatingE, so we
- // detect termination by checking for zero remaining windows.
- if (sApps[i]->state == AppStateRunningE && !sApps[i]->hasMainLoop) {
+ // detect termination by checking for zero remaining windows --
+ // once their work task (shellAppTaskStart) has ended.
+ if (sApps[i]->state == AppStateRunningE && !sApps[i]->hasMainLoop && sApps[i]->workTaskId == 0) {
bool hasWindow = false;
for (int32_t w = 0; w < ctx->stack.count; w++) {
diff --git a/src/libs/kpunch/dvxshell/shellApp.h b/src/libs/kpunch/dvxshell/shellApp.h
index aa4a3ace..c5e63bf9 100644
--- a/src/libs/kpunch/dvxshell/shellApp.h
+++ b/src/libs/kpunch/dvxshell/shellApp.h
@@ -75,6 +75,9 @@
// Use in AppDescriptorT.stackSize to get the default task stack size
#define SHELL_STACK_DEFAULT 0
+// shellAppGetPriority of a program with no task of its own
+#define SHELL_NO_PRIORITY (-1)
+
typedef struct {
uint32_t abiVersion; // DVX_ABI_VERSION at build time; must stay first
char name[SHELL_APP_NAME_MAX];
@@ -156,6 +159,9 @@ typedef struct {
int32_t *taskCodeDepth; // stb_ds by task id: shell calls into this app in progress on that task's stack (see SHELL_APP_CALL)
bool killPending; // force kill requested while its code was live; released by shellReapApps
bool faulted; // it crashed: its kill runs no kill hooks (shellRegisterAppKill)
+ uint32_t workTaskId; // shellAppTaskStart's task while it runs, else 0
+ void (*workFn)(void *arg);
+ void *workArg;
} ShellAppT;
// Bracket every direct shell call into an app's code (appMain of a
@@ -296,6 +302,17 @@ bool shellActivateApp(AppContextT *ctx, int32_t appId);
// Get an active app slot loaded from this path (platformPathSame), or NULL.
ShellAppT *shellAppFindByPath(const char *path);
+// A running program's priority (TS_PRIORITY_LOW .. TS_PRIORITY_HIGH): that
+// of its own task (its main loop's, or its shellAppTaskStart task's while
+// that runs). SHELL_NO_PRIORITY when it has none -- a callback program
+// runs on the shell's task -- or appId is not running (or starting).
+int32_t shellAppGetPriority(int32_t appId);
+
+// Set a running program's priority (its own task's, as above): how many
+// turns it gets beside the others. False when it has no task of its own,
+// is not running, or priority is out of range.
+bool shellAppSetPriority(int32_t appId, int32_t priority);
+
// Total number of app slots (for iteration bounds)
int32_t shellAppSlotCount(void);
@@ -369,11 +386,12 @@ void shellDesktopUpdate(void);
// differs from the value it saw last.
int32_t shellDesktopChangeCount(void);
-// Register an idle handler called every idle frame by the shell main loop,
-// regardless of which app is foreground. Serial/secLink pollers register
-// here so background connections keep pumping. Idempotent on the (fn, ctx)
-// pair. The registering module MUST unregister before it is unloaded (for
-// app DXEs, from the _appShutdown hook) or the handler will dangle.
+// Register an idle handler called every idle frame by the shell main loop
+// (and at least every 20 ms while frames keep painting), regardless of which
+// app is foreground. Serial/secLink pollers register here so background
+// connections keep pumping. Idempotent on the (fn, ctx) pair. The
+// registering module MUST unregister before it is unloaded (for app DXEs,
+// from the _appShutdown hook) or the handler will dangle.
void shellRegisterIdle(void (*fn)(void *ctx), void *ctx);
// Remove a previously registered idle handler. No-op if absent, so a
@@ -416,6 +434,16 @@ void shellRegisterAppKill(ShellAppCleanupFnT fn);
// Remove a kill hook (no-op if absent).
void shellUnregisterAppKill(ShellAppCleanupFnT fn);
+// Run fn(arg) on a task of its own (stackSize bytes; 0: the default) that
+// belongs to the running program (ctx->currentAppId), for a long piece of
+// a callback-only program's work (the IDE's program runs): the shell's
+// main loop goes on meanwhile -- programs that ended are reaped, close and
+// exit requests carried out, the screen saver started. The program is
+// the running one on the task, is not reaped while the task runs, and
+// ending it by force ends the task. One at a time per program. Returns
+// the task's ID, or -1 (one runs already, no program, or no task).
+int32_t shellAppTaskStart(AppContextT *ctx, const char *name, uint32_t stackSize, void (*fn)(void *arg), void *arg);
+
// ============================================================
// Ctrl+Esc handler (set by taskmgr library)
// ============================================================
@@ -500,6 +528,11 @@ const char *ShellCommandProgram(const char *command);
// Exit DVX (every program is asked to close first).
void ShellExitDvx(void);
+// What is installed may have changed (platformInstalledChanged), and its
+// count (platformInstalledChangeCount).
+void ShellInstalledChanged(void);
+int32_t ShellInstalledCount(void);
+
// Copies of this program running, this one included.
int32_t ShellInstanceCount(void);
@@ -576,6 +609,10 @@ int32_t ShellRunningKill(int32_t appId);
const char *ShellRunningName(int32_t appId);
const char *ShellRunningPath(int32_t appId);
+// Another program's priority (shellAppGetPriority / shellAppSetPriority).
+int32_t ShellRunningPriority(int32_t appId);
+int32_t ShellRunningSetPriority(int32_t appId, int32_t priority);
+
// shellRunProgram: the new program's id, or 0.
int32_t ShellRunProgram(const char *program, const char *args, const char *folder);
diff --git a/src/libs/kpunch/dvxshell/shellBas.c b/src/libs/kpunch/dvxshell/shellBas.c
index c681d051..75eae418 100644
--- a/src/libs/kpunch/dvxshell/shellBas.c
+++ b/src/libs/kpunch/dvxshell/shellBas.c
@@ -48,10 +48,9 @@
#define BAS_HISTORY_SEP '\n'
#define BAS_LINE_END '\r'
#define HOTKEY_QUEUE_MAX 16 // presses kept per program that has not asked yet
-#define ACCEL_TEXT_MAX 32
#define ARG_NONE (-1) // argWalk: only count
-#define PRIORITY_NONE (-1) // ShellPriority without a task of its own
#define BAS_RUN_DOS (-1) // shell.bas SHELL_RUN_DOS: a DOS program will run
+#define RUN_DOS_NONE (-1) // ShellRunDosWait: the DOS program did not run
// A program list from ShellProgramScan and the program it belongs to.
typedef struct {
@@ -81,6 +80,7 @@ typedef struct {
static BasProgListT *sProgLists = NULL; // stb_ds; handle = index + 1
static BasHotkeyT *sHotkeys = NULL; // stb_ds
static BasHotkeyPressT *sPresses = NULL; // stb_ds, oldest first
+static BasTextT sText; // the last string result
static int32_t sLastHotkeyId = 0;
// ============================================================
@@ -99,7 +99,6 @@ int32_t HotkeyNext(void);
static void hotkeyPressed(void *userCtx);
void HotkeyRemove(int32_t id);
const char *HotkeyText(const char *keyText);
-static const ShellAppT *mainTaskApp(void);
static const char *orEmpty(const char *text);
static BasProgListT *progListOf(int32_t handle);
static ShellAppT *runningApp(int32_t appId);
@@ -123,6 +122,8 @@ int32_t ShellChangeCount(void);
const char *ShellCommandArgs(const char *command);
const char *ShellCommandProgram(const char *command);
void ShellExitDvx(void);
+void ShellInstalledChanged(void);
+int32_t ShellInstalledCount(void);
int32_t ShellInstanceCount(void);
int32_t ShellIsDosProgram(const char *path);
int32_t ShellIsProgram(const char *path);
@@ -142,6 +143,7 @@ int32_t ShellRun(const char *program, const char *args);
int32_t ShellRunCommand(const char *command);
const char *ShellRunDialog(const char *history);
void ShellRunDos(const char *program, const char *args, const char *folder, int32_t pause);
+int32_t ShellRunDosWait(const char *program, const char *args, const char *folder, int32_t pause);
int32_t ShellRunningActivate(int32_t appId);
int32_t ShellRunningClose(int32_t appId);
int32_t ShellRunningCount(void);
@@ -150,6 +152,8 @@ int32_t ShellRunningId(int32_t index);
int32_t ShellRunningKill(int32_t appId);
const char *ShellRunningName(int32_t appId);
const char *ShellRunningPath(int32_t appId);
+int32_t ShellRunningPriority(int32_t appId);
+int32_t ShellRunningSetPriority(int32_t appId, int32_t priority);
int32_t ShellRunProgram(const char *program, const char *args, const char *folder);
int32_t ShellSetDesktop(const char *program);
int32_t ShellSetPriority(int32_t priority);
@@ -333,9 +337,9 @@ void HotkeyRemove(int32_t id) {
// keyText read and written back by the same code menus use.
const char *HotkeyText(const char *keyText) {
- static char text[ACCEL_TEXT_MAX];
- int32_t key = 0;
- int32_t mods = 0;
+ char text[DVX_ACCEL_TEXT_MAX];
+ int32_t key = 0;
+ int32_t mods = 0;
text[0] = '\0';
@@ -343,16 +347,7 @@ const char *HotkeyText(const char *keyText) {
dvxAccelFormat(key, mods, text, sizeof(text));
}
- return text;
-}
-
-
-// This program when it runs as a task of its own (every BASIC program
-// does), else NULL.
-static const ShellAppT *mainTaskApp(void) {
- const ShellAppT *app = currentApp();
-
- return (app && app->hasMainLoop && app->mainTaskId != TS_MAIN_TASK_ID) ? app : NULL;
+ return basTextSet(&sText, text);
}
@@ -441,10 +436,10 @@ const char *ShellAppPath(void) {
// Argument index of a command line (0 = the program), without its quotes;
// "" past the last.
const char *ShellArg(const char *command, int32_t index) {
- static char arg[SHELL_ARGS_MAX];
+ char arg[SHELL_ARGS_MAX];
argWalk(command, index, arg, (int32_t)sizeof(arg));
- return arg;
+ return basTextSet(&sText, arg);
}
@@ -460,27 +455,27 @@ int32_t ShellArgCount(const char *command) {
// it holds blanks, so ShellArg$ and COMMAND$(n) read it back whole; ""
// when it is too long for one.
const char *ShellArgQuote(const char *arg) {
- static char quoted[SHELL_ARGS_MAX];
+ char quoted[SHELL_ARGS_MAX];
if (dvxArgAppend(quoted, (int32_t)sizeof(quoted), 0, arg ? arg : "") < 0) {
quoted[0] = '\0';
}
- return quoted;
+ return basTextSet(&sText, quoted);
}
// The program that opens document, without the association's argument
// template ("" when none).
const char *ShellAssocCommand(const char *document) {
- static char program[DVX_MAX_PATH];
- char args[SHELL_ARGS_MAX];
+ char program[DVX_MAX_PATH];
+ char args[SHELL_ARGS_MAX];
if (!shellAssocCommand(document, program, sizeof(program), args, sizeof(args))) {
program[0] = '\0';
}
- return program;
+ return basTextSet(&sText, program);
}
@@ -490,37 +485,37 @@ int32_t ShellAssocCount(void) {
const char *ShellAssocExt(int32_t index) {
- static char extension[SHELL_ASSOC_EXT_MAX];
- char value[DVX_MAX_PATH + SHELL_ARGS_MAX];
+ char extension[SHELL_ASSOC_EXT_MAX];
+ char value[DVX_MAX_PATH + SHELL_ARGS_MAX];
if (!shellAssocGet(index, extension, sizeof(extension), value, sizeof(value))) {
extension[0] = '\0';
}
- return extension;
+ return basTextSet(&sText, extension);
}
const char *ShellAssocNormalize(const char *extension) {
- static char normal[SHELL_ASSOC_EXT_MAX];
+ char normal[SHELL_ASSOC_EXT_MAX];
if (!shellAssocNormalize(extension, normal)) {
normal[0] = '\0';
}
- return normal;
+ return basTextSet(&sText, normal);
}
// The program set to open files with this extension ("" when none).
const char *ShellAssocProgram(const char *extension) {
- static char program[DVX_MAX_PATH];
+ char program[DVX_MAX_PATH];
if (!shellAssocLookup(extension, program, sizeof(program))) {
program[0] = '\0';
}
- return program;
+ return basTextSet(&sText, program);
}
@@ -535,14 +530,14 @@ int32_t ShellAssocSet(const char *extension, const char *value) {
const char *ShellAssocValue(int32_t index) {
- static char value[DVX_MAX_PATH + SHELL_ARGS_MAX];
- char extension[SHELL_ASSOC_EXT_MAX];
+ char value[DVX_MAX_PATH + SHELL_ARGS_MAX];
+ char extension[SHELL_ASSOC_EXT_MAX];
if (!shellAssocGet(index, extension, sizeof(extension), value, sizeof(value))) {
value[0] = '\0';
}
- return value;
+ return basTextSet(&sText, value);
}
@@ -553,21 +548,21 @@ int32_t ShellChangeCount(void) {
// The arguments part of a command line, as ShellRunCommand passes them.
const char *ShellCommandArgs(const char *command) {
- static char args[SHELL_ARGS_MAX];
- char program[DVX_MAX_PATH];
+ char args[SHELL_ARGS_MAX];
+ char program[DVX_MAX_PATH];
shellSplitCommand(orEmpty(command), program, (int32_t)sizeof(program), args, (int32_t)sizeof(args));
- return args;
+ return basTextSet(&sText, args);
}
// The program part of a command line, as ShellRunCommand starts it.
const char *ShellCommandProgram(const char *command) {
- static char program[DVX_MAX_PATH];
- char args[SHELL_ARGS_MAX];
+ char program[DVX_MAX_PATH];
+ char args[SHELL_ARGS_MAX];
shellSplitCommand(orEmpty(command), program, (int32_t)sizeof(program), args, (int32_t)sizeof(args));
- return program;
+ return basTextSet(&sText, program);
}
@@ -576,6 +571,20 @@ void ShellExitDvx(void) {
}
+// What is installed may have changed (platformInstalledChanged): a
+// program that put files in the programs folder says so, and Program
+// Manager looks again when it comes to the front.
+void ShellInstalledChanged(void) {
+ platformInstalledChanged();
+}
+
+
+// platformInstalledChangeCount, wrapped into a Long.
+int32_t ShellInstalledCount(void) {
+ return (int32_t)platformInstalledChangeCount();
+}
+
+
// How many copies of this program are running, this one included.
int32_t ShellInstanceCount(void) {
const ShellAppT *self = currentApp();
@@ -608,10 +617,10 @@ int32_t ShellIsProgram(const char *path) {
// else the program that opens the document; "" when none gives one.
// LOADPICTURE of the file shows the icon.
const char *ShellItemIcon(const char *iconPath, const char *path) {
- static char file[DVX_MAX_PATH];
+ char file[DVX_MAX_PATH];
shellItemIconFile(shellContext(), orEmpty(iconPath), orEmpty(path), file, (int32_t)sizeof(file));
- return platformPathNative(file);
+ return basTextSet(&sText, platformPathNative(file));
}
@@ -635,10 +644,7 @@ int32_t ShellOpenDocument(const char *path) {
// This program's priority (TS_PRIORITY_LOW .. TS_PRIORITY_HIGH), -1 when
// it has no task of its own.
int32_t ShellPriority(void) {
- const ShellAppT *app = mainTaskApp();
- int32_t pri = app ? tsGetPriority(app->mainTaskId) : TS_ERR_PARAM;
-
- return pri >= TS_OK ? pri : PRIORITY_NONE;
+ return shellAppGetPriority(ShellAppId());
}
@@ -742,7 +748,7 @@ int32_t ShellRunCommand(const char *command) {
// history holds the remembered commands newest first, one per line.
const char *ShellRunDialog(const char *history) {
- static char command[SHELL_RUN_CMD_MAX];
+ char command[SHELL_RUN_CMD_MAX];
char *copy = strdup(orEmpty(history));
const char **lines = NULL;
@@ -774,7 +780,7 @@ const char *ShellRunDialog(const char *history) {
arrfree(lines);
free(copy);
- return command;
+ return basTextSet(&sText, command);
}
@@ -785,6 +791,27 @@ void ShellRunDos(const char *program, const char *args, const char *folder, int3
}
+// ShellRunDos waiting for the DOS program, as SHELL does (this program
+// is not asked to close, and its events run meanwhile, as in DoEvents):
+// the program's exit code, -1 when it did not run (the other programs
+// were kept, it was not found or could not start) or this program is
+// ending.
+int32_t ShellRunDosWait(const char *program, const char *args, const char *folder, int32_t pause) {
+ int32_t appId = currentAppId();
+ int32_t rc = RUN_DOS_NONE;
+
+ shellRunDosFor(appId, program, args, folder, pause != BAS_FALSE);
+
+ while (!shellRunDosDone(appId, &rc)) {
+ if (!dvxWaitPump()) {
+ return RUN_DOS_NONE;
+ }
+ }
+
+ return rc;
+}
+
+
// Bring a program to the front (Task Manager's Switch To). True when it
// has a window to show.
int32_t ShellRunningActivate(int32_t appId) {
@@ -859,6 +886,20 @@ const char *ShellRunningPath(int32_t appId) {
}
+// Another program's priority, as ShellPriority is this one's; -1 when it
+// has no task of its own or is not running.
+int32_t ShellRunningPriority(int32_t appId) {
+ return shellAppGetPriority(appId);
+}
+
+
+// Set another program's priority, as ShellSetPriority does this one's.
+// True when it was set.
+int32_t ShellRunningSetPriority(int32_t appId, int32_t priority) {
+ return shellAppSetPriority(appId, priority) ? BAS_TRUE : BAS_FALSE;
+}
+
+
// A program, or a document opened by its file type, with file names in
// args found in folder ("" for none). The new program's id, or 0 (DVX
// has said why).
@@ -877,9 +918,7 @@ int32_t ShellSetDesktop(const char *program) {
// Set this program's priority (TS_PRIORITY_LOW .. TS_PRIORITY_HIGH).
// True when it was set.
int32_t ShellSetPriority(int32_t priority) {
- const ShellAppT *app = mainTaskApp();
-
- return (app && tsSetPriority(app->mainTaskId, priority) == TS_OK) ? BAS_TRUE : BAS_FALSE;
+ return shellAppSetPriority(ShellAppId(), priority) ? BAS_TRUE : BAS_FALSE;
}
diff --git a/src/libs/kpunch/dvxshell/shellMain.c b/src/libs/kpunch/dvxshell/shellMain.c
index a2d6d3da..9baf379c 100644
--- a/src/libs/kpunch/dvxshell/shellMain.c
+++ b/src/libs/kpunch/dvxshell/shellMain.c
@@ -66,6 +66,7 @@
#include "shellApp.h"
#include "shellInf.h"
+#include "shellProgCache.h"
#include "shellRun.h"
#include "shellSaver.h"
#include "dvxDlg.h"
@@ -75,7 +76,7 @@
#include "dvxPrefs.h"
#include "dvxPlat.h"
#include "stb_ds_wrap.h"
-#include "../serial/security/security.h" // SEC_SUCCESS only; resolved at run time
+#include "../crypto/security.h" // SEC_SUCCESS only; resolved at run time
#include
#include
@@ -88,7 +89,7 @@
// ABI stamp checked by the loader (see DVX_ABI_VERSION).
DVX_ABI_STAMP(dvxshell);
-// Random-seed file carried across runs for the serial library's RNG
+// Random-seed file carried across runs for the cryptography library's RNG
#define SHELL_SEED_NAME "SECLINK.RND" // in the user folder
// Exit DVX: how long an app may take to close a window it was asked to
@@ -99,6 +100,16 @@ DVX_ABI_STAMP(dvxshell);
#define SHELL_EXIT_TITLE "Exit DVX"
#define SHELL_CLOSE_TITLE "Close Program"
+// While frames keep painting (no idle frame comes), the idle handlers
+// still run this often: audio streams, network polls, and serial
+// pollers must not wait for the screen to settle.
+#define SHELL_BUSY_IDLE_US 20000
+
+// Input is the shell task's (shellInputHere) unless it has not polled for
+// this long (busy in a long call): then any task's frame takes it, so the
+// desktop never stops answering.
+#define SHELL_INPUT_STALE_US 250000
+
// The not-responding question: while exiting DVX, and for one program
#define SHELL_STUCK_EXIT_ASK "'%s' is not responding to the request to close. End it and continue exiting DVX?"
#define SHELL_STUCK_CLOSE_ASK "'%s' is not responding to the request to close. End it?"
@@ -190,9 +201,10 @@ static int32_t *sTaskDepth = NULL; // stb_ds, indexed by task id
static uint32_t sRunningTask = 0; // task whose context was last restored
static int32_t sDepthBase = 0; // dispatchDepth when that task was switched in
-// Serial-library RNG entry points, resolved at run time (the library is
-// optional and nothing links against it). secRngAddEntropy has exactly
-// the DvxEntropyHookT signature and is registered directly.
+// Serial-library RNG entry points, resolved at run time (the shell does
+// not link against the library, and the host tests have none).
+// secRngAddEntropy has exactly the DvxEntropyHookT signature and is
+// registered directly.
typedef int (*RngSeedFileFnT)(const char *path);
static RngSeedFileFnT sRngSaveSeed = NULL;
@@ -201,6 +213,11 @@ static char sSeedPath[DVX_MAX_PATH]; // SHELL_SEED_NAME in the user
// Set by shellRequestExit, acted on at the top of the main loop.
static bool sExitRequested = false;
+// platformMicros when the idle handlers last ran (shellBusyDispatch), and
+// when the shell task last took input (shellInputHere).
+static uint64_t sIdleWalkAt = 0;
+static uint64_t sInputPolledAt = 0;
+
// Programs asked to close (shellRequestAppClose), oldest first: stb_ds of
// app ids, carried out at the top of the main loop.
static int32_t *sCloseRequests = NULL;
@@ -234,6 +251,7 @@ static bool exitWindowGone(const AppContextT *ctx, int32_t winId);
static void f1HelpHandler(void *ctx);
static const char *findSwitch(int argc, char *argv[], const char *name);
static void logVideoMode(int32_t w, int32_t h, int32_t bpp, void *userData);
+static void shellBusyDispatch(void *ctx);
bool shellCloseAllApps(AppContextT *ctx, int32_t keepAppId);
bool shellCloseApp(AppContextT *ctx, int32_t appId);
AppContextT *shellContext(void);
@@ -241,6 +259,7 @@ int32_t shellDesktopChangeCount(void);
void shellDesktopUpdate(void);
static void shellFrame(AppContextT *ctx);
static void shellIdleDispatch(void *ctx);
+static bool shellInputHere(void *ctx);
int shellMain(int argc, char *argv[]);
void shellPurgeAppCallbacks(int32_t appId);
void shellRegisterDesktopUpdate(void (*updateFn)(void));
@@ -373,7 +392,7 @@ static void desktopEnded(int32_t appId) {
}
-// When the serial library is loaded: feed it input timings and restore the
+// When the cryptography library is loaded: feed it input timings and restore the
// seed saved by the previous run (secRngLoadSeed rewrites the file at once).
static void entropyInit(void) {
RngSeedFileFnT loadSeed = (RngSeedFileFnT)dlsym(NULL, DVX_SYM("secRngLoadSeed"));
@@ -615,6 +634,20 @@ static void logVideoMode(int32_t w, int32_t h, int32_t bpp, void *userData) {
// asked running; apps already closed stay closed. A dialog box still
// open (its nested event loop is on some stack) is raised and the exit
// refused, so it can be answered first.
+// Installed as the context's busyCallback: after a frame that painted,
+// give the other tasks their turn (a program pumping dvxUpdate in its own
+// loop -- a progress loop, DoEvents, a modal dialog -- would otherwise
+// keep the CPU for as long as the screen changes), and run the idle
+// handlers at least every SHELL_BUSY_IDLE_US.
+static void shellBusyDispatch(void *ctx) {
+ if (platformMicros() - sIdleWalkAt >= SHELL_BUSY_IDLE_US) {
+ shellIdleDispatch(ctx);
+ } else if (tsActiveCount() > 1) {
+ tsYield();
+ }
+}
+
+
bool shellCloseAllApps(AppContextT *ctx, int32_t keepAppId) {
if (closeRefusedForDialog(ctx)) {
return false;
@@ -680,18 +713,13 @@ void shellDesktopUpdate(void) {
// One main-loop iteration. dvxUpdate processes input, dispatches
-// callbacks, composites and flushes. The explicit yield gives app tasks
-// CPU time even during busy frames: a flurry of mouse moves could
-// otherwise keep dvxUpdate from ever reaching its idle-callback yield.
-// Reaping happens here, at the top level, not inside any callback or
-// compositor operation.
+// callbacks, composites and flushes, and yields to the app tasks through
+// the idle or busy callback, whichever the frame was. Reaping happens
+// here, at the top level, not inside any callback or compositor
+// operation.
static void shellFrame(AppContextT *ctx) {
dvxUpdate(ctx);
- if (tsActiveCount() > 1) {
- tsYield();
- }
-
if (shellReapApps(ctx)) {
shellDesktopUpdate();
}
@@ -699,16 +727,19 @@ static void shellFrame(AppContextT *ctx) {
// Installed as the context's single idleCallback. dvxUpdate calls this when
-// all events are processed and nothing is dirty. We walk the idle handler
-// list (serial/secLink pollers register here), then perform the cooperative
-// yield unconditionally as the tail step -- so a registered poller can never
-// suppress task switching the way the old single-slot scheme did, where BASIC
-// overwrote idleYield outright and never restored it. The tsActiveCount > 1
-// check avoids a scheduler scan when the shell is the only task. Handlers
-// unregistered mid-walk (e.g. via a nested modal dvxUpdate) are only marked
-// dead until the outermost walk ends (see sIdleWalkDepth), so no handler is
-// skipped and arrlen is re-read so none is indexed past the array.
+// all events are processed and nothing is dirty (shellBusyDispatch too, at
+// least every SHELL_BUSY_IDLE_US while frames keep painting). We walk the
+// idle handler list (serial/secLink pollers register here), then perform the
+// cooperative yield unconditionally as the tail step -- so a registered
+// poller can never suppress task switching the way the old single-slot scheme
+// did, where BASIC overwrote idleYield outright and never restored it. The
+// tsActiveCount > 1 check avoids a scheduler scan when the shell is the only
+// task. Handlers unregistered mid-walk (e.g. via a nested modal dvxUpdate)
+// are only marked dead until the outermost walk ends (see sIdleWalkDepth), so
+// no handler is skipped and arrlen is re-read so none is indexed past the
+// array.
static void shellIdleDispatch(void *ctx) {
+ sIdleWalkAt = platformMicros();
shellMinimizeIdle((AppContextT *)ctx);
sIdleWalkDepth++;
@@ -730,6 +761,27 @@ static void shellIdleDispatch(void *ctx) {
}
+// Installed as the context's inputHere: input, events, and widget timers
+// are taken on the shell's own task, so a callback program (Program
+// Manager, Control Panel, the IDE) runs on the shell task and never on a
+// BASIC program's stack, and a BASIC program's events reach its own task
+// (the form runtime queues those dispatched elsewhere). Another task's
+// frame takes them only once the shell task has not for
+// SHELL_INPUT_STALE_US.
+static bool shellInputHere(void *ctx) {
+ uint64_t now = platformMicros();
+
+ (void)ctx;
+
+ if (tsCurrentId() == TS_MAIN_TASK_ID) {
+ sInputPolledAt = now;
+ return true;
+ }
+
+ return now - sInputPolledAt >= SHELL_INPUT_STALE_US;
+}
+
+
int shellMain(int argc, char *argv[]) {
dvxLog("DVX Shell starting...");
@@ -794,9 +846,12 @@ int shellMain(int argc, char *argv[]) {
}
// Set callbacks AFTER dvxInit (which memsets the context to zero).
- // The idle callback runs when dvxUpdate has no work to do; it is the
- // main mechanism for giving app tasks CPU time in quiet periods.
+ // The idle callback runs when dvxUpdate has no work to do, the busy
+ // callback after a frame that painted: between them app tasks get
+ // CPU time on every frame.
sCtx.idleCallback = shellIdleDispatch;
+ sCtx.busyCallback = shellBusyDispatch;
+ sCtx.inputHere = shellInputHere;
sCtx.idleCtx = &sCtx;
sCtx.onCtrlEsc = ctrlEscHandler;
sCtx.ctrlEscCtx = &sCtx;
@@ -810,6 +865,7 @@ int shellMain(int argc, char *argv[]) {
dvxLog("Selected: %ldx%ld %ldbpp (pitch %ld)", (long)sCtx.display.width, (long)sCtx.display.height, (long)sCtx.display.format.bitsPerPixel, (long)sCtx.display.pitch);
entropyInit();
+ shellRegisterIdle(shellProgCacheIdle, NULL);
// Gather system information (CPU, memory, video, drives, etc.)
// after dvxInit so the display fields have real values to report.
@@ -1148,6 +1204,7 @@ static void shellTeardown(void) {
platformSetAppExitHook(NULL);
sRecoveryArmed = false;
entropyShutdown();
+ shellProgCacheShutdown();
tsShutdown();
dvxShutdown(&sCtx);
prefsClose(sPrefs);
@@ -1178,15 +1235,20 @@ void shellTestBind(AppContextT *ctx) {
sDepthBase = 0;
sDesktopWalkDepth = 0;
sIdleWalkDepth = 0;
+ sIdleWalkAt = 0;
+ sInputPolledAt = 0;
sExitRequested = false;
sCtxRef = ctx ? ctx : &sCtx;
shellAppTestReset();
+ shellProgCacheDrop();
if (ctx) {
shellAppInit();
dvxMemAppIdPtr = &ctx->currentAppId;
tsSetContextHooks(appIdSave, appIdRestore);
ctx->idleCallback = shellIdleDispatch;
+ ctx->busyCallback = shellBusyDispatch;
+ ctx->inputHere = shellInputHere;
ctx->idleCtx = ctx;
ctx->onF1 = f1HelpHandler;
ctx->f1Ctx = ctx;
diff --git a/src/libs/kpunch/dvxshell/shellProg.c b/src/libs/kpunch/dvxshell/shellProg.c
index ed88f456..be1cb919 100644
--- a/src/libs/kpunch/dvxshell/shellProg.c
+++ b/src/libs/kpunch/dvxshell/shellProg.c
@@ -21,16 +21,21 @@
// IN THE SOFTWARE.
// shellProg.c -- Installed programs (see shellProg.h)
+//
+// What is read from program and picture files (names, descriptions,
+// pictures) goes through the program cache (shellProgCache.h), so a file
+// that has not changed is not opened again.
#include "shellProg.h"
#include "shellAssoc.h"
+#include "shellProgCache.h"
#include "dvxPlat.h"
+#include "dvxRes.h"
#include
#include
#include
#include
-#include
#include "dvxMem.h"
#include "stb_ds_wrap.h"
@@ -38,45 +43,98 @@
#define PROG_RES_NAME "name"
#define PROG_RES_DESC "description"
+// shellProgramScan's walk: what it skips and reads, and the list
+typedef struct {
+ const char *skipPath;
+ bool readInfo;
+ ShellProgressFnT progress;
+ void *progressCtx;
+ ShellProgramT **list;
+} ScanWalkT;
+
// ============================================================
// Prototypes
// ============================================================
-static uint8_t *itemIconPick(AppContextT *ctx, const char *iconPath, const char *path, int32_t edge, int32_t *outPitch, char *chosen, int32_t chosenSize);
-static void scanDir(const char *dir, const char *skipPath, bool readInfo, ShellProgressFnT progress, void *progressCtx, ShellProgramT **list);
+static uint8_t *iconFit(const AppContextT *ctx, uint8_t *src, int32_t srcW, int32_t srcH, int32_t srcPitch, int32_t edge, int32_t *outPitch);
+static uint8_t *iconScale(const AppContextT *ctx, const uint8_t *src, int32_t srcW, int32_t srcH, int32_t srcPitch, int32_t edge, int32_t *outPitch);
+static uint8_t *itemIconPick(AppContextT *ctx, const char *iconPath, const char *path, int32_t *outW, int32_t *outH, int32_t *outPitch, char *chosen, int32_t chosenSize);
+static uint8_t *pictureLoad(const AppContextT *ctx, const char *path, int32_t *outW, int32_t *outH, int32_t *outPitch);
+static bool scanVisit(const char *path, const char *name, void *ctx);
bool shellIsDosProgram(const char *path);
bool shellIsProgram(const char *path);
uint8_t *shellItemIcon(AppContextT *ctx, const char *iconPath, const char *path, int32_t edge, int32_t *outPitch);
bool shellItemIconFile(AppContextT *ctx, const char *iconPath, const char *path, char *out, int32_t outSize);
+bool shellItemIcons(AppContextT *ctx, const char *iconPath, const char *path, int32_t edge, int32_t smallEdge, ShellIconsT *out);
uint8_t *shellProgramIcon(AppContextT *ctx, const char *path, int32_t edge, int32_t *outPitch);
void shellProgramInfo(const char *path, ShellProgramT *out);
void shellProgramListFree(ShellProgramT *list);
int32_t shellProgramScan(const char *dir, const char *skipPath, bool readInfo, ShellProgressFnT progress, void *progressCtx, ShellProgramT **outList);
+// The picture src (owned: it is returned or freed) at edge x edge pixels:
+// itself when it is that size already, else a scaled copy (iconScale).
+static uint8_t *iconFit(const AppContextT *ctx, uint8_t *src, int32_t srcW, int32_t srcH, int32_t srcPitch, int32_t edge, int32_t *outPitch) {
+ if (!src || (srcW == edge && srcH == edge)) {
+ *outPitch = srcPitch;
+ return src;
+ }
+
+ uint8_t *dst = iconScale(ctx, src, srcW, srcH, srcPitch, edge, outPitch);
+
+ dvxFreeImage(src);
+ return dst;
+}
+
+
+// A copy of the picture src at edge x edge pixels, nearest pixel (icons
+// are small, and a whole number of source pixels per target pixel is not
+// needed). NULL when out of memory.
+static uint8_t *iconScale(const AppContextT *ctx, const uint8_t *src, int32_t srcW, int32_t srcH, int32_t srcPitch, int32_t edge, int32_t *outPitch) {
+ int32_t bpp = ctx->display.format.bytesPerPixel;
+ int32_t dstPitch = edge * bpp;
+ uint8_t *dst = (uint8_t *)malloc((size_t)dstPitch * (size_t)edge);
+
+ if (dst) {
+ for (int32_t y = 0; y < edge; y++) {
+ const uint8_t *srcRow = src + (y * srcH / edge) * srcPitch;
+
+ for (int32_t x = 0; x < edge; x++) {
+ memcpy(dst + y * dstPitch + x * bpp, srcRow + (x * srcW / edge) * bpp, (size_t)bpp);
+ }
+ }
+ }
+
+ *outPitch = dstPitch;
+ return dst;
+}
+
+
// The item icon rule: the picture file iconPath (NULL or "" for none),
// else path's own picture, else for a DOS program SHELL_DOS_ICON, else
-// for a document the icon of the program its file type opens in. chosen (chosenSize bytes, may be NULL) gets
-// the file the icon came from ("" for none).
-static uint8_t *itemIconPick(AppContextT *ctx, const char *iconPath, const char *path, int32_t edge, int32_t *outPitch, char *chosen, int32_t chosenSize) {
+// for a document the icon of the program its file type opens in, decoded
+// at its own size (dvxFreeImage it; NULL when there is none). chosen
+// (chosenSize bytes, may be NULL) gets the file the icon came from (""
+// for none).
+static uint8_t *itemIconPick(AppContextT *ctx, const char *iconPath, const char *path, int32_t *outW, int32_t *outH, int32_t *outPitch, char *chosen, int32_t chosenSize) {
char opener[DVX_MAX_PATH];
char args[SHELL_ARGS_MAX];
const char *from = (iconPath && iconPath[0]) ? iconPath : NULL;
- uint8_t *icon = from ? shellProgramIcon(ctx, from, edge, outPitch) : NULL;
+ uint8_t *icon = from ? pictureLoad(ctx, from, outW, outH, outPitch) : NULL;
- if (!icon) {
+ if (!icon && path && path[0]) {
from = path;
- icon = shellProgramIcon(ctx, from, edge, outPitch);
+ icon = pictureLoad(ctx, from, outW, outH, outPitch);
}
if (!icon && shellIsDosProgram(path)) {
from = SHELL_DOS_ICON;
- icon = shellProgramIcon(ctx, from, edge, outPitch);
+ icon = pictureLoad(ctx, from, outW, outH, outPitch);
}
if (!icon && path && !shellIsProgram(path) && shellAssocCommand(path, opener, sizeof(opener), args, sizeof(args))) {
from = opener;
- icon = shellProgramIcon(ctx, from, edge, outPitch);
+ icon = pictureLoad(ctx, from, outW, outH, outPitch);
}
if (chosen && chosenSize > 0) {
@@ -87,49 +145,66 @@ static uint8_t *itemIconPick(AppContextT *ctx, const char *iconPath, const char
}
-// The names are read in full before anything is opened: DOS has few file
-// handles, and each program's resources are opened while listing.
-static void scanDir(const char *dir, const char *skipPath, bool readInfo, ShellProgressFnT progress, void *progressCtx, ShellProgramT **list) {
- char **names = dvxReadDir(dir);
+// path's picture as dvxLoadImage decodes it (NULL: none), its bytes from
+// the program cache: a file known to have none, or whose bytes are kept,
+// is not opened. A picture too big to keep is loaded from its file each
+// time. A file is noted as having none only when its bytes are no image
+// (a decode that ran out of memory is not).
+static uint8_t *pictureLoad(const AppContextT *ctx, const char *path, int32_t *outW, int32_t *outH, int32_t *outPitch) {
+ uint8_t *bytes = NULL;
+ uint32_t len = 0;
+ int32_t w = 0;
+ int32_t h = 0;
+ uint8_t *pixels;
+ bool none;
- if (!names) {
- return;
+ if (!shellProgCachePicture(path, &bytes, &len)) {
+ pixels = dvxLoadImage(ctx, path, outW, outH, outPitch);
+ none = !pixels && !dvxImageInfo(path, &w, &h);
+ } else {
+ pixels = bytes ? dvxLoadImageFromMemory(ctx, bytes, (int32_t)len, outW, outH, outPitch) : NULL;
+ none = bytes && !pixels && !dvxImageInfoFromMemory(bytes, (int32_t)len, &w, &h);
}
- for (int32_t i = 0; i < arrlen(names); i++) {
- char path[DVX_MAX_PATH];
- struct stat st;
-
- if (!platformPathJoin(path, sizeof(path), dir, names[i])) {
- continue;
- }
-
- if (stat(path, &st) == 0 && S_ISDIR(st.st_mode)) {
- scanDir(path, skipPath, readInfo, progress, progressCtx, list);
- continue;
- }
-
- if (!shellIsProgram(path) || (skipPath && platformPathSame(path, skipPath))) {
- continue;
- }
-
- ShellProgramT prog;
-
- if (readInfo) {
- shellProgramInfo(path, &prog);
- } else {
- memset(&prog, 0, sizeof(prog));
- snprintf(prog.path, sizeof(prog.path), "%s", path);
- }
-
- arrput(*list, prog);
-
- if (progress) {
- progress(progressCtx);
- }
+ if (none) {
+ shellProgCacheNoPicture(path);
}
- dvxReadDirFree(names);
+ free(bytes);
+ return pixels;
+}
+
+
+// A file of shellProgramScan's walk (platformWalkDir: the names are read
+// in full before anything is opened): a program, unless it is skipPath.
+// Reading names checks each program against the program cache while its
+// folder is fresh, so the cache answers for the files that have not
+// changed.
+static bool scanVisit(const char *path, const char *name, void *ctx) {
+ ScanWalkT *walk = (ScanWalkT *)ctx;
+ ShellProgramT prog;
+
+ (void)name;
+
+ if (!shellIsProgram(path) || (walk->skipPath && platformPathSame(path, walk->skipPath))) {
+ return true;
+ }
+
+ if (walk->readInfo) {
+ shellProgCacheCheck(path);
+ shellProgramInfo(path, &prog);
+ } else {
+ memset(&prog, 0, sizeof(prog));
+ snprintf(prog.path, sizeof(prog.path), "%s", path);
+ }
+
+ arrput(*walk->list, prog);
+
+ if (walk->progress) {
+ walk->progress(walk->progressCtx);
+ }
+
+ return true;
}
@@ -153,56 +228,64 @@ bool shellIsProgram(const char *path) {
uint8_t *shellItemIcon(AppContextT *ctx, const char *iconPath, const char *path, int32_t edge, int32_t *outPitch) {
- return itemIconPick(ctx, iconPath, path, edge, outPitch, NULL, 0);
+ int32_t w = 0;
+ int32_t h = 0;
+ int32_t pitch = 0;
+
+ if (!ctx || edge <= 0 || !outPitch) {
+ return NULL;
+ }
+
+ return iconFit(ctx, itemIconPick(ctx, iconPath, path, &w, &h, &pitch, NULL, 0), w, h, pitch, edge, outPitch);
}
bool shellItemIconFile(AppContextT *ctx, const char *iconPath, const char *path, char *out, int32_t outSize) {
+ int32_t w = 0;
+ int32_t h = 0;
int32_t pitch = 0;
- uint8_t *icon = itemIconPick(ctx, iconPath, path, SHELL_PROG_ICON_SMALL, &pitch, out, outSize);
+ uint8_t *icon = ctx ? itemIconPick(ctx, iconPath, path, &w, &h, &pitch, out, outSize) : NULL;
dvxFreeImage(icon);
return icon != NULL;
}
-// Nearest-pixel scaling: a whole number of source pixels per target pixel
-// is not needed, and icons are small.
+// One decode for both sizes.
+bool shellItemIcons(AppContextT *ctx, const char *iconPath, const char *path, int32_t edge, int32_t smallEdge, ShellIconsT *out) {
+ int32_t w = 0;
+ int32_t h = 0;
+ int32_t pitch = 0;
+
+ memset(out, 0, sizeof(*out));
+
+ if (!ctx || edge <= 0 || smallEdge <= 0) {
+ return false;
+ }
+
+ uint8_t *src = itemIconPick(ctx, iconPath, path, &w, &h, &pitch, NULL, 0);
+
+ if (!src) {
+ return false;
+ }
+
+ out->iconSmall = iconScale(ctx, src, w, h, pitch, smallEdge, &out->iconSmallPitch);
+ out->icon = iconFit(ctx, src, w, h, pitch, edge, &out->iconPitch);
+ return out->icon != NULL && out->iconSmall != NULL;
+}
+
+
+// A picture file, or a program's own icon.
uint8_t *shellProgramIcon(AppContextT *ctx, const char *path, int32_t edge, int32_t *outPitch) {
- int32_t srcW = 0;
- int32_t srcH = 0;
- int32_t srcPitch = 0;
- uint8_t *src = NULL;
+ int32_t w = 0;
+ int32_t h = 0;
+ int32_t pitch = 0;
if (!ctx || !path || !path[0] || edge <= 0 || !outPitch) {
return NULL;
}
- // A picture file, or a program's own icon
- src = dvxLoadImage(ctx, path, &srcW, &srcH, &srcPitch);
-
- if (!src || (srcW == edge && srcH == edge)) {
- *outPitch = srcPitch;
- return src;
- }
-
- int32_t bpp = ctx->display.format.bytesPerPixel;
- int32_t dstPitch = edge * bpp;
- uint8_t *dst = (uint8_t *)malloc((size_t)dstPitch * (size_t)edge);
-
- if (dst) {
- for (int32_t y = 0; y < edge; y++) {
- const uint8_t *srcRow = src + (y * srcH / edge) * srcPitch;
-
- for (int32_t x = 0; x < edge; x++) {
- memcpy(dst + y * dstPitch + x * bpp, srcRow + (x * srcW / edge) * bpp, (size_t)bpp);
- }
- }
- }
-
- dvxFreeImage(src);
- *outPitch = dstPitch;
- return dst;
+ return iconFit(ctx, pictureLoad(ctx, path, &w, &h, &pitch), w, h, pitch, edge, outPitch);
}
@@ -218,10 +301,25 @@ void shellProgramInfo(const char *path, ShellProgramT *out) {
out->name[nameLen] = '\0';
out->name[0] = (char)toupper((unsigned char)out->name[0]);
- if (shellIsProgram(path)) {
- dvxResLoadText(path, PROG_RES_NAME, out->name, sizeof(out->name));
- dvxResLoadText(path, PROG_RES_DESC, out->description, sizeof(out->description));
+ if (!shellIsProgram(path) || shellProgCacheInfo(path, out)) {
+ return;
}
+
+ // One resource directory read for both texts
+ DvxResHandleT *res = dvxResOpen(path);
+
+ if (res) {
+ char name[sizeof(out->name)];
+
+ if (dvxResReadText(res, PROG_RES_NAME, name, sizeof(name))) {
+ snprintf(out->name, sizeof(out->name), "%s", name);
+ }
+
+ dvxResReadText(res, PROG_RES_DESC, out->description, sizeof(out->description));
+ dvxResClose(res);
+ }
+
+ shellProgCacheSetInfo(path, out);
}
@@ -234,7 +332,14 @@ int32_t shellProgramScan(const char *dir, const char *skipPath, bool readInfo, S
ShellProgramT *list = NULL;
if (outList) {
- scanDir(dir ? dir : SHELL_PROG_DIR, skipPath, readInfo, progress, progressCtx, &list);
+ ScanWalkT walk;
+
+ walk.skipPath = skipPath;
+ walk.readInfo = readInfo;
+ walk.progress = progress;
+ walk.progressCtx = progressCtx;
+ walk.list = &list;
+ platformWalkDir(dir ? dir : SHELL_PROG_DIR, scanVisit, &walk);
*outList = list;
}
diff --git a/src/libs/kpunch/dvxshell/shellProg.h b/src/libs/kpunch/dvxshell/shellProg.h
index 6ac284d6..d537d96c 100644
--- a/src/libs/kpunch/dvxshell/shellProg.h
+++ b/src/libs/kpunch/dvxshell/shellProg.h
@@ -26,7 +26,9 @@
// description come from its "name" and "description" resources (the file
// name when it has none) and its picture from its "icon32" resource.
// Program Manager's Installed Programs group, File Manager and setup
-// programs all read programs through these calls.
+// programs all read programs through these calls. What they read is kept
+// in the program cache (PROGRAMS.CAC in the user folder: shellProgCache.h),
+// so a program that has not changed is not opened again.
#ifndef SHELL_PROG_H
#define SHELL_PROG_H
@@ -62,6 +64,15 @@ typedef struct {
char description[SHELL_PROG_DESC_MAX];
} ShellProgramT;
+// An item's icon at two sizes (shellItemIcons), each freed with
+// dvxFreeImage.
+typedef struct {
+ uint8_t *icon;
+ uint8_t *iconSmall;
+ int32_t iconPitch;
+ int32_t iconSmallPitch;
+} ShellIconsT;
+
// Called once per program found by shellProgramScan (e.g. to keep the
// screen responsive during a long scan).
typedef void (*ShellProgressFnT)(void *ctx);
@@ -88,6 +99,11 @@ uint8_t *shellItemIcon(AppContextT *ctx, const char *iconPath, const char *path,
// BASIC's LOADPICTURE) loads from that file.
bool shellItemIconFile(AppContextT *ctx, const char *iconPath, const char *path, char *out, int32_t outSize);
+// shellItemIcon at edge and at smallEdge from one decode of the picture
+// (Program Manager's large and small views). False when there is none
+// (out empty) or memory ran out (what was made is in out).
+bool shellItemIcons(AppContextT *ctx, const char *iconPath, const char *path, int32_t edge, int32_t smallEdge, ShellIconsT *out);
+
// A picture edge x edge pixels in display format: a program's icon32
// resource, or any image file DVX can load (BMP, PNG, JPEG, GIF), scaled
// to fit. NULL when there is none. Free with dvxFreeImage.
diff --git a/src/libs/kpunch/dvxshell/shellProgCache.c b/src/libs/kpunch/dvxshell/shellProgCache.c
new file mode 100644
index 00000000..3ce9fef7
--- /dev/null
+++ b/src/libs/kpunch/dvxshell/shellProgCache.c
@@ -0,0 +1,674 @@
+// 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.
+
+// shellProgCache.c -- the program cache (see shellProgCache.h)
+//
+// The file: CACHE_MAGIC, a 16-bit version, 16 bits kept 0, the entry
+// count (32 bits), then each entry: its path (a 16-bit length and the
+// bytes), its file's stamp (size and time, 32 bits each), its flags (8
+// bits), with CACHE_FLAG_INFO the name and description (an 8-bit length
+// and the bytes each), and with CACHE_FLAG_PICTURE the picture (a 32-bit
+// length and the bytes; 0: none). Numbers are little-endian. A file
+// that does not read as this is ignored, and replaced at the next save.
+
+#include "shellProgCache.h"
+#include "dvxApp.h"
+#include "dvxEndian.h"
+#include "dvxMem.h"
+#include "dvxPlat.h"
+
+#include
+#include
+#include
+#include "stb_ds_wrap.h"
+
+#define CACHE_MAGIC "DVXPCACH"
+#define CACHE_MAGIC_LEN 8
+#define CACHE_VERSION 1
+#define CACHE_VERSION_AT CACHE_MAGIC_LEN
+#define CACHE_COUNT_AT (CACHE_MAGIC_LEN + 4)
+#define CACHE_HEAD_LEN (CACHE_MAGIC_LEN + 8)
+#define CACHE_FLAG_INFO 0x01 // the name and description
+#define CACHE_FLAG_PICTURE 0x02 // the picture is known (bytes, or none)
+#define CACHE_MAX_BYTES 262144 // a saved cache at most: entries not asked about this run go first
+#define LEN8_BYTES 1
+#define LEN16_BYTES 2
+#define LEN32_BYTES 4
+#define FLAG_BYTES 1
+#define STAMP_BYTES (2 * LEN32_BYTES)
+#define US_PER_MS 1000
+
+typedef struct {
+ char *path;
+ PlatformStampT stamp;
+ uint32_t checkedCount; // platformInstalledChangeCount when checked
+ uint8_t *picture; // the image file's bytes (pictureLen), NULL for none
+ uint32_t pictureLen;
+ uint8_t flags; // CACHE_FLAG_*
+ bool checked; // against its file, this run
+ bool used; // asked about this run
+ char name[SHELL_APP_NAME_MAX];
+ char description[SHELL_PROG_DESC_MAX];
+} CacheEntryT;
+
+// A save under way: data (len bytes) going to tmpPath, pos of them written
+typedef struct {
+ FILE *f;
+ uint8_t *data;
+ uint32_t len;
+ uint32_t pos;
+ char tmpPath[DVX_MAX_PATH];
+} CacheSaveT;
+
+// What parsing a saved cache has left
+typedef struct {
+ const uint8_t *p;
+ uint32_t left;
+} ReadT;
+
+// ============================================================
+// Prototypes
+// ============================================================
+
+static bool cacheParse(const uint8_t *data, uint32_t len);
+static void cacheRead(void);
+static void changed(void);
+static void entriesFree(void);
+static uint32_t entryBytes(const CacheEntryT *e);
+static CacheEntryT *entryChecked(const char *path, bool now);
+static void entryClear(CacheEntryT *e);
+static int32_t entryIndex(const char *path);
+static void entryRemove(int32_t i);
+static uint8_t *formatCache(uint32_t *outLen);
+static void formatKeep(bool used, int32_t **keep, uint32_t *total);
+static bool parseEntry(ReadT *r, CacheEntryT *e);
+static bool pictureRead(CacheEntryT *e);
+static uint8_t *putEntry(uint8_t *w, const CacheEntryT *e);
+static uint8_t *putText(uint8_t *w, const char *text);
+static void saveBegin(void);
+static void saveEnd(bool written);
+static void saveStep(void);
+void shellProgCacheCheck(const char *path);
+void shellProgCacheDrop(void);
+void shellProgCacheIdle(void *ctx);
+bool shellProgCacheInfo(const char *path, ShellProgramT *out);
+void shellProgCacheNoPicture(const char *path);
+bool shellProgCachePicture(const char *path, uint8_t **outData, uint32_t *outLen);
+void shellProgCacheSetInfo(const char *path, const ShellProgramT *info);
+void shellProgCacheShutdown(void);
+static const uint8_t *take(ReadT *r, uint32_t n);
+static bool takeText(ReadT *r, char *out, int32_t outSize);
+
+// ============================================================
+// Module state
+// ============================================================
+
+static CacheEntryT *sEntries = NULL; // stb_ds
+static bool sLoaded = false; // the saved cache has been read
+static bool sDirty = false; // changed since the last save began
+static uint64_t sChangedUs = 0; // platformMicros of the last change
+static uint64_t sUsedUs = 0; // platformMicros of the last question
+static CacheSaveT sSave;
+
+
+static bool cacheParse(const uint8_t *data, uint32_t len) {
+ ReadT r = { data, len };
+ const uint8_t *head = take(&r, CACHE_HEAD_LEN);
+
+ if (!head || memcmp(head, CACHE_MAGIC, CACHE_MAGIC_LEN) != 0 || dvxGetLe16(head + CACHE_VERSION_AT) != CACHE_VERSION) {
+ return false;
+ }
+
+ uint32_t count = dvxGetLe32(head + CACHE_COUNT_AT);
+
+ for (uint32_t i = 0; i < count; i++) {
+ CacheEntryT e;
+
+ if (!parseEntry(&r, &e)) {
+ return false;
+ }
+
+ arrput(sEntries, e);
+ }
+
+ return r.left == 0;
+}
+
+
+// The saved cache, once, on first use.
+static void cacheRead(void) {
+ char path[DVX_MAX_PATH];
+ int32_t len = 0;
+ uint8_t *data = platformUserPath(path, sizeof(path), SHELL_PROG_CACHE_NAME) ? (uint8_t *)platformReadFile(path, &len) : NULL;
+
+ sLoaded = true;
+
+ if (data && !cacheParse(data, (uint32_t)len)) {
+ entriesFree();
+ dvxLog("Shell: %s is no program cache; it is made again", path);
+ }
+
+ free(data);
+}
+
+
+static void changed(void) {
+ sDirty = true;
+ sChangedUs = platformMicros();
+}
+
+
+static void entriesFree(void) {
+ for (int32_t i = 0; i < (int32_t)arrlen(sEntries); i++) {
+ free(sEntries[i].path);
+ free(sEntries[i].picture);
+ }
+
+ arrfree(sEntries);
+ sEntries = NULL;
+}
+
+
+// What e takes in the saved cache.
+static uint32_t entryBytes(const CacheEntryT *e) {
+ uint32_t n = LEN16_BYTES + (uint32_t)strlen(e->path) + STAMP_BYTES + FLAG_BYTES;
+
+ if (e->flags & CACHE_FLAG_INFO) {
+ n += 2 * LEN8_BYTES + (uint32_t)strlen(e->name) + (uint32_t)strlen(e->description);
+ }
+
+ if (e->flags & CACHE_FLAG_PICTURE) {
+ n += LEN32_BYTES + e->pictureLen;
+ }
+
+ return n;
+}
+
+
+// path's entry, made when it has none, checked against its file when it
+// has not been since what is installed last changed, or always when now.
+// A file that changed loses what was kept for it; NULL (and no entry)
+// when path is no file.
+static CacheEntryT *entryChecked(const char *path, bool now) {
+ PlatformStampT stamp;
+ uint32_t count = platformInstalledChangeCount();
+
+ if (!path || !path[0]) {
+ return NULL;
+ }
+
+ if (!sLoaded) {
+ cacheRead();
+ }
+
+ sUsedUs = platformMicros();
+
+ int32_t i = entryIndex(path);
+
+ if (i >= 0 && !now && sEntries[i].checked && sEntries[i].checkedCount == count) {
+ sEntries[i].used = true;
+ return &sEntries[i];
+ }
+
+ if (!platformFileStamp(path, &stamp)) {
+ if (i >= 0) {
+ entryRemove(i);
+ changed();
+ }
+
+ return NULL;
+ }
+
+ if (i < 0) {
+ CacheEntryT e;
+
+ memset(&e, 0, sizeof(e));
+ e.path = strdup(path);
+ e.stamp = stamp;
+
+ if (!e.path) {
+ return NULL;
+ }
+
+ arrput(sEntries, e);
+ i = (int32_t)arrlen(sEntries) - 1;
+ } else if (sEntries[i].stamp.size != stamp.size || sEntries[i].stamp.time != stamp.time) {
+ entryClear(&sEntries[i]);
+ sEntries[i].stamp = stamp;
+ changed();
+ }
+
+ sEntries[i].checked = true;
+ sEntries[i].checkedCount = count;
+ sEntries[i].used = true;
+ return &sEntries[i];
+}
+
+
+// Forget what was kept for e's file.
+static void entryClear(CacheEntryT *e) {
+ free(e->picture);
+ e->picture = NULL;
+ e->pictureLen = 0;
+ e->flags = 0;
+ e->name[0] = '\0';
+ e->description[0] = '\0';
+}
+
+
+static int32_t entryIndex(const char *path) {
+ for (int32_t i = 0; i < (int32_t)arrlen(sEntries); i++) {
+ if (platformPathSame(sEntries[i].path, path)) {
+ return i;
+ }
+ }
+
+ return -1;
+}
+
+
+static void entryRemove(int32_t i) {
+ free(sEntries[i].path);
+ free(sEntries[i].picture);
+ arrdel(sEntries, i);
+}
+
+
+// The cache as saved: the entries asked about this run, then the others
+// while the whole stays within CACHE_MAX_BYTES (malloc'd; NULL when out of
+// memory).
+static uint8_t *formatCache(uint32_t *outLen) {
+ int32_t *keep = NULL;
+ uint32_t total = CACHE_HEAD_LEN;
+
+ formatKeep(true, &keep, &total);
+ formatKeep(false, &keep, &total);
+
+ uint8_t *data = (uint8_t *)malloc(total);
+
+ if (data) {
+ uint8_t *w = data + CACHE_HEAD_LEN;
+
+ memcpy(data, CACHE_MAGIC, CACHE_MAGIC_LEN);
+ dvxPutLe16(data + CACHE_VERSION_AT, CACHE_VERSION);
+ dvxPutLe16(data + CACHE_VERSION_AT + LEN16_BYTES, 0);
+ dvxPutLe32(data + CACHE_COUNT_AT, (uint32_t)arrlen(keep));
+
+ for (int32_t k = 0; k < (int32_t)arrlen(keep); k++) {
+ w = putEntry(w, &sEntries[keep[k]]);
+ }
+ }
+
+ arrfree(keep);
+ *outLen = total;
+ return data;
+}
+
+
+// The entries (asked about this run, or not) that keep something, onto
+// keep; the ones not asked about only while the total stays within
+// CACHE_MAX_BYTES.
+static void formatKeep(bool used, int32_t **keep, uint32_t *total) {
+ for (int32_t i = 0; i < (int32_t)arrlen(sEntries); i++) {
+ const CacheEntryT *e = &sEntries[i];
+ uint32_t n = entryBytes(e);
+
+ if (e->flags && e->used == used && (used || *total + n <= CACHE_MAX_BYTES)) {
+ *total += n;
+ arrput(*keep, i);
+ }
+ }
+}
+
+
+static bool parseEntry(ReadT *r, CacheEntryT *e) {
+ memset(e, 0, sizeof(*e));
+
+ const uint8_t *lenAt = take(r, LEN16_BYTES);
+ uint32_t pathLen = lenAt ? dvxGetLe16(lenAt) : 0;
+ const uint8_t *path = (pathLen > 0 && pathLen < DVX_MAX_PATH) ? take(r, pathLen) : NULL;
+ const uint8_t *fixed = path ? take(r, STAMP_BYTES + FLAG_BYTES) : NULL;
+
+ if (!fixed) {
+ return false;
+ }
+
+ e->stamp.size = dvxGetLe32(fixed);
+ e->stamp.time = dvxGetLe32(fixed + LEN32_BYTES);
+ e->flags = fixed[STAMP_BYTES];
+
+ if ((e->flags & CACHE_FLAG_INFO) && !(takeText(r, e->name, (int32_t)sizeof(e->name)) && takeText(r, e->description, (int32_t)sizeof(e->description)))) {
+ return false;
+ }
+
+ const uint8_t *picLenAt = (e->flags & CACHE_FLAG_PICTURE) ? take(r, LEN32_BYTES) : NULL;
+ uint32_t picLen = picLenAt ? dvxGetLe32(picLenAt) : 0;
+ const uint8_t *picture = picLen > 0 ? take(r, picLen) : NULL;
+
+ if (((e->flags & CACHE_FLAG_PICTURE) && !picLenAt) || (picLen > 0 && !picture)) {
+ return false;
+ }
+
+ e->path = (char *)malloc(pathLen + 1);
+ e->picture = picture ? (uint8_t *)malloc(picLen) : NULL;
+
+ if (!e->path || (picture && !e->picture)) {
+ free(e->path);
+ free(e->picture);
+ return false;
+ }
+
+ memcpy(e->path, path, pathLen);
+ e->path[pathLen] = '\0';
+
+ if (picture) {
+ memcpy(e->picture, picture, picLen);
+ e->pictureLen = picLen;
+ }
+
+ return true;
+}
+
+
+// e's picture bytes from its file: a DVX file's icon resource, else the
+// file itself when it is small enough (bytes that are no image are found
+// out by the decode: shellProgCacheNoPicture). False, and nothing kept,
+// for a bigger file.
+static bool pictureRead(CacheEntryT *e) {
+ uint32_t len = 0;
+ uint8_t *bytes = (uint8_t *)dvxResLoadIconData(e->path, &len);
+
+ if (!bytes) {
+ int32_t fileLen = 0;
+
+ if (e->stamp.size > SHELL_PROG_CACHE_PICTURE_MAX) {
+ return false;
+ }
+
+ bytes = (uint8_t *)platformReadFile(e->path, &fileLen);
+ len = bytes ? (uint32_t)fileLen : 0;
+ }
+
+ e->picture = bytes;
+ e->pictureLen = len;
+ e->flags |= CACHE_FLAG_PICTURE;
+ changed();
+ return true;
+}
+
+
+static uint8_t *putEntry(uint8_t *w, const CacheEntryT *e) {
+ uint32_t pathLen = (uint32_t)strlen(e->path);
+
+ dvxPutLe16(w, pathLen);
+ memcpy(w + LEN16_BYTES, e->path, pathLen);
+ w += LEN16_BYTES + pathLen;
+ dvxPutLe32(w, e->stamp.size);
+ dvxPutLe32(w + LEN32_BYTES, e->stamp.time);
+ w[STAMP_BYTES] = e->flags;
+ w += STAMP_BYTES + FLAG_BYTES;
+
+ if (e->flags & CACHE_FLAG_INFO) {
+ w = putText(w, e->name);
+ w = putText(w, e->description);
+ }
+
+ if (e->flags & CACHE_FLAG_PICTURE) {
+ dvxPutLe32(w, e->pictureLen);
+ memcpy(w + LEN32_BYTES, e->picture, e->pictureLen);
+ w += LEN32_BYTES + e->pictureLen;
+ }
+
+ return w;
+}
+
+
+// A name or description: its length (it fits a byte: SHELL_APP_NAME_MAX,
+// SHELL_PROG_DESC_MAX) and its bytes.
+static uint8_t *putText(uint8_t *w, const char *text) {
+ uint32_t len = (uint32_t)strlen(text);
+
+ w[0] = (uint8_t)len;
+ memcpy(w + LEN8_BYTES, text, len);
+ return w + LEN8_BYTES + len;
+}
+
+
+// The cache formatted and the scratch file opened; the idle time writes
+// it (saveStep).
+static void saveBegin(void) {
+ char path[DVX_MAX_PATH];
+
+ sDirty = false;
+
+ if (platformIsReadOnly() || !platformUserPath(path, sizeof(path), SHELL_PROG_CACHE_NAME) || !platformPathSibling(sSave.tmpPath, sizeof(sSave.tmpPath), path, PLATFORM_TMP_EXT)) {
+ return;
+ }
+
+ sSave.data = formatCache(&sSave.len);
+ sSave.pos = 0;
+ sSave.f = sSave.data ? fopen(sSave.tmpPath, "wb") : NULL;
+
+ if (!sSave.f) {
+ saveEnd(false);
+ }
+}
+
+
+// The scratch file closed and, when all of it was written, put in place
+// of the saved cache (platformReplaceFile); else it goes.
+static void saveEnd(bool written) {
+ char path[DVX_MAX_PATH];
+ char bakPath[DVX_MAX_PATH];
+ bool ok = written;
+
+ if (sSave.f && fclose(sSave.f) != 0) {
+ ok = false;
+ }
+
+ sSave.f = NULL;
+ ok = ok && platformUserPath(path, sizeof(path), SHELL_PROG_CACHE_NAME) && platformPathSibling(bakPath, sizeof(bakPath), path, PLATFORM_BAK_EXT) && platformReplaceFile(sSave.tmpPath, path, bakPath);
+
+ if (!ok) {
+ remove(sSave.tmpPath);
+ dvxLog("Shell: the program cache could not be saved");
+ }
+
+ free(sSave.data);
+ sSave.data = NULL;
+}
+
+
+static void saveStep(void) {
+ uint32_t n = sSave.len - sSave.pos;
+
+ if (n > SHELL_PROG_CACHE_SAVE_CHUNK) {
+ n = SHELL_PROG_CACHE_SAVE_CHUNK;
+ }
+
+ if (fwrite(sSave.data + sSave.pos, 1, n, sSave.f) != n) {
+ saveEnd(false);
+ return;
+ }
+
+ sSave.pos += n;
+
+ if (sSave.pos == sSave.len) {
+ saveEnd(true);
+ }
+}
+
+
+void shellProgCacheCheck(const char *path) {
+ int32_t prev = dvxMemSetApp(DVX_MEM_SYSTEM);
+
+ entryChecked(path, true);
+ dvxMemSetApp(prev);
+}
+
+
+void shellProgCacheDrop(void) {
+ if (sSave.f) {
+ fclose(sSave.f);
+ sSave.f = NULL;
+ remove(sSave.tmpPath);
+ }
+
+ free(sSave.data);
+ sSave.data = NULL;
+ entriesFree();
+ sLoaded = false;
+ sDirty = false;
+}
+
+
+void shellProgCacheIdle(void *ctx) {
+ uint64_t now = platformMicros();
+ int32_t prev = dvxMemSetApp(DVX_MEM_SYSTEM);
+
+ (void)ctx;
+
+ if (sSave.f) {
+ saveStep();
+ } else if (sDirty && now - sChangedUs >= (uint64_t)SHELL_PROG_CACHE_SAVE_MS * US_PER_MS) {
+ saveBegin();
+ } else if (!sDirty && sLoaded && now - sUsedUs >= (uint64_t)SHELL_PROG_CACHE_DROP_MS * US_PER_MS) {
+ shellProgCacheDrop();
+ }
+
+ dvxMemSetApp(prev);
+}
+
+
+bool shellProgCacheInfo(const char *path, ShellProgramT *out) {
+ int32_t prev = dvxMemSetApp(DVX_MEM_SYSTEM);
+ CacheEntryT *e = entryChecked(path, false);
+ bool hit = e && (e->flags & CACHE_FLAG_INFO);
+
+ if (hit) {
+ snprintf(out->name, sizeof(out->name), "%s", e->name);
+ snprintf(out->description, sizeof(out->description), "%s", e->description);
+ }
+
+ dvxMemSetApp(prev);
+ return hit;
+}
+
+
+// Only a change is saved: none known already is left alone.
+void shellProgCacheNoPicture(const char *path) {
+ int32_t prev = dvxMemSetApp(DVX_MEM_SYSTEM);
+ CacheEntryT *e = entryChecked(path, false);
+
+ if (e && (e->picture || !(e->flags & CACHE_FLAG_PICTURE))) {
+ free(e->picture);
+ e->picture = NULL;
+ e->pictureLen = 0;
+ e->flags |= CACHE_FLAG_PICTURE;
+ changed();
+ }
+
+ dvxMemSetApp(prev);
+}
+
+
+// A copy, so the caller's decode never holds the cache's own bytes (it may
+// let other tasks run, which may ask the cache too).
+bool shellProgCachePicture(const char *path, uint8_t **outData, uint32_t *outLen) {
+ int32_t prev = dvxMemSetApp(DVX_MEM_SYSTEM);
+ CacheEntryT *e = entryChecked(path, false);
+ bool known = e && ((e->flags & CACHE_FLAG_PICTURE) || pictureRead(e));
+ uint8_t *copy = (known && e->picture) ? (uint8_t *)malloc(e->pictureLen) : NULL;
+
+ if (copy) {
+ memcpy(copy, e->picture, e->pictureLen);
+ }
+
+ *outData = copy;
+ *outLen = copy ? e->pictureLen : 0;
+ dvxMemSetApp(prev);
+ return known && (copy || !e->picture);
+}
+
+
+void shellProgCacheSetInfo(const char *path, const ShellProgramT *info) {
+ int32_t prev = dvxMemSetApp(DVX_MEM_SYSTEM);
+ CacheEntryT *e = entryChecked(path, false);
+
+ if (e) {
+ snprintf(e->name, sizeof(e->name), "%s", info->name);
+ snprintf(e->description, sizeof(e->description), "%s", info->description);
+ e->flags |= CACHE_FLAG_INFO;
+ changed();
+ }
+
+ dvxMemSetApp(prev);
+}
+
+
+// A save under way is finished first; a change since it began is saved
+// after it.
+void shellProgCacheShutdown(void) {
+ int32_t prev = dvxMemSetApp(DVX_MEM_SYSTEM);
+
+ while (sSave.f || sDirty) {
+ if (sSave.f) {
+ saveStep();
+ } else {
+ saveBegin();
+ }
+ }
+
+ shellProgCacheDrop();
+ dvxMemSetApp(prev);
+}
+
+
+// The next n bytes of r (NULL, and nothing taken, when fewer are left).
+static const uint8_t *take(ReadT *r, uint32_t n) {
+ const uint8_t *at = r->p;
+
+ if (n > r->left) {
+ return NULL;
+ }
+
+ r->p += n;
+ r->left -= n;
+ return at;
+}
+
+
+// A text putText wrote, into out (outSize bytes); false when it is not
+// there whole or does not fit.
+static bool takeText(ReadT *r, char *out, int32_t outSize) {
+ const uint8_t *lenAt = take(r, LEN8_BYTES);
+ uint32_t len = lenAt ? lenAt[0] : 0;
+ const uint8_t *text = (lenAt && (int32_t)len < outSize) ? take(r, len) : NULL;
+
+ if (!text) {
+ return false;
+ }
+
+ memcpy(out, text, len);
+ out[len] = '\0';
+ return true;
+}
diff --git a/src/libs/kpunch/dvxshell/shellProgCache.h b/src/libs/kpunch/dvxshell/shellProgCache.h
new file mode 100644
index 00000000..b8c86787
--- /dev/null
+++ b/src/libs/kpunch/dvxshell/shellProgCache.h
@@ -0,0 +1,92 @@
+// 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.
+
+// shellProgCache.h -- the program cache (dvxshell's own: see shellProg.c)
+//
+// Reading a program's name, description, and icon takes several file
+// opens, tens of milliseconds each on a slow disk, every time DVX starts.
+// The program cache keeps what was read, with each file's stamp
+// (platformFileStamp), in SHELL_PROG_CACHE_NAME in the user folder, and
+// answers from it while the file is unchanged. shellProgramInfo, the item
+// icon calls, and shellProgramIcon go through it, so Program Manager, File
+// Manager, and BASIC programs gain it without a change.
+//
+// A file is checked against its stamp (one directory search) the first
+// time it is asked about after what is installed last changed
+// (platformInstalledChangeCount), and whenever the program scan passes it
+// (shellProgCacheCheck, while its folder is fresh in DOS's buffers); a
+// file that changed is read again, and one that is gone loses its entry.
+// A picture is kept as the image file's bytes (a DVX file's icon resource,
+// or a picture file up to SHELL_PROG_CACHE_PICTURE_MAX), decoded for the
+// display mode each time.
+//
+// The file is read on first use. A change is saved SHELL_PROG_CACHE_SAVE_MS
+// after the last, SHELL_PROG_CACHE_SAVE_CHUNK bytes at a time in the shell's
+// idle time (shellProgCacheIdle), so no frame waits for the whole write;
+// shellProgCacheShutdown saves what is left. The memory goes once the
+// cache has gone SHELL_PROG_CACHE_DROP_MS unused. It is DVX's memory,
+// whichever program's call made it. Nothing is saved when DVX cannot save
+// (platformIsReadOnly).
+
+#ifndef SHELL_PROG_CACHE_H
+#define SHELL_PROG_CACHE_H
+
+#include "shellProg.h"
+
+#include
+#include
+
+#define SHELL_PROG_CACHE_NAME "PROGRAMS.CAC"
+#define SHELL_PROG_CACHE_PICTURE_MAX 32768 // a picture file's bytes kept at most
+#define SHELL_PROG_CACHE_SAVE_MS 3000 // a change is saved this long after the last
+#define SHELL_PROG_CACHE_SAVE_CHUNK 4096 // bytes written per idle call
+#define SHELL_PROG_CACHE_DROP_MS 60000 // the memory goes after this long unused
+
+// Check path against its file now (the program scan).
+void shellProgCacheCheck(const char *path);
+
+// Free the cache without saving (a save under way is abandoned).
+void shellProgCacheDrop(void);
+
+// The shell's idle time: a save under way goes on, a change old enough
+// starts one, and a cache unused long enough is freed.
+void shellProgCacheIdle(void *ctx);
+
+// path's name and description from the cache; false when it has none.
+bool shellProgCacheInfo(const char *path, ShellProgramT *out);
+
+// path has no picture (the bytes kept for it were no image).
+void shellProgCacheNoPicture(const char *path);
+
+// path's picture bytes in *outData (malloc'd, the caller frees; NULL when
+// path has none), read and kept when the cache did not have them. False
+// when the cache cannot say (no such file, or a picture file too big to
+// keep): load the file itself.
+bool shellProgCachePicture(const char *path, uint8_t **outData, uint32_t *outLen);
+
+// Keep path's name and description.
+void shellProgCacheSetInfo(const char *path, const ShellProgramT *info);
+
+// Save what is left and free the cache (the shell's end).
+void shellProgCacheShutdown(void);
+
+#endif // SHELL_PROG_CACHE_H
diff --git a/src/libs/kpunch/dvxshell/shellRun.c b/src/libs/kpunch/dvxshell/shellRun.c
index 2afca658..af201e69 100644
--- a/src/libs/kpunch/dvxshell/shellRun.c
+++ b/src/libs/kpunch/dvxshell/shellRun.c
@@ -62,21 +62,31 @@ typedef struct {
int32_t framesLeft;
} RunMinimizeT;
-// A DOS program waiting for the main loop (shellRunDos)
+// A DOS program waiting for the main loop (shellRunDos), and the program
+// that waits for it to end (shellRunDosFor; 0 when none)
typedef struct {
- bool pending;
- bool pause;
- char program[DVX_MAX_PATH];
- char args[SHELL_ARGS_MAX];
- char folder[DVX_MAX_PATH];
+ bool pending;
+ bool pause;
+ int32_t waiter;
+ char program[DVX_MAX_PATH];
+ char args[SHELL_ARGS_MAX];
+ char folder[DVX_MAX_PATH];
} RunDosT;
+// How the run a program waits for ended (shellRunDosDone)
+typedef struct {
+ int32_t waiter; // its app id; 0 when none waits
+ bool done;
+ int32_t rc; // the exit code; -1 when it did not run
+} RunDosWaitT;
+
// ============================================================
// Module state
// ============================================================
static RunMinimizeT *sMinimize = NULL; // stb_ds
static RunDosT sDosRun;
+static RunDosWaitT sDosWait;
// ============================================================
// Prototypes
@@ -87,12 +97,16 @@ static void minimizeEnded(int32_t appId);
static void minimizeWindows(AppContextT *ctx, int32_t appId);
static void runDlgBrowse(WidgetT *w);
static bool runDlgOk(WindowT *dlg, void *userCtx);
+static void runDosFinish(int32_t waiter, int32_t rc);
+static void runDosRecord(int32_t waiter, const char *program, const char *args, const char *folder, bool pause);
void shellMinimizeApp(AppContextT *ctx, int32_t appId);
void shellMinimizeIdle(AppContextT *ctx);
void shellResolveArgs(const char *folder, const char *args, char *out, int32_t outSize);
int32_t shellRunCommand(AppContextT *ctx, const char *command);
bool shellRunDialog(AppContextT *ctx, const char *const *history, int32_t historyCount, char *outCommand, int32_t outSize);
void shellRunDos(const char *program, const char *args, const char *folder, bool pause);
+bool shellRunDosDone(int32_t appId, int32_t *rc);
+void shellRunDosFor(int32_t appId, const char *program, const char *args, const char *folder, bool pause);
void shellRunDosRequest(AppContextT *ctx, int32_t keepAppId);
int32_t shellRunProgram(AppContextT *ctx, const char *program, const char *args, const char *folder);
bool shellSplitCommand(const char *command, char *program, int32_t programSize, char *args, int32_t argsSize);
@@ -162,6 +176,37 @@ static bool runDlgOk(WindowT *dlg, void *userCtx) {
}
+// The run waiter waits for is over: rc its exit code (-1: it did not run).
+static void runDosFinish(int32_t waiter, int32_t rc) {
+ if (waiter && sDosWait.waiter == waiter) {
+ sDosWait.done = true;
+ sDosWait.rc = rc;
+ }
+}
+
+
+// Record a run for the main loop. A run waiting already is replaced (its
+// waiter, if any, is told it did not run).
+static void runDosRecord(int32_t waiter, const char *program, const char *args, const char *folder, bool pause) {
+ if (sDosRun.pending) {
+ runDosFinish(sDosRun.waiter, -1);
+ }
+
+ snprintf(sDosRun.program, sizeof(sDosRun.program), "%s", program);
+ snprintf(sDosRun.args, sizeof(sDosRun.args), "%s", args ? args : "");
+ snprintf(sDosRun.folder, sizeof(sDosRun.folder), "%s", folder ? folder : "");
+ sDosRun.pause = pause;
+ sDosRun.waiter = waiter;
+ sDosRun.pending = true;
+
+ if (waiter) {
+ sDosWait.waiter = waiter;
+ sDosWait.done = false;
+ sDosWait.rc = -1;
+ }
+}
+
+
// A second call for the same program starts its frames again.
void shellMinimizeApp(AppContextT *ctx, int32_t appId) {
RunMinimizeT entry = { appId, SHELL_MINIMIZE_FRAMES };
@@ -272,27 +317,55 @@ bool shellRunDialog(AppContextT *ctx, const char *const *history, int32_t histor
// is not safe from the caller's event handler); a request made while one
// waits replaces it.
void shellRunDos(const char *program, const char *args, const char *folder, bool pause) {
- if (!program || !program[0]) {
+ if (program && program[0]) {
+ runDosRecord(0, program, args, folder, pause);
+ }
+}
+
+
+// A waiter's own run is over once shellRunDosRequest has carried it out
+// or refused it; one replaced by another request is over too (not run).
+bool shellRunDosDone(int32_t appId, int32_t *rc) {
+ if (sDosWait.waiter != appId) {
+ *rc = -1;
+ return true;
+ }
+
+ if (!sDosWait.done) {
+ return false;
+ }
+
+ *rc = sDosWait.rc;
+ sDosWait.waiter = 0;
+ return true;
+}
+
+
+// Nothing to run is over at once.
+void shellRunDosFor(int32_t appId, const char *program, const char *args, const char *folder, bool pause) {
+ if (program && program[0]) {
+ runDosRecord(appId, program, args, folder, pause);
return;
}
- snprintf(sDosRun.program, sizeof(sDosRun.program), "%s", program);
- snprintf(sDosRun.args, sizeof(sDosRun.args), "%s", args ? args : "");
- snprintf(sDosRun.folder, sizeof(sDosRun.folder), "%s", folder ? folder : "");
- sDosRun.pause = pause;
- sDosRun.pending = true;
+ sDosWait.waiter = appId;
+ sDosWait.done = true;
+ sDosWait.rc = -1;
}
// A program named with a folder must exist (a bare name is looked for on
// the PATH by the command interpreter). The programs running besides the
-// desktop are named and asked to close first; No, or one that keeps its
-// window, cancels the run.
+// desktop and the program waiting for the run are named and asked to
+// close first; No, or one that keeps its window, cancels the run. The
+// waiter is told how it ended (shellRunDosDone).
void shellRunDosRequest(AppContextT *ctx, int32_t keepAppId) {
char names[SHELL_MSG_MAX];
char msg[SHELL_MSG_MAX];
- int32_t pos = 0;
- int32_t count = 0;
+ int32_t pos = 0;
+ int32_t count = 0;
+ int32_t *others = NULL; // stb_ds: their ids
+ int32_t rc = -1;
struct stat st;
if (!sDosRun.pending) {
@@ -308,6 +381,7 @@ void shellRunDosRequest(AppContextT *ctx, int32_t keepAppId) {
if (base != run.program && stat(run.program, &st) != 0) {
snprintf(msg, sizeof(msg), RUN_NOT_FOUND, run.program);
dvxErrorBox(ctx, RUN_DOS_TITLE, msg);
+ runDosFinish(run.waiter, rc);
return;
}
@@ -316,28 +390,43 @@ void shellRunDosRequest(AppContextT *ctx, int32_t keepAppId) {
const ShellAppT *app = shellGetApp(id);
ShellProgramT info;
- if (id == keepAppId || !app) {
+ if (id == keepAppId || id == run.waiter || !app) {
continue;
}
shellProgramInfo(app->path, &info);
pos = dvxStrAppendf(names, sizeof(names), pos, RUN_DOS_LINE, info.name[0] ? info.name : app->name);
+ arrput(others, id);
count++;
}
- if (count > 0) {
- snprintf(msg, sizeof(msg), RUN_DOS_ASK, base, names);
+ bool agreed = count == 0;
- if (dvxMessageBox(ctx, RUN_DOS_TITLE, msg, MB_YESNO | MB_ICONQUESTION) != ID_YES || !shellCloseAllApps(ctx, keepAppId)) {
- dvxLog("Shell: %s not run: programs kept running", run.program);
- return;
+ if (!agreed) {
+ snprintf(msg, sizeof(msg), RUN_DOS_ASK, base, names);
+ agreed = dvxMessageBox(ctx, RUN_DOS_TITLE, msg, MB_YESNO | MB_ICONQUESTION) == ID_YES;
+
+ for (int32_t i = 0; agreed && i < (int32_t)arrlen(others); i++) {
+ agreed = shellCloseApp(ctx, others[i]);
}
}
- if (dvxRunDos(ctx, run.program, run.args, run.folder, run.pause) < 0) {
+ arrfree(others);
+
+ if (!agreed) {
+ dvxLog("Shell: %s not run: programs kept running", run.program);
+ runDosFinish(run.waiter, rc);
+ return;
+ }
+
+ rc = dvxRunDos(ctx, run.program, run.args, run.folder, run.pause);
+
+ if (rc < 0) {
snprintf(msg, sizeof(msg), RUN_DOS_FAILED, base, strerror(errno));
dvxErrorBox(ctx, RUN_DOS_TITLE, msg);
}
+
+ runDosFinish(run.waiter, rc);
}
diff --git a/src/libs/kpunch/dvxshell/shellRun.h b/src/libs/kpunch/dvxshell/shellRun.h
index 3d567a2e..c0021190 100644
--- a/src/libs/kpunch/dvxshell/shellRun.h
+++ b/src/libs/kpunch/dvxshell/shellRun.h
@@ -88,6 +88,17 @@ int32_t shellRunProgram(AppContextT *ctx, const char *program, const char *args,
// made while one waits replaces it.
void shellRunDos(const char *program, const char *args, const char *folder, bool pause);
+// Whether the run the program appId waits for (shellRunDosFor) is over;
+// then *rc is its exit code, -1 when it did not run (the other programs
+// were kept, it was not found, it could not start, or another request
+// replaced it).
+bool shellRunDosDone(int32_t appId, int32_t *rc);
+
+// shellRunDos for a program that waits for the run to end (BASIC's
+// SHELL): appId is not asked to close, and shellRunDosDone says when the
+// run is over and how it ended.
+void shellRunDosFor(int32_t appId, const char *program, const char *args, const char *folder, bool pause);
+
// Main-loop step: carry out a waiting shellRunDos; keepAppId is the
// desktop, which stays. Pumps the event loop: shell top level only.
void shellRunDosRequest(AppContextT *ctx, int32_t keepAppId);
diff --git a/src/libs/kpunch/fileops/README.md b/src/libs/kpunch/fileops/README.md
new file mode 100644
index 00000000..26a38f0f
--- /dev/null
+++ b/src/libs/kpunch/fileops/README.md
@@ -0,0 +1,26 @@
+# fileops -- File Operations
+
+Files and folders for DVX programs: drive and disk details, attributes
+and dates, folder listings with a wildcard filter, sorted, copying,
+moving, and deleting whole trees a step at a time with progress (so the
+other programs keep running), name checks by DOS rules, and the path
+helpers. The File Manager is built on it; BASIC reaches it through
+src/include/basic/fileops.bas.
+
+## Documentation
+
+Full reference is in `fileops.dhs` (this directory), compiled into the
+DVX System Reference. Open via the Help Viewer app, or read
+`docs/dvx_system_reference.html` after `make`.
+
+## Source Files
+
+- `fileOps.h` -- public API
+- `fileOps.c` -- listings, trees, attributes, names, paths
+- `fileOpsSys.h`, `fileOpsSys.c` -- the system calls behind attributes
+ and dates
+- `fileOpsBas.c` -- BASIC entry points (fileops.bas)
+
+## Build
+
+`make -C src/libs/kpunch/fileops` (invoked by the top-level `make`).
diff --git a/src/libs/kpunch/fileops/fileOps.c b/src/libs/kpunch/fileops/fileOps.c
index b09d5e12..85975ed7 100644
--- a/src/libs/kpunch/fileops/fileOps.c
+++ b/src/libs/kpunch/fileops/fileOps.c
@@ -71,6 +71,10 @@
#define NEW_DIR_MODE 0777
#define NO_PARENT (-1)
+// Folder entries one scan step reads at most (a step stays short on a
+// big folder)
+#define SCAN_STEP_ENTRIES 32
+
// fileOpsPathNormalize: the "C" of "C:\..." (upper case on every disk)
#define DOS_DRIVE_LETTER_LEN 1
@@ -110,6 +114,8 @@ struct FileOpsJobS {
FileOpsAnswerE answer;
FileOpsItemT *items; // stb_ds array, breadth-first
int32_t scanPos; // next item the scan looks at
+ int32_t scanBase; // items before scanPos's were read (a Retry drops the rest)
+ FileOpsSysDirT *scanDir; // the folder scanPos is, while it is being read
int32_t pos; // item being processed in the current phase
FILE *in; // file being copied
FILE *out;
@@ -119,11 +125,14 @@ struct FileOpsJobS {
bool overwriteAll; // "All" given to an overwrite question
bool readOnlyAll; // "All" given to a read-only question
bool cancelled;
+ bool ran; // a step ran: files may have changed
uint8_t clearAttr; // attribute bits cleared on every copied file
char destDir[DVX_MAX_PATH];
char message[FILEOPS_MSG_MAX];
int32_t filesTotal;
int32_t filesDone;
+ int32_t foldersTotal;
+ int32_t unread; // measure: items that could not be read
uint64_t bytesTotal;
uint64_t bytesDone;
const char *current; // path being worked on (points into items)
@@ -136,22 +145,6 @@ typedef struct {
bool ok; // false: out of memory
} ListScanT;
-// scanFolder's scan: the job and the folder item being read.
-typedef struct {
- FileOpsJobT *job;
- int32_t idx;
- bool ok;
-} FolderScanT;
-
-// treeSizeDir's scan: the folder and the totals.
-typedef struct {
- const char *dir;
- uint64_t *bytes;
- int32_t *files;
- int32_t *folders;
- bool ok; // false: something could not be read
-} SizeScanT;
-
// ============================================================
// Prototypes
// ============================================================
@@ -166,8 +159,10 @@ static const char *errorText(int32_t err);
static void keepAncestors(FileOpsJobT *job, int32_t idx);
static bool listScanEntry(const FileOpsSysEntryT *e, void *ctx);
static int32_t percentOf(const FileOpsJobT *job);
-static bool scanFolder(FileOpsJobT *job, int32_t idx);
-static bool scanFolderEntry(const FileOpsSysEntryT *e, void *ctx);
+static int32_t phasePos(const FileOpsJobT *job);
+static void scanClose(FileOpsJobT *job);
+static bool scanEntry(FileOpsJobT *job, int32_t idx, const FileOpsSysEntryT *e);
+static bool scanStep(FileOpsJobT *job, int32_t idx, char *problem, int32_t problemSize);
static void skipItem(FileOpsJobT *job, int32_t idx);
static int32_t statItem(const char *path, struct stat *st);
static FileOpsStepE stepRemove(FileOpsJobT *job);
@@ -175,9 +170,6 @@ static FileOpsStepE stepScan(FileOpsJobT *job);
static FileOpsStepE stepTransfer(FileOpsJobT *job);
static FileOpsStepE stepTransferFile(FileOpsJobT *job, FileOpsItemT *it);
static bool takeAnswer(FileOpsJobT *job, FileOpsQuestionE question, FileOpsAnswerE *answer);
-static bool treeSizeDir(const char *dir, uint64_t *bytes, int32_t *files, int32_t *folders);
-static bool treeSizeEntry(const FileOpsSysEntryT *e, void *ctx);
-static bool treeSizeWalk(const char *path, uint64_t *bytes, int32_t *files, int32_t *folders);
// Sort parameters for compareEntries (qsort has no context argument)
static FileOpsSortE sSortCol = FileOpsSortNameE;
@@ -234,6 +226,7 @@ static FileOpsStepE askUser(FileOpsJobT *job, FileOpsQuestionE question, FileOps
static FileOpsStepE cancelJob(FileOpsJobT *job) {
closeCopy(job, true);
+ scanClose(job);
job->cancelled = true;
job->question = FileOpsQuestionNoneE;
job->phase = FileOpsPhaseDoneE;
@@ -376,7 +369,7 @@ void fileOpsFormatSize(uint64_t bytes, char *out, int32_t outSize) {
bool fileOpsJobAddSource(FileOpsJobT *job, const char *path) {
char dst[DVX_MAX_PATH];
- if (job->op == FileOpsDeleteE) {
+ if (job->op == FileOpsDeleteE || job->op == FileOpsMeasureE) {
return addItem(job, path, NULL, NO_PARENT);
}
@@ -424,12 +417,18 @@ FileOpsJobT *fileOpsJobCreate(FileOpsOpE op, const char *destDir) {
}
+// A copy, move or delete that ran may have changed what is installed.
void fileOpsJobFree(FileOpsJobT *job) {
if (!job) {
return;
}
closeCopy(job, true);
+ scanClose(job);
+
+ if (job->ran && job->op != FileOpsMeasureE) {
+ platformInstalledChanged();
+ }
for (int32_t i = 0; i < (int32_t)arrlen(job->items); i++) {
free(job->items[i].src);
@@ -453,12 +452,14 @@ FileOpsOpE fileOpsJobOp(const FileOpsJobT *job) {
void fileOpsJobProgress(const FileOpsJobT *job, FileOpsProgressT *out) {
- out->filesTotal = job->filesTotal;
- out->filesDone = job->filesDone;
- out->bytesTotal = job->bytesTotal;
- out->bytesDone = job->bytesDone;
- out->percent = percentOf(job);
- out->current = job->current ? job->current : "";
+ out->filesTotal = job->filesTotal;
+ out->filesDone = job->filesDone;
+ out->foldersTotal = job->foldersTotal;
+ out->unread = job->unread;
+ out->bytesTotal = job->bytesTotal;
+ out->bytesDone = job->bytesDone;
+ out->percent = percentOf(job);
+ out->current = job->current ? job->current : "";
}
@@ -494,7 +495,9 @@ FileOpsStepE fileOpsJobStep(FileOpsJobT *job) {
return job->cancelled ? FileOpsStepCancelledE : FileOpsStepDoneE;
}
- if (job->cancelled) {
+ // Cancel, also as the answer to a question, stops at once (the item is
+ // not tried again first)
+ if (job->cancelled || (job->answered && job->answer == FileOpsAnswerCancelE)) {
return cancelJob(job);
}
@@ -502,7 +505,11 @@ FileOpsStepE fileOpsJobStep(FileOpsJobT *job) {
return job->question == FileOpsQuestionErrorE ? FileOpsStepErrorE : FileOpsStepAskE;
}
- FileOpsStepE result = FileOpsStepBusyE;
+ FileOpsPhaseE phase = job->phase;
+ int32_t at = phasePos(job);
+ FileOpsStepE result = FileOpsStepBusyE;
+
+ job->ran = true;
switch (job->phase) {
case FileOpsPhaseScanE:
@@ -521,6 +528,13 @@ FileOpsStepE fileOpsJobStep(FileOpsJobT *job) {
break;
}
+ // An answer still waiting was for an item that went through when tried
+ // again; it is not kept for a later question
+ if (job->answered && (job->phase != phase || phasePos(job) != at)) {
+ job->answered = false;
+ job->question = FileOpsQuestionNoneE;
+ }
+
if (result == FileOpsStepBusyE && job->phase == FileOpsPhaseDoneE) {
result = FileOpsStepDoneE;
}
@@ -654,11 +668,7 @@ bool fileOpsPathIsInside(const char *path, const char *dir) {
bool fileOpsPathIsRoot(const char *path) {
- if (platformPathIsSep(path[0]) && path[1] == '\0') {
- return true;
- }
-
- return path[0] && path[1] == ':' && (path[2] == '\0' || (platformPathIsSep(path[2]) && path[3] == '\0'));
+ return platformPathIsRoot(path);
}
@@ -697,39 +707,7 @@ bool fileOpsPathNormalize(const char *path, char *out, int32_t outSize) {
bool fileOpsPathParent(const char *path, char *out, int32_t outSize) {
- if (fileOpsPathIsRoot(path)) {
- return false;
- }
-
- int32_t len = (int32_t)strlen(path);
- int32_t sep = -1;
-
- if (len > 0 && platformPathIsSep(path[len - 1])) {
- len--;
- }
-
- for (int32_t i = 0; i < len; i++) {
- if (platformPathIsSep(path[i])) {
- sep = i;
- }
- }
-
- if (sep < 0) {
- return false;
- }
-
- // Keep the separator of a root ("/", "C:\").
- bool rootSep = sep == 0 || (sep == 2 && path[1] == ':');
- int32_t keep = rootSep ? sep + 1 : sep;
-
- if (keep >= outSize) {
- return false;
- }
-
- memcpy(out, path, (size_t)keep);
- out[keep] = '\0';
- platformPathNative(out);
- return true;
+ return platformPathParent(path, out, outSize);
}
@@ -772,12 +750,16 @@ bool fileOpsRename(const char *dir, const char *oldName, const char *newName, ch
}
+// A measure job run to its end (it asks nothing).
bool fileOpsTreeSize(const char *path, uint64_t *bytes, int32_t *files, int32_t *folders) {
- *bytes = 0;
- *files = 0;
- *folders = 0;
+ FileOpsJobT *job = fileOpsJobCreate(FileOpsMeasureE, NULL);
+ bool ok = job && fileOpsJobAddSource(job, path) && fileOpsJobRun(job, NULL) == FileOpsStepDoneE && job->unread == 0;
- return treeSizeWalk(path, bytes, files, folders);
+ *bytes = job ? job->bytesTotal : 0;
+ *files = job ? job->filesTotal : 0;
+ *folders = job ? job->foldersTotal : 0;
+ fileOpsJobFree(job);
+ return ok;
}
@@ -863,40 +845,68 @@ static int32_t percentOf(const FileOpsJobT *job) {
}
-// Append the contents of folder item idx, in one pass over the folder.
-// False when it cannot be read (or a path would be too long).
-static bool scanFolder(FileOpsJobT *job, int32_t idx) {
- FolderScanT scan;
-
- scan.job = job;
- scan.idx = idx;
- scan.ok = true;
-
- // The item array may move while appending, but the path string stays.
- return fileOpsSysScan(job->items[idx].src, false, scanFolderEntry, &scan) && scan.ok;
+// The item the job's phase is on.
+static int32_t phasePos(const FileOpsJobT *job) {
+ return job->phase == FileOpsPhaseScanE ? job->scanPos : job->pos;
}
-// One entry of the folder scanFolder reads: an item under it.
-static bool scanFolderEntry(const FileOpsSysEntryT *e, void *ctx) {
- FolderScanT *scan = (FolderScanT *)ctx;
- const FileOpsItemT *dirItem = &scan->job->items[scan->idx];
+// Stop reading the folder the scan is on, if any.
+static void scanClose(FileOpsJobT *job) {
+ fileOpsSysClose(job->scanDir);
+ job->scanDir = NULL;
+}
+
+
+// One entry of folder item idx: an item under it, or for a measure a file
+// counted (only folders need visiting). False (errno set) when its path
+// would be too long or memory runs out.
+static bool scanEntry(FileOpsJobT *job, int32_t idx, const FileOpsSysEntryT *e) {
+ const FileOpsItemT *dirItem = &job->items[idx];
char src[DVX_MAX_PATH];
char dst[DVX_MAX_PATH];
- bool fits = fileOpsPathJoin(src, sizeof(src), dirItem->src, e->name);
+
+ if (job->op == FileOpsMeasureE && !e->isDir) {
+ job->filesTotal++;
+ job->bytesTotal += e->size;
+ return true;
+ }
+
+ bool fits = fileOpsPathJoin(src, sizeof(src), dirItem->src, e->name);
if (fits && dirItem->dst) {
fits = fileOpsPathJoin(dst, sizeof(dst), dirItem->dst, e->name);
}
if (!fits) {
- errno = ENAMETOOLONG;
- scan->ok = false;
+ errno = ENAMETOOLONG;
return false;
}
- scan->ok = appendItem(scan->job, src, dirItem->dst ? dst : NULL, scan->idx, e->isDir, e->size);
- return scan->ok;
+ // The item array may move while appending, so dirItem is not used after
+ return appendItem(job, src, dirItem->dst ? dst : NULL, idx, e->isDir, e->size);
+}
+
+
+// Read up to SCAN_STEP_ENTRIES more entries of folder item idx (the open
+// job->scanDir). True while there are more; false at the end (the folder
+// closed) or with the reason in problem.
+static bool scanStep(FileOpsJobT *job, int32_t idx, char *problem, int32_t problemSize) {
+ FileOpsSysEntryT e;
+
+ for (int32_t n = 0; n < SCAN_STEP_ENTRIES; n++) {
+ if (!fileOpsSysNext(job->scanDir, &e)) {
+ scanClose(job);
+ return false;
+ }
+
+ if (!scanEntry(job, idx, &e)) {
+ snprintf(problem, (size_t)problemSize, "Cannot read %s.\n%s", job->items[idx].src, errorText(errno));
+ return false;
+ }
+ }
+
+ return true;
}
@@ -928,7 +938,7 @@ static FileOpsStepE stepRemove(FileOpsJobT *job) {
return FileOpsStepBusyE;
}
- int32_t idx = job->pos;
+ int32_t idx = job->pos;
FileOpsItemT *it = &job->items[idx];
FileOpsAnswerE answer;
@@ -958,9 +968,6 @@ static FileOpsStepE stepRemove(FileOpsJobT *job) {
job->readOnlyAll = true;
break;
- case FileOpsAnswerCancelE:
- return cancelJob(job);
-
default:
it->approved = true;
break;
@@ -980,10 +987,6 @@ static FileOpsStepE stepRemove(FileOpsJobT *job) {
return askUser(job, FileOpsQuestionErrorE, FileOpsStepErrorE, "Cannot delete %s.\n%s", it->src, errorText(e));
}
- if (answer == FileOpsAnswerCancelE) {
- return cancelJob(job);
- }
-
if (answer == FileOpsAnswerRetryE) {
return FileOpsStepBusyE;
}
@@ -1004,70 +1007,93 @@ static FileOpsStepE stepRemove(FileOpsJobT *job) {
// Scan one item: check a top-level source, try a whole-item rename for a
-// move, read a folder's contents, or count a file.
+// move, read a piece of a folder's contents, or count a file or folder.
+// A measure asks nothing: what cannot be read is skipped and counted.
static FileOpsStepE stepScan(FileOpsJobT *job) {
int32_t count = (int32_t)arrlen(job->items);
if (job->scanPos >= count) {
- bool isDelete = job->op == FileOpsDeleteE;
+ if (job->op == FileOpsMeasureE) {
+ job->phase = FileOpsPhaseDoneE;
+ } else if (job->op == FileOpsDeleteE) {
+ job->phase = FileOpsPhaseRemoveE;
+ job->pos = count - 1;
+ } else {
+ job->phase = FileOpsPhaseTransferE;
+ job->pos = 0;
+ }
- job->phase = isDelete ? FileOpsPhaseRemoveE : FileOpsPhaseTransferE;
- job->pos = isDelete ? count - 1 : 0;
return FileOpsStepBusyE;
}
- int32_t idx = job->scanPos;
- FileOpsItemT *it = &job->items[idx];
+ int32_t idx = job->scanPos;
+ FileOpsItemT *it = &job->items[idx];
+ bool more = false;
FileOpsAnswerE answer;
- char problem[FILEOPS_MSG_MAX];
+ char problem[FILEOPS_MSG_MAX];
job->current = it->src;
problem[0] = '\0';
- if (it->parent == NO_PARENT && it->dst) {
- struct stat st;
+ if (job->scanDir) {
+ more = scanStep(job, idx, problem, sizeof(problem));
+ } else {
+ job->scanBase = count;
- if (fileOpsPathSame(it->src, it->dst)) {
- if (job->op == FileOpsMoveE) {
- // Moving an item to where it already is: nothing to do.
- it->skipped = true;
+ if (it->parent == NO_PARENT && it->dst) {
+ struct stat st;
+
+ if (fileOpsPathSame(it->src, it->dst)) {
+ if (job->op == FileOpsMoveE) {
+ // Moving an item to where it already is: nothing to do.
+ it->skipped = true;
+ job->scanPos++;
+ return FileOpsStepBusyE;
+ }
+
+ snprintf(problem, sizeof(problem), "%s cannot be copied onto itself.", it->src);
+ } else if (it->isDir && fileOpsPathIsInside(it->dst, it->src)) {
+ snprintf(problem, sizeof(problem), "%s cannot be %s into itself.", it->src, job->op == FileOpsMoveE ? "moved" : "copied");
+ } else if (job->op == FileOpsMoveE && stat(it->dst, &st) != 0 && rename(it->src, it->dst) == 0) {
+ it->moved = true;
job->scanPos++;
return FileOpsStepBusyE;
}
-
- snprintf(problem, sizeof(problem), "%s cannot be copied onto itself.", it->src);
- } else if (it->isDir && fileOpsPathIsInside(it->dst, it->src)) {
- snprintf(problem, sizeof(problem), "%s cannot be %s into itself.", it->src, job->op == FileOpsMoveE ? "moved" : "copied");
- } else if (job->op == FileOpsMoveE && stat(it->dst, &st) != 0 && rename(it->src, it->dst) == 0) {
- it->moved = true;
- job->scanPos++;
- return FileOpsStepBusyE;
}
- }
- if (!problem[0] && it->isDir && !scanFolder(job, idx)) {
- snprintf(problem, sizeof(problem), "Cannot read %s.\n%s", job->items[idx].src, errorText(errno));
+ if (!problem[0] && it->isDir) {
+ job->scanDir = fileOpsSysOpen(it->src, false);
+ more = job->scanDir != NULL;
+
+ if (!more) {
+ snprintf(problem, sizeof(problem), "Cannot read %s.\n%s", it->src, errorText(errno));
+ }
+ }
}
if (problem[0]) {
- if (!takeAnswer(job, FileOpsQuestionErrorE, &answer)) {
- return askUser(job, FileOpsQuestionErrorE, FileOpsStepErrorE, "%s", problem);
- }
+ scanClose(job);
- if (answer == FileOpsAnswerCancelE) {
- return cancelJob(job);
- }
-
- if (answer == FileOpsAnswerRetryE) {
- // Drop anything a partial folder read appended.
- while ((int32_t)arrlen(job->items) > count) {
+ if (job->op == FileOpsMeasureE) {
+ // What was read stays counted; only a folder read fails here
+ job->unread++;
+ job->foldersTotal += job->items[idx].parent != NO_PARENT ? 1 : 0;
+ } else {
+ // Drop anything a partial folder read appended
+ while ((int32_t)arrlen(job->items) > job->scanBase) {
FileOpsItemT last = arrpop(job->items);
free(last.src);
free(last.dst);
}
- return FileOpsStepBusyE;
+ if (!takeAnswer(job, FileOpsQuestionErrorE, &answer)) {
+ return askUser(job, FileOpsQuestionErrorE, FileOpsStepErrorE, "%s", problem);
+ }
+
+ if (answer == FileOpsAnswerRetryE) {
+ return FileOpsStepBusyE;
+ }
}
skipItem(job, idx);
@@ -1075,11 +1101,17 @@ static FileOpsStepE stepScan(FileOpsJobT *job) {
return FileOpsStepBusyE;
}
+ if (more) {
+ return FileOpsStepBusyE;
+ }
+
it = &job->items[idx];
if (!it->isDir) {
job->filesTotal++;
job->bytesTotal += it->size;
+ } else if (it->parent != NO_PARENT) {
+ job->foldersTotal++;
}
job->scanPos++;
@@ -1095,7 +1127,7 @@ static FileOpsStepE stepTransfer(FileOpsJobT *job) {
return FileOpsStepBusyE;
}
- int32_t idx = job->pos;
+ int32_t idx = job->pos;
FileOpsItemT *it = &job->items[idx];
FileOpsAnswerE answer;
@@ -1125,10 +1157,6 @@ static FileOpsStepE stepTransfer(FileOpsJobT *job) {
return askUser(job, FileOpsQuestionErrorE, FileOpsStepErrorE, "Cannot create folder %s.\n%s", it->dst, errorText(e));
}
- if (answer == FileOpsAnswerCancelE) {
- return cancelJob(job);
- }
-
if (answer == FileOpsAnswerRetryE) {
return FileOpsStepBusyE;
}
@@ -1145,9 +1173,9 @@ static FileOpsStepE stepTransfer(FileOpsJobT *job) {
// files), copy one chunk, or finish it (dates, attributes, and for a move
// removing the source).
static FileOpsStepE stepTransferFile(FileOpsJobT *job, FileOpsItemT *it) {
- int32_t idx = job->pos;
+ int32_t idx = job->pos;
FileOpsAnswerE answer;
- char problem[FILEOPS_MSG_MAX];
+ char problem[FILEOPS_MSG_MAX];
problem[0] = '\0';
@@ -1172,9 +1200,6 @@ static FileOpsStepE stepTransferFile(FileOpsJobT *job, FileOpsItemT *it) {
job->overwriteAll = true;
break;
- case FileOpsAnswerCancelE:
- return cancelJob(job);
-
default:
it->approved = true;
break;
@@ -1269,10 +1294,6 @@ static FileOpsStepE stepTransferFile(FileOpsJobT *job, FileOpsItemT *it) {
return askUser(job, FileOpsQuestionErrorE, FileOpsStepErrorE, "%s", problem);
}
- if (answer == FileOpsAnswerCancelE) {
- return cancelJob(job);
- }
-
if (answer == FileOpsAnswerRetryE) {
return FileOpsStepBusyE;
}
@@ -1301,57 +1322,3 @@ static bool takeAnswer(FileOpsJobT *job, FileOpsQuestionE question, FileOpsAnswe
job->question = FileOpsQuestionNoneE;
return true;
}
-
-
-// Add everything in folder dir to the totals, one pass per folder.
-static bool treeSizeDir(const char *dir, uint64_t *bytes, int32_t *files, int32_t *folders) {
- SizeScanT scan;
-
- scan.dir = dir;
- scan.bytes = bytes;
- scan.files = files;
- scan.folders = folders;
- scan.ok = true;
-
- return fileOpsSysScan(dir, false, treeSizeEntry, &scan) && scan.ok;
-}
-
-
-// One entry of a folder treeSizeDir reads: a file's size, or a folder
-// counted and walked.
-static bool treeSizeEntry(const FileOpsSysEntryT *e, void *ctx) {
- SizeScanT *scan = (SizeScanT *)ctx;
- char child[DVX_MAX_PATH];
-
- if (!e->isDir) {
- *scan->bytes += e->size;
- (*scan->files)++;
- return true;
- }
-
- (*scan->folders)++;
-
- if (!fileOpsPathJoin(child, sizeof(child), scan->dir, e->name) || !treeSizeDir(child, scan->bytes, scan->files, scan->folders)) {
- scan->ok = false;
- }
-
- return true;
-}
-
-
-// Add path (and, for a folder, everything below it) to the totals.
-static bool treeSizeWalk(const char *path, uint64_t *bytes, int32_t *files, int32_t *folders) {
- struct stat st;
-
- if (statItem(path, &st) != 0) {
- return false;
- }
-
- if (!S_ISDIR(st.st_mode)) {
- *bytes += platformStatSize(&st);
- (*files)++;
- return true;
- }
-
- return treeSizeDir(path, bytes, files, folders);
-}
diff --git a/src/libs/kpunch/fileops/fileOps.h b/src/libs/kpunch/fileops/fileOps.h
index b50297d9..17c8c6d7 100644
--- a/src/libs/kpunch/fileops/fileOps.h
+++ b/src/libs/kpunch/fileops/fileOps.h
@@ -41,10 +41,11 @@
// Jobs recursive copy, move and delete of any number of
// files and folders, with progress, Cancel, "file
// exists" / "read-only" questions (Yes/No/All/Cancel)
-// and error recovery (Retry/Skip/Cancel).
+// and error recovery (Retry/Skip/Cancel); and measure
+// (count files, folders and bytes, changing nothing).
//
// A job is advanced by fileOpsJobStep, one bounded piece of work per call
-// (one folder read, one chunk of one file, one delete), so a cooperative
+// (part of a folder read, one chunk of one file, one delete), so a cooperative
// program can yield, repaint progress and honour Cancel between steps.
// When a step needs the user it returns FileOpsStepAskE or
// FileOpsStepErrorE; fileOpsJobMessage says what happened and the job
@@ -52,7 +53,8 @@
// alternative: it runs a job to the end, calling back for progress and
// questions.
//
-// A job works in phases: scan (read source folders, total the bytes),
+// A job works in phases: scan (read source folders, total the bytes; all a
+// measure does, asking nothing: a folder it cannot read is counted unread),
// transfer (copy/move: create folders, copy files in chunks keeping their
// dates and attributes; a move removes each source file once its copy is
// complete, or renames a whole top-level item in one go when the
@@ -110,7 +112,8 @@ typedef enum {
typedef enum {
FileOpsCopyE,
FileOpsMoveE,
- FileOpsDeleteE
+ FileOpsDeleteE,
+ FileOpsMeasureE // count files, folders and bytes; nothing changes
} FileOpsOpE;
typedef enum {
@@ -134,6 +137,8 @@ typedef enum {
typedef struct {
int32_t filesTotal; // files found so far by the scan
int32_t filesDone;
+ int32_t foldersTotal; // folders found so far inside the sources
+ int32_t unread; // measure: folders that could not be read (all of it or part)
uint64_t bytesTotal;
uint64_t bytesDone;
int32_t percent; // 0..FILEOPS_PERCENT_FULL
@@ -255,8 +260,8 @@ void fileOpsJobClearAttr(FileOpsJobT *job, uint8_t attr);
// file) and returns FileOpsStepCancelledE.
void fileOpsJobCancel(FileOpsJobT *job);
-// Start a job. destDir is the target folder (ignored for delete). NULL
-// when out of memory.
+// Start a job. destDir is the target folder (ignored for delete and
+// measure). NULL when out of memory.
FileOpsJobT *fileOpsJobCreate(FileOpsOpE op, const char *destDir);
// Free a job, closing any open files and removing a partial copy.
@@ -324,9 +329,10 @@ int32_t FileOpsIsFolder(const char *path);
// platformDirIsWritable.
int32_t FileOpsIsWritable(const char *dir);
-// Jobs: create (op 0 copy, 1 move, 2 delete), add sources, then run in
-// time slices (FileOpsJobRunFor returns a FileOpsStepE), answering Ask /
-// Error steps; progress as files done / total, percent and current path.
+// Jobs: create (op 0 copy, 1 move, 2 delete, 3 measure), add sources,
+// then run in time slices (FileOpsJobRunFor returns a FileOpsStepE),
+// answering Ask / Error steps; progress as files done / total, folders
+// inside the sources, unread folders (measure), percent and current path.
int32_t FileOpsJobAddSource(int32_t job, const char *path);
void FileOpsJobAnswer(int32_t job, int32_t answer);
double FileOpsJobBytesDone(int32_t job);
@@ -337,10 +343,12 @@ int32_t FileOpsJobCreate(int32_t op, const char *destDir);
const char *FileOpsJobCurrent(int32_t job);
int32_t FileOpsJobFilesDone(int32_t job);
int32_t FileOpsJobFilesTotal(int32_t job);
+int32_t FileOpsJobFoldersTotal(int32_t job);
void FileOpsJobFree(int32_t job);
const char *FileOpsJobMessage(int32_t job);
int32_t FileOpsJobPercent(int32_t job);
int32_t FileOpsJobRunFor(int32_t job, int32_t milliseconds);
+int32_t FileOpsJobUnread(int32_t job);
// The DVX folders of a kind of file (dvxPlat.h DvxKindE, BASIC
// FILEOPS_KIND_*): platformKindBrowseDir, platformKindDir,
@@ -354,10 +362,12 @@ const char *FileOpsKindName(int32_t kind, const char *path);
// Listings: read a folder (sorted by name, folders first), sort it, read
// its entries, or all of them as ListView SetRows text (name, size, type,
-// date per row).
+// date per row); FileOpsListFolders copies a listing's folders alone,
+// sorted by name, without reading the disk.
int32_t FileOpsList(const char *dir, const char *filter);
int32_t FileOpsListCount(int32_t list);
int32_t FileOpsListFind(int32_t list, const char *name);
+int32_t FileOpsListFolders(int32_t list);
void FileOpsListFree(int32_t list);
int32_t FileOpsListIsFolder(int32_t list, int32_t index);
const char *FileOpsListName(int32_t list, int32_t index);
diff --git a/src/libs/kpunch/fileops/fileOpsBas.c b/src/libs/kpunch/fileops/fileOpsBas.c
index bad36a18..48bb2a83 100644
--- a/src/libs/kpunch/fileops/fileOpsBas.c
+++ b/src/libs/kpunch/fileops/fileOpsBas.c
@@ -35,6 +35,7 @@
// and removes a partly copied one.
#include "fileOps.h"
+#include "dvxApp.h"
#include "dvxPlat.h"
#include "dvxTypes.h"
#include "stb_ds_wrap.h"
@@ -54,10 +55,14 @@
#define LIST_NOT_FOUND (-1)
#define BYTES_PER_KB 1024
#define DRIVE_TEXT_MAX 2 // a drive letter and its NUL
+#define TREE_PUMP_MS 50 // a whole-tree call lets DVX go on this often
typedef struct {
- bool yesToAll; // replace existing / delete read-only files
- int32_t problems; // errors that skipped an item
+ bool yesToAll; // replace existing / delete read-only files
+ bool pumped; // dvxWaitPump has let DVX go on
+ bool noPump; // dvxWaitPump said stop: no more
+ int32_t problems; // errors that skipped an item
+ PlatformTicksT pumpAt; // when DVX last went on (once pumped)
} BasRunT;
// One handle: a listing or a job.
@@ -72,9 +77,9 @@ typedef struct {
// ============================================================
static char sError[FILEOPS_MSG_MAX]; // why the last MakeDir / Rename / ReplaceFile / Valid* failed
-static uint64_t sTreeBytes = 0; // the last FileOpsTreeSize
-static int32_t sTreeFiles = 0;
-static int32_t sTreeFolders = 0;
+static uint64_t sTreeBytes = 0; // the last FileOpsTreeSize
+static int32_t sTreeFiles = 0;
+static int32_t sTreeFolders = 0;
// ============================================================
// Prototypes
@@ -90,7 +95,9 @@ static bool jobProgress(int32_t job, FileOpsProgressT *p);
static const FileOpsEntryT *listEntry(int32_t list, int32_t index);
static BasHandleT *listOf(int32_t list);
static int32_t nameResult(const char *problem);
+static bool runJob(FileOpsJobT *job, const char *src, bool yesToAll, FileOpsProgressT *out);
static int32_t runTree(FileOpsOpE op, const char *src, const char *destDir, bool yesToAll);
+static bool treeProgress(FileOpsJobT *job, void *ctx);
// The listings and jobs, and the last string result
static BasHandlesT sHandles = BAS_HANDLES(handleFree, 0);
@@ -303,10 +310,10 @@ void FileOpsJobClearAttr(int32_t job, int32_t attr) {
}
-// A copy (0), move (1) or delete (2) into destDir; 0 for another op or
-// when out of memory.
+// A copy (0), move (1) or delete (2) into destDir, or a measure (3); 0
+// for another op or when out of memory.
int32_t FileOpsJobCreate(int32_t op, const char *destDir) {
- if (op < FileOpsCopyE || op > FileOpsDeleteE) {
+ if (op < FileOpsCopyE || op > FileOpsMeasureE) {
return BAS_NO_HANDLE;
}
@@ -349,6 +356,14 @@ int32_t FileOpsJobFilesTotal(int32_t job) {
}
+int32_t FileOpsJobFoldersTotal(int32_t job) {
+ FileOpsProgressT p;
+
+ jobProgress(job, &p);
+ return p.foldersTotal;
+}
+
+
void FileOpsJobFree(int32_t job) {
if (jobOf(job)) {
basHandleFree(&sHandles, job);
@@ -392,6 +407,14 @@ int32_t FileOpsJobRunFor(int32_t job, int32_t milliseconds) {
}
+int32_t FileOpsJobUnread(int32_t job) {
+ FileOpsProgressT p;
+
+ jobProgress(job, &p);
+ return p.unread;
+}
+
+
// The folder a file dialog for a kind starts in: the user's when it
// exists, else SHARED's; "" for a kind out of range.
const char *FileOpsKindBrowseDir(int32_t kind) {
@@ -513,6 +536,47 @@ int32_t FileOpsListFind(int32_t list, const char *name) {
}
+// list's folders alone, sorted by name, as a new listing (list stays as
+// it is); 0 when list is not a listing or memory runs out.
+int32_t FileOpsListFolders(int32_t list) {
+ BasHandleT *hd = listOf(list);
+ FileOpsEntryT *entries = NULL;
+
+ if (!hd) {
+ return BAS_NO_HANDLE;
+ }
+
+ for (int32_t i = 0; i < hd->count; i++) {
+ FileOpsEntryT e = hd->entries[i];
+
+ if (!e.isDir) {
+ continue;
+ }
+
+ e.name = strdup(e.name);
+
+ if (!e.name) {
+ fileOpsListFree(entries, (int32_t)arrlen(entries));
+ return BAS_NO_HANDLE;
+ }
+
+ arrput(entries, e);
+ }
+
+ int32_t count = (int32_t)arrlen(entries);
+
+ fileOpsListSort(entries, count, FileOpsSortNameE, false);
+
+ int32_t handle = handleNew(entries, count, NULL);
+
+ if (handle == BAS_NO_HANDLE) {
+ fileOpsListFree(entries, count);
+ }
+
+ return handle;
+}
+
+
void FileOpsListFree(int32_t list) {
if (listOf(list)) {
basHandleFree(&sHandles, list);
@@ -774,9 +838,16 @@ int32_t FileOpsTreeFolders(void) {
// Totals of a file, or of a folder and everything in it (not counting the
// folder itself), for FileOpsTreeBytes / Files / Folders. True when every
-// part could be read.
+// part could be read. A measure job, as fileOpsTreeSize, but letting DVX
+// go on as it works (runJob).
int32_t FileOpsTreeSize(const char *path) {
- return fileOpsTreeSize(path, &sTreeBytes, &sTreeFiles, &sTreeFolders) ? BAS_TRUE : BAS_FALSE;
+ FileOpsProgressT p;
+ bool ok = runJob(fileOpsJobCreate(FileOpsMeasureE, NULL), path, false, &p) && p.unread == 0;
+
+ sTreeBytes = p.bytesTotal;
+ sTreeFiles = p.filesTotal;
+ sTreeFolders = p.foldersTotal;
+ return ok ? BAS_TRUE : BAS_FALSE;
}
@@ -908,18 +979,67 @@ static int32_t nameResult(const char *problem) {
}
-// Run one job over one source; True only when nothing failed.
-static int32_t runTree(FileOpsOpE op, const char *src, const char *destDir, bool yesToAll) {
- FileOpsJobT *job = fileOpsJobCreate(op, destDir);
+// Run job (NULL when out of memory) over source src to its end and free
+// it; true only when it finished with nothing skipped. out (unless NULL)
+// gets its totals (current is ""). The job is a handle while it runs, so
+// it is freed if the program is ended during a pump (treeProgress).
+static bool runJob(FileOpsJobT *job, const char *src, bool yesToAll, FileOpsProgressT *out) {
+ int32_t handle = job ? handleNew(NULL, 0, job) : BAS_NO_HANDLE;
- if (!job) {
- return BAS_FALSE;
+ if (out) {
+ memset(out, 0, sizeof(*out));
+ out->current = "";
}
- BasRunT run = { yesToAll, 0 };
- FileOpsCallbacksT cb = { NULL, basAnswer, &run };
- bool ok = fileOpsJobAddSource(job, src) && fileOpsJobRun(job, &cb) == FileOpsStepDoneE && run.problems == 0;
+ if (handle == BAS_NO_HANDLE) {
+ fileOpsJobFree(job);
+ return false;
+ }
- fileOpsJobFree(job);
- return ok ? BAS_TRUE : BAS_FALSE;
+ BasRunT run;
+
+ memset(&run, 0, sizeof(run));
+ run.yesToAll = yesToAll;
+
+ FileOpsCallbacksT cb = { treeProgress, basAnswer, &run };
+ bool ok = fileOpsJobAddSource(job, src) && fileOpsJobRun(job, &cb) == FileOpsStepDoneE && run.problems == 0;
+
+ if (out) {
+ fileOpsJobProgress(job, out);
+ out->current = "";
+ }
+
+ basHandleFree(&sHandles, handle);
+ return ok;
+}
+
+
+// A whole-tree copy, move or delete of src.
+static int32_t runTree(FileOpsOpE op, const char *src, const char *destDir, bool yesToAll) {
+ return runJob(fileOpsJobCreate(op, destDir), src, yesToAll, NULL) ? BAS_TRUE : BAS_FALSE;
+}
+
+
+// A whole-tree call's progress: at the first step and then every
+// TREE_PUMP_MS DVX and the program's events go on (dvxWaitPump, as
+// DoEvents). When the first pump says stop (code that may not wait) the
+// job runs to its end without pumps; when a later one does, the program
+// is ending and the job is cancelled.
+static bool treeProgress(FileOpsJobT *job, void *ctx) {
+ BasRunT *run = (BasRunT *)ctx;
+
+ (void)job;
+
+ if (run->noPump || (run->pumped && platformClock() - run->pumpAt < PLATFORM_MS_TO_TICKS(TREE_PUMP_MS))) {
+ return true;
+ }
+
+ if (dvxWaitPump()) {
+ run->pumped = true;
+ run->pumpAt = platformClock();
+ return true;
+ }
+
+ run->noPump = true;
+ return !run->pumped;
}
diff --git a/src/libs/kpunch/fileops/fileOpsSys.c b/src/libs/kpunch/fileops/fileOpsSys.c
index 25360f62..854d02ff 100644
--- a/src/libs/kpunch/fileops/fileOpsSys.c
+++ b/src/libs/kpunch/fileops/fileOpsSys.c
@@ -40,7 +40,9 @@
#include "fileOpsSys.h"
#include "dvxPlat.h"
+#include
#include
+#include
#include
#include
@@ -82,6 +84,18 @@
#define DOS_TIME_HOUR_MASK 0x1F
#define DOS_TIME_MIN_MASK 0x3F
#define DOS_TIME_SEC_MASK 0x1F
+#define MINUTES_PER_HOUR 60
+#define SECONDS_PER_MINUTE 60
+
+// dosTime's cache: DOS_TIME_CACHE_SLOTS hours, direct mapped by a
+// Fibonacci hash of the key (date in the high half, the hour's bits of the
+// time in the low, DOS_TIME_KEY_USED set: an empty slot is 0)
+#define DOS_TIME_CACHE_BITS 5
+#define DOS_TIME_CACHE_SLOTS (1 << DOS_TIME_CACHE_BITS)
+#define DOS_TIME_HASH_MUL 2654435761u
+#define DOS_TIME_HASH_BITS 32 // the width of the hash (uint32_t)
+#define DOS_TIME_KEY_DATE_SHIFT 16
+#define DOS_TIME_KEY_USED 1u
// Find first / find next: every file and folder (not the volume label),
// hidden and system ones too, as readdir lists them
@@ -99,6 +113,35 @@
#endif
+// ============================================================
+// Types
+// ============================================================
+
+#ifdef __DJGPP__
+// The hours dosTime has converted during one scan: the time of the start
+// of each. A folder's entries mostly share a few dates, and mktime, which
+// searches for the time it is given, costs far more than reading an entry.
+typedef struct {
+ uint32_t key[DOS_TIME_CACHE_SLOTS];
+ time_t start[DOS_TIME_CACHE_SLOTS];
+} DosTimeCacheT;
+#endif
+
+// A folder being read (fileOpsSysOpen).
+struct FileOpsSysDirS {
+#ifdef __DJGPP__
+ struct ffblk ff; // the search, and the entry it is on
+ DosTimeCacheT times;
+ int rc; // the last find first / find next: 0 = ff holds an entry
+ bool taken; // ff's entry was reported: find the next one first
+ bool lower; // 8.3 names in lower case (FNCASE not set)
+#else
+ DIR *dir;
+ char path[DVX_MAX_PATH];
+ bool followLinks;
+#endif
+};
+
// ============================================================
// Prototypes
// ============================================================
@@ -106,7 +149,8 @@
#ifdef __DJGPP__
static void attrCall(const char *path, bool set, __dpmi_regs *r);
static bool dosInt(uint32_t intNo, __dpmi_regs *r);
-static time_t dosTime(uint16_t date, uint16_t time);
+static time_t dosMktime(uint16_t date, int32_t hour, int32_t minute, int32_t second);
+static time_t dosTime(DosTimeCacheT *cache, uint16_t date, uint16_t time);
static bool putPath(const char *path, __dpmi_regs *r);
#endif
@@ -148,21 +192,51 @@ static bool dosInt(uint32_t intNo, __dpmi_regs *r) {
#ifdef __DJGPP__
-// A DOS packed date and time as local time, exactly as the C library's
-// stat converts them (mktime decides daylight saving time).
-static time_t dosTime(uint16_t date, uint16_t time) {
+// A DOS date with this hour, minute and second as local time, as the C
+// library's stat converts them: mktime normalizes fields out of range and
+// decides daylight saving time.
+static time_t dosMktime(uint16_t date, int32_t hour, int32_t minute, int32_t second) {
struct tm tm;
memset(&tm, 0, sizeof(tm));
tm.tm_isdst = -1;
- tm.tm_sec = (time & DOS_TIME_SEC_MASK) * DOS_TIME_SEC_DIVISOR;
- tm.tm_min = (time >> DOS_TIME_MIN_SHIFT) & DOS_TIME_MIN_MASK;
- tm.tm_hour = (time >> DOS_TIME_HOUR_SHIFT) & DOS_TIME_HOUR_MASK;
+ tm.tm_sec = second;
+ tm.tm_min = minute;
+ tm.tm_hour = hour;
tm.tm_mday = date & DOS_DATE_DAY_MASK;
tm.tm_mon = ((date >> DOS_DATE_MONTH_SHIFT) & DOS_DATE_MONTH_MASK) - 1;
tm.tm_year = (date >> DOS_DATE_YEAR_SHIFT) + DOS_YEAR_BASE - TM_YEAR_BASE;
return mktime(&tm);
}
+
+
+// A DOS packed date and time as local time (dosMktime): the start of its
+// hour, from cache or converted once and kept there, plus the minutes and
+// seconds. Clocks change on the hour, so that is mktime's answer but in a
+// zone that moves them by half an hour; a minute or second out of range
+// (which may fall in the next hour) is converted whole.
+static time_t dosTime(DosTimeCacheT *cache, uint16_t date, uint16_t time) {
+ int32_t hour = (time >> DOS_TIME_HOUR_SHIFT) & DOS_TIME_HOUR_MASK;
+ int32_t minute = (time >> DOS_TIME_MIN_SHIFT) & DOS_TIME_MIN_MASK;
+ int32_t second = (time & DOS_TIME_SEC_MASK) * DOS_TIME_SEC_DIVISOR;
+ uint32_t key = ((uint32_t)date << DOS_TIME_KEY_DATE_SHIFT) | ((uint32_t)hour << DOS_TIME_HOUR_SHIFT) | DOS_TIME_KEY_USED;
+ uint32_t slot = (key * DOS_TIME_HASH_MUL) >> (DOS_TIME_HASH_BITS - DOS_TIME_CACHE_BITS);
+
+ if (minute >= MINUTES_PER_HOUR || second >= SECONDS_PER_MINUTE) {
+ return dosMktime(date, hour, minute, second);
+ }
+
+ if (cache->key[slot] != key) {
+ cache->key[slot] = key;
+ cache->start[slot] = dosMktime(date, hour, 0, 0);
+ }
+
+ if (cache->start[slot] == (time_t)-1) {
+ return (time_t)-1;
+ }
+
+ return cache->start[slot] + minute * SECONDS_PER_MINUTE + second;
+}
#endif
@@ -279,42 +353,49 @@ bool fileOpsSetTime(const char *path, time_t mtime) {
}
-bool fileOpsSysScan(const char *dir, bool followLinks, FileOpsScanFnT fn, void *ctx) {
- bool more = true;
+// A search not run to its end is run to it, so the C library closes a
+// long-name search handle (it has no call that closes one early).
+void fileOpsSysClose(FileOpsSysDirT *d) {
+ if (!d) {
+ return;
+ }
#ifdef __DJGPP__
- char pattern[DVX_MAX_PATH];
- struct ffblk ff;
- bool lower = !_preserve_fncase();
-
- (void)followLinks;
-
- if (!fileOpsPathJoin(pattern, sizeof(pattern), dir, DOS_FIND_ALL)) {
- return false;
+ while (d->rc == 0) {
+ d->rc = findnext(&d->ff);
}
+#else
+ closedir(d->dir);
+#endif
- int rc = findfirst(pattern, &ff, DOS_FIND_ATTR);
+ free(d);
+}
- // Nothing found: an empty root folder, or no folder at all
- if (rc != 0) {
- struct stat st;
- return stat(dir, &st) == 0 && S_ISDIR(st.st_mode);
- }
+bool fileOpsSysNext(FileOpsSysDirT *d, FileOpsSysEntryT *e) {
+#ifdef __DJGPP__
+ for (;;) {
+ if (d->taken) {
+ d->rc = findnext(&d->ff);
+ d->taken = false;
+ }
- // The search runs to its end even after the callback said stop, so a
- // long-name search handle is always closed by the library.
- for (; rc == 0; rc = findnext(&ff)) {
- char *name = ff.ff_name;
+ if (d->rc != 0) {
+ return false;
+ }
+
+ char *name = d->ff.ff_name;
char shortName[DOS_SHORT_NAME_MAX];
- if (!more || strcmp(name, ".") == 0 || strcmp(name, "..") == 0) {
+ d->taken = true;
+
+ if (strcmp(name, ".") == 0 || strcmp(name, "..") == 0) {
continue;
}
// readdir's rule: a name that is its own short (8.3) name is shown
// in lower case
- if (lower && strcmp(_lfn_gen_short_fname(name, shortName), name) == 0) {
+ if (d->lower && strcmp(_lfn_gen_short_fname(name, shortName), name) == 0) {
for (char *c = name; *c; c++) {
if (*c >= 'A' && *c <= 'Z') {
*c = (char)(*c - 'A' + 'a');
@@ -322,26 +403,16 @@ bool fileOpsSysScan(const char *dir, bool followLinks, FileOpsScanFnT fn, void *
}
}
- FileOpsSysEntryT e;
-
- e.name = name;
- e.isDir = (ff.ff_attrib & FA_DIREC) != 0;
- e.size = e.isDir ? 0 : (uint32_t)ff.ff_fsize; // DOS sizes are unsigned (up to 4 GB - 1)
- e.mtime = dosTime(ff.ff_fdate, ff.ff_ftime);
- more = fn(&e, ctx);
+ e->name = name;
+ e->isDir = (d->ff.ff_attrib & FA_DIREC) != 0;
+ e->size = e->isDir ? 0 : (uint32_t)d->ff.ff_fsize; // DOS sizes are unsigned (up to 4 GB - 1)
+ e->mtime = dosTime(&d->times, d->ff.ff_fdate, d->ff.ff_ftime);
+ return true;
}
-
- return true;
#else
- DIR *d = opendir(dir);
-
- if (!d) {
- return false;
- }
-
struct dirent *ent;
- while (more && (ent = readdir(d)) != NULL) {
+ while ((ent = readdir(d->dir)) != NULL) {
char path[DVX_MAX_PATH];
struct stat st;
@@ -349,25 +420,103 @@ bool fileOpsSysScan(const char *dir, bool followLinks, FileOpsScanFnT fn, void *
continue;
}
- if (!fileOpsPathJoin(path, sizeof(path), dir, ent->d_name) || (followLinks ? stat(path, &st) : lstat(path, &st)) != 0) {
+ if (!fileOpsPathJoin(path, sizeof(path), d->path, ent->d_name) || (d->followLinks ? stat(path, &st) : lstat(path, &st)) != 0) {
continue;
}
- FileOpsSysEntryT e;
-
- e.name = ent->d_name;
- e.isDir = S_ISDIR(st.st_mode);
- e.size = e.isDir ? 0 : platformStatSize(&st);
- e.mtime = st.st_mtime;
- more = fn(&e, ctx);
+ e->name = ent->d_name;
+ e->isDir = S_ISDIR(st.st_mode);
+ e->size = e->isDir ? 0 : platformStatSize(&st);
+ e->mtime = st.st_mtime;
+ return true;
}
- closedir(d);
- return true;
+ return false;
#endif
}
+FileOpsSysDirT *fileOpsSysOpen(const char *dir, bool followLinks) {
+ FileOpsSysDirT *d = (FileOpsSysDirT *)calloc(1, sizeof(FileOpsSysDirT));
+
+ if (!d) {
+ errno = ENOMEM;
+ return NULL;
+ }
+
+#ifdef __DJGPP__
+ char pattern[DVX_MAX_PATH];
+
+ (void)followLinks;
+ d->lower = !_preserve_fncase();
+
+ if (!fileOpsPathJoin(pattern, sizeof(pattern), dir, DOS_FIND_ALL)) {
+ free(d);
+ errno = ENAMETOOLONG;
+ return NULL;
+ }
+
+ d->rc = findfirst(pattern, &d->ff, DOS_FIND_ATTR);
+
+ // Nothing found: an empty root folder, or no folder at all
+ if (d->rc != 0) {
+ struct stat st;
+ int32_t err = ENOTDIR;
+
+ if (stat(dir, &st) != 0) {
+ err = errno;
+ } else if (S_ISDIR(st.st_mode)) {
+ err = 0;
+ }
+
+ if (err != 0) {
+ free(d);
+ errno = err;
+ return NULL;
+ }
+ }
+#else
+ if (snprintf(d->path, sizeof(d->path), "%s", dir) >= (int)sizeof(d->path)) {
+ free(d);
+ errno = ENAMETOOLONG;
+ return NULL;
+ }
+
+ d->followLinks = followLinks;
+ d->dir = opendir(dir);
+
+ if (!d->dir) {
+ int32_t err = errno;
+
+ free(d);
+ errno = err;
+ return NULL;
+ }
+#endif
+
+ return d;
+}
+
+
+bool fileOpsSysScan(const char *dir, bool followLinks, FileOpsScanFnT fn, void *ctx) {
+ FileOpsSysDirT *d = fileOpsSysOpen(dir, followLinks);
+ FileOpsSysEntryT e;
+
+ if (!d) {
+ return false;
+ }
+
+ while (fileOpsSysNext(d, &e)) {
+ if (!fn(&e, ctx)) {
+ break;
+ }
+ }
+
+ fileOpsSysClose(d);
+ return true;
+}
+
+
#ifdef __DJGPP__
// Copy a NUL-terminated path into the transfer buffer and point DS:DX
// at it, in the form DOS takes (platformPathTidy: the C library is not in
diff --git a/src/libs/kpunch/fileops/fileOpsSys.h b/src/libs/kpunch/fileops/fileOpsSys.h
index 5d89b619..8863be13 100644
--- a/src/libs/kpunch/fileops/fileOpsSys.h
+++ b/src/libs/kpunch/fileops/fileOpsSys.h
@@ -31,17 +31,31 @@
#include
#include
-// One folder entry as fileOpsSysScan reports it.
+// One folder entry as fileOpsSysScan and fileOpsSysNext report it.
typedef struct {
- const char *name; // leaf name, valid during the callback
+ const char *name; // leaf name: valid during the callback, or until the next fileOpsSysNext
uint64_t size; // bytes (0 for a folder)
time_t mtime; // last modification
bool isDir;
} FileOpsSysEntryT;
+// A folder being read a piece at a time (fileOpsSysOpen).
+typedef struct FileOpsSysDirS FileOpsSysDirT;
+
// Called for each entry; false stops the scan (no more calls).
typedef bool (*FileOpsScanFnT)(const FileOpsSysEntryT *e, void *ctx);
+// Stop reading a folder and free d (NULL is ignored). On DOS a search
+// not at its end is first run to it, which costs the rest of the folder.
+void fileOpsSysClose(FileOpsSysDirT *d);
+
+// The next entry of d into e; false at the end.
+bool fileOpsSysNext(FileOpsSysDirT *d, FileOpsSysEntryT *e);
+
+// Start reading folder dir for fileOpsSysNext, which reports what
+// fileOpsSysScan does. NULL (errno set) when it cannot be read.
+FileOpsSysDirT *fileOpsSysOpen(const char *dir, bool followLinks);
+
// Every entry of dir except "." and ".." (hidden ones too) with its size,
// date and kind. DOS reads them in one pass of the folder (find first /
// find next, which report all of that), not one stat per entry -- a stat
@@ -49,7 +63,7 @@ typedef bool (*FileOpsScanFnT)(const FileOpsSysEntryT *e, void *ctx);
// names come back as readdir gives them (8.3 names in lower case unless
// FNCASE is set). Elsewhere readdir and stat (lstat unless followLinks:
// a link is then an entry of its own); an entry that cannot be read
-// there is left out. False when dir cannot be read.
-bool fileOpsSysScan(const char *dir, bool followLinks, FileOpsScanFnT fn, void *ctx);
+// there is left out. False (errno set) when dir cannot be read.
+bool fileOpsSysScan(const char *dir, bool followLinks, FileOpsScanFnT fn, void *ctx);
#endif // FILE_OPS_SYS_H
diff --git a/src/libs/kpunch/fileops/fileops.dhs b/src/libs/kpunch/fileops/fileops.dhs
index 1afc00ba..15491132 100644
--- a/src/libs/kpunch/fileops/fileops.dhs
+++ b/src/libs/kpunch/fileops/fileops.dhs
@@ -72,7 +72,7 @@ Disks past 2 GB report their real size and free space: platformDiskSpace asks DO
fileOpsFormatSize(bytes, out, size) "1,234,567".
fileOpsFormatDate(t, out, size) "YYYY-MM-DD HH:MM" in local time.
fileOpsParseDate(text, &t) The time fileOpsFormatDate's text stands for ("YYYY-MM-DD HH:MM", optionally ":SS"), local time; false for anything else, a date that does not exist included.
- fileOpsTreeSize(path, &bytes, &files, &folders) Total size of a file or of a folder and everything in it.
+ fileOpsTreeSize(path, &bytes, &files, &folders) Total size of a file or of a folder and everything in it (the folders inside it; a measure job run to its end). False when part of it cannot be read; the totals then cover what was read.
fileOpsMakeDir(dir, name, err, errSize) Create a folder; a reason in err on failure.
fileOpsRename(dir, oldName, newName, err, errSize) Rename within a folder; refuses a name already in use.
fileOpsValidateName(dir, name, err, errSize) Check a new name for folder dir by the rules of its disk (platformValidateFilename): on DOS long names where the drive takes them (platformLongNames), else 8.3; never a device name.
@@ -86,7 +86,7 @@ Disks past 2 GB report their real size and free space: platformDiskSpace asks DO
.h2 Jobs
-A job copies, moves, or deletes any number of files and folders (recursively):
+A job copies, moves, deletes, or measures (FileOpsMeasureE: counts the files, folders, and bytes, changing nothing) any number of files and folders (recursively):
.code
FileOpsJobT *job = fileOpsJobCreate(FileOpsCopyE, "C:\\BACKUP");
@@ -114,7 +114,7 @@ for (;;) {
fileOpsJobFree(job);
.endcode
-Each fileOpsJobStep does one bounded piece of work: read one folder, copy one FILEOPS_COPY_CHUNK of one file, or delete one item. A cooperative program yields between steps.
+Each fileOpsJobStep does one bounded piece of work: read a few dozen entries of a folder (a big folder takes several steps), copy one FILEOPS_COPY_CHUNK of one file, or delete one item. A cooperative program yields between steps.
.table
Step result Meaning
@@ -126,10 +126,11 @@ Each fileOpsJobStep does one bounded piece of work: read one folder, copy one FI
FileOpsStepCancelledE Stopped by Cancel.
.endtable
-Until it is answered, the job keeps returning the same question. What the job does:
+Until it is answered, the job keeps returning the same question. The next step tries the item again and applies the answer if it fails again; an item that goes through leaves the answer unused (it is never kept for a later question). Cancel stops at once, without trying again. What the job does:
.list
-.item Scan: each folder is read once and its files are counted, so fileOpsJobProgress (FileOpsProgressT: files and bytes done and total, percent, current path) is accurate while the transfer runs.
+.item Scan: each folder is read once and its files are counted, so fileOpsJobProgress (FileOpsProgressT: files and bytes done and total, folders inside the sources, percent, current path) is accurate while the transfer runs. A folder that fails part way keeps nothing of what was read before the question.
+.item Measure: only the scan, and it asks nothing: a folder it cannot read is skipped and counted in FileOpsProgressT.unread (what was read of it stays counted).
.item Copy: folders are created before their contents, files are copied in chunks and keep their date and attributes (plus Archive). A read-only destination is made writable before it is replaced.
.item Move: a top-level item whose destination does not exist yet is renamed in one go. Otherwise each file is copied and then its source removed, and the emptied source folders are removed at the end. A folder that still holds skipped items is kept.
.item Delete: files first, deepest first, then folders; read-only files ask first.
@@ -143,6 +144,6 @@ fileOpsJobRun(job, callbacks) runs a job to the end instead: progress(job, ctx)
.h2 BASIC
-src/include/basic/fileops.bas declares the BASIC functions (fileOpsBas.c): whole trees at once (FileOpsCopyTree, FileOpsMoveTree, FileOpsDeleteTree), drives and disks (FileOpsDriveList$, FileOpsDriveType (the PlatformDriveTypeE of platformDriveList, CONSTs FILEOPS_DRIVE_*; -1 for no such drive), FileOpsBootDrive$ (platformBootDrive, "" when none), FileOpsFreeKb, FileOpsFreeBytes, and FileOpsTotalBytes (platformDiskSpace), FileOpsIsWritable (platformDirIsWritable), FileOpsReadOnlySystem (platformIsReadOnly), FileOpsTempDir$ (platformTempDir, "" when none)), DVX's folders (FileOpsUserDir$ (platformUserDir), FileOpsUserPath$ (platformUserPath, "" when it does not fit), FileOpsUserInTemp (platformUserInTemp), and for the kinds of DvxKindE (CONSTs FILEOPS_KIND_*) FileOpsKindDir$ (platformKindDir), FileOpsKindBrowseDir$ (platformKindBrowseDir), FileOpsKindFind$ (platformKindFind), FileOpsKindName$ (platformKindNameOf), and FileOpsKindList (platformKindReadDir as a listing, sorted by name; "" or 0 for a kind out of range)), folder listings (FileOpsList, FileOpsListCount, FileOpsListSort, FileOpsListFind, FileOpsListName$, FileOpsListIsFolder, FileOpsListSize, FileOpsListRows$, FileOpsListFree), paths and names (FileOpsPathNormalize$, FileOpsPathParent$, FileOpsPathJoin$, FileOpsPathSame, FileOpsPathIsRoot, FileOpsPathIsAbsolute (platformPathIsAbsolute), FileOpsPathIsInside, FileOpsPathSep$ (DVX_PATH_SEP), FileOpsPathNative$ (platformPathNative), FileOpsFilterMatch (platformFilterMatch), FileOpsBaseName$, FileOpsExtension$), files and folders (FileOpsMakeDir, FileOpsMakePath (platformMkdirRecursive), FileOpsRename, FileOpsError$, FileOpsTempPath$ and FileOpsReplaceFile (an atomic save: platformPathSibling + platformReplaceFile), FileOpsValidName (platformValidateFilename for a name in a folder), FileOpsValidLongName and FileOpsValidShortName (platformValidateLongName / platformValidateShortName, the rules alone), FileOpsLongNames (platformLongNames), FileOpsShortPath$ (platformShortPath, "" when it does not fit), FileOpsIsFolder, FileOpsGetAttr, FileOpsSetAttr, FileOpsModified$, FileOpsSetModified (fileOpsParseDate + fileOpsSetTime), FileOpsTreeSize with FileOpsTreeBytes / FileOpsTreeFiles / FileOpsTreeFolders, FileOpsFormatSize$), and jobs (FileOpsJobCreate, FileOpsJobAddSource, FileOpsJobClearAttr (masked to FILEOPS_ATTR_EDITABLE), FileOpsJobRunFor, FileOpsJobMessage$, FileOpsJobAnswer, FileOpsJobCancel, FileOpsJobCurrent$, FileOpsJobFilesDone, FileOpsJobFilesTotal, FileOpsJobPercent, FileOpsJobBytesDone, FileOpsJobBytesTotal (FileOpsProgressT; byte counts are DOUBLE), FileOpsJobFree). The fileops.bas CONSTs mirror fileOps.h and dvxPlat.h; the basdev test suite (caseIncludeConsts) checks each against its C value.
+src/include/basic/fileops.bas declares the BASIC functions (fileOpsBas.c): whole trees at once (FileOpsCopyTree, FileOpsMoveTree, FileOpsDeleteTree: a job run to its end, letting DVX and the program's events go on through dvxWaitPump at the first step and every 50 ms after; when the first pump says stop the job runs on without pumps, when a later one does the program is ending and the call is cancelled), drives and disks (FileOpsDriveList$, FileOpsDriveType (the PlatformDriveTypeE of platformDriveList, CONSTs FILEOPS_DRIVE_*; -1 for no such drive), FileOpsBootDrive$ (platformBootDrive, "" when none), FileOpsFreeKb, FileOpsFreeBytes, and FileOpsTotalBytes (platformDiskSpace), FileOpsIsWritable (platformDirIsWritable), FileOpsReadOnlySystem (platformIsReadOnly), FileOpsTempDir$ (platformTempDir, "" when none)), DVX's folders (FileOpsUserDir$ (platformUserDir), FileOpsUserPath$ (platformUserPath, "" when it does not fit), FileOpsUserInTemp (platformUserInTemp), and for the kinds of DvxKindE (CONSTs FILEOPS_KIND_*) FileOpsKindDir$ (platformKindDir), FileOpsKindBrowseDir$ (platformKindBrowseDir), FileOpsKindFind$ (platformKindFind), FileOpsKindName$ (platformKindNameOf), and FileOpsKindList (platformKindReadDir as a listing, sorted by name; "" or 0 for a kind out of range)), folder listings (FileOpsList, FileOpsListCount, FileOpsListSort, FileOpsListFind, FileOpsListFolders (a new listing of a listing's folders, by name, from memory), FileOpsListName$, FileOpsListIsFolder, FileOpsListSize, FileOpsListRows$, FileOpsListFree), paths and names (FileOpsPathNormalize$, FileOpsPathParent$, FileOpsPathJoin$, FileOpsPathSame, FileOpsPathIsRoot, FileOpsPathIsAbsolute (platformPathIsAbsolute), FileOpsPathIsInside, FileOpsPathSep$ (DVX_PATH_SEP), FileOpsPathNative$ (platformPathNative), FileOpsFilterMatch (platformFilterMatch), FileOpsBaseName$, FileOpsExtension$), files and folders (FileOpsMakeDir, FileOpsMakePath (platformMkdirRecursive), FileOpsRename, FileOpsError$, FileOpsTempPath$ and FileOpsReplaceFile (an atomic save: platformPathSibling + platformReplaceFile), FileOpsValidName (platformValidateFilename for a name in a folder), FileOpsValidLongName and FileOpsValidShortName (platformValidateLongName / platformValidateShortName, the rules alone), FileOpsLongNames (platformLongNames), FileOpsShortPath$ (platformShortPath, "" when it does not fit), FileOpsIsFolder, FileOpsGetAttr, FileOpsSetAttr, FileOpsModified$, FileOpsSetModified (fileOpsParseDate + fileOpsSetTime), FileOpsTreeSize (a measure job pumping as the whole-tree calls do) with FileOpsTreeBytes / FileOpsTreeFiles / FileOpsTreeFolders, FileOpsFormatSize$), and jobs (FileOpsJobCreate (FILEOPS_COPY, FILEOPS_MOVE, FILEOPS_DELETE, or FILEOPS_MEASURE), FileOpsJobAddSource, FileOpsJobClearAttr (masked to FILEOPS_ATTR_EDITABLE), FileOpsJobRunFor, FileOpsJobMessage$, FileOpsJobAnswer, FileOpsJobCancel, FileOpsJobCurrent$, FileOpsJobFilesDone, FileOpsJobFilesTotal, FileOpsJobFoldersTotal, FileOpsJobUnread, FileOpsJobPercent, FileOpsJobBytesDone, FileOpsJobBytesTotal (FileOpsProgressT; byte counts are DOUBLE), FileOpsJobFree). The fileops.bas CONSTs mirror fileOps.h and dvxPlat.h; the basdev test suite (caseIncludeConsts) checks each against its C value.
Listings and jobs are handles (small numbers, 0 = none) into a table owned by the library; each records the app that made it, and the shell's app-cleanup hook frees what an app leaves open when it ends. FileOpsJobRunFor(job, ms) steps the job until the time is used or it needs an answer, so a BASIC Timer can run it without blocking the program. A returned string stays valid until the next call returning one (the BASIC runtime copies it at once). Single files are handled by the built-in FILECOPY, KILL, NAME, MKDIR, RMDIR, GETATTR, and SETATTR statements.
diff --git a/src/libs/kpunch/game/README.md b/src/libs/kpunch/game/README.md
new file mode 100644
index 00000000..6da25984
--- /dev/null
+++ b/src/libs/kpunch/game/README.md
@@ -0,0 +1,23 @@
+# game -- Game Helpers
+
+Helpers for programs that run like games inside a DVX window: frame
+pacing on the high-resolution clock (yielding while it waits), held
+keys, timer-service callbacks owned by the program, and nearest-colour
+lookups on a 256-colour desktop. BASIC reaches it through FRAMEWAIT and
+src/include/basic/game.bas.
+
+## Documentation
+
+Full reference is in `game.dhs` (this directory), compiled into the DVX
+System Reference. Open via the Help Viewer app, or read
+`docs/dvx_system_reference.html` after `make`.
+
+## Source Files
+
+- `game.h` -- public API
+- `game.c` -- frames, keys, timers, colours
+- `gameBas.c` -- BASIC entry points (game.bas)
+
+## Build
+
+`make -C src/libs/kpunch/game` (invoked by the top-level `make`).
diff --git a/src/libs/kpunch/game/game.c b/src/libs/kpunch/game/game.c
index a50471cf..949b1222 100644
--- a/src/libs/kpunch/game/game.c
+++ b/src/libs/kpunch/game/game.c
@@ -108,13 +108,15 @@ uint64_t gameFrameNext(GameFramePacerT *p, uint32_t *missed) {
}
+// Yields at least once, even when the frame is already due: a game too
+// slow for its rate (every frame late) must not keep the CPU for good.
uint32_t gameFrameWait(GameFramePacerT *p) {
uint32_t missed = 0;
uint64_t due = gameFrameNext(p, &missed);
- while (platformMicros() < due) {
+ do {
frameYield();
- }
+ } while (platformMicros() < due);
return missed;
}
diff --git a/src/libs/kpunch/game/game.h b/src/libs/kpunch/game/game.h
index d651c5b9..47ac0c38 100644
--- a/src/libs/kpunch/game/game.h
+++ b/src/libs/kpunch/game/game.h
@@ -81,7 +81,8 @@ void gameFrameInit(GameFramePacerT *p, uint32_t hz);
// number of frames missed: 0 when on time, else the frames that already
// ended while the caller was busy, which are skipped rather than caught
// up. A frame that starts late but within its own time counts as on
-// time and returns at once.
+// time. The other programs get a turn even then (a game too slow for its
+// rate never keeps the CPU for good).
uint32_t gameFrameWait(GameFramePacerT *p);
// gameFrameWait without the waiting, for a caller that waits its own way
diff --git a/src/libs/kpunch/hlpfile/README.md b/src/libs/kpunch/hlpfile/README.md
new file mode 100644
index 00000000..ebe75c41
--- /dev/null
+++ b/src/libs/kpunch/hlpfile/README.md
@@ -0,0 +1,23 @@
+# hlpfile -- Help File Reader
+
+Reads compiled DVX help files (.hlp): topics, the table of contents, the
+keyword index, a topic's content records, images, and full-text search.
+The Help Viewer, the HelpPane widget, and BASIC's hlpfile.bas all read
+help files through it.
+
+## Documentation
+
+Full reference is in `hlpfile.dhs` (this directory), compiled into the
+DVX System Reference. Open via the Help Viewer app, or read
+`docs/dvx_system_reference.html` after `make`.
+
+## Source Files
+
+- `hlpFile.h` -- public API (HlpFileT handles)
+- `hlpFormat.h` -- the .hlp file layout, shared with the help compiler
+- `hlpFile.c` -- the reader and search
+- `hlpFileBas.c` -- BASIC entry points (hlpfile.bas)
+
+## Build
+
+`make -C src/libs/kpunch/hlpfile` (invoked by the top-level `make`).
diff --git a/src/libs/kpunch/hlpfile/hlpFile.c b/src/libs/kpunch/hlpfile/hlpFile.c
index c580af22..11b6ddf3 100644
--- a/src/libs/kpunch/hlpfile/hlpFile.c
+++ b/src/libs/kpunch/hlpfile/hlpFile.c
@@ -35,6 +35,7 @@
#include "dvxPlat.h"
#include "dvxTypes.h"
+#include
#include
#include
#include
@@ -54,8 +55,11 @@ struct HlpFileT {
char *stringTable;
HlpTocEntryT *tocEntries;
HlpIndexEntryT *indexEntries;
- uint8_t *searchData; // search section, NULL if absent
+ uint8_t *searchData; // search section, NULL if absent (or not read yet)
uint32_t trigramCount;
+ bool searchTried; // searchIndexLoad has run (the first search)
+ void *cache; // hlpFileSetCache
+ void (*cacheFree)(void *cache);
};
// Search words for topicMatchRecord.
@@ -72,6 +76,7 @@ typedef struct {
static bool containsNoCase(const char *hay, const char *needle);
static const HlpTrigramEntryT *findTrigram(const HlpFileT *f, const uint8_t *tri);
+void *hlpFileCache(const HlpFileT *f);
void hlpFileClose(HlpFileT *f);
int32_t hlpFileFindTopic(const HlpFileT *f, const char *topicId);
bool hlpFileForEachRecord(HlpFileT *f, int32_t topic, HlpRecordFnT fn, void *userData);
@@ -80,6 +85,7 @@ int32_t hlpFileIndexCount(const HlpFileT *f);
HlpFileT *hlpFileOpen(const char *path);
uint8_t *hlpFileReadImage(HlpFileT *f, const HlpImageRefT *ref, uint32_t *outSize);
int32_t hlpFileSearch(HlpFileT *f, const char *query, uint8_t *matches);
+void hlpFileSetCache(HlpFileT *f, void *cache, void (*cacheFree)(void *cache));
int32_t hlpFileStartTopic(const HlpFileT *f, const char *topicId);
const char *hlpFileString(const HlpFileT *f, uint32_t offset);
const HlpTocEntryT *hlpFileToc(const HlpFileT *f, int32_t i);
@@ -92,17 +98,27 @@ bool hlpRecordLink(const HlpRecordHdrT *hdr, const cha
char *hlpWrapText(const char *text, int32_t cols);
static bool readSearchIndex(HlpFileT *f);
static bool readTable(HlpFileT *f, void **out, uint32_t offset, uint32_t count, uint32_t entrySize);
+static void searchIndexLoad(HlpFileT *f);
static int32_t searchRun(HlpFileT *f, HlpSearchT *search, const char *wholeQuery, uint8_t *matches, uint8_t *inPosting);
static bool topicMatchRecord(const HlpRecordHdrT *hdr, const char *payload, void *userData);
static void trigramFilter(const HlpFileT *f, const HlpSearchT *search, uint8_t *matches, uint8_t *inPosting, bool *titlesOnly);
-// Case-insensitive substring test.
+// Case-insensitive substring test. Only a place holding the needle's
+// first letter (either case) is compared whole, so most of the text is
+// passed over with two compares a character.
static bool containsNoCase(const char *hay, const char *needle) {
size_t needleLen = strlen(needle);
+ if (needleLen == 0) {
+ return hay[0] != '\0';
+ }
+
+ char lower = (char)tolower((unsigned char)needle[0]);
+ char upper = (char)toupper((unsigned char)needle[0]);
+
for (const char *p = hay; *p; p++) {
- if (strncasecmp(p, needle, needleLen) == 0) {
+ if ((*p == lower || *p == upper) && strncasecmp(p, needle, needleLen) == 0) {
return true;
}
}
@@ -137,11 +153,18 @@ static const HlpTrigramEntryT *findTrigram(const HlpFileT *f, const uint8_t *tri
}
+void *hlpFileCache(const HlpFileT *f) {
+ return f ? f->cache : NULL;
+}
+
+
void hlpFileClose(HlpFileT *f) {
if (!f) {
return;
}
+ hlpFileSetCache(f, NULL, NULL);
+
if (f->fp) {
fclose(f->fp);
}
@@ -277,14 +300,6 @@ HlpFileT *hlpFileOpen(const char *path) {
f->stringTable[f->header.stringTableSize - 1] = '\0';
}
- // A missing or damaged search index only makes searching slower
- // (every topic is scanned), so it does not fail the open.
- if (!readSearchIndex(f)) {
- free(f->searchData);
- f->searchData = NULL;
- f->trigramCount = 0;
- }
-
return f;
}
@@ -359,6 +374,20 @@ int32_t hlpFileSearch(HlpFileT *f, const char *query, uint8_t *matches) {
}
+void hlpFileSetCache(HlpFileT *f, void *cache, void (*cacheFree)(void *cache)) {
+ if (!f) {
+ return;
+ }
+
+ if (f->cacheFree) {
+ f->cacheFree(f->cache);
+ }
+
+ f->cache = cache;
+ f->cacheFree = cacheFree;
+}
+
+
int32_t hlpFileStartTopic(const HlpFileT *f, const char *topicId) {
if (!f) {
return -1;
@@ -563,6 +592,24 @@ static bool readTable(HlpFileT *f, void **out, uint32_t offset, uint32_t count,
}
+// The search section, read on the first search, so opening a file does
+// not pay for it. A missing or damaged one only makes searching slower
+// (every topic is scanned), so it is dropped.
+static void searchIndexLoad(HlpFileT *f) {
+ if (f->searchTried) {
+ return;
+ }
+
+ f->searchTried = true;
+
+ if (!readSearchIndex(f)) {
+ free(f->searchData);
+ f->searchData = NULL;
+ f->trigramCount = 0;
+ }
+}
+
+
// hlpFileSearch with the words split: the trigram filter, then each
// candidate checked word by word, then the keyword index.
static int32_t searchRun(HlpFileT *f, HlpSearchT *search, const char *wholeQuery, uint8_t *matches, uint8_t *inPosting) {
@@ -570,6 +617,7 @@ static int32_t searchRun(HlpFileT *f, HlpSearchT *search, const char *wholeQuery
int32_t matchCount = 0;
bool titlesOnly = false;
+ searchIndexLoad(f);
memset(matches, 0, topicCount);
if (search->termCount == 0) {
diff --git a/src/libs/kpunch/hlpfile/hlpFile.h b/src/libs/kpunch/hlpfile/hlpFile.h
index 1fc692a6..e0084eb6 100644
--- a/src/libs/kpunch/hlpfile/hlpFile.h
+++ b/src/libs/kpunch/hlpfile/hlpFile.h
@@ -51,7 +51,14 @@ typedef bool (*HlpRecordFnT)(const HlpRecordHdrT *hdr, const char *payload, void
// The help file behind a BASIC handle (hlpfile.bas HlpOpen), or NULL.
HlpFileT *hlpFileBasHandle(int32_t handle);
-// Close the file and free everything read from it (NULL is ignored).
+// What a caller keeps with an open file (a help view's decoded pictures,
+// say): cacheFree frees it when the file closes, or when another cache
+// is set. hlpFileCache is NULL when there is none.
+void *hlpFileCache(const HlpFileT *f);
+void hlpFileSetCache(HlpFileT *f, void *cache, void (*cacheFree)(void *cache));
+
+// Close the file and free everything read from it, and its cache (NULL is
+// ignored).
void hlpFileClose(HlpFileT *f);
// Index of the topic whose ID is topicId, or -1.
diff --git a/src/libs/kpunch/http/Makefile b/src/libs/kpunch/http/Makefile
index 9b24e22e..ab9a4e3a 100644
--- a/src/libs/kpunch/http/Makefile
+++ b/src/libs/kpunch/http/Makefile
@@ -27,7 +27,7 @@
include ../../../common.mk
-CFLAGS = $(DOS_CFLAGS) -I. -I../dvxnet/include -I../libdvx -I../libdvx/platform -I../libdvx/thirdparty
+CFLAGS = $(DOS_CFLAGS) -I. -I.. -I../dvxnet/include -I../libdvx -I../libdvx/platform -I../libdvx/thirdparty
OBJDIR = ../../../../obj/http
LIBSDIR = ../../../../bin/libs
diff --git a/src/libs/kpunch/http/http.c b/src/libs/kpunch/http/http.c
index f764048d..424d8d42 100644
--- a/src/libs/kpunch/http/http.c
+++ b/src/libs/kpunch/http/http.c
@@ -34,10 +34,10 @@
#include "http.h"
#include "dvxTypes.h"
#include "dvxPlat.h"
+#include "dvxnet/dvxNet.h"
#include "stb_ds_wrap.h"
#include
-#include
#include
#include
#include
@@ -98,7 +98,8 @@
typedef enum {
StepIdleE,
- StepConnectE,
+ StepConnectE, // a connection to start (fd NO_SOCKET) or under way
+ StepLookupE, // the host's address being looked up
StepSendE,
StepHeadE,
StepBodyE,
@@ -126,11 +127,18 @@ struct HttpS {
HttpUrlT target;
char **headers; // stb_ds of "Name: value" (strdup'd)
char *body; // the request's body (malloc'd), or NULL
- int32_t bodyLen;
+ int32_t bodyLen; // its bytes, or the body file's
+ char bodyPath[DVX_MAX_PATH]; // the body sent from this file ("": none)
+ FILE *bodyFile; // ... while it is sent
+ int32_t bodySent; // its bytes taken by the socket
+ int32_t bodyBufLen; // read from it
+ int32_t bodyBufPos; // of those, taken
+ char bodyBuf[READ_STEP];
char savePath[DVX_MAX_PATH]; // "": the body to memory
bool resume;
uint32_t timeoutMs;
StepE step;
+ NetLookupT lookup; // StepLookupE
int fd;
uint32_t lastMs; // when something last happened
char *out; // stb_ds: the request as sent
@@ -151,6 +159,8 @@ struct HttpS {
int32_t redirects;
char headerValue[HEADER_VALUE_MAX];
char err[HTTP_ERR_MAX];
+ int32_t polling; // httpPoll calls under way
+ bool freeAsked; // httpFree asked meanwhile: done when they return
};
DVX_ABI_STAMP(http);
@@ -160,6 +170,8 @@ DVX_ABI_STAMP(http);
// ============================================================
static bool bodyFeed(HttpT *h, const char *data, int32_t len);
+static void bodyFileClose(HttpT *h);
+static bool bodyFileSend(HttpT *h);
static bool bodyStart(HttpT *h);
static bool bodyWrite(HttpT *h, const char *data, int32_t len);
static bool buildRequest(HttpT *h);
@@ -171,8 +183,9 @@ static HttpStateE fail(HttpT *h, const char *fmt, ...) __attribute__((format(pr
static HttpStateE finish(HttpT *h);
static const char *headerFind(const char *head, const char *name, char *out, int32_t outSize);
static bool headParse(HttpT *h, int32_t *headLen);
-static bool hostConnect(HttpT *h);
+static bool hostConnect(HttpT *h, uint32_t addr);
const char *httpBody(const HttpT *h, int32_t *len);
+bool httpBodyFile(HttpT *h, const char *path, const char *contentType);
bool httpBodySet(HttpT *h, const void *data, int32_t len, const char *contentType);
HttpT *httpCreate(const char *method, const char *url);
void httpFree(HttpT *h);
@@ -189,6 +202,7 @@ const char *httpUrl(const HttpT *h);
bool httpUrlParse(const char *url, HttpUrlT *out, char *err, int32_t errSize);
static bool isRedirect(int32_t status);
static bool locationResolve(const HttpT *h, const char *loc, char *out, int32_t outSize);
+static HttpStateE pollStep(HttpT *h, uint32_t nowMs);
static void replyReset(HttpT *h);
static bool socketReady(int fd, short events);
static bool textAdd(char **buf, const char *fmt, ...) __attribute__((format(printf, 2, 3)));
@@ -233,6 +247,56 @@ static bool bodyFeed(HttpT *h, const char *data, int32_t len) {
}
+// The body file is closed (it is read from its start when sent again).
+static void bodyFileClose(HttpT *h) {
+ if (h->bodyFile) {
+ fclose(h->bodyFile);
+ }
+
+ h->bodyFile = NULL;
+ h->bodySent = 0;
+ h->bodyBufLen = 0;
+ h->bodyBufPos = 0;
+}
+
+
+// A piece of the body file sent: what the socket takes of the piece read
+// last, the next piece read once it took all of it. False when the file
+// cannot be read (or ends before the length the request gave).
+static bool bodyFileSend(HttpT *h) {
+ if (h->bodyBufPos == h->bodyBufLen) {
+ int32_t want = h->bodyLen - h->bodySent;
+ size_t n;
+
+ if (!h->bodyFile) {
+ h->bodyFile = fopen(h->bodyPath, "rb");
+ }
+
+ n = h->bodyFile ? fread(h->bodyBuf, 1, (size_t)(want < (int32_t)sizeof(h->bodyBuf) ? want : (int32_t)sizeof(h->bodyBuf)), h->bodyFile) : 0;
+
+ if (n == 0) {
+ return false;
+ }
+
+ h->bodyBufLen = (int32_t)n;
+ h->bodyBufPos = 0;
+ }
+
+ ssize_t sent = send(h->fd, h->bodyBuf + h->bodyBufPos, (size_t)(h->bodyBufLen - h->bodyBufPos), MSG_NOSIGNAL);
+
+ if (sent < 0 && !wouldBlock(errno)) {
+ return false;
+ }
+
+ if (sent > 0) {
+ h->bodyBufPos += (int32_t)sent;
+ h->bodySent += (int32_t)sent;
+ }
+
+ return true;
+}
+
+
// The head is read: where the body goes, how it comes, and how much of
// it there is. False (h failed) when the file cannot be opened.
static bool bodyStart(HttpT *h) {
@@ -302,6 +366,7 @@ static bool buildRequest(HttpT *h) {
bool ok;
arrfree(h->out);
+ bodyFileClose(h);
h->sent = 0;
ok = textAdd(&h->out, "%s %s HTTP/1.1" CRLF, h->method, h->target.path);
@@ -321,13 +386,14 @@ static bool buildRequest(HttpT *h) {
ok = ok && textAdd(&h->out, "%s" CRLF, h->headers[i]);
}
- if (h->body || strcmp(h->method, METHOD_POST) == 0 || strcmp(h->method, METHOD_PUT) == 0) {
+ if (h->body || h->bodyPath[0] || strcmp(h->method, METHOD_POST) == 0 || strcmp(h->method, METHOD_PUT) == 0) {
ok = ok && textAdd(&h->out, FIELD_LENGTH ": %ld" CRLF, (long)h->bodyLen);
}
ok = ok && textAdd(&h->out, CRLF);
- if (ok && h->bodyLen > 0) {
+ // A body file is sent after the head, a piece at a time
+ if (ok && h->body && h->bodyLen > 0) {
memcpy(arraddnptr(h->out, h->bodyLen), h->body, (size_t)h->bodyLen);
}
@@ -403,7 +469,10 @@ static bool chunkFeed(HttpT *h, const char *data, int32_t len, int32_t *used) {
}
+// The socket goes, and a lookup under way.
static void closeSocket(HttpT *h) {
+ netLookupCancel(&h->lookup);
+
if (h->fd != NO_SOCKET) {
close(h->fd);
h->fd = NO_SOCKET;
@@ -413,6 +482,8 @@ static void closeSocket(HttpT *h) {
// Look the host up and start connecting (StepConnectE then waits for the
// connection). False when h failed.
+// The request made, and the host's address looked up (pollStep waits for
+// it a poll at a time).
static bool connectStart(HttpT *h) {
replyReset(h);
@@ -421,7 +492,9 @@ static bool connectStart(HttpT *h) {
return false;
}
- return hostConnect(h);
+ netLookupStart(&h->lookup, h->target.host);
+ h->step = StepLookupE;
+ return true;
}
@@ -455,6 +528,7 @@ static HttpStateE fail(HttpT *h, const char *fmt, ...) {
va_end(ap);
h->step = StepFailedE;
closeSocket(h);
+ bodyFileClose(h);
if (h->file) {
fclose(h->file);
@@ -569,37 +643,28 @@ static bool headParse(HttpT *h, int32_t *headLen) {
}
-// The host's address, a socket, and a connection started.
-static bool hostConnect(HttpT *h) {
- struct addrinfo hints;
- struct addrinfo *found = NULL;
- char port[NUMBER_TEXT_MAX];
- unsigned long on = 1;
- int rc;
+// A socket, and a connection to the host (at addr, network order)
+// started.
+static bool hostConnect(HttpT *h, uint32_t addr) {
+ struct sockaddr_in to;
+ unsigned long on = 1;
+ int rc;
- memset(&hints, 0, sizeof(hints));
- hints.ai_family = AF_INET;
- hints.ai_socktype = SOCK_STREAM;
- snprintf(port, sizeof(port), "%u", (unsigned)h->target.port);
-
- if (getaddrinfo(h->target.host, port, &hints, &found) != 0 || !found) {
- fail(h, "Cannot find %s on the network.", h->target.host);
- return false;
- }
-
- h->fd = socket(AF_INET, SOCK_STREAM, 0);
+ memset(&to, 0, sizeof(to));
+ to.sin_family = AF_INET;
+ to.sin_port = htons(h->target.port);
+ to.sin_addr.s_addr = addr;
+ h->fd = socket(AF_INET, SOCK_STREAM, 0);
if (h->fd < 0 || ioctl(h->fd, FIONBIO, &on) != 0) {
int32_t err = errno;
- freeaddrinfo(found);
- h->fd = NO_SOCKET;
+ closeSocket(h);
fail(h, "Cannot connect to %s: %s.", h->target.host, errnoText(err));
return false;
}
- rc = connect(h->fd, found->ai_addr, found->ai_addrlen);
- freeaddrinfo(found);
+ rc = connect(h->fd, (struct sockaddr *)&to, sizeof(to));
if (rc != 0 && errno != EINPROGRESS && !wouldBlock(errno)) {
fail(h, "Cannot connect to %s: %s.", h->target.host, errnoText(errno));
@@ -620,6 +685,21 @@ const char *httpBody(const HttpT *h, int32_t *len) {
}
+bool httpBodyFile(HttpT *h, const char *path, const char *contentType) {
+ struct stat st;
+
+ if (!path || stat(path, &st) != 0 || !S_ISREG(st.st_mode) || (uint64_t)st.st_size > INT32_MAX || snprintf(h->bodyPath, sizeof(h->bodyPath), "%s", path) >= (int)sizeof(h->bodyPath)) {
+ h->bodyPath[0] = '\0';
+ return false;
+ }
+
+ free(h->body);
+ h->body = NULL;
+ h->bodyLen = (int32_t)st.st_size;
+ return !contentType || !contentType[0] || httpHeaderAdd(h, FIELD_TYPE, contentType);
+}
+
+
bool httpBodySet(HttpT *h, const void *data, int32_t len, const char *contentType) {
char *copy = (char *)malloc(len > 0 ? (size_t)len : 1);
@@ -629,7 +709,8 @@ bool httpBodySet(HttpT *h, const void *data, int32_t len, const char *contentTyp
memcpy(copy, data, len > 0 ? (size_t)len : 0);
free(h->body);
- h->body = copy;
+ h->bodyPath[0] = '\0';
+ h->body = copy;
h->bodyLen = len > 0 ? len : 0;
return !contentType || !contentType[0] || httpHeaderAdd(h, FIELD_TYPE, contentType);
}
@@ -656,6 +737,11 @@ void httpFree(HttpT *h) {
return;
}
+ if (h->polling > 0) {
+ h->freeAsked = true;
+ return;
+ }
+
closeSocket(h);
if (h->file) {
@@ -712,150 +798,26 @@ const char *httpMessage(const HttpT *h) {
}
+// A free asked for during a poll (a wait for the network inside it lets
+// other code run) is carried out when the poll returns, which then
+// reports failure: the request outlives the code still using it.
HttpStateE httpPoll(HttpT *h, uint32_t nowMs) {
- if (!h || h->step == StepIdleE || h->step == StepFailedE) {
+ if (!h) {
return HttpFailedE;
}
- if (h->step == StepDoneE) {
- return HttpDoneE;
+ h->polling++;
+
+ HttpStateE st = pollStep(h, nowMs);
+
+ h->polling--;
+
+ if (h->polling == 0 && h->freeAsked) {
+ httpFree(h);
+ return HttpFailedE;
}
- if (h->step == StepConnectE && h->fd == NO_SOCKET) {
- if (!connectStart(h)) {
- return HttpFailedE;
- }
-
- h->lastMs = nowMs;
- }
-
- if (h->step == StepConnectE) {
- if (socketReady(h->fd, POLLOUT)) {
- int soErr = 0;
- socklen_t len = sizeof(soErr);
-
- if (getsockopt(h->fd, SOL_SOCKET, SO_ERROR, &soErr, &len) != 0 || soErr != 0) {
- return fail(h, "Cannot connect to %s: %s.", h->target.host, errnoText(soErr ? soErr : errno));
- }
-
- h->step = StepSendE;
- h->lastMs = nowMs;
- }
- }
-
- if (h->step == StepSendE && socketReady(h->fd, POLLOUT)) {
- ssize_t n = send(h->fd, h->out + h->sent, (size_t)(arrlen(h->out) - h->sent), MSG_NOSIGNAL);
-
- if (n < 0 && !wouldBlock(errno)) {
- return fail(h, "Cannot send to %s: %s.", h->target.host, errnoText(errno));
- }
-
- if (n > 0) {
- h->sent += (int32_t)n;
- h->lastMs = nowMs;
- }
-
- if (h->sent == (int32_t)arrlen(h->out)) {
- h->step = StepHeadE;
- }
- }
-
- if ((h->step == StepHeadE || h->step == StepBodyE) && socketReady(h->fd, POLLIN)) {
- char buf[READ_STEP];
- ssize_t n = recv(h->fd, buf, sizeof(buf), 0);
-
- if (n < 0 && !wouldBlock(errno)) {
- return fail(h, "Lost the connection to %s: %s.", h->target.host, errnoText(errno));
- }
-
- if (n == 0 && h->step == StepHeadE) {
- return fail(h, "%s closed the connection without answering.", h->target.host);
- }
-
- if (n == 0) {
- if (h->bodyMode != BodyCloseE) {
- return fail(h, "%s closed the connection before the whole reply came.", h->target.host);
- }
-
- h->step = StepDoneE;
- }
-
- if (n > 0) {
- h->lastMs = nowMs;
-
- if (h->step == StepHeadE) {
- int32_t headLen = 0;
-
- memcpy(arraddnptr(h->head, n), buf, (size_t)n);
-
- if (!headParse(h, &headLen)) {
- return HttpFailedE;
- }
-
- if (headLen > 0) {
- char loc[HEADER_VALUE_MAX];
- char next[HTTP_URL_MAX];
- int32_t rest = (int32_t)arrlen(h->head) - headLen;
-
- // The body's first bytes, then the head alone (NUL-ended):
- // they came in this read, so they fit in buf
- if (rest > 0 && rest <= (int32_t)sizeof(buf)) {
- memcpy(buf, h->head + headLen, (size_t)rest);
- }
-
- arrsetlen(h->head, headLen);
- textEnd(&h->head);
-
- // A redirect: the same request (a GET after 303, or
- // after 301 and 302 of a POST) to where it points
- if (isRedirect(h->status) && headerFind(h->head, FIELD_LOCATION, loc, sizeof(loc))) {
- if (h->redirects >= HTTP_REDIRECTS_MAX) {
- return fail(h, "%s sent the request elsewhere too many times.", h->target.host);
- }
-
- if (!locationResolve(h, loc, next, sizeof(next)) || !httpUrlParse(next, &h->target, h->err, sizeof(h->err))) {
- return fail(h, "%s sent the request to %s, which is not an http:// address.", h->target.host, loc);
- }
-
- h->redirects++;
- snprintf(h->url, sizeof(h->url), "%s", next);
-
- if (h->status == STATUS_SEE_OTHER || ((h->status == STATUS_MOVED || h->status == STATUS_FOUND) && strcmp(h->method, METHOD_POST) == 0)) {
- snprintf(h->method, sizeof(h->method), "%s", METHOD_GET);
- free(h->body);
- h->body = NULL;
- h->bodyLen = 0;
- }
-
- closeSocket(h);
- h->step = StepConnectE;
- h->lastMs = nowMs;
- return HttpBusyE;
- }
-
- if (!bodyStart(h)) {
- return HttpFailedE;
- }
-
- if (h->step == StepBodyE && rest > 0 && !bodyFeed(h, buf, rest)) {
- return HttpFailedE;
- }
- }
- } else if (!bodyFeed(h, buf, (int32_t)n)) {
- return HttpFailedE;
- }
- }
- }
-
- if (h->step == StepDoneE) {
- return finish(h);
- }
-
- if (nowMs - h->lastMs > h->timeoutMs) {
- return fail(h, "%s did not answer in time (%lu seconds).", h->target.host, (unsigned long)(h->timeoutMs / MS_PER_SECOND));
- }
-
- return HttpBusyE;
+ return st;
}
@@ -996,6 +958,181 @@ static bool locationResolve(const HttpT *h, const char *loc, char *out, int32_t
}
+// httpPoll's work: one step of the request.
+static HttpStateE pollStep(HttpT *h, uint32_t nowMs) {
+ if (h->step == StepIdleE || h->step == StepFailedE) {
+ return HttpFailedE;
+ }
+
+ if (h->step == StepDoneE) {
+ return HttpDoneE;
+ }
+
+ if (h->step == StepConnectE && h->fd == NO_SOCKET && !connectStart(h)) {
+ return HttpFailedE;
+ }
+
+ // The lookup ends itself (DHCP's first address and the DNS server
+ // each have their limit); the timeout counts from its end
+ if (h->step == StepLookupE) {
+ uint32_t addr;
+ int32_t rc = netLookupPoll(&h->lookup, &addr);
+
+ if (rc == NET_LOOKUP_BUSY) {
+ return HttpBusyE;
+ }
+
+ if (rc != 0) {
+ return fail(h, "Cannot find %s on the network.", h->target.host);
+ }
+
+ if (!hostConnect(h, addr)) {
+ return HttpFailedE;
+ }
+
+ h->lastMs = nowMs;
+ }
+
+ if (h->step == StepConnectE) {
+ if (socketReady(h->fd, POLLOUT)) {
+ int soErr = 0;
+ socklen_t len = sizeof(soErr);
+
+ if (getsockopt(h->fd, SOL_SOCKET, SO_ERROR, &soErr, &len) != 0 || soErr != 0) {
+ return fail(h, "Cannot connect to %s: %s.", h->target.host, errnoText(soErr ? soErr : errno));
+ }
+
+ h->step = StepSendE;
+ h->lastMs = nowMs;
+ }
+ }
+
+ // The head (and a body in memory), then a body file's pieces
+ if (h->step == StepSendE && socketReady(h->fd, POLLOUT)) {
+ int32_t before = h->sent + h->bodySent;
+
+ if (h->sent < (int32_t)arrlen(h->out)) {
+ ssize_t n = send(h->fd, h->out + h->sent, (size_t)(arrlen(h->out) - h->sent), MSG_NOSIGNAL);
+
+ if (n < 0 && !wouldBlock(errno)) {
+ return fail(h, "Cannot send to %s: %s.", h->target.host, errnoText(errno));
+ }
+
+ h->sent += n > 0 ? (int32_t)n : 0;
+ } else if (h->bodyPath[0] && !bodyFileSend(h)) {
+ return fail(h, "Cannot send %s to %s.", h->bodyPath, h->target.host);
+ }
+
+ if (h->sent + h->bodySent > before) {
+ h->lastMs = nowMs;
+ }
+
+ if (h->sent == (int32_t)arrlen(h->out) && (!h->bodyPath[0] || h->bodySent == h->bodyLen)) {
+ bodyFileClose(h);
+ h->step = StepHeadE;
+ }
+ }
+
+ if ((h->step == StepHeadE || h->step == StepBodyE) && socketReady(h->fd, POLLIN)) {
+ char buf[READ_STEP];
+ ssize_t n = recv(h->fd, buf, sizeof(buf), 0);
+
+ if (n < 0 && !wouldBlock(errno)) {
+ return fail(h, "Lost the connection to %s: %s.", h->target.host, errnoText(errno));
+ }
+
+ if (n == 0 && h->step == StepHeadE) {
+ return fail(h, "%s closed the connection without answering.", h->target.host);
+ }
+
+ if (n == 0) {
+ if (h->bodyMode != BodyCloseE) {
+ return fail(h, "%s closed the connection before the whole reply came.", h->target.host);
+ }
+
+ h->step = StepDoneE;
+ }
+
+ if (n > 0) {
+ h->lastMs = nowMs;
+
+ if (h->step == StepHeadE) {
+ int32_t headLen = 0;
+
+ memcpy(arraddnptr(h->head, n), buf, (size_t)n);
+
+ if (!headParse(h, &headLen)) {
+ return HttpFailedE;
+ }
+
+ if (headLen > 0) {
+ char loc[HEADER_VALUE_MAX];
+ char next[HTTP_URL_MAX];
+ int32_t rest = (int32_t)arrlen(h->head) - headLen;
+
+ // The body's first bytes, then the head alone (NUL-ended):
+ // they came in this read, so they fit in buf
+ if (rest > 0 && rest <= (int32_t)sizeof(buf)) {
+ memcpy(buf, h->head + headLen, (size_t)rest);
+ }
+
+ arrsetlen(h->head, headLen);
+ textEnd(&h->head);
+
+ // A redirect: the same request (a GET after 303, or
+ // after 301 and 302 of a POST) to where it points
+ if (isRedirect(h->status) && headerFind(h->head, FIELD_LOCATION, loc, sizeof(loc))) {
+ if (h->redirects >= HTTP_REDIRECTS_MAX) {
+ return fail(h, "%s sent the request elsewhere too many times.", h->target.host);
+ }
+
+ if (!locationResolve(h, loc, next, sizeof(next)) || !httpUrlParse(next, &h->target, h->err, sizeof(h->err))) {
+ return fail(h, "%s sent the request to %s, which is not an http:// address.", h->target.host, loc);
+ }
+
+ h->redirects++;
+ snprintf(h->url, sizeof(h->url), "%s", next);
+
+ if (h->status == STATUS_SEE_OTHER || ((h->status == STATUS_MOVED || h->status == STATUS_FOUND) && strcmp(h->method, METHOD_POST) == 0)) {
+ snprintf(h->method, sizeof(h->method), "%s", METHOD_GET);
+ free(h->body);
+ h->body = NULL;
+ h->bodyLen = 0;
+ h->bodyPath[0] = '\0';
+ }
+
+ closeSocket(h);
+ h->step = StepConnectE;
+ h->lastMs = nowMs;
+ return HttpBusyE;
+ }
+
+ if (!bodyStart(h)) {
+ return HttpFailedE;
+ }
+
+ if (h->step == StepBodyE && rest > 0 && !bodyFeed(h, buf, rest)) {
+ return HttpFailedE;
+ }
+ }
+ } else if (!bodyFeed(h, buf, (int32_t)n)) {
+ return HttpFailedE;
+ }
+ }
+ }
+
+ if (h->step == StepDoneE) {
+ return finish(h);
+ }
+
+ if (nowMs - h->lastMs > h->timeoutMs) {
+ return fail(h, "%s did not answer in time (%lu seconds).", h->target.host, (unsigned long)(h->timeoutMs / MS_PER_SECOND));
+ }
+
+ return HttpBusyE;
+}
+
+
// Forget the reply so far (a new connection: the first, or a redirect's).
static void replyReset(HttpT *h) {
arrfree(h->head);
diff --git a/src/libs/kpunch/http/http.h b/src/libs/kpunch/http/http.h
index 9b2c2e94..fdd3fa5d 100644
--- a/src/libs/kpunch/http/http.h
+++ b/src/libs/kpunch/http/http.h
@@ -80,6 +80,11 @@ typedef struct HttpS HttpT;
// counted). "" when it went to a file, or before the reply.
const char *httpBody(const HttpT *h, int32_t *len);
+// Send the file at path as the request's body, of contentType, a piece
+// at a time as the socket takes it (never all in memory). False when it
+// is not a file that can be sent.
+bool httpBodyFile(HttpT *h, const char *path, const char *contentType);
+
// Send data (copied) as the request's body, of contentType.
bool httpBodySet(HttpT *h, const void *data, int32_t len, const char *contentType);
diff --git a/src/libs/kpunch/http/httpBas.c b/src/libs/kpunch/http/httpBas.c
index baaee49b..f32b4f5c 100644
--- a/src/libs/kpunch/http/httpBas.c
+++ b/src/libs/kpunch/http/httpBas.c
@@ -52,6 +52,7 @@ const char *HttpReplyHeader(int32_t h, const char *name);
int32_t HttpRunFor(int32_t h, int32_t milliseconds);
void HttpSaveTo(int32_t h, const char *path, int32_t resume);
int32_t HttpSetBody(int32_t h, const uint8_t *data, int32_t len, const char *contentType);
+int32_t HttpSetBodyFile(int32_t h, const char *path, const char *contentType);
void HttpSetTimeout(int32_t h, int32_t seconds);
int32_t HttpStart(int32_t h);
int32_t HttpStatus(int32_t h);
@@ -158,6 +159,15 @@ int32_t HttpSetBody(int32_t h, const uint8_t *data, int32_t len, const char *con
}
+// Send the file at path as the request's body, of contentType (a piece
+// at a time: a file of any size). False when it is not a file.
+int32_t HttpSetBodyFile(int32_t h, const char *path, const char *contentType) {
+ HttpT *r = requestOf(h);
+
+ return (r && httpBodyFile(r, path, contentType)) ? BAS_TRUE : BAS_FALSE;
+}
+
+
// Give up after seconds without progress.
void HttpSetTimeout(int32_t h, int32_t seconds) {
HttpT *r = requestOf(h);
diff --git a/src/libs/kpunch/http/httpBas.h b/src/libs/kpunch/http/httpBas.h
index 6b9dea59..f798e1e1 100644
--- a/src/libs/kpunch/http/httpBas.h
+++ b/src/libs/kpunch/http/httpBas.h
@@ -39,6 +39,7 @@ const char *HttpReplyHeader(int32_t h, const char *name);
int32_t HttpRunFor(int32_t h, int32_t milliseconds);
void HttpSaveTo(int32_t h, const char *path, int32_t resume);
int32_t HttpSetBody(int32_t h, const uint8_t *data, int32_t len, const char *contentType);
+int32_t HttpSetBodyFile(int32_t h, const char *path, const char *contentType);
void HttpSetTimeout(int32_t h, int32_t seconds);
int32_t HttpStart(int32_t h);
int32_t HttpStatus(int32_t h);
diff --git a/src/libs/kpunch/joystick/README.md b/src/libs/kpunch/joystick/README.md
new file mode 100644
index 00000000..1c946198
--- /dev/null
+++ b/src/libs/kpunch/joystick/README.md
@@ -0,0 +1,27 @@
+# joystick -- Game Controllers
+
+Game controllers for DVX programs behind one API whatever the hardware:
+the devices the drivers found, calibrated reading or events, calibration
+kept in DVX.INI [joystick] (set by the Control Panel), and driver
+modules loaded when a program first asks. Handles belong to the program
+that opened them. BASIC reaches it through STICK / STRIG and
+src/include/basic/joystick.bas.
+
+## Documentation
+
+Full reference is in `joystick.dhs` (this directory), compiled into the
+DVX System Reference. Open via the Help Viewer app, or read
+`docs/dvx_system_reference.html` after `make`.
+
+## Source Files
+
+- `joystick.h`, `joyInt.h` -- public and internal API
+- `joystick.c` -- devices, handles, calibration, events
+- `joyShell.c` -- the ties to the shell: the event idle handler and the
+ app cleanup hook
+- `joyBas.c` -- BASIC entry points (joystick.bas)
+- `drivers/` -- the controller driver modules
+
+## Build
+
+`make -C src/libs/kpunch/joystick` (invoked by the top-level `make`).
diff --git a/src/libs/kpunch/joystick/joyBas.c b/src/libs/kpunch/joystick/joyBas.c
index 3ec7a9b7..f1b593bb 100644
--- a/src/libs/kpunch/joystick/joyBas.c
+++ b/src/libs/kpunch/joystick/joyBas.c
@@ -44,6 +44,7 @@
#define QB_POS_MAX 200
#define QB_STRIGS 8 // STRIG(0)..STRIG(7)
#define QB_LATCH_SLOTS 32 // programs whose STICK(0) readings are kept
+#define BAS_EVENTS_MAX 64 // joystick changes kept for JoyEvent (the oldest go first)
// A program's positions as its last STICK(0) read them (appId 0: free).
typedef struct {
@@ -51,12 +52,23 @@ typedef struct {
int32_t pos[QB_LATCHED];
} StickLatchT;
-static StickLatchT sLatches[QB_LATCH_SLOTS];
+// A change of a joystick a program watches (JoyWatch), until JoyEvent
+// gives it
+typedef struct {
+ int32_t appId;
+ JoyEventT evt;
+} BasJoyEventT;
+
+static StickLatchT sLatches[QB_LATCH_SLOTS];
+static BasJoyEventT sEvents[BAS_EVENTS_MAX];
+static int32_t sEventCount = 0;
+static JoyEventT sLastEvent; // the change JoyEvent gave last
// ============================================================
// Prototypes
// ============================================================
+static void basEvent(int32_t handle, const JoyEventT *evt, void *userData);
static int32_t basHandle(int32_t joy);
static bool basInfo(int32_t joy, JoyInfoT *info);
static bool basRaw(int32_t joy, JoyRawT *raw);
@@ -79,6 +91,10 @@ int32_t JoyClearCalibration(int32_t joy, int32_t axis);
int32_t JoyConnected(int32_t joy);
int32_t JoyCount(void);
int32_t JoyDeadZone(int32_t joy);
+int32_t JoyEvent(void);
+int32_t JoyEventIndex(void);
+int32_t JoyEventJoy(void);
+int32_t JoyEventValue(void);
int32_t JoyHat(int32_t joy, int32_t hat);
int32_t JoyHats(int32_t joy);
int32_t JoyIsKeyboard(int32_t joy);
@@ -101,6 +117,7 @@ void JoySetSetting(const char *key, const char *value);
const char *JoySetting(const char *key, const char *defaultValue);
void JoySettingsLoad(int32_t prefs);
void JoySettingsStore(int32_t prefs);
+int32_t JoyWatch(int32_t joy, int32_t watching);
int32_t JoyX(int32_t joy);
int32_t JoyY(int32_t joy);
static StickLatchT *latchOf(int32_t appId, bool create);
@@ -108,6 +125,23 @@ static bool qbStick(int32_t stick, int32_t *device, int32_t *first);
static int32_t qbStickPos(int32_t stick, int32_t axis);
+// A watched joystick changed: kept for JoyEvent (the call comes as the
+// program that owns the handle).
+static void basEvent(int32_t handle, const JoyEventT *evt, void *userData) {
+ (void)handle;
+ (void)userData;
+
+ if (sEventCount == BAS_EVENTS_MAX) {
+ memmove(&sEvents[0], &sEvents[1], (size_t)(BAS_EVENTS_MAX - 1) * sizeof(sEvents[0]));
+ sEventCount--;
+ }
+
+ sEvents[sEventCount].appId = shellContext()->currentAppId;
+ sEvents[sEventCount].evt = *evt;
+ sEventCount++;
+}
+
+
// The calling program's handle on joystick joy (1-based), -1 if none.
static int32_t basHandle(int32_t joy) {
return joyOpenShared(shellContext(), joy - 1);
@@ -167,6 +201,13 @@ void joyBasForget(int32_t appId) {
if (l) {
l->appId = 0;
}
+
+ for (int32_t i = sEventCount - 1; i >= 0; i--) {
+ if (sEvents[i].appId == appId) {
+ memmove(&sEvents[i], &sEvents[i + 1], (size_t)(sEventCount - i - 1) * sizeof(sEvents[0]));
+ sEventCount--;
+ }
+ }
}
@@ -313,6 +354,44 @@ int32_t JoyDeadZone(int32_t joy) {
}
+// The calling program's next joystick change since it last asked (each
+// once, in order): JOY_EVENT_AXIS, _BUTTON_DOWN, _BUTTON_UP or _HAT, with
+// JoyEventJoy, JoyEventIndex and JoyEventValue describing it; JOY_EVENT_NONE
+// when there is none.
+int32_t JoyEvent(void) {
+ int32_t appId = shellContext()->currentAppId;
+
+ for (int32_t i = 0; i < sEventCount; i++) {
+ if (sEvents[i].appId == appId) {
+ sLastEvent = sEvents[i].evt;
+ memmove(&sEvents[i], &sEvents[i + 1], (size_t)(sEventCount - i - 1) * sizeof(sEvents[0]));
+ sEventCount--;
+ return (int32_t)sLastEvent.kind + JOY_BAS_EVENT_FIRST;
+ }
+ }
+
+ return JOY_BAS_EVENT_NONE;
+}
+
+
+// What the change JoyEvent gave last was about: the joystick, the axis,
+// button or hat (numbered from 1), and an axis's new position or a hat's
+// direction.
+int32_t JoyEventIndex(void) {
+ return sLastEvent.index + 1;
+}
+
+
+int32_t JoyEventJoy(void) {
+ return sLastEvent.device + 1;
+}
+
+
+int32_t JoyEventValue(void) {
+ return sLastEvent.value;
+}
+
+
int32_t JoyHat(int32_t joy, int32_t hat) {
JoyStateT st;
@@ -474,6 +553,15 @@ void JoySettingsStore(int32_t prefs) {
}
+// Start (watching True) or stop keeping joy's changes for JoyEvent; True
+// when there is such a joystick.
+int32_t JoyWatch(int32_t joy, int32_t watching) {
+ int32_t handle = basHandle(joy);
+
+ return basTruth(handle >= 0 && joySetEventFn(handle, watching != BAS_FALSE ? basEvent : NULL, NULL));
+}
+
+
int32_t JoyX(int32_t joy) {
return JoyAxis(joy, 1);
}
diff --git a/src/libs/kpunch/joystick/joystick.dhs b/src/libs/kpunch/joystick/joystick.dhs
index a3daef7a..361db0f2 100644
--- a/src/libs/kpunch/joystick/joystick.dhs
+++ b/src/libs/kpunch/joystick/joystick.dhs
@@ -161,7 +161,7 @@ Up to JOY_MAX_DRIVERS drivers in use get their tick called. A driver is loaded a
.h2 BASIC
-src/include/basic/joystick.bas declares JoyCount, JoyName$, JoyAxes, JoyButtons, JoyX, JoyY, JoyAxis, JoyButton, and JoyHat. Joystick, axis, button, and hat numbers start at 1; each function reads through the calling program's shared handle.
+src/include/basic/joystick.bas declares JoyCount, JoyName$, JoyAxes, JoyButtons, JoyX, JoyY, JoyAxis, JoyButton, and JoyHat. Joystick, axis, button, and hat numbers start at 1; each function reads through the calling program's shared handle. Events: JoyWatch(joy, watching) sets (or clears) that handle's joySetEventFn, which keeps the program's changes, the last BAS_EVENTS_MAX (64); JoyEvent gives the next as JoyEventKindE + JOY_BAS_EVENT_FIRST (JOY_BAS_EVENT_NONE for none; CONSTs JOY_EVENT_*), and JoyEventJoy, JoyEventIndex (both from 1), and JoyEventValue the one it gave last. joyBasForget drops a program's kept changes when it ends.
For a settings program (the Control Panel's Game Controllers page) it also declares: JoyHats, JoyAxisPresent(joy, axis), JoyCanCalibrate(joy) (JoyInfoT hatCount, axisPresent, JOY_CAP_CALIBRATE); JoyConnected(joy) (joyRead's connected); JoyRescan (joyRescanApp for the calling program: its own handles and STICK readings go first; -1 while another program has a joystick open); JoyRawAxis(joy, axis) and JoyRawButtons(joy) (joyGetRaw); JoySetCalibration(joy, axis, minV, centerV, maxV) and JoyClearCalibration(joy, axis) (joySetCalibration); JoyDeadZone / JoySetDeadZone (joyGetDeadZone / joySetDeadZone); JoyChoiceCount(which), JoyChoiceLabel$(which, index), JoyChoice(which), JoySetChoice(which, index) with JOY_CHOICE_GAMEPORT = 0 and JOY_CHOICE_FALLBACK = 1 (JoyChoiceE; joyChoices / joyChoiceGet / joyChoiceSet); JoyKeyCount and JoyKeyName$(index); JoyKbdSlotCount, JoyKbdSlot(slot), JoySetKbdSlot(slot, keyIndex) (joyKbdSlotGet / Set); JoySetting$(key, defaultValue) and JoySetSetting(key, value) ("" removes; joySettingGet / Set); JoySettingsLoad(prefs) and JoySettingsStore(prefs) with a prefs.bas handle (prefsBasHandle); JoyCalibration(joy, axis, part) (joyGetCalibration; part JOY_CAL_MIN / JOY_CAL_CENTER / JOY_CAL_MAX, joystick.h; 0 when joyGetCalibration fails); JoyIsKeyboard(joy) (JOY_CAP_KEYBOARD); JoyKeyIndex(keyName), JoyKeyCode(index), JoyKeyNameOf$(rawCode) (joyKeyIndex / joyKeyCode / joyKeyNameOf). CONSTs JOY_DEADZONE_DEFAULT / _MAX, JOY_POLL_HZ_DEFAULT / _MIN / _MAX, and JOY_KEY_POLL_HZ mirror joystick.h. Lists (choices, keys, slots) are numbered from 0; True is -1.
diff --git a/src/libs/kpunch/joystick/joystick.h b/src/libs/kpunch/joystick/joystick.h
index 949f35c9..fcabb4fa 100644
--- a/src/libs/kpunch/joystick/joystick.h
+++ b/src/libs/kpunch/joystick/joystick.h
@@ -189,6 +189,11 @@ typedef struct {
int32_t value;
} JoyEventT;
+// JoyEvent's answers in BASIC (joystick.bas JOY_EVENT_*): none, else the
+// JoyEventKindE plus JOY_BAS_EVENT_FIRST
+#define JOY_BAS_EVENT_NONE 0
+#define JOY_BAS_EVENT_FIRST 1
+
// Called in the program's context (not at interrupt time): DVX calls are fine.
typedef void (*JoyEventFnT)(int32_t handle, const JoyEventT *evt, void *userData);
@@ -394,7 +399,9 @@ int32_t joyBasStrig(int32_t n);
// ============================================================
//
// Joystick, axis, button and hat numbers start at 1; every function reads
-// through the calling program's shared handle.
+// through the calling program's shared handle. JoyWatch sets that
+// handle's event function to keep the program's changes (the last 64)
+// for JoyEvent.
int32_t JoyAxes(int32_t joy);
int32_t JoyAxis(int32_t joy, int32_t axis);
@@ -410,6 +417,10 @@ int32_t JoyClearCalibration(int32_t joy, int32_t axis);
int32_t JoyConnected(int32_t joy);
int32_t JoyCount(void);
int32_t JoyDeadZone(int32_t joy);
+int32_t JoyEvent(void);
+int32_t JoyEventIndex(void);
+int32_t JoyEventJoy(void);
+int32_t JoyEventValue(void);
int32_t JoyHat(int32_t joy, int32_t hat);
int32_t JoyHats(int32_t joy);
int32_t JoyIsKeyboard(int32_t joy);
@@ -432,6 +443,7 @@ void JoySetSetting(const char *key, const char *value);
const char *JoySetting(const char *key, const char *defaultValue);
void JoySettingsLoad(int32_t prefs);
void JoySettingsStore(int32_t prefs);
+int32_t JoyWatch(int32_t joy, int32_t watching);
int32_t JoyX(int32_t joy);
int32_t JoyY(int32_t joy);
diff --git a/src/libs/kpunch/json/json.c b/src/libs/kpunch/json/json.c
index 01893a36..266694d0 100644
--- a/src/libs/kpunch/json/json.c
+++ b/src/libs/kpunch/json/json.c
@@ -29,7 +29,9 @@
// by offset because the pool moves as it grows.
#include "json.h"
+#include "dvxFontFmt.h"
#include "dvxTypes.h"
+#include "dvxHash.h"
#include "stb_ds_wrap.h"
#include
@@ -39,11 +41,21 @@
#define NO_TEXT (-1) // a node without a name or text
#define HEX_DIGITS_U 4 // the digits of a \u escape
-#define HEX_BASE 16
#define DECIMAL_BASE 10
-#define ASCII_LIMIT 0x80 // \u escapes below this become their character
+#define ASCII_LIMIT 0x80 // characters from here on are written as \u escapes
#define CONTROL_LIMIT 0x20 // characters below this need escaping
-#define UNKNOWN_CHAR '?' // what other \u escapes become
+#define UNKNOWN_CHAR '?' // what a character DVX does not have becomes
+#define ESCAPE_U_LEAD 2 // the \u before an escape's digits
+#define SURROGATE_HIGH 0xD800 // the first of a surrogate pair's first half
+#define SURROGATE_LOW 0xDC00 // the first of a surrogate pair's second half
+#define SURROGATE_LAST 0xDFFF
+#define SURROGATE_BITS 10 // the bits of the code point each half holds
+#define SURROGATE_BASE 0x10000 // the first code point a pair stands for
+#define UTF8_CONT_MASK 0xC0 // a UTF-8 continuation byte is 10xxxxxx
+#define UTF8_CONT_TAG 0x80
+#define UTF8_CONT_BITS 6
+#define UTF8_CONT_DATA 0x3F
+#define UNICODE_MAX 0x10FFFF
#define NUMBER_TEXT_MAX 64 // longest number read
#define WORD_TRUE "true"
#define WORD_FALSE "false"
@@ -52,7 +64,7 @@
#define PATH_INDEX_OPEN '['
#define PATH_INDEX_CLOSE ']'
#define PATH_STOPS ".[" // what ends a member name in a path
-#define ESCAPE_TEXT_MAX 8 // the longest escape jsonQuote writes, \u00XX, and a NUL
+#define ESCAPE_TEXT_MAX 8 // the longest escape jsonQuote writes, \uXXXX, and a NUL
typedef struct {
JsonTypeE type;
@@ -63,6 +75,17 @@ typedef struct {
bool truth;
} NodeT;
+// The lead bytes of UTF-8 sequences: a lead whose mask bits are tag
+// starts one of more continuation bytes, the lead holding its data bits;
+// a code point below min is overlong (not UTF-8).
+typedef struct {
+ uint8_t mask;
+ uint8_t tag;
+ uint8_t data;
+ int32_t more;
+ uint32_t min;
+} Utf8LeadT;
+
struct JsonDocS {
NodeT *nodes; // stb_ds; node 0 is the root
char *pool; // stb_ds: NUL-terminated strings
@@ -80,12 +103,20 @@ typedef struct {
DVX_ABI_STAMP(json);
+static const Utf8LeadT sUtf8Leads[] = {
+ { 0xE0, 0xC0, 0x1F, 1, 0x80 },
+ { 0xF0, 0xE0, 0x0F, 2, 0x800 },
+ { 0xF8, 0xF0, 0x07, 3, 0x10000 }
+};
+
// ============================================================
// Prototypes
// ============================================================
+static unsigned char charOf(uint32_t code);
+static int32_t escapeU(ParserT *ps);
static bool fail(ParserT *ps, const char *fmt, ...) __attribute__((format(printf, 2, 3)));
-static int32_t hexValue(char c);
+static int32_t hexQuad(ParserT *ps);
bool jsonBool(const JsonDocT *doc, int32_t node);
int32_t jsonChild(const JsonDocT *doc, int32_t node, int32_t index);
int32_t jsonCount(const JsonDocT *doc, int32_t node);
@@ -108,6 +139,40 @@ static int32_t parseValue(ParserT *ps, int32_t depth);
static bool parseWord(ParserT *ps, const char *word);
static void quotePut(char *out, int32_t outSize, int32_t *n, const char *piece);
static void skipSpace(ParserT *ps);
+static unsigned char utf8Char(ParserT *ps, unsigned char lead);
+
+
+// The character a code point is in DVX text (UNKNOWN_CHAR when DVX has
+// none)
+static unsigned char charOf(uint32_t code) {
+ int32_t c = dfnTextFromUnicode(code);
+
+ return (c >= 0) ? (unsigned char)c : (unsigned char)UNKNOWN_CHAR;
+}
+
+
+// A \u escape's code point, after its \u (a surrogate pair's second half,
+// \u and all, taken with its first); -1 when its four hex digits are not
+// there.
+static int32_t escapeU(ParserT *ps) {
+ int32_t code = hexQuad(ps);
+
+ if (code >= SURROGATE_HIGH && code < SURROGATE_LOW && ps->end - ps->p >= ESCAPE_U_LEAD && ps->p[0] == '\\' && ps->p[1] == 'u') {
+ const char *mark = ps->p;
+
+ ps->p += ESCAPE_U_LEAD;
+
+ int32_t low = hexQuad(ps);
+
+ if (low >= SURROGATE_LOW && low <= SURROGATE_LAST) {
+ return SURROGATE_BASE + ((code - SURROGATE_HIGH) << SURROGATE_BITS) + (low - SURROGATE_LOW);
+ }
+
+ ps->p = mark;
+ }
+
+ return code;
+}
static bool fail(ParserT *ps, const char *fmt, ...) {
@@ -126,20 +191,23 @@ static bool fail(ParserT *ps, const char *fmt, ...) {
}
-static int32_t hexValue(char c) {
- if (c >= '0' && c <= '9') {
- return c - '0';
+// Four hex digits, the code a \u escape holds; -1 when they are not
+// there.
+static int32_t hexQuad(ParserT *ps) {
+ int32_t code = 0;
+
+ for (int32_t i = 0; i < HEX_DIGITS_U; i++) {
+ int32_t digit = ps->p < ps->end ? dvxHexValue((uint8_t)*ps->p) : -1;
+
+ if (digit < 0) {
+ return -1;
+ }
+
+ code = code * DVX_HEX_RADIX + digit;
+ ps->p++;
}
- if (c >= 'a' && c <= 'f') {
- return c - 'a' + DECIMAL_BASE;
- }
-
- if (c >= 'A' && c <= 'F') {
- return c - 'A' + DECIMAL_BASE;
- }
-
- return -1;
+ return code;
}
@@ -292,8 +360,8 @@ int32_t jsonQuote(const char *text, char *out, int32_t outSize) {
break;
default:
- if (*c < CONTROL_LIMIT) {
- snprintf(esc, sizeof(esc), "\\u%04x", (unsigned)*c);
+ if (*c < CONTROL_LIMIT || *c >= ASCII_LIMIT) {
+ snprintf(esc, sizeof(esc), "\\u%04x", (unsigned)dfnTextToUnicode(*c));
} else {
esc[0] = (char)*c;
esc[1] = '\0';
@@ -516,7 +584,9 @@ static bool parseString(ParserT *ps, int32_t *offset) {
return fail(ps, "a string holds a line end or another control character");
}
- if (c == '\\') {
+ if (c >= ASCII_LIMIT) {
+ c = utf8Char(ps, c);
+ } else if (c == '\\') {
if (ps->p >= ps->end) {
break;
}
@@ -550,20 +620,13 @@ static bool parseString(ParserT *ps, int32_t *offset) {
break;
case 'u': {
- int32_t code = 0;
+ int32_t code = escapeU(ps);
- for (int32_t i = 0; i < HEX_DIGITS_U; i++) {
- int32_t digit = ps->p < ps->end ? hexValue(*ps->p) : -1;
-
- if (digit < 0) {
- return fail(ps, "a \\u escape needs four hex digits");
- }
-
- code = code * HEX_BASE + digit;
- ps->p++;
+ if (code < 0) {
+ return fail(ps, "a \\u escape needs four hex digits");
}
- c = code < ASCII_LIMIT ? (unsigned char)code : (unsigned char)UNKNOWN_CHAR;
+ c = charOf((uint32_t)code);
break;
}
@@ -693,3 +756,42 @@ static void skipSpace(ParserT *ps) {
ps->p++;
}
}
+
+
+// A byte from 0x80 on: when it leads a UTF-8 sequence, the sequence's
+// character (UNKNOWN_CHAR when DVX has none), the sequence taken; else
+// the byte itself, as DVX text.
+static unsigned char utf8Char(ParserT *ps, unsigned char lead) {
+ for (int32_t k = 0; k < DVX_ARRAY_LEN(sUtf8Leads); k++) {
+ const Utf8LeadT *u = &sUtf8Leads[k];
+
+ if ((lead & u->mask) != u->tag) {
+ continue;
+ }
+
+ if (ps->end - ps->p < u->more) {
+ return lead;
+ }
+
+ uint32_t code = lead & u->data;
+
+ for (int32_t i = 0; i < u->more; i++) {
+ unsigned char b = (unsigned char)ps->p[i];
+
+ if ((b & UTF8_CONT_MASK) != UTF8_CONT_TAG) {
+ return lead;
+ }
+
+ code = (code << UTF8_CONT_BITS) | (b & UTF8_CONT_DATA);
+ }
+
+ if (code < u->min || code > UNICODE_MAX || (code >= SURROGATE_HIGH && code <= SURROGATE_LAST)) {
+ return lead;
+ }
+
+ ps->p += u->more;
+ return charOf(code);
+ }
+
+ return lead;
+}
diff --git a/src/libs/kpunch/json/json.dhs b/src/libs/kpunch/json/json.dhs
index 785a220d..d1bfd1da 100644
--- a/src/libs/kpunch/json/json.dhs
+++ b/src/libs/kpunch/json/json.dhs
@@ -56,7 +56,7 @@ jsonParse(text, len, err, errSize) reads a document and returns it (NULL when it
jsonKey(doc, node) A member's name ("" for anything else).
.endtable
-Strings are held decoded: the escapes \n, \t, \", \\, \/, \b, \f, and \r become their characters, \u escapes below 128 their character and others '?'; other bytes, UTF-8 included, pass through. Arrays and objects nest JSON_DEPTH_MAX (64) deep at most.
+Strings are held decoded into DVX text (code page 437): the escapes \n, \t, \", \\, \/, \b, \f, \r, and \u (surrogate pairs included) and UTF-8 sequences become their characters, with '?' for a character DVX does not have; other bytes pass through. Arrays and objects nest JSON_DEPTH_MAX (64) deep at most.
.code
char err[JSON_ERR_MAX];
@@ -73,4 +73,4 @@ jsonFree(doc);
.h2 Writing
-jsonQuote(text, out, outSize) writes text as a JSON string -- quotes, escapes, control characters as \u00XX -- and returns the length it needs (as snprintf does), so a NULL out with size 0 measures it.
+jsonQuote(text, out, outSize) writes text as a JSON string -- quotes, escapes, control characters and characters from 128 on as \u escapes of their Unicode code points, so the result is plain ASCII -- and returns the length it needs (as snprintf does), so a NULL out with size 0 measures it.
diff --git a/src/libs/kpunch/json/json.h b/src/libs/kpunch/json/json.h
index fb7de20c..fbc4c0a6 100644
--- a/src/libs/kpunch/json/json.h
+++ b/src/libs/kpunch/json/json.h
@@ -33,10 +33,10 @@
//
// Members are matched by exact name (the first when a name repeats). A
// name holding '.' or '[' is reached with jsonChild instead. Strings are
-// held decoded: \n, \t, \", \\, \/, \b, \f, \r, and \u escapes below 128
-// become their characters, \u escapes of other characters become '?', and
-// other bytes (UTF-8 included) pass through unchanged. Numbers are
-// doubles.
+// held decoded into DVX text (code page 437): \n, \t, \", \\, \/, \b,
+// \f, \r, \u escapes (surrogate pairs included), and UTF-8 sequences
+// become their characters, '?' for a character DVX does not have; other
+// bytes pass through unchanged. Numbers are doubles.
#ifndef JSON_H
#define JSON_H
diff --git a/src/libs/kpunch/libdvx/Makefile b/src/libs/kpunch/libdvx/Makefile
index f51e2461..71ef08ee 100644
--- a/src/libs/kpunch/libdvx/Makefile
+++ b/src/libs/kpunch/libdvx/Makefile
@@ -33,10 +33,9 @@ CFLAGS = $(DOS_CFLAGS) -I. -Iplatform -I../libtasks -Ithirdparty
OBJDIR = ../../../../obj/core
LIBSDIR = ../../../../bin/libs
-# Core sources. dvxPrefs.c is not among them on DOS: the loader needs it
-# before libdvx loads, so dvx.exe links it and exports prefs* to every DXE.
-SRCS = dvxVideo.c dvxDraw.c dvxComp.c dvxWm.c dvxImage.c dvxImageWrite.c dvxDriver.c \
- dvxApp.c dvxBas.c dvxDialog.c dvxPrefsBas.c dvxResource.c dvxText.c dvxTextBas.c dvxFontFmt.c dvxFlow.c \
+# Core sources
+SRCS = dvxVideo.c dvxDraw.c dvxComp.c dvxWm.c dvxImage.c dvxDriver.c \
+ dvxApp.c dvxBas.c dvxDialog.c dvxPrefs.c dvxPrefsBas.c dvxResource.c dvxResourceBas.c dvxText.c dvxTextBas.c dvxFontFmt.c dvxFlow.c \
widgetClass.c widgetCore.c widgetScrollbar.c \
widgetLayout.c widgetEvent.c widgetOps.c widgetIface.c
@@ -57,7 +56,7 @@ TARGET = $(TARGETDIR)/libdvx.lib
HOSTOBJDIR = $(HOST_OBJROOT)
HOSTTARGET = $(HOSTOBJDIR)/libdvx.a
HOSTCFLAGS = $(HOST_CFLAGS) -I. -Iplatform -I../libtasks -Ithirdparty
-HOST_SRCS = $(SRCS) dvxPrefs.c platform/dvxPlatformHost.c platform/dvxPlatformIrq.c platform/dvxMemTrack.c platform/dvxPlatformUtil.c thirdparty/stb_ds_impl.c
+HOST_SRCS = $(SRCS) platform/dvxPlatformHost.c platform/dvxPlatformIrq.c platform/dvxMemTrack.c platform/dvxPlatformUtil.c thirdparty/stb_ds_impl.c
HOST_OBJS = $(patsubst %.c,$(HOSTOBJDIR)/libdvx/%.o,$(HOST_SRCS) $(notdir $(TOOL_SRCS)))
.PHONY: all clean host host-clean
diff --git a/src/libs/kpunch/libdvx/README.md b/src/libs/kpunch/libdvx/README.md
index 39c389e7..63e603ad 100644
--- a/src/libs/kpunch/libdvx/README.md
+++ b/src/libs/kpunch/libdvx/README.md
@@ -31,13 +31,15 @@ Public headers live alongside implementation:
- `dvxDlg.h` / `dvxDialog.c` -- message/input/choice/file dialogs
- `dvxRes.h` / `dvxResource.c` -- .res resource system
- `dvxMem.h` -- memory allocation wrappers
-- `dvxPrefs.h` / `.c` -- INI-format preferences (on DOS compiled into dvx.exe, not libdvx.lib, and exported to every module)
+- `dvxEndian.h` -- little- and big-endian loads and stores (header only)
+- `dvxHash.h` -- FNV-1a, CRC-16, and hex digits (header only)
+- `dvxPrefs.h` / `.c` -- INI-format preferences
- `dvxFont.h` -- bundled 8x16 bitmap font
- `dvxCur.h` -- mouse cursor bitmaps
- `dvxPal.h` -- 16-color CGA palette
- `widgetCore.c` / `widgetLayout.c` / `widgetEvent.c` / `widgetClass.c`
-- widget runtime helpers shared across all widget plugins
-- `dvxImage.c` / `dvxImageWrite.c` -- image loader/writer (BMP, PNG via stb)
+- `dvxImage.c` -- image codec (stb_image / stb_image_write, static, behind dvxImgInt.h)
- `platform/dvxPlat.h` -- platform-layer interface (implemented per target)
## Build
diff --git a/src/libs/kpunch/libdvx/apiref.dhs b/src/libs/kpunch/libdvx/apiref.dhs
index eb13085a..d45e47ee 100644
--- a/src/libs/kpunch/libdvx/apiref.dhs
+++ b/src/libs/kpunch/libdvx/apiref.dhs
@@ -52,6 +52,8 @@ The DVX GUI is built as a five-layer architecture. Each layer is defined in its
.item dvxRes.h -- DXE-embedded resource system
.item dvxPrefs.h -- INI preferences system
.item dvxMem.h -- Per-app memory tracking
+.item dvxEndian.h -- Byte order loads and stores
+.item dvxHash.h -- Hashes, checksums, and hex digits
.item dvxDrv.h -- Driver modules
.item dvxWgt.h -- Widget system public API
.item dvxWgtP.h -- Widget plugin API
@@ -73,6 +75,8 @@ The DVX GUI is built as a five-layer architecture. Each layer is defined in its
.link api.resource dvxRes.h -- Resource System
.link api.prefs dvxPrefs.h -- Preferences
.link api.mem dvxMem.h -- Memory Tracking
+.link api.endian dvxEndian.h -- Byte Order
+.link api.hash dvxHash.h -- Hashes and Checksums
.link api.driver dvxDrv.h -- Driver Modules
.link api.widget dvxWgt.h -- Widget System
.link api.widgetplugin dvxWgtP.h -- Widget Plugin API
@@ -333,7 +337,7 @@ Callbacks:
.table
Callback Description
-------- -----------
- onPaint(WindowT *win, RectT *dirtyArea) Content repaint requested
+ onPaint(WindowT *win, RectT *dirtyArea) Content repaint requested; a painter that changed only part of dirtyArea may narrow it, and only that part is composited
onKey(WindowT *win, int32_t key, int32_t mod) Key press
onKeyUp(WindowT *win, int32_t scancode, int32_t mod) Key release
onMouse(WindowT *win, int32_t x, int32_t y, int32_t buttons) Mouse event (content-relative)
@@ -646,6 +650,10 @@ Legacy alias for backward compatibility with code that predates multi-cursor sup
.index Font
.index CP437
.index BitmapFontT
+.index dfnCp437
+.index dfnFromUnicode
+.index dfnTextFromUnicode
+.index dfnTextToUnicode
.h1 dvxFont.h -- Embedded Bitmap Font Data
@@ -703,6 +711,8 @@ const BitmapFontT struct describing the 8x16 font, ready to pass to drawText/dra
.index dvxFontRescan
.index dvxFontRetain
.index dvxFontSetSmooth
+.index dvxFontSmoothBegin
+.index dvxFontRebuildStep
.index dvxFontSmooth
.index dvxFontAddFile
.index dvxFontAddMemory
@@ -822,14 +832,16 @@ void dvxFontRescan(void);
Read the FONTS folder again on the next call, after fonts were added or removed. The fonts no one holds are unloaded; open fonts stay valid.
-.h2 dvxFontSetSmooth / dvxFontSmooth
+.h2 dvxFontSetSmooth / dvxFontSmooth / dvxFontSmoothBegin / dvxFontRebuildStep
.code
void dvxFontSetSmooth(bool smooth);
bool dvxFontSmooth(void);
+void dvxFontSmoothBegin(bool smooth);
+bool dvxFontRebuildStep(void);
.endcode
-Smooth fonts where their face has the size smoothed, or stop; dvxFontSmooth says which it is (off at first). The fonts open are built again in place: the same BitmapFontT pointers with new glyphs and a new serial, so code that keeps measurements of a font compares its serial too, and text is laid out again. The fonts no one holds are unloaded. Programs call dvxSetFontSmoothing (dvxApp.h) instead, which follows the user's choice and the screen's depth.
+Smooth fonts where their face has the size smoothed, or stop; dvxFontSmooth says which it is (off at first). The fonts open are built again in place: the same BitmapFontT pointers with new glyphs and a new serial, so code that keeps measurements of a font compares its serial too, and text is laid out again. The fonts no one holds are unloaded. dvxFontSetSmooth builds them all before it returns; dvxFontSmoothBegin only switches and marks them, and each dvxFontRebuildStep builds one of them again (15-150 ms a font), false when none is left (the fonts before the fonts with lines, which copy them). Programs call dvxSetFontSmoothing (dvxApp.h) instead, which follows the user's choice and the screen's depth and builds the fonts a frame at a time.
.h2 dvxFontAddFile / dvxFontAddMemory
@@ -870,6 +882,17 @@ The texts that go with the face font was built from: its notice -- the family an
Returns: The texts; NULL for the system font, and when the font's file cannot be read.
+.h2 Code Page 437 and Unicode
+
+.code
+extern const uint16_t dfnCp437[DFN_CHAR_COUNT];
+int32_t dfnFromUnicode(uint32_t code);
+int32_t dfnTextFromUnicode(uint32_t code);
+uint32_t dfnTextToUnicode(uint8_t ch);
+.endcode
+
+Declared in dvxFontFmt.h. DVX text and its fonts are code page 437. dfnCp437[c] is the Unicode code point of character c's glyph (0 for none), so the characters below 32 are the smileys, card suits, and arrows the fonts draw for them; dfnFromUnicode is the character whose glyph a code point is, -1 for none. For text, dfnTextFromUnicode and dfnTextToUnicode keep ASCII (line ends, tabs, and the other controls included) as itself and map the rest through code page 437; dfnTextFromUnicode is -1 for a code point DVX has no character for. The JSON library reads and writes Unicode this way, and BASIC's ChrW$ and AscW are these two.
+
.topic api.flow
.title dvxFlow.h -- Flowing Text
.toc 1 dvxFlow.h -- Flowing Text
@@ -2799,6 +2822,9 @@ Install an icon that is already in display pixel format (for example from dvxLoa
.index dvxLoadImageFromMemory
.index dvxFreeImage
.index dvxImageInfo
+.index dvxImageInfoFromMemory
+.index dvxSetWaitPump
+.index dvxWaitPump
.index dvxSaveImage
.index dvxScreenshot
.index dvxWindowScreenshot
@@ -2825,6 +2851,7 @@ Install an icon that is already in display pixel format (for example from dvxLoa
.index platformPathAbsolute
.index DOS programs
.index dvxExclusiveEnd
+.index dvxExclusiveMirror
.index dvxExclusiveDisplay
.index dvxExclusiveBlitOps
.index dvxExclusiveFlush
@@ -3397,7 +3424,7 @@ Returns: Pointer to the VideoModeInfoT array.
void dvxSetColor(AppContextT *ctx, ColorIdE id, uint8_t r, uint8_t g, uint8_t b);
.endcode
-Set a single color by ID. Repacks to native pixel format and invalidates the entire screen.
+Set a single color by ID. Repacks to native pixel format; every window is painted again at the next frame (once, however many colors change before it) and the whole screen composited.
.table
Parameter Description
@@ -3577,7 +3604,7 @@ The [desktop] wallpaper and mode keys of an INI handle (mode through prefsWallpa
void dvxSetFontSmoothing(AppContextT *ctx, bool on);
.endcode
-Smooth text, or stop (ctx->fontSmoothing; on after dvxInit). Text is smoothed when on and the screen has more than 8 bits per pixel (dvxFontSetSmooth, dvxText.h), in the faces and sizes that have a smoothed strike; dvxChangeVideoMode applies it again at the new depth. When what is drawn changes, every window is laid out and painted again.
+Smooth text, or stop (ctx->fontSmoothing; on after dvxInit). Text is smoothed when on and the screen has more than 8 bits per pixel (dvxFontSmoothBegin, dvxText.h), in the faces and sizes that have a smoothed strike; dvxChangeVideoMode applies it again at the new depth. The fonts open are built again a font a frame (dvxUpdate calls dvxFontRebuildStep), and every window is laid out and painted again after the last.
.h3 dvxFontsFromPrefs / dvxFontsToPrefs
@@ -3683,6 +3710,17 @@ 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.
+.h3 dvxSetWaitPump / dvxWaitPump
+
+.code
+typedef bool (*DvxWaitPumpT)(void);
+
+void dvxSetWaitPump(DvxWaitPumpT fn);
+bool dvxWaitPump(void);
+.endcode
+
+A library that waits for something (a sound to end, as dvxPlaySoundWait does) calls dvxWaitPump between its checks: DVX goes on meanwhile, and so do the events of the BASIC program that waits, as in DoEvents (the hook the BASIC runtime sets with dvxSetWaitPump). It returns false when the wait should stop: the program is ending, or its code may not wait (a control written in BASIC). Without a hook it yields the processor and returns true.
+
.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.
@@ -3715,6 +3753,16 @@ void dvxExclusiveEnd(AppContextT *ctx, WindowT *win);
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 dvxExclusiveMirror
+
+.code
+int32_t dvxExclusiveMirror(AppContextT *ctx, WindowT *win, ExclusiveFnT onChange, void *userData);
+.endcode
+
+Full screen for a window's own content (a window of widgets, a drawing) rather than a picture the program draws: the smallest mode of at least the content's size in the desktop's pixel format, the content scaled by the largest whole factor that fits and centred on black, and shown again whenever the window paints (any dirty rectangle over its content). The mouse works on the content as in the window -- onMouse gets content coordinates -- and its cursor is drawn, in the shape the content asks for. Keys, suspending, resuming, and giving back are as for dvxExclusiveBegin; the BASIC form runtime's Form.FullScreen is this.
+
+Returns: as dvxExclusiveBegin; DVX_EXCL_ERR_MODE also when there is no mode in the desktop's pixel format.
+
.h3 dvxExclusiveDisplay / dvxExclusiveBlitOps
.code
@@ -3775,7 +3823,7 @@ platformPathAbsolute makes a path absolute against the current folder (it need n
bool dvxLibraryLoaded(const char *name);
.endcode
-Whether the library (or widget) name, its file's base name in any case ("pkgnet", "dvxsql"), is loaded: installed and started with DVX, which loads every library and widget it has at start. It looks for the module's ABI stamp (DVX_ABI_STAMP: name and DVX_ABI_SUFFIX), which the loader checked before it took the module; a module that is not there, or did not load, has none. Some parts of DVX are optional (Setup's "What to install" page installs them as bundles: src/packages.ini), so a program that uses one of their libraries only when it is there asks first, and does not import it: an import of a missing library stops the program from loading. BASIC: SysLibraryLoaded (system.bas).
+Whether the library (or widget) name, its file's base name in any case ("pkgnet", "dvxsql"), is installed and loaded. DVX loads a library when something first needs it, so asking loads one that is installed but not loaded yet. It looks for the module's ABI stamp (DVX_ABI_STAMP: name and DVX_ABI_SUFFIX), which the loader checks before it takes the module; a module that is not there, or did not load, has none. Some parts of DVX are optional (Setup's "What to install" page installs them as bundles: src/packages.ini), so a program that uses one of their libraries only when it is there asks first, and does not import it: an import of a missing library stops the program from loading. BASIC: SysLibraryLoaded (system.bas).
.h2 Idle Time: dvxIdleMs
@@ -4005,6 +4053,8 @@ uint8_t *dvxLoadImage(const AppContextT *ctx, const char *path, int32_t *outW, i
Load an image file (BMP, PNG, JPEG, GIF) and convert to the display's native pixel format. A file that is no image but a DVX program, widget, or library with an icon resource gives that icon (dvxResLoadIconData), so every picture that takes a file name can show a program's icon. Caller must free with dvxFreeImage().
+Called from a program's own task (where the task takes no input: a BASIC program), the image loaders decode a big picture a part at a time, a frame of DVX's every 32 KB read, so the other programs go on; none of the caller's code runs meanwhile. On the task that takes the input they decode in one go.
+
.table
Parameter Description
--------- -----------
@@ -4078,7 +4128,7 @@ Free a pixel buffer returned by dvxLoadImage() or dvxLoadImageFromMemory().
bool dvxImageInfo(const char *path, int32_t *outW, int32_t *outH);
.endcode
-Query image dimensions without decoding the full file.
+Query image dimensions without decoding the full file (its header only: a picture damaged after it may still fail to load).
.table
Parameter Description
@@ -4089,6 +4139,14 @@ Query image dimensions without decoding the full file.
Returns: true on success; false for an unreadable image or one without pixels.
+.h3 dvxImageInfoFromMemory
+
+.code
+bool dvxImageInfoFromMemory(const uint8_t *data, int32_t dataLen, int32_t *outW, int32_t *outH);
+.endcode
+
+dvxImageInfo for an image's bytes in memory (as LoadPicture checks a picture before a control shows it).
+
.h3 dvxSaveImage
.code
@@ -4445,6 +4503,8 @@ A library that has drivers puts them in drivers// in its source folder; it
.index wgtValueDouble
.index WGT_IFACE_DOUBLE
.index wgtValueStr
+.index wgtValueBytes
+.index wgtValueLen
.index wgtValueToInt
.index wgtValueToBool
.index wgtValueToFloat
@@ -5410,6 +5470,7 @@ A widget class whose properties and methods are not C functions with fixed signa
.code
typedef struct {
uint8_t type; // WGT_IFACE_*
+ int32_t len; // STRING: its length when it may hold NUL bytes; 0 = up to its NUL
union {
int32_t i; // INT, BOOL (0 / 1), ENUM
double f; // FLOAT, DOUBLE
@@ -5418,7 +5479,7 @@ typedef struct {
} WgtValueT;
.endcode
-One property value, method argument or result, or event argument. WGT_IFACE_NONE (5) is "no value" (a void method's result). A STRING value never owns its text: an argument's text belongs to the caller for the duration of the call; a returned one (a property read, a method result) belongs to the widget and stays valid until the next call on that widget or until the widget changes or is destroyed -- copy it to keep it. wgtValueInt, wgtValueBool, wgtValueFloat, wgtValueDouble, and wgtValueStr (NULL becomes "") build values. FLOAT is BASIC's Single, DOUBLE its Double: a DOUBLE property or argument keeps every digit (a value through FLOAT is rounded to a float by a static widget's getter and setter, and the form runtime shows it as a Single); as text a DOUBLE has 15 significant digits.
+One property value, method argument or result, or event argument. WGT_IFACE_NONE (5) is "no value" (a void method's result). A STRING value never owns its text: an argument's text belongs to the caller for the duration of the call; a returned one (a property read, a method result) belongs to the widget and stays valid until the next call on that widget or until the widget changes or is destroyed -- copy it to keep it. wgtValueInt, wgtValueBool, wgtValueFloat, wgtValueDouble, and wgtValueStr (NULL becomes "") build values; wgtValueBytes(s, len) builds a STRING of len bytes that may hold NUL bytes (s[len] must still be a NUL). A BASIC string holds any bytes, so the form runtime passes every string this way, and a STRING's length is wgtValueLen(v): its len, or up to its NUL when len is 0. A widget that takes binary text (Terminal Write) reads that length; one that wants C text reads s, which stops at the first NUL. FLOAT is BASIC's Single, DOUBLE its Double: a DOUBLE property or argument keeps every digit (a value through FLOAT is rounded to a float by a static widget's getter and setter, and the form runtime shows it as a Single); as text a DOUBLE has 15 significant digits.
.h3 wgtValueToInt / wgtValueToBool / wgtValueToFloat / wgtValueToText
@@ -6161,6 +6222,7 @@ Returns: 0 on success, -1 if not found, the name is too long, or I/O failed.
.index Preferences
.index PrefsHandleT
.index prefsLoad
+.index prefsDvxIniShared
.index prefsSave
.index prefsGetString
.index prefsGetInt
@@ -6220,6 +6282,14 @@ Create an empty preferences handle with no associated file. Use this when buildi
Returns: New handle, or NULL on allocation failure.
+.h3 prefsDvxIniShared
+
+.code
+PrefsHandleT *prefsDvxIniShared(void);
+.endcode
+
+DVX.INI for reading, shared: parsed again only when the file's text changed (its text is read and compared, a fraction of a parse), for code that reads settings often (every sound, every song). Its users do not close it, change it, or keep it past their own call: the next call may read the file again.
+
.h3 prefsLoad
.code
@@ -6506,10 +6576,12 @@ Return the index of the choice named by the key's value; the index of defaultNam
.index dvxStrdup
.index dvxMemGetAppUsage
.index dvxMemResetApp
+.index dvxMemSetApp
+.index DVX_MEM_SYSTEM
.h1 dvxMem.h -- Per-App Memory Tracking
-Wraps malloc/free/calloc/realloc/strdup with a 16-byte header that records the owning app ID and allocation size. The Task Manager displays per-app memory usage, and leaks are detected when an app is terminated.
+Wraps malloc/free/calloc/realloc/strdup with a 16-byte header that records the owning app ID and allocation size. The Task Manager displays per-app memory usage. The accounting is all it is: when an app ends its count starts over, and nothing it allocated is freed for it.
DXE code does NOT need to include this header for tracking to work. The DXE export table maps malloc/free/calloc/realloc/strdup to these wrappers transparently. Call sites that need the explicit dvxXxx names (e.g. dvxMemGetAppUsage) or the dvxMemAppIdPtr declaration should include dvxMem.h directly.
@@ -6589,7 +6661,7 @@ Returns: Bytes allocated for this app.
void dvxMemResetApp(int32_t appId);
.endcode
-Free every tracked allocation charged to the given app. Called by the shell after an app exits or crashes to reclaim its memory.
+Start the given app's accounting over: its usage reads 0, and a block it allocated that is freed later is not subtracted again (the block carries the old generation). Nothing is freed: a library's buffers made while the app ran stay valid. The shell calls it when an app ends.
.table
Parameter Description
@@ -6597,6 +6669,97 @@ Free every tracked allocation charged to the given app. Called by the shell afte
appId App ID to flush
.endtable
+.h3 dvxMemSetApp
+
+.code
+int32_t dvxMemSetApp(int32_t appId);
+.endcode
+
+Make appId the current app (the one what is allocated from now is charged to, as the shell sees it too) and return the one before, to set back afterwards. A library keeps its own memory as DVX's (DVX_MEM_SYSTEM, 0) whichever app's call made it, so the Task Manager does not charge a cache or a font to the program that happened to ask first; the audio library calls a program's callback as that program. Without a shell (host tools) nothing changes and 0 comes back.
+
+.code
+int32_t prev = dvxMemSetApp(DVX_MEM_SYSTEM);
+// ... allocations that belong to the library ...
+dvxMemSetApp(prev);
+.endcode
+
+.topic api.endian
+.title dvxEndian.h -- Byte Order
+.toc 1 dvxEndian.h -- Byte Order
+.index dvxEndian.h
+.index dvxGetLe16
+.index dvxGetLe32
+.index dvxGetBe16
+.index dvxGetBe32
+.index dvxPutLe16
+.index dvxPutLe32
+.index dvxPutBe32
+
+.h1 dvxEndian.h -- Byte Order
+
+File formats and wire protocols fix their byte order. These inline functions read and write it a byte at a time, so they work at any alignment and on any computer. Header only: include it and call, nothing to link.
+
+.code
+uint16_t dvxGetBe16(const uint8_t *p);
+uint32_t dvxGetBe32(const uint8_t *p);
+uint16_t dvxGetLe16(const uint8_t *p);
+uint32_t dvxGetLe24(const uint8_t *p);
+uint32_t dvxGetLe32(const uint8_t *p);
+void dvxPutBe32(uint8_t *p, uint32_t v);
+void dvxPutLe16(uint8_t *p, uint32_t v);
+void dvxPutLe32(uint8_t *p, uint32_t v);
+.endcode
+
+Get reads the value at p, high byte first (Be) or low byte first (Le); Put writes v at p the same way (dvxPutLe16 writes the low 16 bits of v). Cast a load to a signed type for a signed field, for example (int16_t)dvxGetLe16(p) for a 16-bit sample.
+
+.topic api.hash
+.title dvxHash.h -- Hashes and Checksums
+.toc 1 dvxHash.h -- Hashes and Checksums
+.index dvxHash.h
+.index dvxCrc16
+.index dvxFnvByte
+.index dvxFnvBytes
+.index dvxHexValue
+.index DVX_FNV_BASIS
+.index DVX_HEX_DIGITS
+
+.h1 dvxHash.h -- Hashes, Checksums, and Hex Digits
+
+Small inline functions every part of DVX uses for the same jobs: FNV-1a for hash tables and change detection, CRC-16-CCITT for serial packets and XMODEM / ZMODEM, and hex digits both ways. Header only: include it and call, nothing to link.
+
+.h2 FNV-1a
+
+.code
+#define DVX_FNV_BASIS 2166136261u
+#define DVX_FNV_PRIME 16777619u
+
+uint32_t dvxFnvByte(uint32_t hash, uint8_t byte);
+uint32_t dvxFnvBytes(uint32_t hash, const void *data, int32_t len);
+.endcode
+
+Start from DVX_FNV_BASIS and fold in bytes. dvxFnvByte takes one, so a caller can fold case or skip characters as it goes; dvxFnvBytes takes len bytes. Both return the hash carried on.
+
+.h2 CRC-16-CCITT
+
+.code
+uint16_t dvxCrc16(uint16_t crc, const uint8_t *data, int32_t len);
+.endcode
+
+The CRC of len bytes (polynomial 0x1021, high bit first), carried on from crc, computed a byte at a time without a table. Seed 0 for XMODEM and ZMODEM, 0xFFFF for CCITT-FALSE (the serial packet layer's).
+
+.h2 Hex Digits
+
+.code
+#define DVX_HEX_DIGITS "0123456789abcdef"
+#define DVX_HEX_RADIX 16
+#define DVX_NIBBLE_BITS 4
+#define DVX_NIBBLE_MASK 0x0F
+
+int32_t dvxHexValue(int32_t c);
+.endcode
+
+DVX_HEX_DIGITS[n] is the digit for the nibble n. dvxHexValue is a digit's value (0 to 15, either case), -1 for any other character.
+
.topic api.widgetplugin
.title dvxWgtP.h -- Widget Plugin API
.toc 1 dvxWgtP.h -- Widget Plugin API
@@ -7054,6 +7217,8 @@ stb_ds dynamic array. Each widget DXE appends its class pointer via wgtRegisterC
.index platformPathTidy
.index Path Separators
.index platformPathSibling
+.index platformPathParent
+.index platformPathIsRoot
.index platformReplaceFile
.index platformFilterMatch
.index platformDriveList
@@ -7070,6 +7235,8 @@ stb_ds dynamic array. Each widget DXE appends its class pointer via wgtRegisterC
.index platformValidateShortName
.index platformLongNameChar
.index platformStatSize
+.index platformFileStamp
+.index PlatformStampT
.index platformShortPath
.index PLATFORM_LONG_NAME_MAX
.index Long File Names
@@ -7198,7 +7365,7 @@ Monotonic tick counter for UI timing (double-click, tooltip delay, cursor blink,
uint64_t platformEntropyStamp(void);
.endcode
-Fine-grained, free-running timestamp for input-timing entropy. It is not a clock: units and wrap point are backend-specific. On DOS it returns the CPU time-stamp counter (RDTSC) when CPUID reports one, otherwise the BIOS tick count shifted left 16 bits with the live PIT channel 0 counter (about 0.84 us resolution) in the low 16 bits. Used by dvxSetInputEntropyHook.
+Fine-grained, free-running timestamp for entropy. It is not a clock: units and wrap point are backend-specific. On DOS it reads the live PIT channel 0 counter (about 0.84 us resolution, its own crystal) and returns it in the top 16 bits of the CPU time-stamp counter (RDTSC) when CPUID reports one, so a sample carries the jitter between the two clocks; otherwise the BIOS tick count shifted left 16 bits with the PIT counter in the low 16 bits. Used by dvxSetInputEntropyHook and by the security library's entropy gathering.
.h3 platformMicros
@@ -7502,6 +7669,19 @@ uint64_t platformStatSize(const struct stat *st);
The size stat reported for a file. A FAT32 file reaches 4 GB - 1 byte, but the DOS C library's st_size is a signed 32-bit off_t, so a file past 2 GB reads as negative there; platformStatSize reads it as the unsigned 32-bit size DOS keeps. Use it wherever a file size comes from stat or fstat.
+.h3 platformFileStamp
+
+.code
+typedef struct {
+ uint32_t size; // bytes (a file of 4 GB or more: 0xFFFFFFFF)
+ uint32_t time; // its date and time, as the platform keeps them
+} PlatformStampT;
+
+bool platformFileStamp(const char *path, PlatformStampT *out);
+.endcode
+
+A file's size and time from its folder alone, for telling whether a file read before has changed: compare two stamps for equality only. On DOS it is one directory search; stat makes several DOS calls there, tens of milliseconds each on a slow disk. Elsewhere it is stat, with the time's nanoseconds folded in. False when path is no file, or holds a wildcard. DOS keeps a file's time to two seconds, so a file rewritten at the same size within two seconds keeps its stamp. The shell's program cache checks the programs it keeps with it.
+
.h3 platformMkdirRecursive
.code
@@ -7539,6 +7719,8 @@ DVX paths follow DOS rules on every platform: '/' and '\\' are the same separato
.item platformPathIsUnder: when path is dir or lies below it, the rest of path after dir and its separator ("" for dir itself); NULL otherwise.
.item platformPathJoin: dir and name with one separator between them (none added when dir ends in one), every separator native; an absolute name, or an empty dir, gives name alone. False when it does not fit.
.item platformPathDirName: the folder part of path in native separators: a root keeps its separator ("C:\\X.TXT" gives "C:\\", since "C:" alone is the drive's current folder), "C:X.TXT" gives "C:", a bare name gives ".". False when it does not fit.
+.item platformPathParent: the folder holding path, a trailing separator ignored, in native separators ("C:/DVX/APPS/" gives "C:\\DVX", "/a" gives "/"). False for a root, a name without a folder, or when it does not fit.
+.item platformPathIsRoot: true for a drive or file-system root: "C:", "C:\\", "/", or "\\".
.item platformPathTidy: platformPathNative, then each run of separators made one (a leading pair, a network "\\\\server", kept) and a trailing separator dropped unless it is a root's: the form for a path handed to DOS directly, without the C library in between. platformShortPath and the file operations library's attribute calls use it.
.endlist
@@ -7604,6 +7786,18 @@ bool platformWriteFile(const char *path, const void *data, int32_t len);
Write len bytes of data as the whole of path, all or nothing: into the PLATFORM_TMP_EXT sibling, then swapped in with platformReplaceFile, so a full disk or an I/O error leaves the file as it was. False when it was not done. The preferences system, the IDE's saves, Setup, and the package records use it.
+.index platformWalkDir
+
+.h3 platformWalkDir
+
+.code
+typedef bool (*PlatformWalkFnT)(const char *path, const char *name, void *ctx);
+
+bool platformWalkDir(const char *dir, PlatformWalkFnT fn, void *ctx);
+.endcode
+
+Visit every file below dir, depth first, entering folders as they come and skipping names that start with '.'. A folder's names are read in full before any is visited (DOS has few file handles), and which are folders comes with them (platformDirEntryIsDir), not from a stat of each. fn gets each file's full path and its name; returning false stops the walk, and platformWalkDir then returns false too. A folder that cannot be read is skipped. The loader's module and help scans and the shell's program list use it.
+
.h3 platformReadFile
.code
@@ -7715,7 +7909,7 @@ The DVX folder (the working directory) holds the program, which Setup installs,
Only packages put files in SHARED: DVX's own, and add-on packages (pkg.lib), each in its vendor's folders. A program reads it but never writes or installs into it; a program's own files live with it (its DATA folder, or resources inside the .app), under its own vendor and program folder, so two programs' files never collide.
-platformUserInit, called once by the loader, decides the user folder: the folder the DVXUSER environment variable names, else USER in the DVX folder, made when missing. When it cannot be written (DVX on a CD-ROM without DVXUSER), DVXUSER in the TEMP folder is used instead and platformUserInTemp is true; without a TEMP folder nothing can be saved and platformIsReadOnly is true. The first start copies SYSTEM\DVX.INI to the user folder. platformUserDir is the folder (DVXUSER or USER before platformUserInit has decided); platformUserPath joins a name to it; platformDvxIniPath is its DVX.INI. platformUserDirOf is the user folder of a DVX installed in another folder (DVXUSER, else that folder's USER), for Setup.
+platformUserInit, called once by the loader (and not exported to modules), decides the user folder: the folder the DVXUSER environment variable names, else USER in the DVX folder, made when missing. When it cannot be written (DVX on a CD-ROM without DVXUSER), DVXUSER in the TEMP folder is used instead and platformUserInTemp is true; without a TEMP folder nothing can be saved and platformIsReadOnly is true. The first start copies SYSTEM\DVX.INI to the user folder. platformUserDir is the folder (DVXUSER or USER before platformUserInit has decided); platformUserPath joins a name to it; platformDvxIniPath is its DVX.INI. platformUserDirOf is the user folder of a DVX installed in another folder (DVXUSER, else that folder's USER), for Setup.
platformAppUserDir is a program's folder in the user folder: APPS\KPUNCH\TERMINAL\TERMINAL.APP gives USER\APPS\KPUNCH\TERMINAL; a program outside the DVX folder gives USER\APPS\. It is not made here (the shell's userDir and BASIC's App.User are made on first use).
@@ -7807,7 +8001,7 @@ Append arg to the command line in buf at pos as one argument that dvxNextArg rea
void platformRegisterDxeExports(void);
.endcode
-Register platform and C runtime symbols with the dynamic module loader so DXE modules can resolve them at load time.
+Register platform and C runtime symbols with the dynamic module loader so DXE modules can resolve them at load time. The loader's own, so not exported to modules.
.h3 platformRegisterSymOverrides
@@ -7815,7 +8009,7 @@ Register platform and C runtime symbols with the dynamic module loader so DXE mo
void platformRegisterSymOverrides(const PlatformSymOverrideT *entries);
.endcode
-Register function pointer overrides for subsequently loaded modules. entries is terminated by {NULL, NULL}. On DOS this wraps dlregsym(). Nothing in DVX itself calls it: it is SDK API for embedders and test harnesses that need to substitute a symbol (e.g. a stub) before loading a module.
+Register function pointer overrides for subsequently loaded modules. entries is terminated by {NULL, NULL}. On DOS this wraps dlregsym(). Nothing in DVX itself calls it: it is SDK API for embedders and test harnesses that need to substitute a symbol (e.g. a stub) before loading a module, so code linked with the platform layer, not a module (it is not exported).
.h2 Interrupt-Safe Memory
diff --git a/src/libs/kpunch/libdvx/arch.dhs b/src/libs/kpunch/libdvx/arch.dhs
index ed6d5577..ccf5d54a 100644
--- a/src/libs/kpunch/libdvx/arch.dhs
+++ b/src/libs/kpunch/libdvx/arch.dhs
@@ -554,7 +554,8 @@ Explicit use (e.g. in the Task Manager) can include dvxMem.h to call:
Function Purpose
-------- -------
dvxMemGetAppUsage Query current bytes allocated for an app
- dvxMemResetApp Free every tracked allocation charged to an app
+ dvxMemResetApp Start an app's accounting over (nothing is freed)
+ dvxMemSetApp Make an app the current one (a library's own memory: DVX_MEM_SYSTEM)
.endtable
The dvxMemAppIdPtr pointer is set by the shell to &ctx->currentAppId so the allocator always knows which app to charge.
@@ -577,7 +578,7 @@ DVX uses DJGPP's DXE3 (Dynamic eXtension) format for all loadable modules. DXE3
Extension Directory Purpose Examples
--------- --------- ------- --------
.lib LIBS/ Core libraries loaded first. Provide infrastructure APIs. Each exports an ABI stamp. libtasks.lib, libdvx.lib, dvxshell.lib
- .wgt WIDGETS/ Widget type plugins. Each exports wgtRegister() and an ABI stamp. button.wgt, listview.wgt, terminal.wgt
+ .wgt WIDGETS/ Widget type plugins. Each exports wgtRegister() and an ABI stamp. button.wgt, listview.wgt, ansiterm.wgt
.app APPS/*/ Application modules. Each exports appDescriptor (which records its ABI) and appMain(). Loaded on demand by the shell. progman.app, notepad.app, cpanel.app
.endtable
@@ -586,7 +587,7 @@ DVX uses DJGPP's DXE3 (Dynamic eXtension) format for all loadable modules. DXE3
.index DVX_ABI_VERSION
.index ABI stamp
-DVX_ABI_VERSION (dvxTypes.h) is one number for the binary interface between all modules. Each .lib and .wgt exports AbiVersion (written by DVX_ABI_STAMP(baseName) in its main source file); each app's AppDescriptorT starts with abiVersion (SHELL_APP_DESCRIPTOR fills it); each widget API struct starts with a WgtApiHeaderT; each widget class carries WGT_CLASS_VERSION, which is the same number. The loader checks every .lib and .wgt right after loading it and stops the boot with a message naming a module whose stamp is missing or different; the shell refuses a mismatched app with a message box; wgtGetApi and wgtRegisterClass refuse mismatched tables and classes. Details always go to DVX.LOG. The bump rule and the size-table test that enforces it are described with DVX_ABI_VERSION in the API reference (dvxTypes.h).
+DVX_ABI_VERSION (dvxTypes.h) is one number for the binary interface between all modules. Each .lib and .wgt exports AbiVersion (written by DVX_ABI_STAMP(baseName) in its main source file); each app's AppDescriptorT starts with abiVersion (SHELL_APP_DESCRIPTOR fills it); each widget API struct starts with a WgtApiHeaderT; each widget class carries WGT_CLASS_VERSION, which is the same number. The loader checks every .lib and .wgt right after loading it: at the start it stops the boot with a message naming a module whose stamp is missing or different, and a library loaded later (see Boot Sequence) is refused and logged; the shell refuses a mismatched app with a message box; wgtGetApi and wgtRegisterClass refuse mismatched tables and classes. Details always go to DVX.LOG. The bump rule and the size-table test that enforces it are described with DVX_ABI_VERSION in the API reference (dvxTypes.h).
.h2 Boot Sequence
@@ -601,19 +602,30 @@ DVX_ABI_VERSION (dvxTypes.h) is one number for the binary interface between all
|
+-- Read .dep files for each module (dependency base names)
|
- +-- Topological sort: load modules in dependency order
- | - dlopen() with RTLD_GLOBAL
- | - ABI stamp checked; a mismatch stops the boot
- | - Each .wgt that exports wgtRegister() has it called
+ +-- Index the names each library exports (its file's DXE header and
+ | export table only; a hash and the library for each name)
|
- +-- Find and call shellMain() (exported by dvxshell.lib)
+ +-- Load, each after the libraries it needs:
+ | - the libraries with constructors (code that runs as they load:
+ | audio, music, joystick, the network, Task Manager, texthelp)
+ | - the library with shellMain (dvxshell.lib)
+ | - every widget; each .wgt that exports wgtRegister() has it called
+ | - dlopen() with RTLD_GLOBAL; ABI stamp checked; a mismatch stops
+ | the boot
+ |
+ +-- Find and call shellMain()
+ |
+ +-- Every other library loads when something first needs one of
+ | its names: a module being opened (platformModuleOpen), or a
+ | dlsym(NULL) (BASIC's DECLARE LIBRARY, dvxLibraryLoaded)
|
+-- dvxInit() -- video mode, input, font, colors, cursors
|
+-- Load desktop app (progman.app)
|
+-- Main loop:
- dvxUpdate() -> tsYield() -> shellReapApps()
+ dvxUpdate() (yields) -> shellReapApps() -> close, exit,
+ and DOS-run requests -> screen saver
.endcode
.h2 Application Lifecycle
@@ -626,7 +638,9 @@ appMain() creates windows, registers callbacks, and returns. The app lives throu
.h3 Main-loop (hasMainLoop = true)
-A dedicated cooperative task is created. appMain() runs in that task with its own loop, calling tsYield() to share CPU. Needed for apps with continuous work (terminal emulators, games). Lifecycle ends when appMain() returns.
+A dedicated cooperative task is created. appMain() runs in that task with its own loop, calling tsYield() (or dvxUpdate, which yields) to share CPU. Needed for apps with continuous work (terminal emulators, games). Lifecycle ends when appMain() returns.
+
+Input is taken on the shell's own task (see Event Model), so every window callback -- a callback-only app's and a main-loop app's alike -- runs there, never on another program's stack; a main-loop app's own loop sees what its callbacks did when it next runs. A BASIC program's events are the exception: its form runtime queues those the shell's task dispatches and runs them on the program's own task at its next turn (DoEvents, its event loop), so a handler that waits (DoEvents, SHELL, a modal form) holds only its own program. Its UserControls' events (their mouse, keys, focus, Resize, and Paint) wait for that turn the same way: a program's code never runs on two tasks at once.
.h2 Crash Recovery
@@ -652,17 +666,17 @@ All allocations route through dvxMalloc/dvxFree wrappers that prepend a 16-byte
.h1 Event Model
-DVX uses a cooperative polling model. The main loop (dvxRun / dvxUpdate) runs this cycle each frame:
+DVX uses a cooperative polling model. The main loop (dvxRun / dvxUpdate) runs this cycle each frame. The first four steps run only in a frame that takes input: the context's inputHere hook decides, and the shell lets only its own task take it (or any task, once the shell's has not for a quarter of a second, busy in a long call), so callbacks run on the shell's task; a frame on another task just paints and yields.
.list
.item Poll mouse -- platformMousePoll() returns position and button bitmask. Compare with previous frame for press/release edge detection.
.item Poll keyboard -- platformKeyboardRead() returns ASCII + scancode. Non-blocking; returns false if buffer is empty. Shift/Ctrl/Alt+F-keys arrive as the plain F-key plus modifier flags. If an input entropy hook is installed (dvxSetInputEntropyHook), input events are timestamped and fed to it here.
.item Dispatch to focused window -- the event loop fires window callbacks (onKey, onMouse, etc.) on the focused window. If the window has a widget tree, the widget system's installed handlers dispatch to individual widgets.
.item Tooltips, widget polling (WCLASS_NEEDS_POLL), cursor blink, and widget timers.
-.item Deferred paint flush -- every window whose paintNeeded was raised by wgtInvalidate / wgtInvalidatePaint during the frame is painted once (the highest requested level wins). Windows queued for destruction are skipped.
+.item Deferred paint flush -- every window whose paintNeeded was raised by wgtInvalidate / wgtInvalidatePaint during the frame is painted once (the highest requested level wins). Windows queued for destruction are skipped. After a partial repaint (wgtInvalidatePaint only) the widget paint narrows the window's dirty area to the boxes of the widgets it repainted, joined and widened two pixels (a container that paints its children itself counts as its own box), so only that part of the screen is composited and flushed; a full repaint, an open popup list, and a window that paints itself composite the whole window.
.item Deferred destroy flush -- windows destroyed from inside callbacks are torn down once the dispatch depth is back to zero.
.item Compositor pass -- merge dirty rects, composite, flush to LFB.
-.item Yield -- platformYield() or idle callback.
+.item Yield -- the idle callback after a frame with nothing to paint, the busy callback after one that painted (the shell's run the idle handlers -- audio streams, network, serial pollers -- at least every 20 ms either way, and yield to the other tasks every frame), or platformYield() without them.
.endlist
.h2 Event Dispatch Chain
@@ -1043,37 +1057,34 @@ The -E flag specifies exported symbols (prefixed with underscore per DJGPP conve
.code
dvxgui/
- +-- core/ Core library sources (dvxVideo, dvxDraw, dvxComp, dvxWm, dvxApp, widget infra)
- | +-- platform/ Platform abstraction (dvxPlat.h, dvxPlatformDos.c, dvxPlatformUtil.c)
- | +-- thirdparty/ stb_image, stb_ds, stb_image_write
- +-- loader/ Bootstrap loader (dvx.exe)
- +-- tasks/ Cooperative task switcher (libtasks.lib)
- +-- shell/ DVX Shell (dvxshell.lib)
- +-- widgets/ Widget DXE modules (*.wgt), each in its own subdirectory
- | +-- box/ VBox/HBox layout containers
- | +-- button/ Push button
- | +-- textInput/ Text entry field
- | +-- listView/ Multi-column list
- | +-- ... (29 widget types total)
- +-- texthelp/ Shared text editing helpers
- +-- listhelp/ Shared dropdown/list helpers
- +-- apps/ Application DXE modules (*.app)
- | +-- progman/ Program Manager (desktop)
- | +-- notepad/ Text editor
- | +-- cpanel/ Control Panel
- | +-- imgview/ Image viewer
- | +-- clock/ Clock
- | +-- dvxdemo/ Demo / showcase app
- | +-- dvxbasic/ DVX BASIC compiler and VM
- +-- tools/ Build tools (dvxres resource compiler)
- +-- rs232/ ISR-driven UART driver
- +-- packet/ HDLC framing, CRC-16, sliding window
- +-- security/ DH key exchange, XTEA cipher, DRBG RNG
- +-- seclink/ Encrypted channel wrapper
- +-- serial/ Combined serial stack DXE
- +-- proxy/ Linux proxy (emulator <-> secLink <-> telnet)
- +-- sql/ SQLite integration
- +-- bin/ Build output (dvx.exe, system/, libs/, widgets/, apps/, shared/)
- +-- obj/ Intermediate object files
- +-- docs/ Documentation
+ +-- src/
+ | +-- loader/ Bootstrap loader (dvx.exe)
+ | +-- libs/kpunch/ Libraries (*.lib), each in its own folder
+ | | +-- libdvx/ Core: video, drawing, compositor, windows, widget infrastructure
+ | | | +-- platform/ Platform layer (dvxPlat.h, dvxPlatformDos.c, dvxPlatformUtil.c)
+ | | | +-- thirdparty/ stb_image, stb_ds, stb_image_write
+ | | +-- libtasks/ Cooperative task switcher
+ | | +-- dvxshell/ DVX Shell (Program Manager's host, program loading)
+ | | +-- texthelp/ Shared text editing helpers
+ | | +-- listhelp/ Shared dropdown/list helpers
+ | | +-- serial/ rs232: ISR-driven UART driver
+ | | +-- crypto/ X25519, XChaCha20-Poly1305, Ed25519 (Monocypher), DH, XTEA, SHA-256/HMAC/HKDF, DRBG
+ | | +-- seclink/ Packet transport (HDLC, CRC-16, Go-Back-N) and the secure link
+ | | +-- dvxnet/ TCP/IP (lwIP) and the BSD socket layer
+ | | +-- pkg/ Add-on packages (ZIP, manifests, signatures, install and remove)
+ | | +-- sql/ SQLite integration
+ | | +-- ... audio, music, print, conn, xfer, http, json, and more
+ | +-- widgets/kpunch/ Widget modules (*.wgt), each in its own folder (35)
+ | +-- apps/kpunch/ Programs (*.app): progman, notepad, cpanel, dvxbasic, terminal, ...
+ | +-- savers/kpunch/ Screen savers
+ | +-- addons/ Add-on package sources (fonts, instrument banks)
+ | +-- include/basic/ BASIC include files (*.bas)
+ | +-- installer/ SETUP.EXE and the package tools
+ | +-- sdk/ SDK templates
+ | +-- tools/ Build tools (dvxres, dvxhlpc, dxeGc.sh, ...); proxy/ is the Linux secure link proxy
+ | +-- test/ Test suites (host and DOS)
+ +-- bin/ Build output (dvx.exe, system/, libs/, widgets/, apps/, shared/)
+ +-- obj/ Intermediate object files
+ +-- dist/ Release media and the SDK
+ +-- docs/ Documentation
.endcode
diff --git a/src/libs/kpunch/libdvx/dvxApp.c b/src/libs/kpunch/libdvx/dvxApp.c
index a47dd392..6b9ec719 100644
--- a/src/libs/kpunch/libdvx/dvxApp.c
+++ b/src/libs/kpunch/libdvx/dvxApp.c
@@ -59,6 +59,8 @@
#include "dvxRes.h"
#include "dvxText.h"
#include "dvxPrefs.h"
+#include "dvxImgInt.h"
+#include "dvxResInt.h"
#include "thirdparty/stb_ds_wrap.h"
#include
@@ -71,8 +73,6 @@
#include
#include
-#include "thirdparty/stb_image_wrap.h"
-#include "thirdparty/stb_image_write_wrap.h"
// ABI stamp checked by the loader (see DVX_ABI_VERSION).
DVX_ABI_STAMP(libdvx);
@@ -107,12 +107,20 @@ DVX_ABI_STAMP(libdvx);
#define ACCEL_TEXT_CTRL "Ctrl"
#define ACCEL_TEXT_ALT "Alt"
#define ACCEL_TEXT_SHIFT "Shift"
-#define ACCEL_TEXT_MAX 32 // longest key text taken apart
#define ACCEL_FKEY_PREFIX 'F'
#define ACCEL_FKEY_FMT "F%ld"
#define ACCEL_FKEY_COUNT 12
#define ACCEL_FKEY_BASE 10
+// A frame's composite (compositeDirty) stops once COMPOSITE_BUDGET_US has
+// gone and keeps what is left of the dirty rects for the next frame, so a
+// full-screen repaint on a slow bus (0.2 - 1 s at 1024 x 768 x 32 on ISA)
+// is spread over frames, in bands of COMPOSITE_BAND_ROWS, the other tasks
+// running between; any other composite (compositeAndFlush) runs to the end.
+#define COMPOSITE_BUDGET_US 40000
+#define COMPOSITE_BAND_ROWS 64
+#define COMPOSITE_NO_BUDGET 0
+
// Cursor dirty region: covers all cursor shapes (16x16 with hotspot at 0,0 or 7,7)
#define CURSOR_HOTSPOT_X 7
#define CURSOR_HOTSPOT_Y 7
@@ -132,6 +140,13 @@ DVX_ABI_STAMP(libdvx);
// Wallpaper scaler yields to the event loop every N rows (power of two)
#define WALLPAPER_YIELD_ROWS 32
+// The wallpaper's decoded picture is kept for its next build (a wallpaper
+// mode or video mode change) when it takes no more than this
+#define WALLPAPER_KEEP_BYTES (2560 * 1024)
+
+// Image decodes that pump (imageDecode): a frame every so many bytes read
+#define IMAGE_PUMP_BYTES (32 * 1024)
+
// Ordered dithering (4x4 Bayer) is applied in the 15/16-bit modes
#define BAYER_SIZE 4
#define BAYER_MASK (BAYER_SIZE - 1)
@@ -155,6 +170,7 @@ DVX_ABI_STAMP(libdvx);
// Screenshot format selection and messages
#define BMP_EXT ".bmp"
+
#define SCREENSHOT_NOMEM_MSG "Not enough memory to capture the image."
#define SCREENSHOT_FAIL_MSG "The image could not be saved."
@@ -210,21 +226,21 @@ DVX_ABI_STAMP(libdvx);
// window callback, then restores the previous value. This ensures
// allocations made during menu handlers, close handlers, paint,
// etc. are attributed to the correct app. The call is also booked as
-// the app being busy (appCallEnter/appCallLeave, see dvxAppIsHung); the
+// the app being busy (dvxAppCallEnter/dvxAppCallLeave, see dvxAppIsHung); the
// app id is captured first because the callback may destroy the window.
#define WIN_CALLBACK(ctx, win, call) do { \
int32_t _prevAppId = (ctx)->currentAppId; \
int32_t _appId = (win)->appId; \
(ctx)->currentAppId = _appId; \
- appCallEnter(_appId); \
+ dvxAppCallEnter(_appId); \
call; \
- appCallLeave(_appId); \
+ dvxAppCallLeave(_appId); \
(ctx)->currentAppId = _prevAppId; \
} while (0)
// Per-channel expansion tables for converting a direct-color pixel to
-// 24-bit RGB (see bufferToRgb).
+// 24-bit RGB (see dvxBufferToRgb).
typedef struct {
int32_t shift[RGB_CHANNELS];
uint32_t mask[RGB_CHANNELS];
@@ -247,6 +263,18 @@ typedef struct {
uint32_t keyColor;
} DragFeedbackT;
+// An image decoded through stb's read callbacks (imageDecode): a file
+// (fp) or bytes (data, len, read at pos), and the context pumped every
+// IMAGE_PUMP_BYTES read (NULL: none)
+typedef struct {
+ FILE *fp;
+ const uint8_t *data;
+ int32_t len;
+ int32_t pos;
+ AppContextT *pumpCtx;
+ int32_t sincePump;
+} ImageSrcT;
+
// ============================================================
// Prototypes
@@ -256,11 +284,8 @@ static int32_t accelKeyFromKeystroke(int32_t scancode, int32_t ascii, int32_t ke
static void activatePopupItem(AppContextT *ctx, int32_t itemIdx);
static void addSysMenuItem(AppContextT *ctx, const char *label, int32_t cmd, bool enabled);
static void addSysMenuSeparator(AppContextT *ctx);
-static void appCallEnter(int32_t appId);
static int32_t appCallFind(int32_t appId);
-static void appCallLeave(int32_t appId);
static void blurFocusedWindow(AppContextT *ctx);
-static uint8_t *bufferToRgb(const DisplayT *d, const uint8_t *buf, int32_t w, int32_t h, int32_t pitch);
static void buildChannelLut(ChannelLutT *lut, const PixelFormatT *fmt);
static uint8_t *buildWallpaperBuf(AppContextT *ctx, const uint8_t *rgb, int32_t imgW, int32_t imgH, WallpaperModeE mode, int32_t *outPitch);
static void calcPopupSize(const AppContextT *ctx, const MenuT *menu, int32_t *pw, int32_t *ph, bool *hasCheck);
@@ -275,7 +300,10 @@ static void closeSysMenu(AppContextT *ctx);
static void closeWindowTransients(AppContextT *ctx, const WindowT *win);
static uint32_t *colorSlot(ColorSchemeT *cs, ColorIdE id);
static void compositeAndFlush(AppContextT *ctx);
+static void compositeDirty(AppContextT *ctx, uint64_t budgetUs);
+static void compositeRect(AppContextT *ctx, RectT *dr, const int32_t *visibleIdx, int32_t visibleCount);
static void consumeMousePress(AppContextT *ctx);
+static int32_t contentCursorShape(AppContextT *ctx, WindowT *win, int32_t cx, int32_t cy);
static uint8_t *convertRgbToNative(const DisplayT *d, const uint8_t *src, int32_t channels, int32_t w, int32_t h, uint32_t keyColor, bool *outHasAlpha, int32_t *outPitch);
static int32_t countVisibleWindows(const AppContextT *ctx);
static int32_t cursorForResizeEdge(int32_t edge);
@@ -302,8 +330,13 @@ 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 exclusiveMirrorDirty(AppContextT *ctx, const DirtyListT *dl);
+static void exclusiveMirrorMouse(AppContextT *ctx);
+static int32_t exclusiveMirrorPlace(AppContextT *ctx, RectT *place);
+static void exclusiveMirrorShow(AppContextT *ctx, const RectT *r);
static void exclusiveMouse(AppContextT *ctx);
static void exclusiveNotify(AppContextT *ctx, WindowT *win, ExclusiveFnT fn, void *userData, ExclusiveStateE state);
+static int32_t exclusiveStart(AppContextT *ctx, WindowT *win, int32_t width, int32_t height, int32_t bpp, ExclusiveFnT onChange, void *userData, bool mirror);
static void exclusiveSync(AppContextT *ctx);
static void executeSysMenuCmd(AppContextT *ctx, int32_t cmd);
static WindowT *findWindowById(AppContextT *ctx, int32_t id);
@@ -315,18 +348,25 @@ static void flushWindowPaint(AppContextT *ctx, WindowT *win);
static WindowT *focusedWindow(const AppContextT *ctx);
static void focusTopmostAlive(AppContextT *ctx);
static void focusWindowAt(AppContextT *ctx, int32_t idx);
-static bool fontSmoothSet(const AppContextT *ctx);
+static void fontSmoothSet(const AppContextT *ctx);
static void handleKbMoveResizeKey(AppContextT *ctx, int32_t scancode, int32_t ascii);
static void handleMouseButton(AppContextT *ctx, int32_t mx, int32_t my, int32_t buttons);
static void handlePopupKey(AppContextT *ctx, int32_t scancode, int32_t ascii);
static void handleRightButton(AppContextT *ctx, int32_t mx, int32_t my, int32_t buttons);
static bool handleSysMenuKey(AppContextT *ctx, int32_t scancode, int32_t ascii);
static void handleTabKey(AppContextT *ctx, WindowT *win, int32_t key, int32_t shiftFlags);
+static uint8_t *imageDecode(ImageSrcT *src, int *imgW, int *imgH, int channels);
+static bool imageInfoTake(int w, int h, int32_t *outW, int32_t *outH);
+static AppContextT *imagePumpCtx(const AppContextT *ctx);
+static int imageSrcEof(void *user);
+static int imageSrcRead(void *user, char *out, int size);
+static void imageSrcSkip(void *user, int n);
static void initColorScheme(AppContextT *ctx);
static void interactiveSaveBuffer(AppContextT *ctx, const char *title, const uint8_t *src, int32_t w, int32_t h, int32_t pitch);
static void interactiveScreenshot(AppContextT *ctx);
static void interactiveWindowScreenshot(AppContextT *ctx, WindowT *win);
static void invalidateAllWindows(AppContextT *ctx);
+static void invalidateAllWindowsLater(AppContextT *ctx);
static bool isFocusCandidate(const WindowT *win);
static bool isModalWindow(const AppContextT *ctx, const WindowT *win);
static void keyFocusSync(AppContextT *ctx);
@@ -361,7 +401,6 @@ 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);
@@ -458,6 +497,20 @@ static int32_t sEntropyLen = 0;
static DvxSoundHookT sSoundHook = NULL;
static int32_t sOpenSoundWinId = 0;
+// dvxSetWaitPump (NULL: waits only yield)
+static DvxWaitPumpT sWaitPump = NULL;
+
+// The wallpaper's decoded picture (RGB) and its file, kept while it is
+// the wallpaper (see WALLPAPER_KEEP_BYTES); NULL: none kept
+static uint8_t *sWallRgb = NULL;
+static int sWallRgbW = 0;
+static int sWallRgbH = 0;
+static char sWallRgbPath[DVX_MAX_PATH];
+
+// Fonts are being built again for a smoothing change (dvxUpdate steps
+// them): every window is laid out again after the last
+static bool sFontsRebuilding = false;
+
// The drag feedback shown (see DragFeedbackT); one mouse, so one for the
// whole desktop.
static DragFeedbackT sDragFeedback;
@@ -637,21 +690,6 @@ static void addSysMenuSeparator(AppContextT *ctx) {
}
-// Book the start of a window callback of appId (see WIN_CALLBACK).
-static void appCallEnter(int32_t appId) {
- int32_t idx = appCallFind(appId);
-
- if (idx < 0) {
- AppCallT call = { appId, 0, platformClock() };
-
- arrput(sAppCalls, call);
- idx = (int32_t)arrlen(sAppCalls) - 1;
- }
-
- sAppCalls[idx].depth++;
-}
-
-
// Index of appId's entry in sAppCalls, or -1.
static int32_t appCallFind(int32_t appId) {
for (int32_t i = 0; i < (int32_t)arrlen(sAppCalls); i++) {
@@ -664,17 +702,6 @@ static int32_t appCallFind(int32_t appId) {
}
-// Book the end of a window callback of appId. The entry may already be
-// gone (dvxAppHungForget when the app was ended inside the callback).
-static void appCallLeave(int32_t appId) {
- int32_t idx = appCallFind(appId);
-
- if (idx >= 0 && --sAppCalls[idx].depth == 0) {
- arrdel(sAppCalls, idx);
- }
-}
-
-
// Drops window-manager focus from the focused window, firing its onBlur
// under its own app id, and leaves focusedIdx at -1. Shared by the
// attributed focus switch, dvxHideWindow and dvxMinimizeWindow.
@@ -696,64 +723,6 @@ static void blurFocusedWindow(AppContextT *ctx) {
}
-// ============================================================
-// bufferToRgb -- convert native pixel format to 24-bit RGB
-// ============================================================
-//
-// Screenshots must produce standard RGB data for stb_image_write, but the
-// backbuffer uses whatever native pixel format VESA gave us (8/16/32bpp).
-// The 8bpp path uses the VGA palette for lookup. Direct-color channels
-// are widened through per-channel lookup tables built once per call with
-// bit replication, so full-scale values reach 255 (a 5-bit 31 becomes
-// 255, not 248). The pixel-width branch is hoisted out of the pixel loop.
-
-static uint8_t *bufferToRgb(const DisplayT *d, const uint8_t *buf, int32_t w, int32_t h, int32_t pitch) {
- uint8_t *rgb = (uint8_t *)malloc((size_t)w * h * RGB_CHANNELS);
-
- if (!rgb) {
- return NULL;
- }
-
- uint8_t *dst = rgb;
-
- if (d->format.bitsPerPixel == PALETTE_BPP) {
- for (int32_t y = 0; y < h; y++) {
- const uint8_t *row = buf + y * pitch;
-
- for (int32_t x = 0; x < w; x++) {
- memcpy(dst, d->palette + row[x] * RGB_CHANNELS, RGB_CHANNELS);
- dst += RGB_CHANNELS;
- }
- }
-
- return rgb;
- }
-
- ChannelLutT lut;
- buildChannelLut(&lut, &d->format);
-
- bool wide = (d->format.bytesPerPixel == sizeof(uint32_t));
-
- for (int32_t y = 0; y < h; y++) {
- const uint8_t *row = buf + y * pitch;
-
- if (wide) {
- for (int32_t x = 0; x < w; x++) {
- pixelToRgb(&lut, ((const uint32_t *)row)[x], dst);
- dst += RGB_CHANNELS;
- }
- } else {
- for (int32_t x = 0; x < w; x++) {
- pixelToRgb(&lut, ((const uint16_t *)row)[x], dst);
- dst += RGB_CHANNELS;
- }
- }
- }
-
- return rgb;
-}
-
-
// Fills the per-channel expansion tables for a direct-color format: every
// n-bit channel value maps to 8 bits by replicating its bits from the top
// down, so the maximum value maps to 255.
@@ -811,7 +780,7 @@ static uint8_t *buildWallpaperBuf(AppContextT *ctx, const uint8_t *rgb, int32_t
// Fill with the desktop color first (the border area in center mode):
// one row pixel by pixel, then replicate it.
for (int32_t x = 0; x < screenW; x++) {
- storePixel(buf, x, bytesPerPx, ctx->colors.desktop);
+ pixelPut(buf + x * bytesPerPx, ctx->colors.desktop, bytesPerPx);
}
for (int32_t y = 1; y < screenH; y++) {
@@ -869,7 +838,7 @@ static uint8_t *buildWallpaperBuf(AppContextT *ctx, const uint8_t *rgb, int32_t
int32_t g = (p00[1] * w00 + p10[1] * w10 + p01[1] * w01 + p11[1] * w11) >> FP_INT_SHIFT;
int32_t b = (p00[2] * w00 + p10[2] * w10 + p01[2] * w01 + p11[2] * w11) >> FP_INT_SHIFT;
- storePixel(dst, x, bytesPerPx, packDithered(d, r, g, b, y, x, dither));
+ pixelPut(dst + x * bytesPerPx, packDithered(d, r, g, b, y, x, dither), bytesPerPx);
}
}
@@ -888,7 +857,7 @@ static uint8_t *buildWallpaperBuf(AppContextT *ctx, const uint8_t *rgb, int32_t
for (int32_t x = 0; x < screenW; x++) {
const uint8_t *src = srcRow + srcX * RGB_CHANNELS;
- storePixel(dst, x, bytesPerPx, packDithered(d, src[0], src[1], src[2], y, x, dither));
+ pixelPut(dst + x * bytesPerPx, packDithered(d, src[0], src[1], src[2], y, x, dither), bytesPerPx);
srcX++;
if (srcX == imgW) {
@@ -923,7 +892,7 @@ static uint8_t *buildWallpaperBuf(AppContextT *ctx, const uint8_t *rgb, int32_t
int32_t dx = offX + sx;
const uint8_t *src = srcRow + sx * RGB_CHANNELS;
- storePixel(dst, dx, bytesPerPx, packDithered(d, src[0], src[1], src[2], dy, dx, dither));
+ pixelPut(dst + dx * bytesPerPx, packDithered(d, src[0], src[1], src[2], dy, dx, dither), bytesPerPx);
}
}
}
@@ -1211,15 +1180,27 @@ static uint32_t *colorSlot(ColorSchemeT *cs, ColorIdE id) {
// clipped by the draw layer.
static void compositeAndFlush(AppContextT *ctx) {
- DisplayT *d = &ctx->display;
- BlitOpsT *ops = &ctx->blitOps;
- DirtyListT *dl = &ctx->dirty;
- WindowStackT *ws = &ctx->stack;
+ compositeDirty(ctx, COMPOSITE_NO_BUDGET);
+}
+
+
+// The dirty rects composited and flushed, in bands of COMPOSITE_BAND_ROWS
+// when budgetUs is set: once that has gone, what is left stays in the
+// dirty list for the next frame (COMPOSITE_NO_BUDGET: all of it now).
+static void compositeDirty(AppContextT *ctx, uint64_t budgetUs) {
+ DisplayT *d = &ctx->display;
+ DirtyListT *dl = &ctx->dirty;
+ WindowStackT *ws = &ctx->stack;
+ uint64_t start = platformMicros();
// 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) {
+ if (ctx->exclusive.mirror) {
+ exclusiveMirrorDirty(ctx, dl);
+ }
+
dirtyListClear(dl);
return;
}
@@ -1241,122 +1222,52 @@ static void compositeAndFlush(AppContextT *ctx) {
}
for (int32_t i = 0; i < dl->count; i++) {
- RectT *dr = &dl->rects[i];
+ RectT whole = dl->rects[i];
// Clip dirty rect to screen bounds
- if (dr->x < 0) {
- dr->w += dr->x;
- dr->x = 0;
+ if (whole.x < 0) {
+ whole.w += whole.x;
+ whole.x = 0;
}
- if (dr->y < 0) {
- dr->h += dr->y;
- dr->y = 0;
+ if (whole.y < 0) {
+ whole.h += whole.y;
+ whole.y = 0;
}
- if (dr->x + dr->w > d->width) {
- dr->w = d->width - dr->x;
+ if (whole.x + whole.w > d->width) {
+ whole.w = d->width - whole.x;
}
- if (dr->y + dr->h > d->height) {
- dr->h = d->height - dr->y;
+ if (whole.y + whole.h > d->height) {
+ whole.h = d->height - whole.y;
}
- if (dr->w <= 0 || dr->h <= 0) {
- continue;
- }
+ while (whole.w > 0 && whole.h > 0) {
+ RectT band = whole;
- // Set clip rect to this dirty rect
- setClipRect(d, dr->x, dr->y, dr->w, dr->h);
+ band.h = budgetUs ? DVX_MIN(whole.h, COMPOSITE_BAND_ROWS) : whole.h;
+ compositeRect(ctx, &band, visibleIdx, visibleCount);
+ whole.y += band.h;
+ whole.h -= band.h;
- // Occlusion: find the topmost window whose frame covers the whole
- // dirty rect; nothing below it can show through.
- int32_t firstVi = 0;
- bool covered = false;
+ // Out of time: the rest waits for the next frame
+ if (budgetUs && platformMicros() - start >= budgetUs && (whole.h > 0 || i + 1 < dl->count)) {
+ int32_t kept = 0;
- for (int32_t vi = visibleCount - 1; vi >= 0; vi--) {
- if (windowCoversRect(ws->windows[visibleIdx[vi]], dr)) {
- firstVi = vi;
- covered = true;
- break;
- }
- }
-
- if (!covered) {
- // 1. Draw desktop background (wallpaper or solid color)
- if (ctx->wallpaperBuf) {
- int32_t bpp = d->format.bytesPerPixel;
-
- for (int32_t row = dr->y; row < dr->y + dr->h; row++) {
- ops->spanCopy(d->backBuf + row * d->pitch + dr->x * bpp, ctx->wallpaperBuf + row * ctx->wallpaperPitch + dr->x * bpp, dr->w);
+ if (whole.w > 0 && whole.h > 0) {
+ dl->rects[kept++] = whole;
}
- } else {
- rectFill(d, ops, dr->x, dr->y, dr->w, dr->h, ctx->colors.desktop);
- }
- // 2. Draw minimized window icons (under all windows)
- wmDrawMinimizedIcons(d, ops, &ctx->colors, ws, dr);
- }
+ for (int32_t j = i + 1; j < dl->count; j++) {
+ dl->rects[kept++] = dl->rects[j];
+ }
- // 3. Walk pre-filtered visible window list bottom-to-top
- for (int32_t vi = firstVi; vi < visibleCount; vi++) {
- WindowT *win = ws->windows[visibleIdx[vi]];
-
- // Check if window intersects this dirty rect
- RectT winRect = {win->x, win->y, win->w, win->h};
- RectT isect;
-
- if (!rectIntersect(dr, &winRect, &isect)) {
- continue;
- }
-
- wmDrawChrome(d, ops, &ctx->font, &ctx->colors, win, dr);
- wmDrawContent(d, ops, win, dr);
-
- if (win->vScroll || win->hScroll) {
- wmDrawScrollbars(d, ops, &ctx->colors, win, dr);
+ dl->count = kept;
+ resetClipRect(d);
+ return;
}
}
-
- // 4. Draw popup menu if active (all levels)
- if (ctx->popup.active) {
- // Draw parent levels first (bottom to top)
- for (int32_t lvl = 0; lvl < ctx->popup.depth; lvl++) {
- PopupLevelT *pl = &ctx->popup.parentStack[lvl];
- drawPopupLevel(ctx, d, ops, pl->menu, pl->popupX, pl->popupY, pl->popupW, pl->popupH, pl->hoverItem, pl->hasCheck, dr);
- }
-
- // Draw current (deepest) level
- drawPopupLevel(ctx, d, ops, ctx->popup.menu, ctx->popup.popupX, ctx->popup.popupY, ctx->popup.popupW, ctx->popup.popupH, ctx->popup.hoverItem, ctx->popup.hasCheck, dr);
- }
-
- // 4b. Draw system menu if active
- if (ctx->sysMenu.active) {
- drawSysMenu(ctx, d, ops, dr);
- }
-
- // 5. Draw tooltip
- if (ctx->tooltipText) {
- RectT ttRect = { ctx->tooltipX, ctx->tooltipY, ctx->tooltipW, ctx->tooltipH };
- RectT ttIsect;
-
- if (rectIntersect(dr, &ttRect, &ttIsect)) {
- rectFill(d, ops, ctx->tooltipX, ctx->tooltipY, ctx->tooltipW, ctx->tooltipH, ctx->colors.menuBg);
- drawRectOutline(d, ops, ctx->tooltipX, ctx->tooltipY, ctx->tooltipW, ctx->tooltipH, ctx->colors.contentFg);
- drawText(d, ops, &ctx->font, ctx->tooltipX + TOOLTIP_PAD, ctx->tooltipY + TOOLTIP_PAD, ctx->tooltipText, ctx->colors.menuFg, ctx->colors.menuBg, true);
- }
- }
-
- // 5b. Draw the drag outline or picture
- if (sDragFeedback.active) {
- drawDragFeedback(ctx, dr);
- }
-
- // 6. Draw cursor
- drawCursorAt(ctx, ctx->mouseX, ctx->mouseY);
-
- // 7. Flush this dirty rect to LFB
- flushRect(d, dr);
}
resetClipRect(d);
@@ -1364,6 +1275,108 @@ static void compositeAndFlush(AppContextT *ctx) {
}
+// One dirty rect (on the screen) composited: the desktop where no window
+// covers it all, the windows bottom to top, menus, the tooltip, drag
+// feedback, the cursor; then flushed to the screen.
+static void compositeRect(AppContextT *ctx, RectT *dr, const int32_t *visibleIdx, int32_t visibleCount) {
+ DisplayT *d = &ctx->display;
+ BlitOpsT *ops = &ctx->blitOps;
+ WindowStackT *ws = &ctx->stack;
+
+ // Set clip rect to this dirty rect
+ setClipRect(d, dr->x, dr->y, dr->w, dr->h);
+
+ // Occlusion: find the topmost window whose frame covers the whole
+ // dirty rect; nothing below it can show through.
+ int32_t firstVi = 0;
+ bool covered = false;
+
+ for (int32_t vi = visibleCount - 1; vi >= 0; vi--) {
+ if (windowCoversRect(ws->windows[visibleIdx[vi]], dr)) {
+ firstVi = vi;
+ covered = true;
+ break;
+ }
+ }
+
+ if (!covered) {
+ // 1. Draw desktop background (wallpaper or solid color)
+ if (ctx->wallpaperBuf) {
+ int32_t bpp = d->format.bytesPerPixel;
+
+ for (int32_t row = dr->y; row < dr->y + dr->h; row++) {
+ ops->spanCopy(d->backBuf + row * d->pitch + dr->x * bpp, ctx->wallpaperBuf + row * ctx->wallpaperPitch + dr->x * bpp, dr->w);
+ }
+ } else {
+ rectFill(d, ops, dr->x, dr->y, dr->w, dr->h, ctx->colors.desktop);
+ }
+
+ // 2. Draw minimized window icons (under all windows)
+ wmDrawMinimizedIcons(d, ops, &ctx->colors, ws, dr);
+ }
+
+ // 3. Walk pre-filtered visible window list bottom-to-top
+ for (int32_t vi = firstVi; vi < visibleCount; vi++) {
+ WindowT *win = ws->windows[visibleIdx[vi]];
+
+ // Check if window intersects this dirty rect
+ RectT winRect = {win->x, win->y, win->w, win->h};
+ RectT isect;
+
+ if (!rectIntersect(dr, &winRect, &isect)) {
+ continue;
+ }
+
+ wmDrawChrome(d, ops, &ctx->font, &ctx->colors, win, dr);
+ wmDrawContent(d, ops, win, dr);
+
+ if (win->vScroll || win->hScroll) {
+ wmDrawScrollbars(d, ops, &ctx->colors, win, dr);
+ }
+ }
+
+ // 4. Draw popup menu if active (all levels)
+ if (ctx->popup.active) {
+ // Draw parent levels first (bottom to top)
+ for (int32_t lvl = 0; lvl < ctx->popup.depth; lvl++) {
+ PopupLevelT *pl = &ctx->popup.parentStack[lvl];
+ drawPopupLevel(ctx, d, ops, pl->menu, pl->popupX, pl->popupY, pl->popupW, pl->popupH, pl->hoverItem, pl->hasCheck, dr);
+ }
+
+ // Draw current (deepest) level
+ drawPopupLevel(ctx, d, ops, ctx->popup.menu, ctx->popup.popupX, ctx->popup.popupY, ctx->popup.popupW, ctx->popup.popupH, ctx->popup.hoverItem, ctx->popup.hasCheck, dr);
+ }
+
+ // 4b. Draw system menu if active
+ if (ctx->sysMenu.active) {
+ drawSysMenu(ctx, d, ops, dr);
+ }
+
+ // 5. Draw tooltip
+ if (ctx->tooltipText) {
+ RectT ttRect = { ctx->tooltipX, ctx->tooltipY, ctx->tooltipW, ctx->tooltipH };
+ RectT ttIsect;
+
+ if (rectIntersect(dr, &ttRect, &ttIsect)) {
+ rectFill(d, ops, ctx->tooltipX, ctx->tooltipY, ctx->tooltipW, ctx->tooltipH, ctx->colors.menuBg);
+ drawRectOutline(d, ops, ctx->tooltipX, ctx->tooltipY, ctx->tooltipW, ctx->tooltipH, ctx->colors.contentFg);
+ drawText(d, ops, &ctx->font, ctx->tooltipX + TOOLTIP_PAD, ctx->tooltipY + TOOLTIP_PAD, ctx->tooltipText, ctx->colors.menuFg, ctx->colors.menuBg, true);
+ }
+ }
+
+ // 5b. Draw the drag outline or picture
+ if (sDragFeedback.active) {
+ drawDragFeedback(ctx, dr);
+ }
+
+ // 6. Draw cursor
+ drawCursorAt(ctx, ctx->mouseX, ctx->mouseY);
+
+ // 7. Flush this dirty rect to LFB
+ flushRect(d, dr);
+}
+
+
// Consumes the current left-button press: marks the edge already-seen and
// swallows the matching release. Called before a press handler pumps a
// nested dvxUpdate loop (sys-menu command, popup-menu command) so the
@@ -1383,6 +1396,32 @@ static void consumeMousePress(AppContextT *ctx) {
// threshold are replaced by keyColor and *outHasAlpha is set. Returns a
// malloc'd buffer (pitch in *outPitch) or NULL on allocation failure.
+// The cursor win's content asks for at content point (cx, cy): its
+// onCursorQuery's, else the widget's under it -- one inside a clipping
+// container (a text box in a ScrollPane) first, then the container itself
+// (a Splitter's divider) -- else the arrow.
+static int32_t contentCursorShape(AppContextT *ctx, WindowT *win, int32_t cx, int32_t cy) {
+ int32_t shape = 0;
+
+ if (win->onCursorQuery) {
+ WIN_CALLBACK(ctx, win, shape = win->onCursorQuery(win, cx, cy));
+ }
+
+ if (shape <= 0 && win->widgetRoot) {
+ WidgetT *target = widgetCallbackTarget(win->widgetRoot, cx, cy);
+ WidgetT *hit = widgetHitTest(win->widgetRoot, cx, cy);
+
+ shape = target ? wclsGetCursorShape(target, cx, cy) : 0;
+
+ if (shape <= 0 && hit && hit != target) {
+ shape = wclsGetCursorShape(hit, cx, cy);
+ }
+ }
+
+ return (shape > 0) ? shape : CURSOR_ARROW;
+}
+
+
static uint8_t *convertRgbToNative(const DisplayT *d, const uint8_t *src, int32_t channels, int32_t w, int32_t h, uint32_t keyColor, bool *outHasAlpha, int32_t *outPitch) {
int32_t bpp = d->format.bytesPerPixel;
int32_t pitch = w * bpp;
@@ -1407,7 +1446,7 @@ static uint8_t *convertRgbToNative(const DisplayT *d, const uint8_t *src, int32_
color = packColor(d, px[0], px[1], px[2]);
}
- storePixel(dstRow, x, bpp, color);
+ pixelPut(dstRow + x * bpp, color, bpp);
}
}
@@ -1812,6 +1851,14 @@ static void dispatchEvents(AppContextT *ctx) {
ctx->modalWindow = NULL;
}
+ // A modal dialog keeps the top and the focus: a window another
+ // program shows or raises while the dialog waits (its task runs every
+ // frame) goes behind it, or keys typed for the dialog would reach that
+ // window.
+ if (ctx->modalWindow && focusedWindow(ctx) != ctx->modalWindow) {
+ raiseAndFocus(ctx, windowIndex(ctx, ctx->modalWindow));
+ }
+
// Full screen: the mouse is the program's alone.
if (ctx->exclusive.active) {
exclusiveMouse(ctx);
@@ -2591,7 +2638,7 @@ void dvxAccelFormat(int32_t key, int32_t modifiers, char *out, int32_t outSize)
// or digit, F1 to F12, or a name from sAccelKeyNames (case does not
// matter, blanks around the parts are ignored).
bool dvxAccelParse(const char *text, int32_t *outKey, int32_t *outMods) {
- char buf[ACCEL_TEXT_MAX];
+ char buf[DVX_ACCEL_TEXT_MAX];
int32_t mods = 0;
int32_t key = 0;
@@ -2684,6 +2731,32 @@ void dvxAddAccel(AccelTableT *table, int32_t key, int32_t modifiers, int32_t cmd
}
+// Book the start of a handler of appId (a window callback: WIN_CALLBACK).
+void dvxAppCallEnter(int32_t appId) {
+ int32_t idx = appCallFind(appId);
+
+ if (idx < 0) {
+ AppCallT call = { appId, 0, platformClock() };
+
+ arrput(sAppCalls, call);
+ idx = (int32_t)arrlen(sAppCalls) - 1;
+ }
+
+ sAppCalls[idx].depth++;
+}
+
+
+// Book the end of a handler of appId. The entry may already be gone
+// (dvxAppHungForget when the app was ended inside the handler).
+void dvxAppCallLeave(int32_t appId) {
+ int32_t idx = appCallFind(appId);
+
+ if (idx >= 0 && --sAppCalls[idx].depth == 0) {
+ arrdel(sAppCalls, idx);
+ }
+}
+
+
void dvxAppHungForget(int32_t appId) {
int32_t idx = appCallFind(appId);
@@ -2718,13 +2791,20 @@ void dvxApplyColorScheme(AppContextT *ctx) {
#ifdef DVX_HOST_TEST
void dvxAppTestReset(void) {
- sKeyPressedBtn = NULL;
- sCursorBlinkFn = NULL;
- sTooltipGen = 0;
- sEntropyHook = NULL;
- sEntropyLen = 0;
- sSoundHook = NULL;
- sOpenSoundWinId = 0;
+ free(sWallRgb);
+ sKeyPressedBtn = NULL;
+ sCursorBlinkFn = NULL;
+ sTooltipGen = 0;
+ sEntropyHook = NULL;
+ sEntropyLen = 0;
+ sSoundHook = NULL;
+ sWaitPump = NULL;
+ sWallRgb = NULL;
+ sWallRgbW = 0;
+ sWallRgbH = 0;
+ sFontsRebuilding = false;
+ sOpenSoundWinId = 0;
+ sWallRgbPath[0] = '\0';
memset(sEntropyBuf, 0, sizeof(sEntropyBuf));
arrfree(sHotkeys);
sHotkeys = NULL;
@@ -2742,6 +2822,64 @@ void dvxBeep(void) {
}
+// ============================================================
+// dvxBufferToRgb -- convert native pixel format to 24-bit RGB
+// ============================================================
+//
+// Screenshots must produce standard RGB data for stb_image_write, but the
+// backbuffer uses whatever native pixel format VESA gave us (8/16/32bpp).
+// The 8bpp path uses the VGA palette for lookup. Direct-color channels
+// are widened through per-channel lookup tables built once per call with
+// bit replication, so full-scale values reach 255 (a 5-bit 31 becomes
+// 255, not 248). The pixel-width branch is hoisted out of the pixel loop.
+
+uint8_t *dvxBufferToRgb(const DisplayT *d, const uint8_t *buf, int32_t w, int32_t h, int32_t pitch) {
+ uint8_t *rgb = (uint8_t *)malloc((size_t)w * h * RGB_CHANNELS);
+
+ if (!rgb) {
+ return NULL;
+ }
+
+ uint8_t *dst = rgb;
+
+ if (d->format.bitsPerPixel == PALETTE_BPP) {
+ for (int32_t y = 0; y < h; y++) {
+ const uint8_t *row = buf + y * pitch;
+
+ for (int32_t x = 0; x < w; x++) {
+ memcpy(dst, d->palette + row[x] * RGB_CHANNELS, RGB_CHANNELS);
+ dst += RGB_CHANNELS;
+ }
+ }
+
+ return rgb;
+ }
+
+ ChannelLutT lut;
+ buildChannelLut(&lut, &d->format);
+
+ bool wide = (d->format.bytesPerPixel == sizeof(uint32_t));
+
+ for (int32_t y = 0; y < h; y++) {
+ const uint8_t *row = buf + y * pitch;
+
+ if (wide) {
+ for (int32_t x = 0; x < w; x++) {
+ pixelToRgb(&lut, ((const uint32_t *)row)[x], dst);
+ dst += RGB_CHANNELS;
+ }
+ } else {
+ for (int32_t x = 0; x < w; x++) {
+ pixelToRgb(&lut, ((const uint16_t *)row)[x], dst);
+ dst += RGB_CHANNELS;
+ }
+ }
+ }
+
+ return rgb;
+}
+
+
// ============================================================
// dvxCascadeWindows
// ============================================================
@@ -2874,8 +3012,8 @@ int32_t dvxChangeVideoMode(AppContextT *ctx, int32_t requestedW, int32_t request
ctx->mouseY = ctx->display.height - 1;
}
- // Smoothed text or not at the new depth (the loop below lays out and
- // paints every window)
+ // Smoothed text or not at the new depth (the fonts are built again in
+ // the frames after; the loop below lays out and paints every window)
fontSmoothSet(ctx);
// Clamp and reallocate all window content buffers
@@ -3251,42 +3389,7 @@ void dvxDragOutlineShow(AppContextT *ctx, int32_t w, int32_t h, int32_t offX, in
// 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;
+ return exclusiveStart(ctx, win, width, height, bpp, onChange, userData, false);
}
@@ -3328,6 +3431,11 @@ void dvxExclusiveFlush(AppContextT *ctx, const RectT *r) {
}
+int32_t dvxExclusiveMirror(AppContextT *ctx, WindowT *win, ExclusiveFnT onChange, void *userData) {
+ return win ? exclusiveStart(ctx, win, win->contentW, win->contentH, ctx->display.format.bitsPerPixel, onChange, userData, true) : DVX_EXCL_ERR_ARGS;
+}
+
+
// 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
@@ -3372,7 +3480,7 @@ bool dvxExclusivePresent8(AppContextT *ctx, const uint8_t *src, int32_t srcW, in
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);
+ pixelPut(dst + x * bpp, px, bpp);
}
}
}
@@ -3587,40 +3695,18 @@ uint32_t dvxIdleMs(const AppContextT *ctx) {
bool dvxImageInfo(const char *path, int32_t *outW, int32_t *outH) {
- if (!path) {
- return false;
- }
+ int w = 0;
+ int h = 0;
- int w = 0;
- int h = 0;
- int comp = 0;
+ return path && dvxImgInfoFile(path, &w, &h) && imageInfoTake(w, h, outW, outH);
+}
- if (!stbi_info(path, &w, &h, &comp)) {
- return false;
- }
- // stb_image gives a top-down BMP's height as stored, negative, and
- // loads that image at its magnitude. INT_MIN has none (the load
- // refuses that height as well).
- if (h < 0 && h != INT_MIN) {
- h = -h;
- }
+bool dvxImageInfoFromMemory(const uint8_t *data, int32_t dataLen, int32_t *outW, int32_t *outH) {
+ int w = 0;
+ int h = 0;
- // An image without pixels does not load (rejectEmptyImage), so it has
- // no size either.
- if (w <= 0 || h <= 0) {
- return false;
- }
-
- if (outW) {
- *outW = w;
- }
-
- if (outH) {
- *outH = h;
- }
-
- return true;
+ return data && dataLen > 0 && dvxImgInfoMemory(data, dataLen, &w, &h) && imageInfoTake(w, h, outW, outH);
}
@@ -3803,7 +3889,8 @@ void dvxInvalidateWindow(AppContextT *ctx, WindowT *win) {
//
// A loaded module exports its ABI stamp (DVX_ABI_STAMP: its name, in
// lower case, and DVX_ABI_SUFFIX), which the loader checked as it loaded
-// it; a module that did not load has none to find.
+// it; a module that did not load has none to find. Looking for it loads
+// a library that is installed but not loaded yet (the loader's resolver).
bool dvxLibraryLoaded(const char *name) {
char lower[LIBRARY_NAME_MAX];
@@ -3839,18 +3926,30 @@ uint8_t *dvxLoadImage(const AppContextT *ctx, const char *path, int32_t *outW, i
return NULL;
}
- int imgW;
- int imgH;
- int channels;
- uint8_t *rgb = rejectEmptyImage(stbi_load(path, &imgW, &imgH, &channels, RGB_CHANNELS), &imgW, &imgH);
+ int imgW;
+ int imgH;
+ ImageSrcT src;
+
+ memset(&src, 0, sizeof(src));
+ src.fp = fopen(path, "rb");
+ src.pumpCtx = imagePumpCtx(ctx);
+
+ uint8_t *rgb = src.fp ? imageDecode(&src, &imgW, &imgH, RGB_CHANNELS) : NULL;
if (rgb) {
+ fclose(src.fp);
return finishLoadImage(&ctx->display, rgb, imgW, imgH, outW, outH, outPitch);
}
- // A DVX program, widget or library file: its icon
- uint32_t size = 0;
- uint8_t *icon = (uint8_t *)dvxResLoadIconData(path, &size);
+ // A DVX program, widget or library file: its icon, read from the file
+ // already open
+ uint32_t size = 0;
+ uint8_t *icon = (uint8_t *)dvxResIconRead(src.fp, &size);
+
+ if (src.fp) {
+ fclose(src.fp);
+ }
+
uint8_t *pixels = icon ? dvxLoadImageFromMemory(ctx, icon, (int32_t)size, outW, outH, outPitch) : NULL;
free(icon);
@@ -3865,10 +3964,16 @@ uint8_t *dvxLoadImageAlpha(const AppContextT *ctx, const uint8_t *data, int32_t
const DisplayT *d = &ctx->display;
- int imgW;
- int imgH;
- int channels;
- uint8_t *rgba = rejectEmptyImage(stbi_load_from_memory(data, dataLen, &imgW, &imgH, &channels, RGBA_CHANNELS), &imgW, &imgH);
+ int imgW;
+ int imgH;
+ ImageSrcT src;
+
+ memset(&src, 0, sizeof(src));
+ src.data = data;
+ src.len = dataLen;
+ src.pumpCtx = imagePumpCtx(ctx);
+
+ uint8_t *rgba = imageDecode(&src, &imgW, &imgH, RGBA_CHANNELS);
if (!rgba) {
return NULL;
@@ -3878,7 +3983,7 @@ uint8_t *dvxLoadImageAlpha(const AppContextT *ctx, const uint8_t *data, int32_t
int32_t pitch;
uint8_t *buf = convertRgbToNative(d, rgba, RGBA_CHANNELS, imgW, imgH, keyColor, outHasAlpha, &pitch);
- stbi_image_free(rgba);
+ free(rgba);
if (!buf) {
return NULL;
@@ -3909,10 +4014,16 @@ uint8_t *dvxLoadImageFromMemory(const AppContextT *ctx, const uint8_t *data, int
return NULL;
}
- int imgW;
- int imgH;
- int channels;
- uint8_t *rgb = rejectEmptyImage(stbi_load_from_memory(data, dataLen, &imgW, &imgH, &channels, RGB_CHANNELS), &imgW, &imgH);
+ int imgW;
+ int imgH;
+ ImageSrcT src;
+
+ memset(&src, 0, sizeof(src));
+ src.data = data;
+ src.len = dataLen;
+ src.pumpCtx = imagePumpCtx(ctx);
+
+ uint8_t *rgb = imageDecode(&src, &imgW, &imgH, RGB_CHANNELS);
return finishLoadImage(&ctx->display, rgb, imgW, imgH, outW, outH, outPitch);
}
@@ -4221,20 +4332,15 @@ uint8_t *dvxResLoadIcon(AppContextT *ctx, const char *dxePath, const char *resNa
}
+// One open of the file for the directory and the picture.
void *dvxResLoadIconData(const char *dxePath, uint32_t *outSize) {
- DvxResHandleT *res = dxePath ? dvxResOpen(dxePath) : NULL;
+ FILE *f = dxePath ? fopen(dxePath, "rb") : NULL;
+ void *data = dvxResIconRead(f, outSize);
- if (!res) {
- return NULL;
+ if (f) {
+ fclose(f);
}
- void *data = dvxResRead(res, DVX_RES_NAME_ICON, outSize);
-
- if (!data) {
- data = dvxResRead(res, DVX_RES_NAME_WIDGET_ICON, outSize);
- }
-
- dvxResClose(res);
return data;
}
@@ -4245,24 +4351,10 @@ bool dvxResLoadText(const char *dxePath, const char *resName, char *buf, int32_t
}
DvxResHandleT *res = dvxResOpen(dxePath);
+ bool ok = res && dvxResReadText(res, resName, buf, bufSize);
- if (!res) {
- return false;
- }
-
- uint32_t size = 0;
- void *data = dvxResRead(res, resName, &size);
dvxResClose(res);
-
- if (!data) {
- return false;
- }
-
- int32_t copyLen = (int32_t)size < bufSize - 1 ? (int32_t)size : bufSize - 1;
- memcpy(buf, data, copyLen);
- buf[copyLen] = '\0';
- free(data);
- return true;
+ return ok;
}
@@ -4353,7 +4445,9 @@ int32_t dvxRunDos(AppContextT *ctx, const char *program, const char *args, const
if (rc < 0) {
dvxLog("DOS: %s could not be run: %s", command, strerror(err));
} else {
+ // It may have installed or removed anything
dvxLog("DOS: back from %s", command);
+ platformInstalledChanged();
}
platformKeyStateClear();
@@ -4483,7 +4577,7 @@ void dvxSetColor(AppContextT *ctx, ColorIdE id, uint8_t r, uint8_t g, uint8_t b)
uint32_t packed = packColor(&ctx->display, r, g, b);
*colorSlot(&ctx->colors, id) = packed;
- invalidateAllWindows(ctx);
+ invalidateAllWindowsLater(ctx);
}
@@ -4491,15 +4585,12 @@ void dvxSetColor(AppContextT *ctx, ColorIdE id, uint8_t r, uint8_t g, uint8_t b)
// dvxSetFontSmoothing
// ============================================================
//
-// The open fonts are built again in place, so text measured with them
-// is laid out again: every window is.
+// The open fonts are built again in place, a frame at a time, so text
+// measured with them is laid out again: every window is, after the last.
void dvxSetFontSmoothing(AppContextT *ctx, bool on) {
ctx->fontSmoothing = on;
-
- if (fontSmoothSet(ctx)) {
- invalidateAllWindows(ctx);
- }
+ fontSmoothSet(ctx);
}
@@ -4551,7 +4642,9 @@ void dvxSetSoundHook(DvxSoundHookT fn) {
// ============================================================
void dvxSetTitle(AppContextT *ctx, WindowT *win, const char *title) {
- if (!win) {
+ // The title the window already has changes nothing (no repaint, no
+ // title-change notice for a caption set on every pass of a loop)
+ if (!win || (title && strcmp(win->title, title) == 0)) {
return;
}
@@ -4566,6 +4659,15 @@ void dvxSetTitle(AppContextT *ctx, WindowT *win, const char *title) {
}
+// ============================================================
+// dvxSetWaitPump
+// ============================================================
+
+void dvxSetWaitPump(DvxWaitPumpT fn) {
+ sWaitPump = fn;
+}
+
+
// ============================================================
// dvxSetWallpaper
// ============================================================
@@ -4573,8 +4675,10 @@ void dvxSetTitle(AppContextT *ctx, WindowT *win, const char *title) {
// Loads and renders into locals; the previous wallpaper (buffer and
// path) is only replaced once the new one is complete, so a bad path or
// an out-of-memory build leaves the current wallpaper untouched. The
-// builder pumps dvxUpdate; if a timer or idle callback changed the video
-// mode meanwhile, the buffer is sized for the old mode and is rebuilt.
+// decode and the builder pump dvxUpdate; if a timer or idle callback
+// changed the video mode meanwhile, the buffer is sized for the old mode
+// and is rebuilt. The decoded picture is kept (sWallRgb) while it is the
+// wallpaper, so a wallpaper mode or video mode change only builds.
bool dvxSetWallpaper(AppContextT *ctx, const char *path) {
// buildWallpaperBuf pumps dvxUpdate to keep the UI alive, and a timer
@@ -4589,6 +4693,8 @@ bool dvxSetWallpaper(AppContextT *ctx, const char *path) {
if (!path) {
free(ctx->wallpaperBuf);
+ free(sWallRgb);
+ sWallRgb = NULL;
ctx->wallpaperBuf = NULL;
ctx->wallpaperPitch = 0;
ctx->wallpaperPath[0] = '\0';
@@ -4603,17 +4709,27 @@ bool dvxSetWallpaper(AppContextT *ctx, const char *path) {
bool wasBusy = ctx->busy;
dvxSetBusy(ctx, true);
+ sBuilding = true;
- int imgW;
- int imgH;
- int channels;
- uint8_t *rgb = rejectEmptyImage(stbi_load(newPath, &imgW, &imgH, &channels, RGB_CHANNELS), &imgW, &imgH);
- uint8_t *buf = NULL;
- int32_t pitch = 0;
+ int imgW = sWallRgbW;
+ int imgH = sWallRgbH;
+ bool kept = sWallRgb && platformPathSame(sWallRgbPath, newPath);
+ uint8_t *rgb = kept ? sWallRgb : NULL;
+ uint8_t *buf = NULL;
+ int32_t pitch = 0;
+ ImageSrcT src;
+
+ memset(&src, 0, sizeof(src));
+ src.pumpCtx = ctx;
+
+ src.fp = kept ? NULL : fopen(newPath, "rb");
+
+ if (src.fp != NULL) {
+ rgb = imageDecode(&src, &imgW, &imgH, RGB_CHANNELS);
+ fclose(src.fp);
+ }
if (rgb) {
- sBuilding = true;
-
for (;;) {
int32_t builtW = ctx->display.width;
int32_t builtH = ctx->display.height;
@@ -4628,10 +4744,20 @@ bool dvxSetWallpaper(AppContextT *ctx, const char *path) {
free(buf);
}
- sBuilding = false;
- stbi_image_free(rgb);
+ // Kept for the next build when it is the wallpaper now and small
+ // enough; another picture kept before goes
+ if (!kept && buf && (int64_t)imgW * imgH * RGB_CHANNELS <= WALLPAPER_KEEP_BYTES) {
+ free(sWallRgb);
+ sWallRgb = rgb;
+ sWallRgbW = imgW;
+ sWallRgbH = imgH;
+ snprintf(sWallRgbPath, sizeof(sWallRgbPath), "%s", newPath);
+ } else if (!kept) {
+ free(rgb);
+ }
}
+ sBuilding = false;
dvxSetBusy(ctx, wasBusy);
if (!buf) {
@@ -4754,7 +4880,9 @@ void dvxShutdown(AppContextT *ctx) {
ctx->dirty.cap = 0;
free(ctx->wallpaperBuf);
+ free(sWallRgb);
ctx->wallpaperBuf = NULL;
+ sWallRgb = NULL;
arrfree(ctx->videoModes);
ctx->videoModes = NULL;
videoShutdown(&ctx->display);
@@ -4957,20 +5085,35 @@ bool dvxUpdate(AppContextT *ctx) {
// happens once every callback frame has unwound.
ctx->dispatchDepth++;
- pollMouse(ctx);
- pollKeyboard(ctx);
- dispatchEvents(ctx);
- exclusiveSync(ctx);
- updateTooltip(ctx);
- pollWidgets(ctx);
+ // Input, events, and timers only where the host takes them (inputHere)
+ bool input = !ctx->inputHere || ctx->inputHere(ctx->idleCtx);
- if (sCursorBlinkFn) {
- sCursorBlinkFn();
+ if (input) {
+ pollMouse(ctx);
+ pollKeyboard(ctx);
+ dispatchEvents(ctx);
+ exclusiveSync(ctx);
+ updateTooltip(ctx);
+ pollWidgets(ctx);
+
+ if (sCursorBlinkFn) {
+ sCursorBlinkFn();
+ }
+
+ wgtUpdateTimers();
}
- wgtUpdateTimers();
playOpenSound(ctx);
+ // A font built again for a smoothing change; every window laid out
+ // and painted again after the last
+ if (dvxFontRebuildStep()) {
+ sFontsRebuilding = true;
+ } else if (sFontsRebuilding) {
+ sFontsRebuilding = false;
+ invalidateAllWindows(ctx);
+ }
+
// Flush deferred paints. paintNeeded is set by wgtInvalidatePaint
// (PARTIAL) or wgtInvalidate (FULL). Multiple calls per frame are
// batched into one paint -- the highest level wins. This loop runs
@@ -4997,18 +5140,24 @@ bool dvxUpdate(AppContextT *ctx) {
}
if (ctx->dirty.count > 0) {
- compositeAndFlush(ctx);
+ compositeDirty(ctx, COMPOSITE_BUDGET_US);
+
+ if (ctx->busyCallback) {
+ ctx->busyCallback(ctx->idleCtx);
+ }
} else if (ctx->idleCallback) {
ctx->idleCallback(ctx->idleCtx);
} else {
platformYield();
}
- releaseKeyPressedButton(ctx);
+ if (input) {
+ releaseKeyPressedButton(ctx);
- ctx->prevMouseX = ctx->mouseX;
- ctx->prevMouseY = ctx->mouseY;
- ctx->prevMouseButtons = ctx->mouseButtons;
+ ctx->prevMouseX = ctx->mouseX;
+ ctx->prevMouseY = ctx->mouseY;
+ ctx->prevMouseButtons = ctx->mouseButtons;
+ }
return ctx->running;
}
@@ -5021,6 +5170,20 @@ bool dvxUpdate(AppContextT *ctx) {
// A missing wallpaper key means none. The image is only built again
// when the path or the mode differs from the current one.
+// ============================================================
+// dvxWaitPump
+// ============================================================
+
+bool dvxWaitPump(void) {
+ if (sWaitPump) {
+ return sWaitPump();
+ }
+
+ platformYield();
+ return true;
+}
+
+
bool dvxWallpaperFromPrefs(AppContextT *ctx, PrefsHandleT *h) {
WallpaperModeE mode = (WallpaperModeE)prefsWallpaperChoices[prefsGetChoice(h, DVX_DESKTOP_SECTION, DESKTOP_KEY_MODE, prefsWallpaperChoices, PREFS_WALLPAPER_CHOICE_COUNT, WALLPAPER_MODE_DEFAULT)].value;
const char *value = prefsGetString(h, DVX_DESKTOP_SECTION, DESKTOP_KEY_WALLPAPER, "");
@@ -5147,10 +5310,16 @@ static void enumModeCb(int32_t w, int32_t h, int32_t bpp, void *userData) {
// 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;
+ ExclusiveStateT *ex = &ctx->exclusive;
+ DisplayT *d = &ex->display;
+ bool set = platformVideoExclusiveBegin(d, ex->reqW, ex->reqH, ex->reqBpp) == 0;
- if (platformVideoExclusiveBegin(d, ex->reqW, ex->reqH, ex->reqBpp) != 0) {
+ // A mirror copies the window's pixels as they are
+ if (set && ex->mirror && memcmp(&d->format, &ctx->display.format, sizeof(d->format)) != 0) {
+ set = false;
+ }
+
+ if (!set) {
dvxLog("Video: no %ldx%ld full-screen mode", (long)ex->reqW, (long)ex->reqH);
platformVideoExclusiveEnd(d, &ctx->display);
dirtyListInit(&ctx->dirty);
@@ -5193,6 +5362,14 @@ static bool exclusiveEnter(AppContextT *ctx) {
ctx->prevMouseY = ctx->mouseY;
ex->active = true;
+
+ if (ex->mirror) {
+ RectT all = { 0, 0, d->width, d->height };
+
+ memset(&ex->mirrorCursor, 0, sizeof(ex->mirrorCursor));
+ exclusiveMirrorShow(ctx, &all);
+ }
+
exclusiveNotify(ctx, ex->owner, ex->onChange, ex->userData, ExclusiveActiveE);
return true;
}
@@ -5228,7 +5405,153 @@ static void exclusiveLutSet(ExclusiveStateT *ex, int32_t first, int32_t count) {
}
-// Full screen: any mouse change goes to the owner in screen coordinates.
+// A mirror: the part of the owner's content the frame's dirty rectangles
+// (screen coordinates of the desktop) cover, shown again.
+static void exclusiveMirrorDirty(AppContextT *ctx, const DirtyListT *dl) {
+ WindowT *win = ctx->exclusive.owner;
+ RectT content = { win->x + win->contentX, win->y + win->contentY, win->contentW, win->contentH };
+ int32_t x0 = INT32_MAX;
+ int32_t y0 = INT32_MAX;
+ int32_t x1 = INT32_MIN;
+ int32_t y1 = INT32_MIN;
+
+ for (int32_t i = 0; i < dl->count; i++) {
+ RectT part;
+
+ if (rectIntersect(&dl->rects[i], &content, &part)) {
+ x0 = DVX_MIN(x0, part.x);
+ y0 = DVX_MIN(y0, part.y);
+ x1 = DVX_MAX(x1, part.x + part.w);
+ y1 = DVX_MAX(y1, part.y + part.h);
+ }
+ }
+
+ if (x1 <= x0) {
+ return;
+ }
+
+ RectT place;
+ int32_t scale = exclusiveMirrorPlace(ctx, &place);
+ RectT shown = { place.x + (x0 - content.x) * scale, place.y + (y0 - content.y) * scale, (x1 - x0) * scale, (y1 - y0) * scale };
+
+ exclusiveMirrorShow(ctx, &shown);
+}
+
+
+// A mirror: the mouse in the owner's content coordinates, and its cursor
+// (the shape the content asks for) moved.
+static void exclusiveMirrorMouse(AppContextT *ctx) {
+ ExclusiveStateT *ex = &ctx->exclusive;
+ WindowT *win = ex->owner;
+ RectT place;
+ int32_t scale = exclusiveMirrorPlace(ctx, &place);
+ int32_t cx = (ctx->mouseX - place.x) / scale;
+ int32_t cy = (ctx->mouseY - place.y) / scale;
+
+ if (ctx->mouseX != ctx->prevMouseX || ctx->mouseY != ctx->prevMouseY) {
+ RectT old = ex->mirrorCursor;
+
+ ctx->cursorId = ctx->busy ? CURSOR_BUSY : contentCursorShape(ctx, win, cx, cy);
+ exclusiveMirrorShow(ctx, &old);
+
+ const CursorT *cur = &ctx->cursors[ctx->cursorId];
+ RectT now = { ctx->mouseX - cur->hotX, ctx->mouseY - cur->hotY, cur->width, cur->height };
+
+ exclusiveMirrorShow(ctx, &now);
+ }
+
+ if (win->onMouse && !win->destroyPending) {
+ WIN_CALLBACK(ctx, win, win->onMouse(win, cx, cy, ctx->mouseButtons));
+ }
+}
+
+
+// Where a mirror shows the owner's content on the full-screen display
+// (*place) and its scale: the largest whole factor that fits, centered.
+static int32_t exclusiveMirrorPlace(AppContextT *ctx, RectT *place) {
+ ExclusiveStateT *ex = &ctx->exclusive;
+ DisplayT *d = &ex->display;
+ WindowT *win = ex->owner;
+ int32_t srcW = DVX_MAX(1, win->contentW);
+ int32_t srcH = DVX_MAX(1, win->contentH);
+ int32_t scale = DVX_MAX(1, DVX_MIN(d->width / srcW, d->height / srcH));
+
+ place->w = DVX_MIN(srcW * scale, d->width);
+ place->h = DVX_MIN(srcH * scale, d->height);
+ place->x = (d->width - place->w) / 2;
+ place->y = (d->height - place->h) / 2;
+ return scale;
+}
+
+
+// A mirror: screen rectangle r drawn again from the owner's content
+// (black around it) -- all of the screen when the content's place moved
+// (a new size) -- the mouse cursor on top, and flushed.
+static void exclusiveMirrorShow(AppContextT *ctx, const RectT *r) {
+ ExclusiveStateT *ex = &ctx->exclusive;
+ DisplayT *d = &ex->display;
+ WindowT *win = ex->owner;
+ RectT screen = { 0, 0, d->width, d->height };
+ RectT clip;
+ RectT place;
+ int32_t scale = exclusiveMirrorPlace(ctx, &place);
+ int32_t bpp = d->format.bytesPerPixel;
+
+ if (memcmp(&place, &ex->presentRect, sizeof(place)) != 0) {
+ ex->presentRect = place;
+ r = &screen;
+ }
+
+ if (!win->contentBuf || !rectIntersect(r, &screen, &clip)) {
+ return;
+ }
+
+ // Each row: black, the content's pixels (each repeated scale times),
+ // black
+ int32_t right = clip.x + clip.w;
+
+ for (int32_t y = clip.y; y < clip.y + clip.h; y++) {
+ uint8_t *dst = d->backBuf + (size_t)y * (size_t)d->pitch;
+ bool inRows = y >= place.y && y < place.y + place.h;
+ int32_t in0 = inRows ? DVX_MAX(clip.x, place.x) : right;
+ int32_t in1 = inRows ? DVX_MAX(in0, DVX_MIN(right, place.x + place.w)) : right;
+
+ memset(dst + (size_t)clip.x * (size_t)bpp, 0, (size_t)(in0 - clip.x) * (size_t)bpp);
+ memset(dst + (size_t)in1 * (size_t)bpp, 0, (size_t)(right - in1) * (size_t)bpp);
+
+ if (in1 <= in0) {
+ continue;
+ }
+
+ const uint8_t *src = win->contentBuf + (size_t)((y - place.y) / scale) * (size_t)win->contentPitch;
+
+ if (scale == 1) {
+ memcpy(dst + (size_t)in0 * (size_t)bpp, src + (size_t)(in0 - place.x) * (size_t)bpp, (size_t)(in1 - in0) * (size_t)bpp);
+ continue;
+ }
+
+ for (int32_t x = in0; x < in1; x++) {
+ pixelPut(dst + x * bpp, pixelGet(src + (x - place.x) / scale * bpp, bpp), bpp);
+ }
+ }
+
+ const CursorT *cur = &ctx->cursors[ctx->cursorId];
+ RectT cursor = { ctx->mouseX - cur->hotX, ctx->mouseY - cur->hotY, cur->width, cur->height };
+ RectT over;
+
+ if (rectIntersect(&cursor, &clip, &over)) {
+ setClipRect(d, clip.x, clip.y, clip.w, clip.h);
+ drawMaskedBitmap(d, &ex->blitOps, cursor.x, cursor.y, cur->width, cur->height, cur->andMask, cur->xorData, ctx->colors.cursorFg, ctx->colors.cursorBg);
+ resetClipRect(d);
+ }
+
+ ex->mirrorCursor = cursor;
+ platformVideoExclusiveFlush(d, &clip);
+}
+
+
+// Full screen: any mouse change goes to the owner in screen coordinates
+// (a mirror's in content coordinates, exclusiveMirrorMouse).
static void exclusiveMouse(AppContextT *ctx) {
WindowT *win = ctx->exclusive.owner;
@@ -5236,6 +5559,11 @@ static void exclusiveMouse(AppContextT *ctx) {
return;
}
+ if (ctx->exclusive.mirror) {
+ exclusiveMirrorMouse(ctx);
+ return;
+ }
+
if (win->onMouse && !win->destroyPending) {
WIN_CALLBACK(ctx, win, win->onMouse(win, ctx->mouseX, ctx->mouseY, ctx->mouseButtons));
}
@@ -5250,6 +5578,49 @@ static void exclusiveNotify(AppContextT *ctx, WindowT *win, ExclusiveFnT fn, voi
}
+// dvxExclusiveBegin and dvxExclusiveMirror: win takes the whole screen
+// (a mirror of its content when mirror is set).
+static int32_t exclusiveStart(AppContextT *ctx, WindowT *win, int32_t width, int32_t height, int32_t bpp, ExclusiveFnT onChange, void *userData, bool mirror) {
+ 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;
+ ex->mirror = mirror;
+
+ 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;
+}
+
+
// 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.
@@ -5384,7 +5755,7 @@ static uint8_t *finishLoadImage(const DisplayT *d, uint8_t *rgb, int32_t imgW, i
int32_t pitch;
uint8_t *buf = convertRgbToNative(d, rgb, RGB_CHANNELS, imgW, imgH, 0, NULL, &pitch);
- stbi_image_free(rgb);
+ free(rgb);
if (!buf) {
return NULL;
@@ -5498,18 +5869,28 @@ static void flushPendingDestroys(AppContextT *ctx) {
// wgtInvalidatePaint PAINT_PARTIAL) now. Shared by the dvxUpdate flush
// and the content-buffer readers that must not see a stale buffer. A
// deferred-destroyed window is skipped: its app state may be torn down.
+// A painter that narrowed the area (widgetOnPaint after a partial pass)
+// changed only that part, and only that part is composited; else the
+// whole window is.
static void flushWindowPaint(AppContextT *ctx, WindowT *win) {
if (win->destroyPending || !win->paintNeeded || !win->onPaint) {
return;
}
RectT fullRect = {0, 0, win->contentW, win->contentH};
- WIN_CALLBACK(ctx, win, win->onPaint(win, &fullRect));
+ RectT area = fullRect;
+
+ WIN_CALLBACK(ctx, win, win->onPaint(win, &area));
// widgetOnPaint clears paintNeeded for widget windows.
// For raw-paint windows, clear it here.
win->paintNeeded = PAINT_NONE;
win->iconNeedsRefresh = true;
- dirtyListAdd(&ctx->dirty, win->x, win->y, win->w, win->h);
+
+ if (area.x == fullRect.x && area.y == fullRect.y && area.w == fullRect.w && area.h == fullRect.h) {
+ dirtyListAdd(&ctx->dirty, win->x, win->y, win->w, win->h);
+ } else if (area.w > 0 && area.h > 0) {
+ dvxInvalidateRect(ctx, win, area.x, area.y, area.w, area.h);
+ }
}
@@ -5568,17 +5949,11 @@ static void focusWindowAt(AppContextT *ctx, int32_t idx) {
}
-// Smooth the fonts (dvxFontSetSmooth) when the user wants it and the
-// screen has direct color; true when that changed what text looks like
-static bool fontSmoothSet(const AppContextT *ctx) {
- bool smooth = ctx->fontSmoothing && ctx->display.format.bitsPerPixel >= FONT_SMOOTH_MIN_BPP;
-
- if (smooth == dvxFontSmooth()) {
- return false;
- }
-
- dvxFontSetSmooth(smooth);
- return true;
+// Smooth the fonts when the user wants it and the screen has direct
+// color: the fonts open are built again a frame at a time (dvxUpdate,
+// dvxFontRebuildStep), and every window laid out again after the last.
+static void fontSmoothSet(const AppContextT *ctx) {
+ dvxFontSmoothBegin(ctx->fontSmoothing && ctx->display.format.bitsPerPixel >= FONT_SMOOTH_MIN_BPP);
}
@@ -6172,6 +6547,105 @@ static void handleTabKey(AppContextT *ctx, WindowT *win, int32_t key, int32_t sh
// illusion. The dark charcoal active title bar distinguishes it from
// GEOS's blue, giving DVX its own identity.
+// Decode an image through stb's read callbacks from src (channels per
+// pixel asked for); NULL when it is no image (or one without pixels).
+static uint8_t *imageDecode(ImageSrcT *src, int *imgW, int *imgH, int channels) {
+ static const DvxImgIoT io = { imageSrcRead, imageSrcSkip, imageSrcEof };
+
+ return rejectEmptyImage(dvxImgDecode(&io, src, imgW, imgH, channels), imgW, imgH);
+}
+
+
+// An image's size as stb_image's info gives it (w, h), for dvxImageInfo:
+// a top-down BMP's height comes as stored, negative, and it loads at its
+// magnitude (INT_MIN has none: the load refuses that height as well); an
+// image without pixels does not load (rejectEmptyImage), so it has no
+// size either.
+static bool imageInfoTake(int w, int h, int32_t *outW, int32_t *outH) {
+ if (h < 0 && h != INT_MIN) {
+ h = -h;
+ }
+
+ if (w <= 0 || h <= 0) {
+ return false;
+ }
+
+ if (outW) {
+ *outW = w;
+ }
+
+ if (outH) {
+ *outH = h;
+ }
+
+ return true;
+}
+
+
+// The context an image decode pumps (a frame every IMAGE_PUMP_BYTES) for a
+// caller on ctx: only on a task that takes no input (a program on its own
+// task), where a frame runs the other tasks and none of the caller's
+// code; NULL elsewhere (a frame there could run the caller's own handlers
+// inside its call).
+static AppContextT *imagePumpCtx(const AppContextT *ctx) {
+ return (ctx->inputHere && !ctx->inputHere(ctx->idleCtx)) ? (AppContextT *)ctx : NULL;
+}
+
+
+static int imageSrcEof(void *user) {
+ const ImageSrcT *src = (const ImageSrcT *)user;
+
+ return src->fp ? (feof(src->fp) || ferror(src->fp)) : src->pos >= src->len;
+}
+
+
+// stb_image's read: from the file or the bytes; a frame pumped every
+// IMAGE_PUMP_BYTES when the source has a context to pump.
+static int imageSrcRead(void *user, char *out, int size) {
+ ImageSrcT *src = (ImageSrcT *)user;
+ int got;
+
+ if (src->fp) {
+ got = (int)fread(out, 1, (size_t)size, src->fp);
+ } else {
+ got = DVX_MIN(size, src->len - src->pos);
+ memcpy(out, src->data + src->pos, (size_t)got);
+ src->pos += got;
+ }
+
+ src->sincePump += got;
+
+ if (src->pumpCtx && src->sincePump >= IMAGE_PUMP_BYTES) {
+ src->sincePump = 0;
+ dvxUpdate(src->pumpCtx);
+ }
+
+ return got;
+}
+
+
+// stb_image's skip: n bytes on (or back, when negative). A file reads a
+// byte after the seek and puts it back, as stb's own stdio skip does: a
+// seek clears the end-of-file flag, and one past the end with no read
+// after it would leave imageSrcEof false for ever (a truncated JPEG then
+// looped in stb's marker scan).
+static void imageSrcSkip(void *user, int n) {
+ ImageSrcT *src = (ImageSrcT *)user;
+
+ if (src->fp) {
+ fseek(src->fp, n, SEEK_CUR);
+
+ int ch = fgetc(src->fp);
+
+ if (ch != EOF) {
+ ungetc(ch, src->fp);
+ }
+ } else {
+ src->pos = clampInt(src->pos + n, 0, src->len);
+ }
+}
+
+
static void initColorScheme(AppContextT *ctx) {
// Load defaults into the RGB source array, then pack all at once
memcpy(ctx->colorRgb, sDefaultColors, sizeof(sDefaultColors));
@@ -6270,6 +6744,18 @@ static void invalidateAllWindows(AppContextT *ctx) {
}
+// Every window painted in full at the next frame's paint (flushWindowPaint)
+// and the whole screen composited: however many changes come before that
+// (a program setting every colour, one call each), one paint.
+static void invalidateAllWindowsLater(AppContextT *ctx) {
+ for (int32_t i = 0; i < ctx->stack.count; i++) {
+ ctx->stack.windows[i]->paintNeeded = PAINT_FULL;
+ }
+
+ dirtyListAdd(&ctx->dirty, 0, 0, ctx->display.width, ctx->display.height);
+}
+
+
// True when a window can take keyboard focus: visible, not minimized
// (a minimized window is only an icon) and not queued for destroy. The
// single rule for Alt+Tab and every focus-successor choice.
@@ -7035,7 +7521,7 @@ static void refreshMinimizedIcons(AppContextT *ctx) {
static uint8_t *rejectEmptyImage(uint8_t *pixels, const int *imgW, const int *imgH) {
if (pixels && (*imgW <= 0 || *imgH <= 0)) {
- stbi_image_free(pixels);
+ free(pixels);
return NULL;
}
@@ -7131,20 +7617,14 @@ static int32_t saveImageAs(const DisplayT *fmt, const uint8_t *data, int32_t w,
return -1;
}
- uint8_t *rgb = bufferToRgb(fmt, data, w, h, pitch);
+ uint8_t *rgb = dvxBufferToRgb(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);
- }
+ bool ok = dvxImgWrite(path, ext && strcasecmp(ext, BMP_EXT) == 0, w, h, rgb);
free(rgb);
@@ -7278,18 +7758,6 @@ static const char *splitMenuLabel(const char *label, char leftBuf[MAX_MENU_LABEL
}
-// Stores one packed pixel at column x of a row in the display format.
-static inline void storePixel(uint8_t *row, int32_t x, int32_t bytesPerPx, uint32_t px) {
- if (bytesPerPx == sizeof(uint16_t)) {
- ((uint16_t *)row)[x] = (uint16_t)px;
- } else if (bytesPerPx == sizeof(uint32_t)) {
- ((uint32_t *)row)[x] = px;
- } else {
- row[x] = (uint8_t)px;
- }
-}
-
-
// 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) {
@@ -7377,32 +7845,7 @@ static void updateCursorShape(AppContextT *ctx) {
int32_t cy;
windowContentPoint(win, mx, my, &cx, &cy);
-
- if (win->onCursorQuery) {
- int32_t shape;
- WIN_CALLBACK(ctx, win, shape = win->onCursorQuery(win, cx, cy));
-
- if (shape > 0) {
- newCursor = shape;
- }
- }
-
- // The widget under the cursor inside a clipping container (a
- // text box in a ScrollPane) decides first, then the container
- // itself (a Splitter's divider).
- if (newCursor == CURSOR_ARROW && win->widgetRoot) {
- WidgetT *target = widgetCallbackTarget(win->widgetRoot, cx, cy);
- WidgetT *hit = widgetHitTest(win->widgetRoot, cx, cy);
- int32_t shape = target ? wclsGetCursorShape(target, cx, cy) : 0;
-
- if (shape <= 0 && hit && hit != target) {
- shape = wclsGetCursorShape(hit, cx, cy);
- }
-
- if (shape > 0) {
- newCursor = shape;
- }
- }
+ newCursor = contentCursorShape(ctx, win, cx, cy);
}
}
diff --git a/src/libs/kpunch/libdvx/dvxApp.h b/src/libs/kpunch/libdvx/dvxApp.h
index 9cca1d15..0827419a 100644
--- a/src/libs/kpunch/libdvx/dvxApp.h
+++ b/src/libs/kpunch/libdvx/dvxApp.h
@@ -65,6 +65,7 @@ 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)
+ bool mirror; // the owner's content is shown (dvxExclusiveMirror)
int32_t reqW; // what the owner asked for
int32_t reqH;
int32_t reqBpp;
@@ -74,7 +75,8 @@ typedef struct {
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
+ RectT presentRect; // where dvxExclusivePresent8 or the mirror last put the picture
+ RectT mirrorCursor; // where the mirror last drew the mouse cursor
int32_t savedMouseX; // desktop mouse position to go back to
int32_t savedMouseY;
} ExclusiveStateT;
@@ -137,7 +139,17 @@ typedef struct AppContextT {
// The idle callback allows the host application (e.g. the DVX shell)
// to do background work (poll serial ports, service DXE apps) during
// frames where no GUI events occurred, instead of just yielding the CPU.
+ // The busy callback runs after a frame that painted instead (never
+ // both), so the host's background work and cooperative yield go on
+ // while the screen keeps changing. Both are given idleCtx.
void (*idleCallback)(void *ctx); // called instead of yield when non-NULL
+ void (*busyCallback)(void *ctx); // called after a painted frame when non-NULL
+ // Whether this frame takes input (polls the mouse and keyboard,
+ // dispatches events, fires widget timers): the shell lets only its own
+ // task do it, so a program's callbacks never run on another program's
+ // stack. A frame that does not just paints. NULL: every frame does.
+ // Given idleCtx.
+ bool (*inputHere)(void *ctx);
void *idleCtx;
WindowT *modalWindow; // if non-NULL, only this window receives input
// Deferred window destruction. Destroying a window from inside a
@@ -225,8 +237,9 @@ void dvxMouseToPrefs(const AppContextT *ctx, PrefsHandleT *h);
// Smooth (anti-alias) text, or stop: on (the default) it is smoothed at
// more than 8 bits per pixel, in the faces and sizes the FONTS folder
-// has smoothed. Every window is laid out and painted again when what is
-// drawn changes; a video mode change applies it to the new depth.
+// has smoothed. The fonts open are built again a frame at a time, and
+// every window is laid out and painted again after the last; a video mode
+// change applies it to the new depth.
void dvxSetFontSmoothing(AppContextT *ctx, bool on);
// Apply the DVX.INI [fonts] settings in h: smoothing (on when missing).
@@ -282,6 +295,16 @@ void dvxPlaySoundWait(DvxSoundE event);
// The default beep: dvxPlaySound(SoundDefaultE).
void dvxBeep(void);
+// Waiting in a library (for a sound to end, say): dvxWaitPump between the
+// checks lets DVX go on, and with it the events of the BASIC program that
+// waits (the hook the BASIC runtime sets), as DoEvents does. False: stop
+// waiting (the program is ending, or its code may not wait). Without a
+// hook it only yields the processor.
+typedef bool (*DvxWaitPumpT)(void);
+
+void dvxSetWaitPump(DvxWaitPumpT fn);
+bool dvxWaitPump(void);
+
// ============================================================
// Full-screen exclusive mode
// ============================================================
@@ -324,6 +347,15 @@ DisplayT *dvxExclusiveDisplay(AppContextT *ctx);
// have full screen.
void dvxExclusiveEnd(AppContextT *ctx, WindowT *win);
+// Take the whole screen for win's own content (a window of controls, a
+// drawing): the smallest mode of at least its content's size in the
+// desktop's pixel format, the content scaled by the largest whole factor
+// that fits and centered on black, and shown again as the window paints.
+// The mouse works on the content as in the window (onMouse gets content
+// coordinates) and its cursor is drawn. Otherwise as dvxExclusiveBegin;
+// DVX_EXCL_ERR_MODE also when there is no mode in the desktop's format.
+int32_t dvxExclusiveMirror(AppContextT *ctx, WindowT *win, ExclusiveFnT onChange, void *userData);
+
// Copy a rectangle of the backbuffer to the screen (NULL: all of it).
void dvxExclusiveFlush(AppContextT *ctx, const RectT *r);
@@ -362,8 +394,9 @@ void dvxExclusiveWaitRetrace(void);
int32_t dvxRunDos(AppContextT *ctx, const char *program, const char *args, const char *folder, bool pause);
// Whether the library (or widget) name -- its file's base name, any case,
-// as DECLARE LIBRARY names it: "pkgnet", "dvxsql" -- is loaded, that is
-// installed and started with DVX (the loader loads every one at start).
+// as DECLARE LIBRARY names it: "pkgnet", "dvxsql" -- is installed and
+// loaded. The loader loads a library when something first needs it, so
+// asking loads one that is installed and not loaded yet.
// A program using a library of an optional part of DVX (Networking,
// Databases, ...) asks before it calls one of its functions.
bool dvxLibraryLoaded(const char *name);
@@ -383,8 +416,9 @@ uint32_t dvxIdleMs(const AppContextT *ctx);
// Color scheme
// ============================================================
-// Set a single color by ID. Repacks to native pixel format and
-// invalidates the entire screen.
+// Set a single color by ID. Repacks to native pixel format; every
+// window is painted again at the next frame (once, however many colors
+// change before it) and the whole screen composited.
void dvxSetColor(AppContextT *ctx, ColorIdE id, uint8_t r, uint8_t g, uint8_t b);
// Get a color's RGB values by ID.
@@ -458,10 +492,11 @@ void dvxShutdown(AppContextT *ctx);
// becomes false (typically from dvxQuit() or a close callback).
void dvxRun(AppContextT *ctx);
-// Process exactly one frame of the event loop. Provided for applications
-// that need to integrate the GUI into their own main loop (e.g. the DVX
-// shell polling serial ports between frames). Returns false when the GUI
-// wants to exit.
+// Process exactly one frame of the event loop: input, events, and timers
+// (where the context takes them: inputHere), paints, the composite, and the
+// idle or busy callback. Provided for applications that need to integrate
+// the GUI into their own main loop (e.g. the DVX shell polling serial ports
+// between frames). Returns false when the GUI wants to exit.
bool dvxUpdate(AppContextT *ctx);
// Create a window at an explicit screen position. The window is raised
@@ -599,7 +634,10 @@ int32_t dvxWindowScreenshot(AppContextT *ctx, WindowT *win, const char *path);
// Down, End, Enter, Esc, Home, Ins, Left, PgDn, PgUp, Right, Space, Tab,
// Up); "" for key 0. dvxAccelParse reads such text back (case does not
// matter; Delete and Insert are also accepted); false, with *outKey 0,
-// for anything else.
+// for anything else. DVX_ACCEL_TEXT_MAX holds the longest such text
+// ("Ctrl+Alt+Shift+Backspace") with its NUL.
+#define DVX_ACCEL_TEXT_MAX 32
+
void dvxAccelFormat(int32_t key, int32_t modifiers, char *out, int32_t outSize);
bool dvxAccelParse(const char *text, int32_t *outKey, int32_t *outMods);
@@ -660,6 +698,13 @@ bool dvxAppIsHung(const AppContextT *ctx, int32_t appId);
// goes away, even from inside one of its callbacks).
void dvxAppHungForget(int32_t appId);
+// Book a handler of appId as running (dvxAppIsHung counts from the
+// outermost one's start), and its end. Window callbacks are booked by
+// DVX itself; a host that runs an app's handlers outside them (the BASIC
+// runtime, running events on the program's own task) brackets each.
+void dvxAppCallEnter(int32_t appId);
+void dvxAppCallLeave(int32_t appId);
+
// Window arrangement functions -- operate on all visible, non-minimized
// windows. These mirror the classic Windows 3.x "Window" menu commands.
@@ -695,10 +740,25 @@ uint8_t *dvxLoadImageAlpha(const AppContextT *ctx, const uint8_t *data, int32_t
// Free a pixel buffer returned by dvxLoadImage.
void dvxFreeImage(uint8_t *data);
-// Query image dimensions without decoding the full file.
-// Returns true on success, false if the file can't be read.
+// The image loaders decode a big picture (seconds for a large JPEG) a
+// part at a time when called from a program's own task: DVX and the other
+// programs run between the parts, none of the caller's code does. On the
+// task that takes the input (C programs' callbacks) they decode in one go.
+
+// Query image dimensions without decoding the full file (its header
+// only: a damaged picture may still fail to load). Returns true on
+// success, false if the file can't be read or is no picture.
bool dvxImageInfo(const char *path, int32_t *outW, int32_t *outH);
+// dvxImageInfo for a picture's bytes in memory.
+bool dvxImageInfoFromMemory(const uint8_t *data, int32_t dataLen, int32_t *outW, int32_t *outH);
+
+// Native-format pixels (in d's format: a buffer as dvxLoadImage gives it,
+// a window's content, a canvas's) as R, G, B bytes, malloc'd (NULL when
+// out of memory). Table driven: the fast way to hand the screen's pixels
+// to code that wants RGB (image files, printing).
+uint8_t *dvxBufferToRgb(const DisplayT *d, const uint8_t *buf, int32_t w, int32_t h, int32_t pitch);
+
// Save native-format pixel data to an image file: BMP when path ends in
// .bmp (any case), PNG otherwise. The pixel data must be in the display's
// native format (as returned by dvxLoadImage or captured from a content
@@ -765,9 +825,10 @@ uint8_t *dvxResLoadIcon(AppContextT *ctx, const char *dxePath, const char *resNa
// neither (or is no DVX file). Free with free(); *outSize gets the size.
void *dvxResLoadIconData(const char *dxePath, uint32_t *outSize);
-// Load a text resource into a caller-provided buffer.
+// Load a text resource into a caller-provided buffer (dvxResReadText).
// Returns true on success. The text is null-terminated and truncated
-// to fit bufSize. Returns false if the resource is not found.
+// to fit bufSize. Returns false if the resource is not found (buf is
+// left alone) or cannot be read (buf is then "").
bool dvxResLoadText(const char *dxePath, const char *resName, char *buf, int32_t bufSize);
// Load a raw binary resource. Returns a malloc'd buffer that the
diff --git a/src/libs/kpunch/libdvx/dvxBas.c b/src/libs/kpunch/libdvx/dvxBas.c
index c92a9f38..c5084754 100644
--- a/src/libs/kpunch/libdvx/dvxBas.c
+++ b/src/libs/kpunch/libdvx/dvxBas.c
@@ -30,6 +30,8 @@
#include "thirdparty/stb_ds_wrap.h"
#include
+#include
+#include
#include
#define SHELL_CLEANUP_SYM "shellRegisterAppCleanup"
@@ -50,9 +52,11 @@ static void appEnded(int32_t appId);
int32_t basHandleAdd(BasHandlesT *t, void *obj);
void basHandleFree(BasHandlesT *t, int32_t handle);
void *basHandleGet(const BasHandlesT *t, int32_t handle);
+void basHandlesAppEnded(BasHandlesT *t, int32_t appId);
void basTextAppend(BasTextT *t, const char *s);
void basTextAppendLen(BasTextT *t, const char *s, int32_t len);
const char *basTextEnd(BasTextT *t);
+const char *basTextFormat(BasTextT *t, const char *fmt, ...);
void basTextRow(BasTextT *t, const char *const *cells, int32_t count);
const char *basTextSet(BasTextT *t, const char *s);
void basTextStart(BasTextT *t);
@@ -61,11 +65,7 @@ void basTextStart(BasTextT *t);
// The shell's app cleanup: what the program that ended still holds.
static void appEnded(int32_t appId) {
for (int32_t t = 0; t < (int32_t)arrlen(sTables); t++) {
- for (int32_t i = 0; i < (int32_t)arrlen(sTables[t]->objs); i++) {
- if (sTables[t]->objs[i] && sTables[t]->owners[i] == appId) {
- basHandleFree(sTables[t], i + 1);
- }
- }
+ basHandlesAppEnded(sTables[t], appId);
}
}
@@ -141,6 +141,15 @@ void *basHandleGet(const BasHandlesT *t, int32_t handle) {
}
+void basHandlesAppEnded(BasHandlesT *t, int32_t appId) {
+ for (int32_t i = 0; i < (int32_t)arrlen(t->objs); i++) {
+ if (t->objs[i] && t->owners[i] == appId) {
+ basHandleFree(t, i + 1);
+ }
+ }
+}
+
+
void basTextAppend(BasTextT *t, const char *s) {
basTextAppendLen(t, s, (int32_t)strlen(s));
}
@@ -166,6 +175,34 @@ const char *basTextEnd(BasTextT *t) {
}
+// Measured first, then formatted straight into the result.
+const char *basTextFormat(BasTextT *t, const char *fmt, ...) {
+ va_list ap;
+
+ va_start(ap, fmt);
+ int32_t n = (int32_t)vsnprintf(NULL, 0, fmt, ap);
+ va_end(ap);
+
+ basTextStart(t);
+
+ if (n < 0) {
+ return basTextEnd(t);
+ }
+
+ arrsetlen(t->buf, n + 1);
+
+ if ((int32_t)arrlen(t->buf) != n + 1) {
+ arrsetlen(t->buf, 0);
+ return "";
+ }
+
+ va_start(ap, fmt);
+ vsnprintf(t->buf, (size_t)n + 1, fmt, ap);
+ va_end(ap);
+ return t->buf;
+}
+
+
void basTextRow(BasTextT *t, const char *const *cells, int32_t count) {
for (int32_t i = 0; i < count; i++) {
if (i > 0) {
diff --git a/src/libs/kpunch/libdvx/dvxBas.h b/src/libs/kpunch/libdvx/dvxBas.h
index 5e1f2852..c73df2c3 100644
--- a/src/libs/kpunch/libdvx/dvxBas.h
+++ b/src/libs/kpunch/libdvx/dvxBas.h
@@ -71,6 +71,11 @@ int32_t basHandleAdd(BasHandlesT *t, void *obj);
// Free handle's object (freeFn) and the handle; a bad handle is ignored.
void basHandleFree(BasHandlesT *t, int32_t handle);
+// Free every handle of t the program appId holds. The shell's app
+// cleanup does this for every table; a library that has its own end hook
+// can call it too.
+void basHandlesAppEnded(BasHandlesT *t, int32_t appId);
+
// The object of handle, or NULL for a bad handle.
void *basHandleGet(const BasHandlesT *t, int32_t handle);
@@ -88,6 +93,9 @@ void basTextRow(BasTextT *t, const char *const *cells, int32_t count);
// The result built: a NUL-terminated copy ("" when out of memory).
const char *basTextEnd(BasTextT *t);
+// printf-style text as the result ("" when out of memory).
+const char *basTextFormat(BasTextT *t, const char *fmt, ...) __attribute__((format(printf, 2, 3)));
+
// s (NULL: "") as the result: basTextStart, basTextAppend, basTextEnd.
const char *basTextSet(BasTextT *t, const char *s);
diff --git a/src/libs/kpunch/libdvx/dvxDialog.c b/src/libs/kpunch/libdvx/dvxDialog.c
index c5f566bc..3a255c1f 100644
--- a/src/libs/kpunch/libdvx/dvxDialog.c
+++ b/src/libs/kpunch/libdvx/dvxDialog.c
@@ -1779,14 +1779,7 @@ static void fdLoadDir(FileDialogStateT *fd) {
continue;
}
- char fullPath[DVX_MAX_PATH];
- struct stat st;
-
- if (!platformPathJoin(fullPath, sizeof(fullPath), fd->curDir, ent->d_name) || stat(fullPath, &st) != 0) {
- continue;
- }
-
- bool isDir = S_ISDIR(st.st_mode);
+ bool isDir = platformDirEntryIsDir(fd->curDir, ent->d_name, ent->d_type);
// Apply filter to files only; a folder dialog lists no files
if (!isDir && ((fd->flags & FD_FOLDER) || !platformFilterMatch(ent->d_name, pattern))) {
diff --git a/src/libs/kpunch/libdvx/dvxDraw.c b/src/libs/kpunch/libdvx/dvxDraw.c
index fc661fc3..c2daabf6 100644
--- a/src/libs/kpunch/libdvx/dvxDraw.c
+++ b/src/libs/kpunch/libdvx/dvxDraw.c
@@ -152,7 +152,7 @@ static inline void nibbleRow16(uint8_t *dst, const NibbleTableT *t, uint8_t bi
static inline void nibbleRow32(uint8_t *dst, const NibbleTableT *t, uint8_t bits);
static inline void nibbleRow8(uint8_t *dst, const NibbleTableT *t, uint8_t bits);
static void nibbleTableSet(NibbleTableT *t, uint32_t fg, uint32_t bg, int32_t bpp);
-static inline void putPixel(uint8_t *dst, uint32_t color, int32_t bpp);
+static inline void scaleStep(int32_t *q, int32_t *r, int32_t qStep, int32_t rStep, int32_t dstW);
static void termCellColors(uint8_t attr, const uint32_t *palette, bool blinkVisible, bool cursor, uint32_t *fg, uint32_t *bg);
// Bit lookup tables for glyph and mask rendering. On a 486, a variable
@@ -312,10 +312,18 @@ static inline void clipRect(const DisplayT *d, int32_t *x, int32_t *y, int32_t *
int32_t rx2 = rx1 + *w;
int32_t ry2 = ry1 + *h;
- if (__builtin_expect(rx1 < d->clipX, 0)) { rx1 = d->clipX; }
- if (__builtin_expect(ry1 < d->clipY, 0)) { ry1 = d->clipY; }
- if (__builtin_expect(rx2 > cx2, 0)) { rx2 = cx2; }
- if (__builtin_expect(ry2 > cy2, 0)) { ry2 = cy2; }
+ if (__builtin_expect(rx1 < d->clipX, 0)) {
+ rx1 = d->clipX;
+ }
+ if (__builtin_expect(ry1 < d->clipY, 0)) {
+ ry1 = d->clipY;
+ }
+ if (__builtin_expect(rx2 > cx2, 0)) {
+ rx2 = cx2;
+ }
+ if (__builtin_expect(ry2 > cy2, 0)) {
+ ry2 = cy2;
+ }
*x = rx1;
*y = ry1;
@@ -507,13 +515,21 @@ static int32_t drawCharFixed(DisplayT *d, const BlitOpsT *ops, const BitmapFontT
int32_t rowStart = 0;
int32_t rowEnd = chh;
- if (y < clipY1) { rowStart = clipY1 - y; }
- if (y + chh > clipY2) { rowEnd = clipY2 - y; }
+ if (y < clipY1) {
+ rowStart = clipY1 - y;
+ }
+ if (y + chh > clipY2) {
+ rowEnd = clipY2 - y;
+ }
int32_t colStart = 0;
int32_t colEnd = cw;
- if (x < clipX1) { colStart = clipX1 - x; }
- if (x + cw > clipX2) { colEnd = clipX2 - x; }
+ if (x < clipX1) {
+ colStart = clipX1 - x;
+ }
+ if (x + cw > clipX2) {
+ colEnd = clipX2 - x;
+ }
// Unclipped fast path: when the character cell is fully within the
// clip rect we can skip per-pixel clip checks and use the fully
@@ -644,7 +660,7 @@ static int32_t drawCharFixed(DisplayT *d, const BlitOpsT *ops, const BitmapFontT
// ============================================================
//
// Draws a dotted (every-other-pixel) rectangle to indicate keyboard
-// focus, matching the Windows/Motif convention. Uses putPixel per
+// focus, matching the Windows/Motif convention. Uses pixelPut per
// dot rather than spanFill because the alternating pattern can't be
// expressed as a span fill (which writes uniform color).
//
@@ -769,7 +785,7 @@ static void drawGlyphBits(DisplayT *d, const BlitOpsT *ops, const uint8_t *bits,
for (int32_t col = 0; col < GLYPH_COLS; col++) {
if ((byte & sGlyphBit[col]) && px + col >= clipX1 && px + col < clipX2) {
- putPixel(line + (px + col) * bpp, fg, bpp);
+ pixelPut(line + (px + col) * bpp, fg, bpp);
}
}
}
@@ -806,7 +822,7 @@ static void drawGlyphGray(DisplayT *d, const BlitOpsT *ops, const uint8_t *bits,
}
if (level == FONT_GRAY_FULL || (bpp == 1 && level >= FONT_GRAY_HALF)) {
- putPixel(dst, fg, bpp);
+ pixelPut(dst, fg, bpp);
} else if (bpp > 1) {
uint32_t back = (bpp == 2) ? *(const uint16_t *)dst : *(const uint32_t *)dst;
@@ -816,7 +832,7 @@ static void drawGlyphGray(DisplayT *d, const BlitOpsT *ops, const uint8_t *bits,
have[level] = true;
}
- putPixel(dst, lastOut[level], bpp);
+ pixelPut(dst, lastOut[level], bpp);
}
}
}
@@ -920,13 +936,21 @@ void drawMaskedBitmap(DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t y, in
int32_t rowStart = 0;
int32_t rowEnd = h;
- if (y < clipY1) { rowStart = clipY1 - y; }
- if (y + h > clipY2) { rowEnd = clipY2 - y; }
+ if (y < clipY1) {
+ rowStart = clipY1 - y;
+ }
+ if (y + h > clipY2) {
+ rowEnd = clipY2 - y;
+ }
int32_t colStart = 0;
int32_t colEnd = w;
- if (x < clipX1) { colStart = clipX1 - x; }
- if (x + w > clipX2) { colEnd = clipX2 - x; }
+ if (x < clipX1) {
+ colStart = clipX1 - x;
+ }
+ if (x + w > clipX2) {
+ colEnd = clipX2 - x;
+ }
// The mask rows index sMaskBit[col], so a width above the table size
// would read past it. Today the only caller passes width 16, but
@@ -1016,6 +1040,98 @@ void drawRectOutline(DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t y, int
}
+// Clipped once (row and column ranges), each visible row's source row
+// found exactly and drawn by drawScaleRow.
+void drawScaled(DisplayT *d, int32_t dstX, int32_t dstY, int32_t dstW, int32_t dstH, const uint8_t *src, int32_t srcW, int32_t srcH, int32_t srcPitch, bool keyed, uint32_t key) {
+ int32_t bpp = d->format.bytesPerPixel;
+ int32_t col0 = DVX_MAX(0, d->clipX - dstX);
+ int32_t col1 = DVX_MIN(dstW, d->clipX + d->clipW - dstX);
+ int32_t row0 = DVX_MAX(0, d->clipY - dstY);
+ int32_t row1 = DVX_MIN(dstH, d->clipY + d->clipH - dstY);
+
+ if (dstW <= 0 || dstH <= 0 || srcW <= 0 || srcH <= 0 || col0 >= col1) {
+ return;
+ }
+
+ for (int32_t y = row0; y < row1; y++) {
+ const uint8_t *srcRow = src + (int64_t)((int64_t)y * srcH / dstH) * srcPitch;
+ uint8_t *dstRow = d->backBuf + (int64_t)(dstY + y) * d->pitch + (int64_t)(dstX + col0) * bpp;
+
+ drawScaleRow(dstRow, srcRow, bpp, col0, col1 - col0, dstW, srcW, keyed, key);
+ }
+}
+
+
+// The source pixel walks (q, and r over dstW) one destination pixel on,
+// so no pixel costs a divide; the switch runs once per row.
+void drawScaleRow(uint8_t *dst, const uint8_t *src, int32_t bytesPerPx, int32_t first, int32_t count, int32_t dstW, int32_t srcW, bool keyed, uint32_t key) {
+ uint64_t start = (uint64_t)first * (uint64_t)srcW;
+ int32_t q = (int32_t)(start / (uint64_t)dstW);
+ int32_t r = (int32_t)(start % (uint64_t)dstW);
+ int32_t qStep = srcW / dstW;
+ int32_t rStep = srcW % dstW;
+
+ switch (bytesPerPx) {
+ case 1:
+ for (int32_t i = 0; i < count; i++) {
+ if (!keyed || src[q] != (uint8_t)key) {
+ dst[i] = src[q];
+ }
+
+ scaleStep(&q, &r, qStep, rStep, dstW);
+ }
+
+ break;
+
+ case 2: {
+ const uint16_t *s16 = (const uint16_t *)src;
+ uint16_t *d16 = (uint16_t *)dst;
+
+ for (int32_t i = 0; i < count; i++) {
+ if (!keyed || s16[q] != (uint16_t)key) {
+ d16[i] = s16[q];
+ }
+
+ scaleStep(&q, &r, qStep, rStep, dstW);
+ }
+
+ break;
+ }
+
+ case 4: {
+ const uint32_t *s32 = (const uint32_t *)src;
+ uint32_t *d32 = (uint32_t *)dst;
+
+ for (int32_t i = 0; i < count; i++) {
+ if (!keyed || s32[q] != key) {
+ d32[i] = s32[q];
+ }
+
+ scaleStep(&q, &r, qStep, rStep, dstW);
+ }
+
+ break;
+ }
+
+ default:
+ for (int32_t i = 0; i < count; i++) {
+ const uint8_t *px = src + (int64_t)q * bytesPerPx;
+ uint32_t v = 0;
+
+ memcpy(&v, px, (size_t)bytesPerPx);
+
+ if (!keyed || v != key) {
+ memcpy(dst + (int64_t)i * bytesPerPx, px, (size_t)bytesPerPx);
+ }
+
+ scaleStep(&q, &r, qStep, rStep, dstW);
+ }
+
+ break;
+ }
+}
+
+
// Axis-agnostic scrollbar painter shared by the window manager and the
// widget toolkit. Lays down a sunken trough bevel, an arrow button at
// each end, and a raised thumb at (barW + thumbPos) along the track.
@@ -1025,7 +1141,6 @@ void drawRectOutline(DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t y, int
// ends, and finally the thumb bevel sits in the middle. A thumbSize of
// 0 (or less) suppresses the thumb, used when the range is too small or
// the track is too short to display one.
-
void drawScrollbar(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colors, ScrollbarOrientE orient, int32_t x, int32_t y, int32_t length, int32_t barW, int32_t thumbPos, int32_t thumbSize) {
BevelStyleT troughBevel = BEVEL_TROUGH(colors);
BevelStyleT btnBevel = BEVEL_SB_BUTTON(colors);
@@ -1127,8 +1242,12 @@ void drawTermRow(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int3
// Vertical clip for glyph scanlines
int32_t rowStart = 0;
int32_t rowEnd = ch;
- if (y < clipY1) { rowStart = clipY1 - y; }
- if (y + ch > clipY2) { rowEnd = clipY2 - y; }
+ if (y < clipY1) {
+ rowStart = clipY1 - y;
+ }
+ if (y + ch > clipY2) {
+ rowEnd = clipY2 - y;
+ }
// Horizontal clip: find first and last visible column
int32_t rowW = cols * cw;
@@ -1145,7 +1264,9 @@ void drawTermRow(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int3
if (x + rowW > clipX2) {
lastCol = (clipX2 - x + cw - 1) / cw;
- if (lastCol > cols) { lastCol = cols; }
+ if (lastCol > cols) {
+ lastCol = cols;
+ }
}
// Per-column clip for partially visible edge cells
@@ -1388,9 +1509,18 @@ static void drawTextLines(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *f
// Horizontal clipping is done at the character level (firstChar/
// lastChar) to avoid iterating invisible characters, with per-pixel
// edge clipping only for the partially visible first and last chars.
+// A row wholly above or below the clip rect (its underline and strike
+// lines included) is passed over before anything is measured: a long
+// document draws only what shows.
void drawTextN(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int32_t x, int32_t y, const char *text, int32_t count, uint32_t fg, uint32_t bg, bool opaque) {
- if (count <= 0) {
+ int32_t bottom = y + font->charHeight;
+
+ if (font->style & FONT_STYLE_LINES) {
+ bottom = DVX_MAX(bottom, y + DVX_MAX(font->underlineY, font->strikeY) + font->lineThick);
+ }
+
+ if (count <= 0 || bottom <= d->clipY || y >= d->clipY + d->clipH) {
return;
}
@@ -1432,8 +1562,12 @@ static void drawTextNFixed(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *
// Vertical clip for glyph scanlines
int32_t rowStart = 0;
int32_t rowEnd = ch;
- if (y < clipY1) { rowStart = clipY1 - y; }
- if (y + ch > clipY2) { rowEnd = clipY2 - y; }
+ if (y < clipY1) {
+ rowStart = clipY1 - y;
+ }
+ if (y + ch > clipY2) {
+ rowEnd = clipY2 - y;
+ }
// Horizontal clip: find first and last visible column (character index)
int32_t firstChar = 0;
@@ -1445,7 +1579,9 @@ static void drawTextNFixed(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *
if (x + totalW > clipX2) {
lastChar = (clipX2 - x + cw - 1) / cw;
- if (lastChar > count) { lastChar = count; }
+ if (lastChar > count) {
+ lastChar = count;
+ }
}
// Per-pixel clip for partially visible edge characters
@@ -1461,8 +1597,12 @@ static void drawTextNFixed(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *
int32_t fillX1 = x + firstChar * cw;
int32_t fillX2 = x + lastChar * cw;
- if (fillX1 < clipX1) { fillX1 = clipX1; }
- if (fillX2 > clipX2) { fillX2 = clipX2; }
+ if (fillX1 < clipX1) {
+ fillX1 = clipX1;
+ }
+ if (fillX2 > clipX2) {
+ fillX2 = clipX2;
+ }
int32_t fillW = fillX2 - fillX1;
@@ -1756,9 +1896,15 @@ void drawVLine(DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t y, int32_t h
int32_t y1 = y;
int32_t y2 = y + h;
- if (y1 < d->clipY) { y1 = d->clipY; }
- if (y2 > d->clipY + d->clipH) { y2 = d->clipY + d->clipH; }
- if (y1 >= y2) { return; }
+ if (y1 < d->clipY) {
+ y1 = d->clipY;
+ }
+ if (y2 > d->clipY + d->clipH) {
+ y2 = d->clipY + d->clipH;
+ }
+ if (y1 >= y2) {
+ return;
+ }
int32_t bpp = ops->bytesPerPixel;
uint8_t *dst = d->backBuf + y1 * d->pitch + x * bpp;
@@ -1810,7 +1956,7 @@ static void focusDot(void *ctx, int32_t x, int32_t y) {
const DisplayT *d = dc->d;
if (x >= d->clipX && x < d->clipX + d->clipW && y >= d->clipY && y < d->clipY + d->clipH) {
- putPixel(d->backBuf + y * d->pitch + x * dc->bpp, dc->color, dc->bpp);
+ pixelPut(d->backBuf + y * d->pitch + x * dc->bpp, dc->color, dc->bpp);
}
}
@@ -1995,7 +2141,7 @@ static inline void nibbleRow8(uint8_t *dst, const NibbleTableT *t, uint8_t bits)
// t's patterns for fg on bg at bpp bytes a pixel (kept when they already
-// are): each nibble's pixels written as putPixel writes them
+// are): each nibble's pixels written as pixelPut writes them
static void nibbleTableSet(NibbleTableT *t, uint32_t fg, uint32_t bg, int32_t bpp) {
if (t->valid && t->fg == fg && t->bg == bg && t->bpp == bpp) {
return;
@@ -2005,7 +2151,7 @@ static void nibbleTableSet(NibbleTableT *t, uint32_t fg, uint32_t bg, int32_t bp
uint8_t px[NIBBLE_WORDS_MAX * sizeof(uint32_t)];
for (int32_t p = 0; p < NIBBLE_BITS; p++) {
- putPixel(px + p * bpp, ((n << p) & NIBBLE_LEFT_BIT) ? fg : bg, bpp);
+ pixelPut(px + p * bpp, ((n << p) & NIBBLE_LEFT_BIT) ? fg : bg, bpp);
}
memcpy(t->words[n], px, (size_t)(NIBBLE_BITS * bpp));
@@ -2018,27 +2164,6 @@ static void nibbleTableSet(NibbleTableT *t, uint32_t fg, uint32_t bg, int32_t bp
}
-// ============================================================
-// putPixel
-// ============================================================
-//
-// Writes a single pixel at an already-computed buffer address, for
-// drawFocusRect's alternating dot pattern and nibbleTableSet.
-// Marked static inline so it compiles to a direct store at the
-// call site with no function call overhead. The bpp chain here
-// is acceptable because focus rect drawing is infrequent.
-
-static inline void putPixel(uint8_t *dst, uint32_t color, int32_t bpp) {
- if (bpp == 2) {
- *(uint16_t *)dst = (uint16_t)color;
- } else if (bpp == 4) {
- *(uint32_t *)dst = color;
- } else {
- *dst = (uint8_t)color;
- }
-}
-
-
// ============================================================
// rectCopy
// ============================================================
@@ -2305,6 +2430,18 @@ void rectFill(DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t y, int32_t w,
// The colors of a terminal cell of attribute attr: palette foreground
// and background, the foreground hidden in a blink's off phase, the two
// swapped under the cursor
+// drawScaleRow's step: the next destination pixel's source pixel.
+static inline void scaleStep(int32_t *q, int32_t *r, int32_t qStep, int32_t rStep, int32_t dstW) {
+ *q += qStep;
+ *r += rStep;
+
+ if (*r >= dstW) {
+ *r -= dstW;
+ (*q)++;
+ }
+}
+
+
static void termCellColors(uint8_t attr, const uint32_t *palette, bool blinkVisible, bool cursor, uint32_t *fg, uint32_t *bg) {
*fg = palette[attr & TERM_ATTR_FG_MASK];
*bg = palette[(attr >> TERM_ATTR_BG_SHIFT) & TERM_ATTR_BG_MASK];
diff --git a/src/libs/kpunch/libdvx/dvxDraw.h b/src/libs/kpunch/libdvx/dvxDraw.h
index 472e067c..f4db9111 100644
--- a/src/libs/kpunch/libdvx/dvxDraw.h
+++ b/src/libs/kpunch/libdvx/dvxDraw.h
@@ -198,10 +198,51 @@ void drawScrollbarArrow(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *co
// drawScrollbarArrow glyph pointing dir centred on it.
void drawScrollbarArrowButton(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colors, int32_t x, int32_t y, int32_t size, ScrollArrowDirE dir);
+// Nearest-neighbour scaling of a srcW x srcH picture in d's pixel format
+// (srcPitch bytes a row) to dstW x dstH at (dstX, dstY), clipped; with
+// keyed, source pixels equal to key are not drawn. Pixel (x, y) is the
+// source pixel (x * srcW / dstW, y * srcH / dstH), exactly.
+void drawScaled(DisplayT *d, int32_t dstX, int32_t dstY, int32_t dstW, int32_t dstH, const uint8_t *src, int32_t srcW, int32_t srcH, int32_t srcPitch, bool keyed, uint32_t key);
+
+// Nearest-neighbour scaling of one row of pixels (bytesPerPx 1 to 4): dst
+// gets count pixels, pixel i the source pixel (first + i) * srcW / dstW,
+// exactly, stepped without a divide per pixel. With keyed, a source pixel
+// equal to key is not copied (dst keeps its own).
+void drawScaleRow(uint8_t *dst, const uint8_t *src, int32_t bytesPerPx, int32_t first, int32_t count, int32_t dstW, int32_t srcW, bool keyed, uint32_t key);
+
// Horizontal line (1px tall rectangle fill, but with simpler clipping).
void drawHLine(DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t y, int32_t w, uint32_t color);
// Vertical line (1px wide, drawn pixel-by-pixel down each scanline).
void drawVLine(DisplayT *d, const BlitOpsT *ops, int32_t x, int32_t y, int32_t h, uint32_t color);
+// One packed pixel of bpp bytes (1, 2 or 4: a display's format) written
+// at dst, or read from src.
+static inline uint32_t pixelGet(const uint8_t *src, int32_t bpp);
+static inline void pixelPut(uint8_t *dst, uint32_t color, int32_t bpp);
+
+
+static inline uint32_t pixelGet(const uint8_t *src, int32_t bpp) {
+ if (bpp == sizeof(uint16_t)) {
+ return *(const uint16_t *)src;
+ }
+
+ if (bpp == sizeof(uint32_t)) {
+ return *(const uint32_t *)src;
+ }
+
+ return *src;
+}
+
+
+static inline void pixelPut(uint8_t *dst, uint32_t color, int32_t bpp) {
+ if (bpp == sizeof(uint16_t)) {
+ *(uint16_t *)dst = (uint16_t)color;
+ } else if (bpp == sizeof(uint32_t)) {
+ *(uint32_t *)dst = color;
+ } else {
+ *dst = (uint8_t)color;
+ }
+}
+
#endif // DVX_DRAW_H
diff --git a/src/libs/kpunch/libdvx/dvxDriver.c b/src/libs/kpunch/libdvx/dvxDriver.c
index 4ebebdc0..e2b77cde 100644
--- a/src/libs/kpunch/libdvx/dvxDriver.c
+++ b/src/libs/kpunch/libdvx/dvxDriver.c
@@ -84,7 +84,7 @@ DvxDriverModT *dvxDriversLoad(const char *dir, const DvxDriverClassT *cls) {
}
snprintf(path, sizeof(path), "%s" DVX_PATH_SEP "%s", dir, names[i]);
- mod.handle = dlopen(path, RTLD_NOW);
+ mod.handle = platformModuleOpen(path, RTLD_NOW);
if (!mod.handle) {
dvxLog("Drivers: %s did not load: %s", path, dlerror());
diff --git a/src/libs/kpunch/libdvx/dvxEndian.h b/src/libs/kpunch/libdvx/dvxEndian.h
new file mode 100644
index 00000000..768b76f4
--- /dev/null
+++ b/src/libs/kpunch/libdvx/dvxEndian.h
@@ -0,0 +1,83 @@
+// 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.
+
+// dvxEndian.h -- Byte order: little- and big-endian loads and stores
+//
+// File formats and wire protocols fix their byte order; these read and
+// write it a byte at a time, so they work on any host and at any
+// alignment. Header only, so the core, the libraries, the tools, and the
+// Linux proxy use the same code without linking anything.
+
+#ifndef DVX_ENDIAN_H
+#define DVX_ENDIAN_H
+
+#include
+
+#define DVX_BYTE_BITS 8
+#define DVX_BYTE_MASK 0xFFu
+
+
+static inline uint16_t dvxGetBe16(const uint8_t *p) {
+ return (uint16_t)((p[0] << DVX_BYTE_BITS) | p[1]);
+}
+
+
+static inline uint32_t dvxGetBe32(const uint8_t *p) {
+ return ((uint32_t)dvxGetBe16(p) << (2 * DVX_BYTE_BITS)) | dvxGetBe16(p + 2);
+}
+
+
+static inline uint16_t dvxGetLe16(const uint8_t *p) {
+ return (uint16_t)(p[0] | (p[1] << DVX_BYTE_BITS));
+}
+
+
+static inline uint32_t dvxGetLe24(const uint8_t *p) {
+ return (uint32_t)dvxGetLe16(p) | ((uint32_t)p[2] << (2 * DVX_BYTE_BITS));
+}
+
+
+static inline uint32_t dvxGetLe32(const uint8_t *p) {
+ return dvxGetLe24(p) | ((uint32_t)p[3] << (3 * DVX_BYTE_BITS));
+}
+
+
+static inline void dvxPutBe32(uint8_t *p, uint32_t v) {
+ p[0] = (uint8_t)(v >> (3 * DVX_BYTE_BITS));
+ p[1] = (uint8_t)(v >> (2 * DVX_BYTE_BITS));
+ p[2] = (uint8_t)(v >> DVX_BYTE_BITS);
+ p[3] = (uint8_t)(v & DVX_BYTE_MASK);
+}
+
+
+static inline void dvxPutLe16(uint8_t *p, uint32_t v) {
+ p[0] = (uint8_t)(v & DVX_BYTE_MASK);
+ p[1] = (uint8_t)((v >> DVX_BYTE_BITS) & DVX_BYTE_MASK);
+}
+
+
+static inline void dvxPutLe32(uint8_t *p, uint32_t v) {
+ dvxPutLe16(p, v);
+ dvxPutLe16(p + 2, v >> (2 * DVX_BYTE_BITS));
+}
+
+#endif // DVX_ENDIAN_H
diff --git a/src/libs/kpunch/libdvx/dvxFontFmt.c b/src/libs/kpunch/libdvx/dvxFontFmt.c
index a2de4525..ed891b2c 100644
--- a/src/libs/kpunch/libdvx/dvxFontFmt.c
+++ b/src/libs/kpunch/libdvx/dvxFontFmt.c
@@ -32,6 +32,9 @@
#include
#include
+// Below this, DVX text and Unicode agree
+#define DFN_ASCII_LIMIT 0x80
+
// A source being read: its bytes (a font in memory), else its file and
// where the font starts in it; the font's length
typedef struct {
@@ -71,6 +74,8 @@ bool dfnLoadStrike(const DfnSourceT *src, const DfnFileT *f, int32_t inde
char *dfnLoadText(const DfnSourceT *src, const DfnFileT *f, DfnTextE which, char *err, int32_t errSize);
bool dfnOpen(const DfnSourceT *src, DfnFileT *out, char *err, int32_t errSize);
void dfnSourceFile(DfnSourceT *src, const char *path);
+int32_t dfnTextFromUnicode(uint32_t code);
+uint32_t dfnTextToUnicode(uint8_t ch);
static bool fail(char *err, int32_t errSize, const DfnSourceT *src, const char *why);
static bool inFont(uint32_t length, uint32_t offset, uint32_t bytes);
static bool readAt(const DfnReaderT *r, uint32_t offset, void *buf, uint32_t len);
@@ -230,6 +235,16 @@ void dfnSourceFile(DfnSourceT *src, const char *path) {
}
+int32_t dfnTextFromUnicode(uint32_t code) {
+ return (code < DFN_ASCII_LIMIT) ? (int32_t)code : dfnFromUnicode(code);
+}
+
+
+uint32_t dfnTextToUnicode(uint8_t ch) {
+ return (ch < DFN_ASCII_LIMIT) ? ch : dfnCp437[ch];
+}
+
+
// The reason, with the font named. Always false.
static bool fail(char *err, int32_t errSize, const DfnSourceT *src, const char *why) {
if (err && errSize > 0) {
diff --git a/src/libs/kpunch/libdvx/dvxFontFmt.h b/src/libs/kpunch/libdvx/dvxFontFmt.h
index 48b2c585..6c25f6b6 100644
--- a/src/libs/kpunch/libdvx/dvxFontFmt.h
+++ b/src/libs/kpunch/libdvx/dvxFontFmt.h
@@ -154,6 +154,13 @@ typedef struct {
extern const uint16_t dfnCp437[DFN_CHAR_COUNT];
int32_t dfnFromUnicode(uint32_t code);
+// DVX text as Unicode: ASCII (line ends, tabs, and the other controls
+// included) is itself, and the rest is its code page 437 character's
+// code point. dfnTextFromUnicode is -1 for a code point DVX has no
+// character for.
+int32_t dfnTextFromUnicode(uint32_t code);
+uint32_t dfnTextToUnicode(uint8_t ch);
+
// Free a strike dfnLoadStrike filled (safe on a zeroed one).
void dfnFreeStrike(DfnStrikeDataT *s);
diff --git a/src/libs/kpunch/libdvx/dvxHash.h b/src/libs/kpunch/libdvx/dvxHash.h
new file mode 100644
index 00000000..33e6b4db
--- /dev/null
+++ b/src/libs/kpunch/libdvx/dvxHash.h
@@ -0,0 +1,103 @@
+// 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.
+
+// dvxHash.h -- Small hashes, checksums, and hex digits shared across DVX
+//
+// FNV-1a (32 bits) for hash tables and change detection, CRC-16-CCITT
+// for the serial packet layer and file transfers, and hex digits both
+// ways. Header only, so the core, the libraries, the tools, and the
+// Linux proxy use the same code without linking anything.
+
+#ifndef DVX_HASH_H
+#define DVX_HASH_H
+
+#include
+
+#define DVX_FNV_BASIS 2166136261u // FNV-1a start value
+#define DVX_FNV_PRIME 16777619u
+
+#define DVX_CRC16_WIDTH 16 // register bits
+#define DVX_CRC16_BYTE_BITS 8 // the register takes a byte at a time
+#define DVX_CRC16_TAP_HI 12 // the x^12 and x^5 terms of 0x1021
+#define DVX_CRC16_TAP_LO 5
+#define DVX_CRC16_LOW_BYTE 0xFFu
+
+// DVX_HEX_DIGITS[n] is nibble n's digit; dvxHexValue reads one back
+#define DVX_HEX_DIGITS "0123456789abcdef"
+#define DVX_HEX_RADIX 16
+#define DVX_HEX_LETTER_VAL 10 // the value of 'a' and 'A'
+#define DVX_NIBBLE_BITS 4
+#define DVX_NIBBLE_MASK 0x0F
+
+
+// One CRC-16-CCITT step per byte (polynomial 0x1021, MSB first) without
+// a table: the shifts are the polynomial's terms. Carry crc on from the
+// previous call; seed 0 for XMODEM/ZMODEM, 0xFFFF for CCITT-FALSE.
+static inline uint16_t dvxCrc16(uint16_t crc, const uint8_t *data, int32_t len) {
+ for (int32_t i = 0; i < len; i++) {
+ crc = (uint16_t)((crc >> DVX_CRC16_BYTE_BITS) | (crc << DVX_CRC16_BYTE_BITS));
+ crc ^= data[i];
+ crc ^= (uint16_t)((crc & DVX_CRC16_LOW_BYTE) >> (DVX_CRC16_WIDTH - DVX_CRC16_TAP_HI));
+ crc ^= (uint16_t)(crc << DVX_CRC16_TAP_HI);
+ crc ^= (uint16_t)((crc & DVX_CRC16_LOW_BYTE) << DVX_CRC16_TAP_LO);
+ }
+
+ return crc;
+}
+
+
+// One FNV-1a step: fold a byte into hash. Start from DVX_FNV_BASIS.
+static inline uint32_t dvxFnvByte(uint32_t hash, uint8_t byte) {
+ return (hash ^ byte) * DVX_FNV_PRIME;
+}
+
+
+// FNV-1a over len bytes, carried on from hash.
+static inline uint32_t dvxFnvBytes(uint32_t hash, const void *data, int32_t len) {
+ const uint8_t *p = (const uint8_t *)data;
+
+ for (int32_t i = 0; i < len; i++) {
+ hash = dvxFnvByte(hash, p[i]);
+ }
+
+ return hash;
+}
+
+
+// A hex digit's value, either case; -1 for any other character.
+static inline int32_t dvxHexValue(int32_t c) {
+ if (c >= '0' && c <= '9') {
+ return c - '0';
+ }
+
+ if (c >= 'a' && c <= 'f') {
+ return c - 'a' + DVX_HEX_LETTER_VAL;
+ }
+
+ if (c >= 'A' && c <= 'F') {
+ return c - 'A' + DVX_HEX_LETTER_VAL;
+ }
+
+ return -1;
+}
+
+#endif // DVX_HASH_H
diff --git a/src/libs/kpunch/libdvx/dvxImage.c b/src/libs/kpunch/libdvx/dvxImage.c
index 70d93a4a..dbf71edd 100644
--- a/src/libs/kpunch/libdvx/dvxImage.c
+++ b/src/libs/kpunch/libdvx/dvxImage.c
@@ -19,16 +19,15 @@
// 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.
-
-// dvxImage.c -- stb_image implementation for DVX GUI
+// dvxImage.c -- libdvx's image codec: stb_image and stb_image_write
//
-// This file exists solely to instantiate the stb_image implementation.
-// stb_image is a single-header library: you #define STB_IMAGE_IMPLEMENTATION
-// in exactly one .c file to generate the actual function bodies. Putting
-// this in its own translation unit keeps the stb code isolated so it:
-// 1. Compiles once, not in every file that #includes stb_image.h
-// 2. Can have its own warning suppressions without affecting project code
-// 3. Doesn't slow down incremental rebuilds (only recompiles if stb changes)
+// Both single-header libraries are compiled here, static (STB_IMAGE_STATIC,
+// STB_IMAGE_WRITE_STATIC), behind the few calls of dvxImgInt.h: a module
+// exports every global, so exported stb functions would all be kept,
+// while static ones DVX never calls are dropped (about 21 KB: the other
+// loaders and writers, the float paths). Kept in a translation unit of
+// its own so the stb code compiles once and its warning suppressions
+// touch nothing else.
//
// stb_image was chosen over libpng/libjpeg because:
// - Single header, no external dependencies -- critical for DJGPP cross-compile
@@ -37,8 +36,18 @@
// - Small code footprint suitable for DOS targets
//
// STBI_ONLY_* defines strip out support for unused formats (TGA, PSD, HDR,
-// PIC, PNM) to reduce binary size. STBI_NO_SIMD is required because DJGPP
-// targets 486/Pentium which lack SSE; on the Linux/SDL build it's harmless.
+// PIC, PNM) and STBI_NO_LINEAR the float conversions. STBI_NO_SIMD is
+// required because DJGPP targets 486/Pentium which lack SSE; on the host
+// build it's harmless.
+
+#include "dvxImgInt.h"
+
+// PNG saving: stb's defaults (level 8, all five row filters tried on
+// every row) take 3-5 times as long for files a few percent smaller.
+// Level 5 is the least stb takes; Paeth is the one filter used.
+#define PNG_SAVE_LEVEL 5
+#define PNG_SAVE_FILTER_PAETH 4
+#define RGB_CHANNELS 3
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wunused-function"
@@ -48,7 +57,49 @@
#define STBI_ONLY_JPEG
#define STBI_ONLY_GIF
#define STBI_NO_SIMD
+#define STBI_NO_LINEAR
+#define STB_IMAGE_STATIC
#define STB_IMAGE_IMPLEMENTATION
#include "thirdparty/stb_image_wrap.h"
+#define STB_IMAGE_WRITE_STATIC
+#define STB_IMAGE_WRITE_IMPLEMENTATION
+#include "thirdparty/stb_image_write_wrap.h"
+
#pragma GCC diagnostic pop
+
+
+uint8_t *dvxImgDecode(const DvxImgIoT *io, void *user, int *w, int *h, int channels) {
+ stbi_io_callbacks cb;
+ int had;
+
+ cb.read = io->read;
+ cb.skip = io->skip;
+ cb.eof = io->eof;
+ return stbi_load_from_callbacks(&cb, user, w, h, &had, channels);
+}
+
+
+bool dvxImgInfoFile(const char *path, int *w, int *h) {
+ int comp;
+
+ return stbi_info(path, w, h, &comp) != 0;
+}
+
+
+bool dvxImgInfoMemory(const uint8_t *data, int32_t len, int *w, int *h) {
+ int comp;
+
+ return stbi_info_from_memory(data, len, w, h, &comp) != 0;
+}
+
+
+bool dvxImgWrite(const char *path, bool bmp, int32_t w, int32_t h, const uint8_t *rgb) {
+ if (bmp) {
+ return stbi_write_bmp(path, w, h, RGB_CHANNELS, rgb) != 0;
+ }
+
+ stbi_write_png_compression_level = PNG_SAVE_LEVEL;
+ stbi_write_force_png_filter = PNG_SAVE_FILTER_PAETH;
+ return stbi_write_png(path, w, h, RGB_CHANNELS, rgb, w * RGB_CHANNELS) != 0;
+}
diff --git a/src/libs/kpunch/libdvx/dvxImgInt.h b/src/libs/kpunch/libdvx/dvxImgInt.h
new file mode 100644
index 00000000..323c15d5
--- /dev/null
+++ b/src/libs/kpunch/libdvx/dvxImgInt.h
@@ -0,0 +1,60 @@
+// 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.
+
+// dvxImgInt.h -- libdvx's image codec, private to libdvx
+//
+// stb_image and stb_image_write are compiled static in dvxImage.c, so
+// only what DVX calls is kept (about 21 KB less than exporting all of
+// both). These are its calls; dvxApp.c's image functions and the host
+// platform's screen capture go through them.
+
+#ifndef DVX_IMG_INT_H
+#define DVX_IMG_INT_H
+
+#include
+#include
+
+// The reads a decode makes (stb_image's callbacks): size bytes into out
+// (how many there were), n bytes on or back, and whether the end is
+// reached
+typedef struct {
+ int (*read)(void *user, char *out, int size);
+ void (*skip)(void *user, int n);
+ int (*eof)(void *user);
+} DvxImgIoT;
+
+// Decode an image read through io, channels per pixel (malloc'd, the
+// caller frees); NULL when it is not a BMP, PNG, JPEG, or GIF image. *w
+// and *h are its size.
+uint8_t *dvxImgDecode(const DvxImgIoT *io, void *user, int *w, int *h, int channels);
+
+// An image's size from its header, the file at path or len bytes at
+// data; false when it is no image. *h may come back negative (a
+// top-down BMP).
+bool dvxImgInfoFile(const char *path, int *w, int *h);
+bool dvxImgInfoMemory(const uint8_t *data, int32_t len, int *w, int *h);
+
+// Write w x h 24-bit RGB pixels to path as a BMP (bmp) or a PNG
+// (compression and filter DVX's own); false when it was not written.
+bool dvxImgWrite(const char *path, bool bmp, int32_t w, int32_t h, const uint8_t *rgb);
+
+#endif // DVX_IMG_INT_H
diff --git a/src/libs/kpunch/libdvx/dvxMem.h b/src/libs/kpunch/libdvx/dvxMem.h
index b4aa436b..8314f74f 100644
--- a/src/libs/kpunch/libdvx/dvxMem.h
+++ b/src/libs/kpunch/libdvx/dvxMem.h
@@ -36,6 +36,9 @@
#include
#include
+// The program id of DVX itself (the shell and the libraries' own memory)
+#define DVX_MEM_SYSTEM 0
+
extern int32_t *dvxMemAppIdPtr;
void *dvxCalloc(size_t nmemb, size_t size);
@@ -43,6 +46,14 @@ void dvxFree(void *ptr);
void *dvxMalloc(size_t size);
uint32_t dvxMemGetAppUsage(int32_t appId);
void dvxMemResetApp(int32_t appId);
+
+// Make appId the current program (the one what is allocated from now is
+// charged to, as the shell sees it too) and return the one before, to be
+// set back: a library keeps its own memory as DVX_MEM_SYSTEM's, or calls
+// a program's callback as that program. Without a shell (host tools)
+// nothing changes and 0 comes back.
+int32_t dvxMemSetApp(int32_t appId);
+
void *dvxRealloc(void *ptr, size_t size);
char *dvxStrdup(const char *s);
diff --git a/src/libs/kpunch/libdvx/dvxPrefs.c b/src/libs/kpunch/libdvx/dvxPrefs.c
index 60609d3a..f4062088 100644
--- a/src/libs/kpunch/libdvx/dvxPrefs.c
+++ b/src/libs/kpunch/libdvx/dvxPrefs.c
@@ -30,8 +30,10 @@
#include "dvxPrefs.h"
#include "dvxTypes.h"
+#include "dvxHash.h"
#include "platform/dvxPlat.h"
+#include
#include
#include
#include
@@ -70,9 +72,21 @@ typedef struct {
struct PrefsHandleT {
PrefsEntryT *entries; // stb_ds dynamic array
char *filePath; // path used by prefsSave
+ int32_t *index; // the lookup index (indexFind): entry + 1 per slot, 0 empty; NULL: not built
+ int32_t indexCap; // its slots (a power of two)
+ int32_t indexUsed;
bool loadFailed; // file exists but could not be read: prefsSave refuses
};
+// The lookup index (indexFind) holds the first entry of each key (by its
+// section and name) and of each section header (by its name), hashed with
+// the names folded to lower case. Built on the first lookup, kept up as
+// entries are appended, dropped when one is inserted before the end or
+// removed (built again on the next lookup).
+#define PREFS_INDEX_MIN 64 // slots at least
+#define PREFS_INDEX_ROOM 4 // slots per entry when built
+#define PREFS_INDEX_LOAD 2 // built again once more than 1 / 2 full
+
// A settings file saved while DVX's folder cannot be written to
// (platformIsReadOnly): kept here until DVX exits, and read back by
// prefsLoad in place of the file on disk.
@@ -117,6 +131,12 @@ static PrefsMemFileT *sMemFiles = NULL;
// prefsSetSecret calls so far: no two in one run share a salt
static uint32_t sSecretCalls = 0;
+// The shared DVX.INI (prefsDvxIniShared) and the text it was read from
+// (NULL: there was none)
+static PrefsHandleT *sDvxIni = NULL;
+static char *sDvxIniText = NULL;
+static int32_t sDvxIniLen = 0;
+
// ============================================================
// Shared preference vocabularies
@@ -147,10 +167,17 @@ const PrefsChoiceT prefsWheelDirChoices[PREFS_WHEEL_DIR_CHOICE_COUNT] = {
static void addEntry(PrefsHandleT *h, PrefsLineE kind, const char *section, const char *key, const char *value, const char *raw);
static void appendText(char **buf, const char *a, const char *b, const char *c);
+static uint32_t entryHash(PrefsLineE kind, const char *section, const char *key);
+static bool entryIs(const PrefsEntryT *e, PrefsLineE kind, const char *section, const char *key);
static int32_t findEntry(PrefsHandleT *h, const char *section, const char *key);
static int32_t findSection(PrefsHandleT *h, const char *section);
static char *formatText(const PrefsHandleT *h);
static void freeEntry(PrefsEntryT *e);
+static void indexAppended(PrefsHandleT *h);
+static void indexDrop(PrefsHandleT *h);
+static int32_t indexFind(PrefsHandleT *h, PrefsLineE kind, const char *section, const char *key);
+static void indexPut(PrefsHandleT *h, int32_t entry);
+static bool indexReady(PrefsHandleT *h);
static bool matchWord(const char *val, const char *const *words, int32_t count);
static char *memFileCopy(const char *path, int32_t *len);
static bool memFileStore(const char *path, const char *text, int32_t len);
@@ -179,6 +206,7 @@ static void addEntry(PrefsHandleT *h, PrefsLineE kind, const char *section, cons
}
arrput(h->entries, e);
+ indexAppended(h);
}
@@ -194,29 +222,39 @@ static void appendText(char **buf, const char *a, const char *b, const char *c)
}
-static int32_t findEntry(PrefsHandleT *h, const char *section, const char *key) {
- for (int32_t i = 0; i < arrlen(h->entries); i++) {
- PrefsEntryT *e = &h->entries[i];
+// The hash the lookup index files an entry under: a key's section and
+// name, a section header's name, folded to lower case.
+static uint32_t entryHash(PrefsLineE kind, const char *section, const char *key) {
+ uint32_t hash = dvxFnvByte(DVX_FNV_BASIS, (uint8_t)kind);
- if (e->kind == PrefsLineKeyE && strcasecmp(e->section, section) == 0 && strcasecmp(e->key, key) == 0) {
- return i;
- }
+ for (const char *p = section; *p; p++) {
+ hash = dvxFnvByte(hash, (uint8_t)tolower((uint8_t)*p));
}
- return -1;
+ for (const char *p = (kind == PrefsLineKeyE) ? key : ""; *p; p++) {
+ hash = dvxFnvByte(hash, (uint8_t)tolower((uint8_t)*p));
+ }
+
+ return hash;
}
+// e is the key (kind PrefsLineKeyE) section / key, or the header of
+// section (PrefsLineSectionE), names compared in any case.
+static bool entryIs(const PrefsEntryT *e, PrefsLineE kind, const char *section, const char *key) {
+ return e->kind == kind && strcasecmp(e->section, section) == 0 && (kind != PrefsLineKeyE || strcasecmp(e->key, key) == 0);
+}
+
+
+// The first entry of key in section, or -1.
+static int32_t findEntry(PrefsHandleT *h, const char *section, const char *key) {
+ return indexFind(h, PrefsLineKeyE, section, key);
+}
+
+
+// The first header of section, or -1.
static int32_t findSection(PrefsHandleT *h, const char *section) {
- for (int32_t i = 0; i < arrlen(h->entries); i++) {
- PrefsEntryT *e = &h->entries[i];
-
- if (e->kind == PrefsLineSectionE && strcasecmp(e->section, section) == 0) {
- return i;
- }
- }
-
- return -1;
+ return indexFind(h, PrefsLineSectionE, section, NULL);
}
@@ -254,6 +292,112 @@ static void freeEntry(PrefsEntryT *e) {
}
+// The last entry was appended: into a built index while it has room
+// (else the index goes, built again bigger on the next lookup).
+static void indexAppended(PrefsHandleT *h) {
+ if (!h->index) {
+ return;
+ }
+
+ if ((h->indexUsed + 1) * PREFS_INDEX_LOAD > h->indexCap) {
+ indexDrop(h);
+ return;
+ }
+
+ indexPut(h, (int32_t)arrlen(h->entries) - 1);
+}
+
+
+// The entries moved (one inserted before the end, or removed): the index
+// is built again on the next lookup.
+static void indexDrop(PrefsHandleT *h) {
+ free(h->index);
+ h->index = NULL;
+ h->indexCap = 0;
+ h->indexUsed = 0;
+}
+
+
+// The first entry of a key (kind PrefsLineKeyE) or a section header
+// (PrefsLineSectionE), or -1. Through the lookup index; through every
+// entry when there is no memory for one.
+static int32_t indexFind(PrefsHandleT *h, PrefsLineE kind, const char *section, const char *key) {
+ if (!indexReady(h)) {
+ for (int32_t i = 0; i < arrlen(h->entries); i++) {
+ if (entryIs(&h->entries[i], kind, section, key)) {
+ return i;
+ }
+ }
+
+ return -1;
+ }
+
+ uint32_t mask = (uint32_t)h->indexCap - 1;
+
+ for (uint32_t slot = entryHash(kind, section, key) & mask; h->index[slot] != 0; slot = (slot + 1) & mask) {
+ if (entryIs(&h->entries[h->index[slot] - 1], kind, section, key)) {
+ return h->index[slot] - 1;
+ }
+ }
+
+ return -1;
+}
+
+
+// File entry in the index (which has room) unless it is not a key or a
+// header, or an earlier entry has its names (the one lookups return).
+static void indexPut(PrefsHandleT *h, int32_t entry) {
+ const PrefsEntryT *e = &h->entries[entry];
+
+ if (e->kind == PrefsLineOtherE) {
+ return;
+ }
+
+ uint32_t mask = (uint32_t)h->indexCap - 1;
+ uint32_t slot = entryHash(e->kind, e->section, e->key) & mask;
+
+ for (; h->index[slot] != 0; slot = (slot + 1) & mask) {
+ if (entryIs(&h->entries[h->index[slot] - 1], e->kind, e->section, e->key)) {
+ return;
+ }
+ }
+
+ h->index[slot] = entry + 1;
+ h->indexUsed++;
+}
+
+
+// The lookup index, built when missing; false when there is no memory
+// for it.
+static bool indexReady(PrefsHandleT *h) {
+ if (h->index) {
+ return true;
+ }
+
+ int32_t count = (int32_t)arrlen(h->entries);
+ int32_t cap = PREFS_INDEX_MIN;
+
+ while (cap < count * PREFS_INDEX_ROOM) {
+ cap *= 2;
+ }
+
+ h->index = (int32_t *)calloc((size_t)cap, sizeof(int32_t));
+
+ if (!h->index) {
+ return false;
+ }
+
+ h->indexCap = cap;
+ h->indexUsed = 0;
+
+ for (int32_t i = 0; i < count; i++) {
+ indexPut(h, i);
+ }
+
+ return true;
+}
+
+
// True when val equals one of words, ignoring case.
static bool matchWord(const char *val, const char *const *words, int32_t count) {
for (int32_t i = 0; i < count; i++) {
@@ -401,6 +545,7 @@ void prefsClose(PrefsHandleT *h) {
}
arrfree(h->entries);
+ indexDrop(h);
free(h->filePath);
free(h);
}
@@ -412,6 +557,33 @@ PrefsHandleT *prefsCreate(void) {
}
+// The file's text (as prefsLoad reads it: one held in memory first, see
+// prefsSaveAs) is compared with the text the handle was read from: a
+// file's time cannot tell (FAT keeps it to 2 s), and reading it costs a
+// small part of parsing it.
+PrefsHandleT *prefsDvxIniShared(void) {
+ const char *path = platformDvxIniPath();
+ int32_t len = 0;
+ char *text = memFileCopy(path, &len);
+
+ if (!text) {
+ text = platformReadFile(path, &len);
+ }
+
+ if (sDvxIni && (text ? sDvxIniText && len == sDvxIniLen && memcmp(text, sDvxIniText, (size_t)len) == 0 : !sDvxIniText)) {
+ free(text);
+ return sDvxIni;
+ }
+
+ prefsClose(sDvxIni);
+ free(sDvxIniText);
+ sDvxIni = prefsLoad(path);
+ sDvxIniText = text;
+ sDvxIniLen = len;
+ return sDvxIni;
+}
+
+
bool prefsGetBool(PrefsHandleT *h, const char *section, const char *key, bool defaultVal) {
const char *val = prefsGetString(h, section, key, NULL);
@@ -612,6 +784,7 @@ void prefsRemove(PrefsHandleT *h, const char *section, const char *key) {
if (idx >= 0) {
freeEntry(&h->entries[idx]);
arrdel(h->entries, idx);
+ indexDrop(h);
}
}
@@ -771,7 +944,15 @@ void prefsSetString(PrefsHandleT *h, const char *section, const char *key, const
return;
}
+ bool atEnd = insertAt == arrlen(h->entries);
+
arrins(h->entries, insertAt, newEntry);
+
+ if (atEnd) {
+ indexAppended(h);
+ } else {
+ indexDrop(h);
+ }
}
diff --git a/src/libs/kpunch/libdvx/dvxPrefs.h b/src/libs/kpunch/libdvx/dvxPrefs.h
index 5ece74fc..e8959cc0 100644
--- a/src/libs/kpunch/libdvx/dvxPrefs.h
+++ b/src/libs/kpunch/libdvx/dvxPrefs.h
@@ -81,6 +81,13 @@ bool prefsSaveAs(PrefsHandleT *h, const char *filename);
// Release all memory held by the handle.
void prefsClose(PrefsHandleT *h);
+// DVX.INI (platformDvxIniPath) for reading, shared: parsed again only
+// when the file's text changed, so code that reads settings often (every
+// sound, every song) does not parse the file each time. Its users do not
+// close it, change it, or keep it past their own call (the next call may
+// read the file again).
+PrefsHandleT *prefsDvxIniShared(void);
+
// Retrieve a string value. Returns defaultVal if the key is not present.
// The returned pointer is valid until the key is modified or prefsClose.
const char *prefsGetString(PrefsHandleT *h, const char *section, const char *key, const char *defaultVal);
diff --git a/src/libs/kpunch/libdvx/dvxRes.h b/src/libs/kpunch/libdvx/dvxRes.h
index 8324e003..9ebad32d 100644
--- a/src/libs/kpunch/libdvx/dvxRes.h
+++ b/src/libs/kpunch/libdvx/dvxRes.h
@@ -99,6 +99,11 @@ DvxResHandleT *dvxResOpen(const char *path);
// Caller must free the returned buffer.
void *dvxResRead(DvxResHandleT *h, const char *name, uint32_t *outSize);
+// Read a text resource into buf (up to bufSize - 1 bytes, NUL added).
+// False when there is no such resource (buf is left alone) or it cannot
+// be read (buf is then "").
+bool dvxResReadText(DvxResHandleT *h, const char *name, char *buf, int32_t bufSize);
+
// Find a resource by name and return its directory entry.
// Returns NULL if not found. The returned pointer is valid until
// dvxResClose is called.
diff --git a/src/libs/kpunch/libdvx/dvxImageWrite.c b/src/libs/kpunch/libdvx/dvxResInt.h
similarity index 58%
rename from src/libs/kpunch/libdvx/dvxImageWrite.c
rename to src/libs/kpunch/libdvx/dvxResInt.h
index 276e8ef3..14a876ab 100644
--- a/src/libs/kpunch/libdvx/dvxImageWrite.c
+++ b/src/libs/kpunch/libdvx/dvxResInt.h
@@ -20,23 +20,24 @@
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
// IN THE SOFTWARE.
-// dvxImageWrite.c -- stb_image_write implementation for DVX GUI
+// dvxResInt.h -- libdvx's resource reads on a file already open, private
+// to libdvx
//
-// Companion to dvxImage.c: instantiates stb_image_write for PNG output
-// (used by dvxScreenshot and dvxWindowScreenshot). Same rationale as
-// dvxImage.c for using stb -- zero external dependencies, single header,
-// public domain. Kept in a separate translation unit from the read side
-// so projects that don't need screenshot support can omit this file and
-// save the code size.
-//
-// STBI_WRITE_NO_SIMD disables SSE codepaths for the same reason as
-// STBI_NO_SIMD in dvxImage.c: the DOS target lacks SSE support.
+// On DOS opening a file is most of what reading a program's icon costs
+// (each open walks the path's folders on disk), so dvxLoadImage tries the
+// file as a picture and, when it is none, reads the icon from the same
+// open stream.
-#pragma GCC diagnostic push
-#pragma GCC diagnostic ignored "-Wunused-function"
+#ifndef DVX_RES_INT_H
+#define DVX_RES_INT_H
-#define STBI_WRITE_NO_SIMD
-#define STB_IMAGE_WRITE_IMPLEMENTATION
-#include "thirdparty/stb_image_write_wrap.h"
+#include
+#include
-#pragma GCC diagnostic pop
+// The icon of the DVX program, widget, or library file open as f (NULL is
+// allowed), as the bytes of its image file: its DVX_RES_NAME_ICON
+// resource, else DVX_RES_NAME_WIDGET_ICON. NULL when it has neither.
+// Free with free(); *outSize gets the size. Moves f's position.
+void *dvxResIconRead(FILE *f, uint32_t *outSize);
+
+#endif // DVX_RES_INT_H
diff --git a/src/libs/kpunch/libdvx/dvxResource.c b/src/libs/kpunch/libdvx/dvxResource.c
index 08b1629e..715ede62 100644
--- a/src/libs/kpunch/libdvx/dvxResource.c
+++ b/src/libs/kpunch/libdvx/dvxResource.c
@@ -26,6 +26,7 @@
// block is located by reading the footer at the end of the file.
#include "dvxRes.h"
+#include "dvxResInt.h"
#include "../../../tools/dvxResWrite.h"
#include
@@ -37,7 +38,78 @@
// Prototypes
// ============================================================
-static int dvxResEntryCmp(const void *a, const void *b);
+static DvxResDirEntryT *dirRead(FILE *f, uint32_t *outCount);
+int32_t dvxResAppend(const char *path, const char *name, uint32_t type, const void *data, uint32_t dataSize);
+void dvxResClose(DvxResHandleT *h);
+static int dvxResEntryCmp(const void *a, const void *b);
+const DvxResDirEntryT *dvxResFind(DvxResHandleT *h, const char *name);
+void *dvxResIconRead(FILE *f, uint32_t *outSize);
+DvxResHandleT *dvxResOpen(const char *path);
+void *dvxResRead(DvxResHandleT *h, const char *name, uint32_t *outSize);
+bool dvxResReadText(DvxResHandleT *h, const char *name, char *buf, int32_t bufSize);
+int32_t dvxResRemove(const char *path, const char *name);
+static void *entryRead(FILE *f, const DvxResDirEntryT *entry, uint32_t *outSize);
+
+// The pictures dvxResIconRead looks for, in order
+static const char *const sIconNames[] = { DVX_RES_NAME_ICON, DVX_RES_NAME_WIDGET_ICON };
+
+
+// The directory of the resource block of f (malloc'd, names terminated;
+// *outCount entries), or NULL when f has none.
+static DvxResDirEntryT *dirRead(FILE *f, uint32_t *outCount) {
+ DvxResFooterT footer;
+
+ // Measure the file so the directory size read from the (possibly
+ // corrupt/hostile) footer can be bounds-checked before allocating.
+ if (fseek(f, 0, SEEK_END) != 0) {
+ return NULL;
+ }
+
+ int64_t fileSize = ftell(f);
+
+ // Read footer from end of file
+ if (fseek(f, -(int32_t)sizeof(DvxResFooterT), SEEK_END) != 0 || fread(&footer, sizeof(footer), 1, f) != 1) {
+ return NULL;
+ }
+
+ if (footer.magic != DVX_RES_MAGIC || footer.entryCount == 0) {
+ return NULL;
+ }
+
+ // Reject an entryCount that can't fit in the file: this both stops a
+ // size_t overflow in the malloc below (entryCount * 48 wraps on a
+ // 32-bit target) and prevents the fread from overrunning a tiny
+ // allocation when the footer is corrupt.
+ if (fileSize < 0 || (int64_t)footer.dirOffset > fileSize || (int64_t)footer.entryCount > fileSize / (int64_t)sizeof(DvxResDirEntryT)) {
+ return NULL;
+ }
+
+ // Read directory
+ if (fseek(f, footer.dirOffset, SEEK_SET) != 0) {
+ return NULL;
+ }
+
+ DvxResDirEntryT *entries = (DvxResDirEntryT *)malloc(footer.entryCount * sizeof(DvxResDirEntryT));
+
+ if (!entries) {
+ return NULL;
+ }
+
+ if (fread(entries, sizeof(DvxResDirEntryT), footer.entryCount, f) != footer.entryCount) {
+ free(entries);
+ return NULL;
+ }
+
+ // Force-terminate names read from the file before any strcmp (qsort /
+ // bsearch); a corrupt unterminated name would otherwise read past the
+ // field, and past the allocation for the last entry.
+ for (uint32_t i = 0; i < footer.entryCount; i++) {
+ entries[i].name[DVX_RES_NAME_MAX - 1] = '\0';
+ }
+
+ *outCount = footer.entryCount;
+ return entries;
+}
int32_t dvxResAppend(const char *path, const char *name, uint32_t type, const void *data, uint32_t dataSize) {
@@ -70,88 +142,47 @@ const DvxResDirEntryT *dvxResFind(DvxResHandleT *h, const char *name) {
DvxResDirEntryT key;
memset(&key, 0, sizeof(key));
memcpy(key.name, name, strlen(name) + 1);
- return (const DvxResDirEntryT *)bsearch(&key, h->entries, h->entryCount,
- sizeof(DvxResDirEntryT), dvxResEntryCmp);
+ return (const DvxResDirEntryT *)bsearch(&key, h->entries, h->entryCount, sizeof(DvxResDirEntryT), dvxResEntryCmp);
+}
+
+
+// One directory read and one data read on the stream the caller has open
+// (dvxResOpen and dvxResRead would open the file twice more).
+void *dvxResIconRead(FILE *f, uint32_t *outSize) {
+ uint32_t count = 0;
+ DvxResDirEntryT *entries = f ? dirRead(f, &count) : NULL;
+ void *data = NULL;
+
+ for (int32_t n = 0; entries && !data && n < DVX_ARRAY_LEN(sIconNames); n++) {
+ for (uint32_t i = 0; i < count; i++) {
+ if (strcmp(entries[i].name, sIconNames[n]) == 0) {
+ data = entryRead(f, &entries[i], outSize);
+ break;
+ }
+ }
+ }
+
+ free(entries);
+ return data;
}
DvxResHandleT *dvxResOpen(const char *path) {
- if (!path) {
- return NULL;
- }
-
- FILE *f = fopen(path, "rb");
+ FILE *f = path ? fopen(path, "rb") : NULL;
if (!f) {
return NULL;
}
- // Measure the file so the directory size read from the (possibly
- // corrupt/hostile) footer can be bounds-checked before allocating.
- if (fseek(f, 0, SEEK_END) != 0) {
- fclose(f);
- return NULL;
- }
-
- int64_t fileSize = ftell(f);
-
- // Read footer from end of file
- if (fseek(f, -(int32_t)sizeof(DvxResFooterT), SEEK_END) != 0) {
- fclose(f);
- return NULL;
- }
-
- DvxResFooterT footer;
-
- if (fread(&footer, sizeof(footer), 1, f) != 1) {
- fclose(f);
- return NULL;
- }
-
- if (footer.magic != DVX_RES_MAGIC || footer.entryCount == 0) {
- fclose(f);
- return NULL;
- }
-
- // Reject an entryCount that can't fit in the file: this both stops a
- // size_t overflow in the malloc below (entryCount * 48 wraps on a
- // 32-bit target) and prevents the fread from overrunning a tiny
- // allocation when the footer is corrupt.
- if (fileSize < 0 ||
- (int64_t)footer.dirOffset > fileSize ||
- (int64_t)footer.entryCount > fileSize / (int64_t)sizeof(DvxResDirEntryT)) {
- fclose(f);
- return NULL;
- }
-
- // Read directory
- if (fseek(f, footer.dirOffset, SEEK_SET) != 0) {
- fclose(f);
- return NULL;
- }
-
- DvxResDirEntryT *entries = (DvxResDirEntryT *)malloc(footer.entryCount * sizeof(DvxResDirEntryT));
-
- if (!entries) {
- fclose(f);
- return NULL;
- }
-
- if (fread(entries, sizeof(DvxResDirEntryT), footer.entryCount, f) != footer.entryCount) {
- free(entries);
- fclose(f);
- return NULL;
- }
-
- // Force-terminate names read from the file before any strcmp (qsort /
- // bsearch); a corrupt unterminated name would otherwise read past the
- // field, and past the allocation for the last entry.
- for (uint32_t i = 0; i < footer.entryCount; i++) {
- entries[i].name[DVX_RES_NAME_MAX - 1] = '\0';
- }
+ uint32_t count = 0;
+ DvxResDirEntryT *entries = dirRead(f, &count);
fclose(f);
+ if (!entries) {
+ return NULL;
+ }
+
DvxResHandleT *h = (DvxResHandleT *)malloc(sizeof(DvxResHandleT));
if (!h) {
@@ -162,7 +193,7 @@ DvxResHandleT *dvxResOpen(const char *path) {
strncpy(h->path, path, sizeof(h->path) - 1);
h->path[sizeof(h->path) - 1] = '\0';
h->entries = entries;
- h->entryCount = footer.entryCount;
+ h->entryCount = count;
// Sort once so dvxResFind can bsearch.
qsort(h->entries, h->entryCount, sizeof(DvxResDirEntryT), dvxResEntryCmp);
@@ -173,42 +204,35 @@ DvxResHandleT *dvxResOpen(const char *path) {
void *dvxResRead(DvxResHandleT *h, const char *name, uint32_t *outSize) {
const DvxResDirEntryT *entry = dvxResFind(h, name);
-
- if (!entry) {
- return NULL;
- }
-
- FILE *f = fopen(h->path, "rb");
+ FILE *f = entry ? fopen(h->path, "rb") : NULL;
if (!f) {
return NULL;
}
- if (fseek(f, entry->offset, SEEK_SET) != 0) {
- fclose(f);
- return NULL;
- }
-
- void *buf = malloc(entry->size);
-
- if (!buf) {
- fclose(f);
- return NULL;
- }
-
- if (fread(buf, 1, entry->size, f) != entry->size) {
- free(buf);
- fclose(f);
- return NULL;
- }
+ void *buf = entryRead(f, entry, outSize);
fclose(f);
+ return buf;
+}
- if (outSize) {
- *outSize = entry->size;
+
+// Straight into buf: no buffer of the resource's own size.
+bool dvxResReadText(DvxResHandleT *h, const char *name, char *buf, int32_t bufSize) {
+ const DvxResDirEntryT *entry = (h && buf && bufSize > 0) ? dvxResFind(h, name) : NULL;
+ FILE *f = entry ? fopen(h->path, "rb") : NULL;
+
+ if (!f) {
+ return false;
}
- return buf;
+ size_t room = (size_t)bufSize - 1;
+ size_t want = entry->size < room ? entry->size : room;
+ bool ok = fseek(f, entry->offset, SEEK_SET) == 0 && fread(buf, 1, want, f) == want;
+
+ fclose(f);
+ buf[ok ? want : 0] = '\0';
+ return ok;
}
@@ -258,3 +282,29 @@ int32_t dvxResRemove(const char *path, const char *name) {
free(layout.entries);
return result;
}
+
+
+// The data of entry (malloc'd; *outSize, when given, gets its size), or
+// NULL when it cannot be read.
+static void *entryRead(FILE *f, const DvxResDirEntryT *entry, uint32_t *outSize) {
+ if (fseek(f, entry->offset, SEEK_SET) != 0) {
+ return NULL;
+ }
+
+ void *buf = malloc(entry->size);
+
+ if (!buf) {
+ return NULL;
+ }
+
+ if (fread(buf, 1, entry->size, f) != entry->size) {
+ free(buf);
+ return NULL;
+ }
+
+ if (outSize) {
+ *outSize = entry->size;
+ }
+
+ return buf;
+}
diff --git a/src/libs/kpunch/libdvx/dvxResourceBas.c b/src/libs/kpunch/libdvx/dvxResourceBas.c
new file mode 100644
index 00000000..8e917f15
--- /dev/null
+++ b/src/libs/kpunch/libdvx/dvxResourceBas.c
@@ -0,0 +1,237 @@
+// 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.
+
+// dvxResourceBas.c -- DVX BASIC entry points of the resource functions
+// (src/include/basic/resource.bas)
+//
+// Called through DECLARE LIBRARY, so the signatures use only BASIC-
+// marshalable types; True (-1) / False (0) for yes / no. A returned
+// string stays valid until the next call that returns one (the BASIC
+// runtime copies it at once). Handles from ResOpen (1, 2, ...; 0 means
+// none) belong to the program that opened them, as many as it likes;
+// whatever a program leaves open is closed when it ends.
+
+#include "dvxRes.h"
+#include "dvxPlat.h"
+#include "dvxBas.h"
+
+#include
+#include
+#include
+#include
+
+// ============================================================
+// Prototypes
+// ============================================================
+
+int32_t ResAddData(const char *path, const char *name, int32_t type, const uint8_t *data, int32_t len);
+int32_t ResAddFile(const char *path, const char *name, int32_t type, const char *srcFile);
+int32_t ResAddText(const char *path, const char *name, const char *text);
+void ResClose(int32_t handle);
+int32_t ResCount(int32_t handle);
+static const DvxResDirEntryT *resEntry(int32_t handle, int32_t index);
+int32_t ResExtract(const char *path, const char *name, const char *outFile);
+static void resFree(void *obj);
+const uint8_t *ResGetData(const char *path, const char *name, int32_t *outLen);
+const char *ResGetText(const char *path, const char *name);
+const char *ResName(int32_t handle, int32_t index);
+int32_t ResOpen(const char *path);
+static void *resReadNamed(const char *path, const char *name, uint32_t *outSize);
+int32_t ResRemove(const char *path, const char *name);
+int32_t ResSize(int32_t handle, int32_t index);
+int32_t ResType(int32_t handle, int32_t index);
+
+static BasHandlesT sFiles = BAS_HANDLES(resFree, 0); // the open resource files
+static BasTextT sText; // the last string result
+static uint8_t *sData = NULL; // the last ResGetData$ bytes
+
+
+// ResAddData: a resource of any bytes (BINARY STRING); True on success.
+int32_t ResAddData(const char *path, const char *name, int32_t type, const uint8_t *data, int32_t len) {
+ if (!path || !name || !data || len < 0) {
+ return BAS_FALSE;
+ }
+
+ return dvxResAppend(path, name, (uint32_t)type, data, (uint32_t)len) == 0 ? BAS_TRUE : BAS_FALSE;
+}
+
+
+int32_t ResAddFile(const char *path, const char *name, int32_t type, const char *srcFile) {
+ if (!srcFile) {
+ return BAS_FALSE;
+ }
+
+ int32_t fileSize = 0;
+ uint8_t *buf = (uint8_t *)platformReadFile(srcFile, &fileSize);
+ int32_t result = (buf && fileSize > 0) ? ResAddData(path, name, type, buf, fileSize) : BAS_FALSE;
+
+ free(buf);
+ return result;
+}
+
+
+int32_t ResAddText(const char *path, const char *name, const char *text) {
+ if (!text) {
+ text = "";
+ }
+
+ return ResAddData(path, name, DVX_RES_TEXT, (const uint8_t *)text, (int32_t)strlen(text) + 1);
+}
+
+
+void ResClose(int32_t handle) {
+ basHandleFree(&sFiles, handle);
+}
+
+
+int32_t ResCount(int32_t handle) {
+ DvxResHandleT *h = (DvxResHandleT *)basHandleGet(&sFiles, handle);
+
+ return h ? (int32_t)h->entryCount : 0;
+}
+
+
+// Entry index of the file open as handle, or NULL.
+static const DvxResDirEntryT *resEntry(int32_t handle, int32_t index) {
+ DvxResHandleT *h = (DvxResHandleT *)basHandleGet(&sFiles, handle);
+
+ return (h && index >= 0 && index < (int32_t)h->entryCount) ? &h->entries[index] : NULL;
+}
+
+
+int32_t ResExtract(const char *path, const char *name, const char *outFile) {
+ if (!outFile) {
+ return BAS_FALSE;
+ }
+
+ uint32_t size = 0;
+ void *data = resReadNamed(path, name, &size);
+
+ if (!data) {
+ return BAS_FALSE;
+ }
+
+ FILE *f = fopen(outFile, "wb");
+
+ if (!f) {
+ free(data);
+ return BAS_FALSE;
+ }
+
+ int32_t result = (fwrite(data, 1, size, f) == size) ? BAS_TRUE : BAS_FALSE;
+
+ fclose(f);
+ free(data);
+ return result;
+}
+
+
+static void resFree(void *obj) {
+ dvxResClose((DvxResHandleT *)obj);
+}
+
+
+// ResGetData$: every byte of a resource (BINARY STRING); "" when it is
+// not there.
+const uint8_t *ResGetData(const char *path, const char *name, int32_t *outLen) {
+ uint32_t size = 0;
+
+ free(sData);
+ sData = (uint8_t *)resReadNamed(path, name, &size);
+ *outLen = sData ? (int32_t)size : 0;
+ return sData;
+}
+
+
+// ResGetText$: a resource's text, up to its first NUL; "" when it is not
+// there.
+const char *ResGetText(const char *path, const char *name) {
+ uint32_t size = 0;
+ char *data = (char *)resReadNamed(path, name, &size);
+ uint32_t len = 0;
+
+ while (data && len < size && data[len] != '\0') {
+ len++;
+ }
+
+ basTextStart(&sText);
+ basTextAppendLen(&sText, data ? data : "", (int32_t)len);
+ free(data);
+ return basTextEnd(&sText);
+}
+
+
+const char *ResName(int32_t handle, int32_t index) {
+ const DvxResDirEntryT *e = resEntry(handle, index);
+
+ return e ? e->name : "";
+}
+
+
+int32_t ResOpen(const char *path) {
+ return path ? basHandleAdd(&sFiles, dvxResOpen(path)) : BAS_NO_HANDLE;
+}
+
+
+// A named resource of the file at path, read in one go (malloc'd, the
+// caller frees); NULL when the file or the resource is not there.
+static void *resReadNamed(const char *path, const char *name, uint32_t *outSize) {
+ *outSize = 0;
+
+ if (!path || !name) {
+ return NULL;
+ }
+
+ DvxResHandleT *h = dvxResOpen(path);
+
+ if (!h) {
+ return NULL;
+ }
+
+ void *data = dvxResRead(h, name, outSize);
+
+ dvxResClose(h);
+ return data;
+}
+
+
+int32_t ResRemove(const char *path, const char *name) {
+ if (!path || !name) {
+ return BAS_FALSE;
+ }
+
+ return dvxResRemove(path, name) == 0 ? BAS_TRUE : BAS_FALSE;
+}
+
+
+int32_t ResSize(int32_t handle, int32_t index) {
+ const DvxResDirEntryT *e = resEntry(handle, index);
+
+ return e ? (int32_t)e->size : 0;
+}
+
+
+int32_t ResType(int32_t handle, int32_t index) {
+ const DvxResDirEntryT *e = resEntry(handle, index);
+
+ return e ? (int32_t)e->type : 0;
+}
diff --git a/src/libs/kpunch/libdvx/dvxText.c b/src/libs/kpunch/libdvx/dvxText.c
index 78ee7b7d..08467b1f 100644
--- a/src/libs/kpunch/libdvx/dvxText.c
+++ b/src/libs/kpunch/libdvx/dvxText.c
@@ -176,6 +176,7 @@ typedef struct FontEntryT {
int32_t refs;
uint32_t lastUse; // sUseClock when last given up
struct FontEntryT *next;
+ bool stale; // made for the other smoothing (dvxFontSmoothBegin)
} FontEntryT;
// What a font is built from (entryBuild)
@@ -251,12 +252,14 @@ const char *dvxFontFamilyName(int32_t index);
const char *dvxFontFamilyOf(const BitmapFontT *font);
char *dvxFontLicence(const BitmapFontT *font);
BitmapFontT *dvxFontOpen(const char *family, int32_t pixelSize, uint32_t style);
+bool dvxFontRebuildStep(void);
void dvxFontRemoveOwner(int32_t owner);
void dvxFontRescan(void);
BitmapFontT *dvxFontRetain(BitmapFontT *font);
void dvxFontSetSmooth(bool smooth);
int32_t dvxFontSizes(const char *family, uint32_t style, int32_t *sizes, int32_t max);
bool dvxFontSmooth(void);
+void dvxFontSmoothBegin(bool smooth);
static FontEntryT *entryBuild(int32_t owner, const FontPickT *pick, uint32_t style);
static void entryFinish(FontEntryT *e, const BuildT *b);
static void entryFree(FontEntryT *e);
@@ -281,8 +284,6 @@ static void idleTrim(int32_t keep);
static bool isBox(int32_t ch);
static void levelRect(uint8_t *px, int32_t w, int32_t h, int32_t x0, int32_t y0, int32_t x1, int32_t y1);
static void levelsFrom(BuildT *b, const FontGlyphT *g, const uint8_t *bits, int32_t bpp);
-static int32_t memAsSystem(void);
-static void memRestore(int32_t prev);
static int32_t nearestSize(const FontFaceT *a, const FontFaceT *b, int32_t pixelSize);
static void ownerDetach(int32_t owner, const char *family);
static bool pickFaces(int32_t owner, const char *family, uint32_t style, const FontFaceT **mine, const FontFaceT **base, uint32_t *synth);
@@ -524,10 +525,44 @@ BitmapFontT *dvxFontOpen(const char *family, int32_t pixelSize, uint32_t style)
}
+// The fonts first, then the fonts with lines, which copy them.
+bool dvxFontRebuildStep(void) {
+ FontEntryT *font = NULL;
+ FontEntryT *lines = NULL;
+
+ for (FontEntryT *e = sEntries; e && !font; e = e->next) {
+ if (e->stale && !e->base) {
+ font = e;
+ } else if (e->stale && !lines) {
+ lines = e;
+ }
+ }
+
+ FontEntryT *next = font ? font : lines;
+
+ if (!next) {
+ return false;
+ }
+
+ int32_t prev = dvxMemSetApp(DVX_MEM_SYSTEM);
+
+ next->stale = false;
+
+ if (next->base) {
+ entryLinesCopy(next, next->font.style & FONT_STYLE_LINES);
+ } else {
+ entryRebuild(next);
+ }
+
+ dvxMemSetApp(prev);
+ return true;
+}
+
+
// Every face added for owner forgotten (for OWNER_ALL, the added ones:
// the installed faces stay), then the families left with none
void dvxFontRemoveOwner(int32_t owner) {
- int32_t prev = memAsSystem();
+ int32_t prev = dvxMemSetApp(DVX_MEM_SYSTEM);
for (int32_t i = (int32_t)arrlen(sFamilies) - 1; i > SYSTEM_FAMILY_INDEX; i--) {
if (sFamilies[i].owner == owner) {
@@ -538,19 +573,19 @@ void dvxFontRemoveOwner(int32_t owner) {
ownerDetach(owner, NULL);
sViewOwner = OWNER_GONE;
- memRestore(prev);
+ dvxMemSetApp(prev);
}
// Read the FONTS folder again on the next call; unload the fonts no one
// holds (added families stay)
void dvxFontRescan(void) {
- int32_t prev = memAsSystem();
+ int32_t prev = dvxMemSetApp(DVX_MEM_SYSTEM);
idleTrim(0);
sScanned = false;
sViewOwner = OWNER_GONE;
- memRestore(prev);
+ dvxMemSetApp(prev);
}
@@ -566,38 +601,13 @@ BitmapFontT *dvxFontRetain(BitmapFontT *font) {
// Fonts smoothed (anti-aliased) where their face has the size smoothed:
-// the idle fonts are unloaded and the held ones built again in place --
-// the fonts first, then the fonts with lines that copy them
+// the idle fonts are unloaded and the held ones built again in place
+// (dvxFontSmoothBegin, then every dvxFontRebuildStep).
void dvxFontSetSmooth(bool smooth) {
- if (smooth == sSmooth) {
- return;
+ dvxFontSmoothBegin(smooth);
+
+ while (dvxFontRebuildStep()) {
}
-
- int32_t prev = memAsSystem();
- FontEntryT **held = NULL;
-
- sSmooth = smooth;
- scanFonts();
- idleTrim(0);
-
- for (FontEntryT *e = sEntries; e; e = e->next) {
- arrput(held, e);
- }
-
- for (int32_t i = 0; i < arrlen(held); i++) {
- if (!held[i]->base) {
- entryRebuild(held[i]);
- }
- }
-
- for (int32_t i = 0; i < arrlen(held); i++) {
- if (held[i]->base) {
- entryLinesCopy(held[i], held[i]->font.style & FONT_STYLE_LINES);
- }
- }
-
- arrfree(held);
- memRestore(prev);
}
@@ -653,6 +663,26 @@ bool dvxFontSmooth(void) {
}
+// The idle fonts are unloaded; the held ones wait for dvxFontRebuildStep.
+void dvxFontSmoothBegin(bool smooth) {
+ if (smooth == sSmooth) {
+ return;
+ }
+
+ int32_t prev = dvxMemSetApp(DVX_MEM_SYSTEM);
+
+ sSmooth = smooth;
+ scanFonts();
+ idleTrim(0);
+
+ for (FontEntryT *e = sEntries; e; e = e->next) {
+ e->stale = true;
+ }
+
+ dvxMemSetApp(prev);
+}
+
+
// The font pick says (its face, NULL: the system font) in style for
// owner, from the size's gray strike when the pick is gray, built and
// added to the cache with one holder; NULL when out of memory. A face
@@ -870,6 +900,7 @@ static FontEntryT *entryLines(int32_t owner, const FontPickT *pick, uint32_t sty
e->base = (FontEntryT *)base->priv;
e->refs = 1;
e->owner = owner;
+ e->stale = e->base->stale;
entryLinesCopy(e, style & FONT_STYLE_LINES);
snprintf(e->family, sizeof(e->family), "%s", pick->family);
e->next = sEntries;
@@ -982,7 +1013,7 @@ static bool faceAdd(int32_t owner, const DfnSourceT *src) {
return false;
}
- int32_t prev = memAsSystem();
+ int32_t prev = dvxMemSetApp(DVX_MEM_SYSTEM);
int32_t fam = familyMake(file.header.family, owner);
bool ok = faceMerge(&sFamilies[fam].faces[file.header.style & FONT_STYLE_FACE], src, &file);
@@ -992,7 +1023,7 @@ static bool faceAdd(int32_t owner, const DfnSourceT *src) {
familyDropEmpty(fam);
sViewOwner = OWNER_GONE;
- memRestore(prev);
+ dvxMemSetApp(prev);
return ok;
}
@@ -1189,10 +1220,10 @@ static BitmapFontT *fontOpen(int32_t owner, const char *family, int32_t pixelSiz
}
}
- int32_t prev = memAsSystem();
+ int32_t prev = dvxMemSetApp(DVX_MEM_SYSTEM);
FontEntryT *e = (style & FONT_STYLE_LINES) ? entryLines(owner, &pick, style) : entryBuild(owner, &pick, style);
- memRestore(prev);
+ dvxMemSetApp(prev);
return e ? &e->font : NULL;
}
@@ -1315,26 +1346,6 @@ static void levelsFrom(BuildT *b, const FontGlyphT *g, const uint8_t *bits, int3
}
-// Make the system (app 0) the owner of what is allocated until
-// memRestore(the value returned)
-static int32_t memAsSystem(void) {
- int32_t prev = dvxMemAppIdPtr ? *dvxMemAppIdPtr : 0;
-
- if (dvxMemAppIdPtr) {
- *dvxMemAppIdPtr = 0;
- }
-
- return prev;
-}
-
-
-static void memRestore(int32_t prev) {
- if (dvxMemAppIdPtr) {
- *dvxMemAppIdPtr = prev;
- }
-}
-
-
// The size of the two faces (either may be NULL) nearest pixelSize, the
// smaller of two as near
static int32_t nearestSize(const FontFaceT *a, const FontFaceT *b, int32_t pixelSize) {
@@ -1426,7 +1437,7 @@ static void scanFonts(void) {
return;
}
- int32_t prev = memAsSystem();
+ int32_t prev = dvxMemSetApp(DVX_MEM_SYSTEM);
char **names = platformKindReadDir(DvxKindFontsE);
char cwd[DVX_MAX_PATH];
bool haveCwd = getcwd(cwd, sizeof(cwd)) != NULL;
@@ -1497,7 +1508,7 @@ static void scanFonts(void) {
}
dvxReadDirFree(names);
- memRestore(prev);
+ dvxMemSetApp(prev);
}
@@ -1659,7 +1670,7 @@ static void viewFor(int32_t owner) {
return;
}
- int32_t prev = memAsSystem();
+ int32_t prev = dvxMemSetApp(DVX_MEM_SYSTEM);
arrsetlen(sView, 0);
@@ -1673,7 +1684,7 @@ static void viewFor(int32_t owner) {
qsort(sView + 1, (size_t)(arrlen(sView) - 1), sizeof(sView[0]), viewCompare);
sViewOwner = owner;
- memRestore(prev);
+ dvxMemSetApp(prev);
}
diff --git a/src/libs/kpunch/libdvx/dvxText.h b/src/libs/kpunch/libdvx/dvxText.h
index 3e7ac42a..bed07914 100644
--- a/src/libs/kpunch/libdvx/dvxText.h
+++ b/src/libs/kpunch/libdvx/dvxText.h
@@ -114,6 +114,13 @@ BitmapFontT *dvxFontRetain(BitmapFontT *font);
void dvxFontSetSmooth(bool smooth);
bool dvxFontSmooth(void);
+// dvxFontSetSmooth a font at a time (building a font takes 15-150 ms):
+// Begin switches and marks the fonts open; each RebuildStep builds one
+// of them again, false when none is left (dvxUpdate steps a font a frame
+// and lays every window out again after the last).
+bool dvxFontRebuildStep(void);
+void dvxFontSmoothBegin(bool smooth);
+
// Read the FONTS folder again on the next call (after fonts were added or
// removed); the fonts no one holds are unloaded. Open fonts stay valid.
// Families added for programs stay.
diff --git a/src/libs/kpunch/libdvx/dvxTypes.h b/src/libs/kpunch/libdvx/dvxTypes.h
index 118e137e..ce96fdee 100644
--- a/src/libs/kpunch/libdvx/dvxTypes.h
+++ b/src/libs/kpunch/libdvx/dvxTypes.h
@@ -883,6 +883,10 @@ typedef struct WindowT {
// low (no queuing/dispatching overhead) and matches the callback-driven
// model that DOS programs expect. The widget system installs its own
// handlers here via wgtInitWindow() and dispatches to individual widgets.
+ // onPaint's dirtyArea is the content to paint (content coordinates);
+ // a painter that changed only part of it may narrow it to that part,
+ // and only that part is composited (the widget paint does, after a
+ // partial repaint).
void *userData;
void (*onPaint)(struct WindowT *win, RectT *dirtyArea);
void (*onKey)(struct WindowT *win, int32_t key, int32_t mod);
diff --git a/src/libs/kpunch/libdvx/dvxWgt.h b/src/libs/kpunch/libdvx/dvxWgt.h
index b70de196..c6aaa3cf 100644
--- a/src/libs/kpunch/libdvx/dvxWgt.h
+++ b/src/libs/kpunch/libdvx/dvxWgt.h
@@ -240,7 +240,10 @@ typedef struct WidgetClassT {
// text: an argument's text belongs to the caller for the duration of the
// call; a returned one (property get, method result) belongs to the
// widget and stays valid until the next call on that widget or until
-// the widget changes or is destroyed -- copy it to keep it.
+// the widget changes or is destroyed -- copy it to keep it. A STRING's
+// s always ends in a NUL; len, when above 0, is its length in bytes and
+// may count NUL bytes before that one (a BASIC string holds any bytes:
+// wgtValueBytes, wgtValueLen). 0 means up to the first NUL.
#define WGT_IFACE_STRING 0
#define WGT_IFACE_INT 1
@@ -252,6 +255,7 @@ typedef struct WidgetClassT {
typedef struct {
uint8_t type; // WGT_IFACE_*
+ int32_t len; // STRING only: its bytes when it may hold NUL bytes; 0 = up to its NUL
union {
int32_t i;
double f;
@@ -259,8 +263,10 @@ typedef struct {
};
} WgtValueT;
-// Value constructors (wgtValueStr turns NULL into "").
+// Value constructors (wgtValueStr turns NULL into ""). wgtValueBytes
+// takes len bytes of s, which may include NUL bytes; s[len] must be a NUL.
WgtValueT wgtValueBool(bool b);
+WgtValueT wgtValueBytes(const char *s, int32_t len);
WgtValueT wgtValueDouble(double f);
WgtValueT wgtValueFloat(double f);
WgtValueT wgtValueInt(int32_t i);
@@ -280,6 +286,9 @@ double wgtValueToFloat(const WgtValueT *v);
int32_t wgtValueToInt(const WgtValueT *v);
const char *wgtValueToText(const WgtValueT *v, char *buf, int32_t bufSize);
+// A STRING value's length in bytes: its len, else up to its NUL.
+int32_t wgtValueLen(const WgtValueT *v);
+
// The .frm / property-sheet boolean rule: "True" or "-1" is true, "False"
// or "0" false (case-insensitive), anything else defaultValue.
bool wgtParseBool(const char *text, bool defaultValue);
@@ -1039,6 +1048,15 @@ typedef struct {
void *fn; // function pointer
} WgtMethodDescT;
+// The method a control showing a picture (Image, ImageButton, PictureBox)
+// offers for the bytes of a picture file, WGT_SIG_DYNAMIC: a STRING of
+// any bytes and, optionally, the name its Picture property reads back
+// afterwards; True when they are a picture DVX can show. The BASIC form
+// runtime calls it to show a LoadPictureData picture.
+#define WGT_METHOD_SET_PICTURE_DATA "SetPictureData"
+#define WGT_PICTURE_DATA_ARGS_MIN 1
+#define WGT_PICTURE_DATA_ARGS_MAX 2
+
// Event descriptor. params is the handler's parameter list as BASIC
// writes it ("Topic As String", "" for none) -- the one place a widget's
// own event parameters are written down (IDE handler stubs, designer.bas
diff --git a/src/libs/kpunch/libdvx/dvxWgtP.h b/src/libs/kpunch/libdvx/dvxWgtP.h
index 152b6af9..f155db2e 100644
--- a/src/libs/kpunch/libdvx/dvxWgtP.h
+++ b/src/libs/kpunch/libdvx/dvxWgtP.h
@@ -218,9 +218,13 @@ WidgetT *widgetHitTest(WidgetT *w, int32_t x, int32_t y);
// Scrollbar helpers. widgetScrollbarThumb yields a zero-size thumb when
// trackLen or totalSize is <= 0 and clamps scrollPos, so callers need no
-// guards of their own.
+// guards of their own. widgetScrollbarThumbMin is the same rule with the
+// smallest thumb given (a bar's own width, so it is never shorter than
+// square): widgetScrollbarThumb is it with SB_MIN_THUMB, and the window
+// manager's scrollbars use it with SCROLLBAR_WIDTH.
void widgetScrollbarThumb(int32_t trackLen, int32_t totalSize, int32_t visibleSize, int32_t scrollPos, int32_t *thumbPos, int32_t *thumbSize);
int32_t widgetScrollbarThumbDragScroll(int32_t trackLen, int32_t total, int32_t visible, int32_t relMouse, int32_t maxScroll);
+void widgetScrollbarThumbMin(int32_t trackLen, int32_t totalSize, int32_t visibleSize, int32_t scrollPos, int32_t minThumb, int32_t *thumbPos, int32_t *thumbSize);
// ============================================================
// Scrollbar drawing and hit testing (widgetScrollbar.c)
@@ -279,7 +283,13 @@ void widgetOnScroll(WindowT *win, ScrollbarOrientE orient, int32_t value);
// ============================================================
void widgetPaintOne(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors);
-void widgetPaintOverlays(WidgetT *root, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors);
+bool widgetPaintOverlays(WidgetT *root, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors);
+
+// What the last partial wgtPaint pass changed (content coordinates): the
+// boxes of the widgets it repainted, joined and widened a little; false
+// when it repainted nothing. widgetOnPaint narrows the window's dirty
+// area to it.
+bool widgetPaintedArea(RectT *out);
// A display onto w's window content buffer, clipped to w and its
// ancestors, for a quick repaint (WGT_METHOD_QUICK_REPAINT) outside the
diff --git a/src/libs/kpunch/libdvx/dvxWm.c b/src/libs/kpunch/libdvx/dvxWm.c
index 8c5f2a79..8d6a50d4 100644
--- a/src/libs/kpunch/libdvx/dvxWm.c
+++ b/src/libs/kpunch/libdvx/dvxWm.c
@@ -59,6 +59,7 @@
#include "dvxFont.h"
#include "dvxComp.h"
#include "dvxWgt.h"
+#include "dvxWgtP.h"
#include "dvxPlat.h"
#include
@@ -127,7 +128,6 @@ static void copyLabel(char *dst, const char *src, size_t cap);
static void drawBorderFrame(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colors, int32_t x, int32_t y, int32_t w, int32_t h, const RectT *well, bool fillCorner);
static void drawMenuBar(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors, WindowT *win);
static void drawResizeBreaks(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colors, WindowT *win);
-static void drawScaledRect(DisplayT *d, int32_t dstX, int32_t dstY, int32_t dstW, int32_t dstH, const uint8_t *src, int32_t srcW, int32_t srcH, int32_t srcPitch, int32_t bpp);
static void drawTitleBar(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors, WindowT *win);
static void drawTitleGadget(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colors, int32_t x, int32_t y, int32_t size);
static void freeMenuItemSubMenu(MenuItemT *item);
@@ -484,91 +484,6 @@ static void drawResizeBreaks(DisplayT *d, const BlitOpsT *ops, const ColorScheme
}
-// Nearest-neighbor scale blit used to render minimized window icons.
-// Icons are ICON_SIZE x ICON_SIZE (64x64); the source can be the window's
-// content buffer (live thumbnail) or a loaded icon image of any size.
-//
-// Optimization strategy for 486/Pentium:
-// - Fixed-point 16.16 reciprocals replace per-pixel division with
-// multiply+shift. Division is 40+ cycles on 486; multiply is 13.
-// - Source coordinate lookup tables (srcXTab/srcYTab) are pre-computed
-// once per call and indexed in the inner loop, eliminating repeated
-// multiply+shift per pixel.
-// - Static lookup table buffers avoid stack frame growth (ICON_TOTAL_SIZE=68
-// entries * 4 bytes = 272 bytes per table that would otherwise be allocated
-// on every call during the compositing loop). Sized to the largest dstW/dstH
-// ever passed (a provided icon scaled to ICON_TOTAL_SIZE), not just ICON_SIZE.
-// - The bpp switch is outside the column loop so the branch is taken
-// once per row rather than once per pixel.
-// - Clipping is done once up front by adjusting row/col start/end ranges,
-// so the inner loop runs with zero per-pixel clip checks.
-
-static void drawScaledRect(DisplayT *d, int32_t dstX, int32_t dstY, int32_t dstW, int32_t dstH, const uint8_t *src, int32_t srcW, int32_t srcH, int32_t srcPitch, int32_t bpp) {
- int32_t rowStart = 0;
- int32_t rowEnd = dstH;
- int32_t colStart = 0;
- int32_t colEnd = dstW;
-
- if (dstY < d->clipY) {
- rowStart = d->clipY - dstY;
- }
-
- if (dstY + dstH > d->clipY + d->clipH) {
- rowEnd = d->clipY + d->clipH - dstY;
- }
-
- if (dstX < d->clipX) {
- colStart = d->clipX - dstX;
- }
-
- if (dstX + dstW > d->clipX + d->clipW) {
- colEnd = d->clipX + d->clipW - dstX;
- }
-
- if (rowStart >= rowEnd || colStart >= colEnd) {
- return;
- }
-
- static int32_t srcXTab[ICON_TOTAL_SIZE];
- static int32_t srcYTab[ICON_TOTAL_SIZE];
- int32_t visibleCols = colEnd - colStart;
- int32_t visibleRows = rowEnd - rowStart;
-
- // Fixed-point 16.16 scale factors
- uint32_t recipW = ((uint32_t)srcW << 16) / (uint32_t)dstW;
- uint32_t recipH = ((uint32_t)srcH << 16) / (uint32_t)dstH;
-
- // Pre-multiply X offsets by bpp so the inner loop does a single array index
- for (int32_t dx = 0; dx < visibleCols; dx++) {
- srcXTab[dx] = (int32_t)(((uint32_t)(colStart + dx) * recipW) >> 16) * bpp;
- }
-
- for (int32_t dy = 0; dy < visibleRows; dy++) {
- srcYTab[dy] = (int32_t)(((uint32_t)(rowStart + dy) * recipH) >> 16);
- }
-
- for (int32_t dy = rowStart; dy < rowEnd; dy++) {
- int32_t sy = srcYTab[dy - rowStart];
- uint8_t *dstRow = d->backBuf + (dstY + dy) * d->pitch + (dstX + colStart) * bpp;
- const uint8_t *srcRow = src + sy * srcPitch;
-
- if (bpp == 1) {
- for (int32_t dx = 0; dx < visibleCols; dx++) {
- dstRow[dx] = srcRow[srcXTab[dx]];
- }
- } else if (bpp == 2) {
- for (int32_t dx = 0; dx < visibleCols; dx++) {
- *(uint16_t *)(dstRow + dx * 2) = *(const uint16_t *)(srcRow + srcXTab[dx]);
- }
- } else {
- for (int32_t dx = 0; dx < visibleCols; dx++) {
- *(uint32_t *)(dstRow + dx * 4) = *(const uint32_t *)(srcRow + srcXTab[dx]);
- }
- }
- }
-}
-
-
// Renders the title bar: background fill, close gadget (left), minimize
// and maximize gadgets (right), and centered title text.
//
@@ -874,21 +789,11 @@ static int32_t scrollbarThumbInfo(const ScrollbarT *sb, int32_t *thumbPos, int32
// The WM scrollbar thumb rule, shared by the window scrollbars and the
-// standalone ones (scrollbarAtThumb). Thumb size is proportional to the visible page
-// relative to the total scrollable range: trackLen * pageSize /
-// (range + pageSize). This gives a thumb that shrinks as content grows,
-// matching Motif/Windows scrollbars. The size is clamped up to
-// SCROLLBAR_WIDTH so it stays grabbable with huge content, then down to
-// the track so it never spills over the arrow buttons of a short bar; the
-// value is clamped into [min, max] so the thumb stays inside the track.
-//
-// The int64_t casts prevent overflow: trackLen can be ~400px, pageSize and
-// range can be arbitrarily large (e.g. a 10000-line text buffer), and
-// 400 * 10000 exceeds int32_t range.
-//
-// When range <= 0 (all content visible), the thumb fills the entire track,
-// visually indicating there's nothing to scroll.
-
+// standalone ones (scrollbarAtThumb): the widget scrollbars' rule
+// (widgetScrollbarThumbMin, page out of range + page, the thumb never
+// shorter than the bar is wide), except that when range <= 0 (all content
+// visible) the thumb fills the entire track, visually indicating there's
+// nothing to scroll.
static void thumbGeometry(int32_t trackLen, int32_t min, int32_t max, int32_t value, int32_t pageSize, int32_t *thumbPos, int32_t *thumbSize) {
int32_t range = max - min;
@@ -898,17 +803,7 @@ static void thumbGeometry(int32_t trackLen, int32_t min, int32_t max, int32_t va
return;
}
- int32_t size = (int32_t)(((int64_t)pageSize * trackLen) / ((int64_t)range + pageSize));
-
- size = DVX_MAX(size, SCROLLBAR_WIDTH);
- size = DVX_MIN(size, trackLen);
-
- // Map value to pixel offset: value maps linearly to the range
- // [0, trackLen - thumbSize]
- int32_t clamped = DVX_MIN(DVX_MAX(value, min), max);
-
- *thumbSize = size;
- *thumbPos = (int32_t)(((int64_t)(clamped - min) * (trackLen - size)) / range);
+ widgetScrollbarThumbMin(trackLen, range + pageSize, pageSize, value - min, SCROLLBAR_WIDTH, thumbPos, thumbSize);
}
@@ -1558,12 +1453,9 @@ void wmDrawMinimizedIcons(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *
continue;
}
- int32_t bpp = d->format.bytesPerPixel;
-
if (win->iconData && win->iconW > 0 && win->iconH > 0) {
// Provided icon image -- draw edge-to-edge, no bevel
- drawScaledRect(d, ix, iy, ICON_TOTAL_SIZE, ICON_TOTAL_SIZE,
- win->iconData, win->iconW, win->iconH, win->iconPitch, bpp);
+ drawScaled(d, ix, iy, ICON_TOTAL_SIZE, ICON_TOTAL_SIZE, win->iconData, win->iconW, win->iconH, win->iconPitch, false, 0);
} else {
// Live thumbnail or grey fill -- draw with beveled border
BevelStyleT bevel;
@@ -1577,8 +1469,7 @@ void wmDrawMinimizedIcons(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *
int32_t contentY = iy + ICON_BORDER;
if (win->contentBuf && win->contentW > 0 && win->contentH > 0) {
- drawScaledRect(d, contentX, contentY, ICON_SIZE, ICON_SIZE,
- win->contentBuf, win->contentW, win->contentH, win->contentPitch, bpp);
+ drawScaled(d, contentX, contentY, ICON_SIZE, ICON_SIZE, win->contentBuf, win->contentW, win->contentH, win->contentPitch, false, 0);
} else {
rectFill(d, ops, contentX, contentY, ICON_SIZE, ICON_SIZE, colors->windowFace);
}
@@ -2270,7 +2161,9 @@ void wmRaiseWindow(WindowStackT *stack, DirtyListT *dl, int32_t idx) {
// The buffer is initialized to white so any area the app doesn't paint
// shows a clean background rather than garbage. In direct-color modes an
// all-ones byte fill is white; in 8-bit modes 0xFF would be palette index
-// 255 (a reserved black entry), so the white palette index is used.
+// 255 (a reserved black entry), so the white palette index is used. A
+// widget window is not filled: its full paint, which follows, clears every
+// pixel (a maximize or a resize step then clears once, not twice).
int32_t wmReallocContentBuf(WindowT *win, const DisplayT *d) {
free(win->contentBuf);
@@ -2289,9 +2182,11 @@ int32_t wmReallocContentBuf(WindowT *win, const DisplayT *d) {
return -1;
}
- uint8_t clearByte = (d->format.bytesPerPixel == 1) ? (uint8_t)packColor(d, CONTENT_CLEAR_RGB, CONTENT_CLEAR_RGB, CONTENT_CLEAR_RGB) : CONTENT_CLEAR_BYTE;
+ if (win->onPaint != widgetOnPaint) {
+ uint8_t clearByte = (d->format.bytesPerPixel == 1) ? (uint8_t)packColor(d, CONTENT_CLEAR_RGB, CONTENT_CLEAR_RGB, CONTENT_CLEAR_RGB) : CONTENT_CLEAR_BYTE;
- memset(win->contentBuf, clearByte, bufSize);
+ memset(win->contentBuf, clearByte, bufSize);
+ }
return 0;
}
diff --git a/src/libs/kpunch/libdvx/games.dhs b/src/libs/kpunch/libdvx/games.dhs
index b0ba0bb1..7564ca51 100644
--- a/src/libs/kpunch/libdvx/games.dhs
+++ b/src/libs/kpunch/libdvx/games.dhs
@@ -240,6 +240,7 @@ if (!dvxExclusivePresent8(ctx, fb, 320, 200, 320)) {
.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.
+.item dvxExclusiveMirror instead shows the window's own content on the whole screen -- its widgets, or what it draws into its content buffer -- in the desktop's pixel format, scaled and centred the same way and kept up to date as the window paints; the mouse works on the content as in the window, with its cursor drawn. A BASIC form does this with FullScreen = True (and hears the changes in its FullScreen event); a BASIC SCREEN program uses FULLSCREEN.
.endlist
.topic games.sound
diff --git a/src/libs/kpunch/libdvx/platform/dvxMemTrack.c b/src/libs/kpunch/libdvx/platform/dvxMemTrack.c
index ec0f767d..d44866eb 100644
--- a/src/libs/kpunch/libdvx/platform/dvxMemTrack.c
+++ b/src/libs/kpunch/libdvx/platform/dvxMemTrack.c
@@ -198,6 +198,18 @@ void dvxMemResetApp(int32_t appId) {
}
+int32_t dvxMemSetApp(int32_t appId) {
+ if (!dvxMemAppIdPtr) {
+ return DVX_MEM_SYSTEM;
+ }
+
+ int32_t prev = *dvxMemAppIdPtr;
+
+ *dvxMemAppIdPtr = appId;
+ return prev;
+}
+
+
void *dvxRealloc(void *ptr, size_t size) {
if (!ptr) {
return dvxMalloc(size);
diff --git a/src/libs/kpunch/libdvx/platform/dvxPlat.h b/src/libs/kpunch/libdvx/platform/dvxPlat.h
index 65fd6782..63c8325d 100644
--- a/src/libs/kpunch/libdvx/platform/dvxPlat.h
+++ b/src/libs/kpunch/libdvx/platform/dvxPlat.h
@@ -112,6 +112,12 @@ const char *platformLogMemory(void);
bool platformIsReadOnly(void);
void platformSetReadOnly(bool readOnly);
+// Whether the entry name that readdir gave for folder dir, with its
+// d_type type, is a folder: from type when it tells, else (unknown, or
+// a link) from stat. A stat per entry costs a search of the folder on
+// DOS, so a listing that asks it for every entry takes quadratic time.
+bool platformDirEntryIsDir(const char *dir, const char *name, int32_t type);
+
// True when a file can be created in dir (a scratch file is made and
// removed again).
bool platformDirIsWritable(const char *dir);
@@ -232,6 +238,14 @@ bool platformKindNameOf(DvxKindE kind, const char *path, char *out, int32_t outS
// dvxReadDirFree).
char **platformKindReadDir(DvxKindE kind);
+// What is installed (programs, and the files of the kinds above) may have
+// changed: a package went in or out, a DOS program ran, a file job ended.
+// platformInstalledChangeCount moves on (wrapping around) each time, so a
+// program that shows what is installed (Program Manager) looks again only
+// when it differs from the count it saw last.
+uint32_t platformInstalledChangeCount(void);
+void platformInstalledChanged(void);
+
// Notes the loader leaves for the shell to show once the desktop is up
// (an add-on that was not loaded, and why); each text is copied. They
// hold PLATFORM_BOOT_NOTES_MAX bytes in all (the NUL counted); past that a
@@ -260,10 +274,10 @@ typedef uint32_t PlatformTicksT;
PlatformTicksT platformClock(void);
PlatformTicksT platformClockHz(void);
-// Fine-grained, free-running timestamp for input-timing entropy (not a
-// clock: units and wrap point are backend-specific). DOS returns the TSC
-// when the CPU has one, else BIOS ticks with the PIT counter in the low
-// 16 bits.
+// Fine-grained, free-running timestamp for entropy (not a clock: units
+// and wrap point are backend-specific). DOS returns the TSC with the PIT
+// counter in its top 16 bits when the CPU has a TSC, else BIOS ticks with
+// the PIT counter in the low 16 bits.
uint64_t platformEntropyStamp(void);
// Convert a millisecond interval to ticks at the platform rate.
@@ -301,6 +315,13 @@ uint64_t platformEntropyStamp(void);
// for use inside a timer callback.
uint64_t platformMicros(void);
+// The BIOS clock ticks (PLATFORM_PIT_HZ / PLATFORM_PIT_BIOS_DIVISOR, about
+// 18.2 a second) since the timer service started, as the word the timer
+// interrupt advances: reading it costs one load, for a check made too often
+// for platformMicros (the BASIC VM's slice clock). Wraps; compare by
+// subtraction.
+const volatile uint32_t *platformBiosTicks(void);
+
// A timer callback. It runs INSIDE THE TIMER INTERRUPT, with interrupts
// disabled, on a small interrupt stack: keep it to a few dozen
// microseconds, do not call the C library, DVX or DOS, do not use floating
@@ -703,7 +724,7 @@ int32_t platformBiosScanCode(int32_t key, int32_t mod);
// Gather hardware information (CPU, clock, memory, DOS/DPMI version,
// video, mouse, disk drives) and return as a pre-formatted text string.
// The display pointer provides the current video mode info. Returns a
-// pointer to a static buffer valid for the lifetime of the process.
+// pointer valid until the next call ("" when out of memory).
// On DOS this uses CPUID, RDTSC, DPMI, INT 21h, INT 33h, and VBE.
const char *platformGetSystemInfo(const DisplayT *display);
@@ -724,6 +745,19 @@ bool platformShortPath(const char *path, char *out, int32_t outSize);
struct stat;
+// A file's size and time as its folder lists them, for telling whether a
+// file read before has changed (compare two for equality only).
+typedef struct {
+ uint32_t size; // bytes (a file of 4 GB or more: 0xFFFFFFFF)
+ uint32_t time; // its date and time, as the platform keeps them
+} PlatformStampT;
+
+// The stamp of the file at path, from its folder alone: on DOS one
+// directory search (stat makes several calls there, tens of milliseconds
+// on a slow disk), elsewhere stat. False when path is no file (or has a
+// wildcard).
+bool platformFileStamp(const char *path, PlatformStampT *out);
+
// The size stat reported for a file. DOS sizes are 32-bit unsigned (a
// FAT32 file reaches 4 GB - 1) where the DOS C library's st_size is a
// signed 32-bit off_t, so a file past 2 GB reads as negative there.
@@ -797,6 +831,9 @@ bool platformPathDirName(const char *path, char *out, int32_t outSize);
// a drive letter ("C:...").
bool platformPathIsAbsolute(const char *path);
+// True for a drive or file-system root: "C:", "C:\\", "/", or "\\".
+bool platformPathIsRoot(const char *path);
+
// True when c separates the parts of a path: '/' and '\\' alike, on every
// platform, so a path a program writes with '/' works on DOS exactly as
// with '\\' (DOS and the host build also share INI files, projects and
@@ -819,6 +856,11 @@ bool platformPathJoin(char *out, int32_t outSize, const char *dir, const char *n
// library hands back "c:/dvx"). Returns path (NULL stays NULL).
char *platformPathNative(char *path);
+// out = the folder holding path (a trailing separator ignored), native
+// separators: "C:\\DVX" for "C:/DVX/APPS/", "/" for "/a". False for a
+// root, a name without a folder, or when it does not fit.
+bool platformPathParent(const char *path, char *out, int32_t outSize);
+
// True when a and b name the same place by DOS rules, on every platform
// (DVX's own paths mix spellings such as "APPS/X.APP" and "apps\\x.app"):
// the separators interchangeable, a trailing one ignored, case ignored.
@@ -863,6 +905,16 @@ bool platformReplaceFile(const char *tmpPath, const char *path, const char *bakP
// done.
bool platformWriteFile(const char *path, const void *data, int32_t len);
+// Visit every file below dir, depth first, folders entered as they come
+// and names starting with '.' skipped. A folder's names are read in full
+// before any is visited (DOS has few file handles), and which are folders
+// comes with them (platformDirEntryIsDir). fn gets each file's full path
+// and its name; returning false stops the walk, and then so does
+// platformWalkDir. A folder that cannot be read is skipped.
+typedef bool (*PlatformWalkFnT)(const char *path, const char *name, void *ctx);
+
+bool platformWalkDir(const char *dir, PlatformWalkFnT fn, void *ctx);
+
// Return a pointer to the leaf (basename) portion of path: after the last
// separator, or after a drive ("C:A.TXT" -> "A.TXT"). Never returns
// NULL -- with neither, the whole path is the basename.
@@ -874,6 +926,11 @@ const char *platformPathBaseName(const char *path);
// returns true.
bool platformCopyFile(const char *srcPath, const char *dstPath);
+// platformCopyFile asking goOn(ctx) between pieces (a caller that lets
+// other work go on while a big file copies); false from goOn stops the
+// copy, which then fails like any other (its partial copy removed).
+bool platformCopyFileWith(const char *srcPath, const char *dstPath, bool (*goOn)(void *ctx), void *ctx);
+
// Slurp a whole file into a freshly-malloc'd, NUL-terminated buffer.
// Returns NULL on error (file missing, OOM, read truncated). On success,
// *outLen (if non-NULL) receives the byte length (not counting the NUL).
@@ -891,6 +948,12 @@ const char *dvxSkipWs(const char *s);
// as pos propagates, collapsing a chain of appends to one truncation check.
int32_t dvxStrAppendf(char *buf, int32_t bufSize, int32_t pos, const char *fmt, ...) __attribute__((format(printf, 4, 5)));
+// As dvxStrAppendf, but text that does not fit is cut to what does: the
+// position returned is then bufSize - 1. A pos outside the buffer (a
+// caller's negative code included) or a vsnprintf failure comes back
+// unchanged.
+int32_t dvxStrAppendfCut(char *buf, int32_t bufSize, int32_t pos, const char *fmt, ...) __attribute__((format(printf, 4, 5)));
+
// Strip trailing ' ', '\t', '\r', '\n' from buf in place. Returns the
// new length. buf may be NULL or empty.
int32_t dvxTrimRight(char *buf);
@@ -1018,6 +1081,29 @@ int32_t platformStripLineEndings(char *buf, int32_t len);
#endif
#define DVX_SYM(name) DVX_SYM_PREFIX name
+// Calls fn with each name a DXE3 module file exports (as the file has
+// it, DJGPP's leading underscore and all) and sets *runsAtLoad when the
+// module has code that runs as it loads (constructors). False when path
+// is not a readable, whole DXE3 file. The loader indexes the libraries
+// it leaves for later with it.
+typedef void (*PlatformDxeNameFnT)(void *ctx, const char *name);
+bool platformDxeExports(const char *path, PlatformDxeNameFnT fn, void *ctx, bool *runsAtLoad);
+
+// Open a module, as dlopen does, the way the loader opens one: a library
+// not loaded yet that exports a name the module needs is loaded first
+// (with the libraries its .dep file names). A module calls this, never
+// dlopen (dvx.exe does not export dlopen). On hosts without DXE it is
+// dlopen.
+void *platformModuleOpen(const char *path, int32_t mode);
+
+// The loader's: what platformModuleOpen calls (until it is set, dlopen),
+// and the last-resort resolver for a name that dvx.exe and the loaded
+// modules lack (DJGPP's _dlsymresolver; nothing on the host).
+typedef void *(*PlatformModuleOpenFnT)(const char *path, int32_t mode);
+typedef void *(*PlatformSymbolResolverFnT)(const char *name);
+void platformSetModuleOpen(PlatformModuleOpenFnT fn);
+void platformSetSymbolResolver(PlatformSymbolResolverFnT fn);
+
// Register platform and C runtime symbols with the dynamic module
// loader so that DXE modules can resolve them at load time. On DOS
// this calls dlregsym() with the full DJGPP libc/libm/libgcc/platform
diff --git a/src/libs/kpunch/libdvx/platform/dvxPlatHost.h b/src/libs/kpunch/libdvx/platform/dvxPlatHost.h
index 5790a75e..d28e7a16 100644
--- a/src/libs/kpunch/libdvx/platform/dvxPlatHost.h
+++ b/src/libs/kpunch/libdvx/platform/dvxPlatHost.h
@@ -86,6 +86,15 @@ void hostClockAdvanceUs(uint64_t us);
// ignored while hostClockSetRealTime(true) is in effect.
void hostClockSet(uint32_t ms);
+// Make every platformClock read advance the fake clock by ms milliseconds
+// once it has been read (0, the default, leaves the clock alone), so code
+// that checks the time between pieces of work sees time pass.
+void hostClockSetReadStep(uint32_t ms);
+
+// The same for platformMicros: every read advances the fake clock by us
+// microseconds once it has been read (0, the default: none).
+void hostClockSetMicrosStep(uint32_t us);
+
// Switch between the fake clock (default, deterministic) and wall time
// for interactive use. Entering real-time mode continues from the
// current fake value; leaving it freezes at the current wall value.
diff --git a/src/libs/kpunch/libdvx/platform/dvxPlatInt.h b/src/libs/kpunch/libdvx/platform/dvxPlatInt.h
index c8427d32..783edb6e 100644
--- a/src/libs/kpunch/libdvx/platform/dvxPlatInt.h
+++ b/src/libs/kpunch/libdvx/platform/dvxPlatInt.h
@@ -23,8 +23,9 @@
// dvxPlatInt.h -- what the platform layer's own files share
//
-// Private to the platform layer (not in the SDK): the DOS interrupt and
-// real-mode facts dvxPlatformDos.c and dvxPlatformUtil.c both use, and
+// Private to the platform layer (not in the SDK): the DOS interrupt,
+// real-mode, timer, and CPU facts dvxPlatformDos.c and dvxPlatformUtil.c
+// both use, and
// the host's root and current folder (dvxPlatformHost.c and
// dvxPlatformUtil.c).
@@ -47,4 +48,43 @@
#define HOST_ROOT "/"
#define HOST_CURRENT_DIR "."
+#ifdef __DJGPP__
+#include
+#include
+
+// BIOS timer tick counter at 0040:006C (linear 0x46C)
+#define BIOS_TICK_ADDR 0x46C
+
+// PIT channel 0 (the system timer): command port, data port, the command
+// that latches the count, and the count's width
+#define PIT_CMD_PORT 0x43
+#define PIT_CH0_PORT 0x40
+#define PIT_LATCH_CH0 0x00
+#define PIT_COUNT_BITS 16
+
+// EFLAGS ID bit: toggleable only when CPUID exists
+#define EFLAGS_ID_BIT 0x200000U
+
+// CPUID leaf 1 and its EDX time stamp counter bit
+#define CPUID_LEAF_FEATURES 1
+#define CPUID_FEAT_TSC (1U << 4)
+
+#define TSC_HI_SHIFT 32 // rdtsc: EDX holds the high dword
+
+// CPUID exists; the CPU has a time stamp counter (probed once). In
+// dvxPlatformUtil.c, so a DOS tool that links only it has them too.
+bool platformHasCpuid(void);
+bool platformHasTsc(void);
+
+// The time stamp counter (the caller knows the CPU has one:
+// platformHasTsc). Inline, so the timer interrupt can read it.
+static inline __attribute__((always_inline)) uint64_t platformReadTsc(void) {
+ uint32_t lo;
+ uint32_t hi;
+
+ __asm__ __volatile__ ("rdtsc" : "=a"(lo), "=d"(hi));
+ return ((uint64_t)hi << TSC_HI_SHIFT) | lo;
+}
+#endif
+
#endif // DVX_PLAT_INT_H
diff --git a/src/libs/kpunch/libdvx/platform/dvxPlatformDos.c b/src/libs/kpunch/libdvx/platform/dvxPlatformDos.c
index 13d22e14..260bc50d 100644
--- a/src/libs/kpunch/libdvx/platform/dvxPlatformDos.c
+++ b/src/libs/kpunch/libdvx/platform/dvxPlatformDos.c
@@ -55,8 +55,6 @@
#include "dvxPlatInt.h"
#include "dvxPlatIsr.h"
#include "dvxPal.h"
-#include "dvxPrefs.h"
-#include "../../../../tools/hlpcCompile.h"
#include
#include
@@ -80,6 +78,7 @@
#include
#include
#include
+#include
// DJGPP-specific headers -- this is the ONLY file that includes these
#include
@@ -101,23 +100,18 @@
// RDTSC calibration: measure over this many BIOS timer ticks
#define CLOCK_MEAS_TICKS 3
-// BIOS timer tick counter at 0040:006C (linear 0x46C). It counts at
-// 1193182 / 65536 Hz (54925.4 us per tick) and wraps to 0 at midnight
-// after BIOS_TICKS_PER_DAY ticks.
-#define BIOS_TICK_ADDR 0x46C
+// The BIOS timer tick counter (BIOS_TICK_ADDR) counts at 1193182 / 65536
+// Hz (54925.4 us per tick) and wraps to 0 at midnight after
+// BIOS_TICKS_PER_DAY ticks.
#define BIOS_TICK_US 54925ULL
#define BIOS_TICKS_PER_DAY 0x1800B0UL
-// PIT channel 0 (the system timer): command port, data port, and the
-// command bytes the timer service uses: latch the count, rate generator
-// (mode 2, low byte then high byte) while the service runs, and the BIOS's
-// own square wave (mode 3) to hand the PIT back. A count of 0 means 65536.
-#define PIT_CMD_PORT 0x43
-#define PIT_CH0_PORT 0x40
-#define PIT_LATCH_CH0 0x00
+// PIT channel 0 (PIT_CMD_PORT, PIT_CH0_PORT): the command bytes the timer
+// service uses beside the latch, rate generator (mode 2, low byte then
+// high byte) while the service runs, and the BIOS's own square wave
+// (mode 3) to hand the PIT back. A count of 0 means 65536.
#define PIT_CH0_RATE_GEN 0x34
#define PIT_CH0_SQUARE_WAVE 0x36
-#define PIT_COUNT_BITS 16
#define BYTE_BITS 8
#define DWORD_SHIFT 2 // bytes to doublewords
#define DWORD_MASK 3 // the bytes past the last whole doubleword
@@ -135,19 +129,14 @@
// CPU interrupt flag in EFLAGS (platformIrqSave / platformIrqRestore)
#define EFLAGS_IF_BIT 0x200U
-#define TSC_HI_SHIFT 32 // rdtsc: EDX holds the high dword
// Real-mode interrupt vectors
#define INT_VIDEO 0x10
#define INT_KEYBOARD 0x16
#define INT_MOUSE 0x33
-// EFLAGS ID bit: toggleable only when CPUID exists
-#define EFLAGS_ID_BIT 0x200000U
-
// CPUID leaves, EAX(1) field layout, and EDX(1) feature bits
#define CPUID_LEAF_VENDOR 0
-#define CPUID_LEAF_FEATURES 1
#define CPUID_EXT_BASE 0x80000000U
#define CPUID_BRAND_FIRST 0x80000002U
#define CPUID_BRAND_LAST 0x80000004U
@@ -164,7 +153,6 @@
#define CPUID_FAMILY_EXTENDED 0x0F
#define CPUID_FAMILY_P6 6
#define CPUID_FEAT_FPU (1U << 0)
-#define CPUID_FEAT_TSC (1U << 4)
#define CPUID_FEAT_MMX (1U << 23)
#define CPUID_FEAT_SSE (1U << 25)
#define CPUID_FEAT_SSE2 (1U << 26)
@@ -247,6 +235,8 @@
#define MOUSE_FN_RESET 0x0000
#define MOUSE_FN_GET_STATE 0x0003
#define MOUSE_FN_SET_POS 0x0004
+#define MOUSE_FN_PRESS_INFO 0x0005 // presses of button BX since the last call (BX), counter cleared
+#define MOUSE_FN_RELEASE_INFO 0x0006 // releases likewise
#define MOUSE_FN_SET_XRANGE 0x0007
#define MOUSE_FN_SET_YRANGE 0x0008
#define MOUSE_FN_READ_MICKEYS 0x000B
@@ -257,6 +247,7 @@
#define MOUSE_WHEEL_SIG 0x574D // 'WM' returned by the CuteMouse WheelAPI
#define MOUSE_WHEEL_PRESENT 0x0001 // CX bit 0: wheel physically present
#define MOUSE_WHEEL_SHIFT 8 // BH of fn 03h BX holds the wheel counter
+#define MOUSE_BUTTONS 3 // left, right, middle: fn 05h / 06h's button numbers 0..2
#define MOUSE_SETTING_UNSET (-1)
// INT 16h enhanced keyboard functions
@@ -415,6 +406,13 @@ typedef struct {
uint8_t bluePos;
} VbeModeT;
+// A mode's shape, all platformVideoEnumModes compares
+typedef struct {
+ int32_t width;
+ int32_t height;
+ int32_t bpp;
+} ModeKeyT;
+
// A banked mode's window (platformBankChunk): where it is in the address
// space, its size and step, and which part of video memory it shows.
typedef struct {
@@ -472,14 +470,12 @@ static void bankSet(uint8_t window, uint32_t winStart);
static void biosKeyFlush(void);
static uint8_t biosVideoMode(void);
static inline void copySpan(uint8_t *dst, const uint8_t *src, uint32_t bytes);
-static bool cpuHasTsc(void);
static void critErrorOwn(bool ours);
static int32_t desktopModeRestore(const DisplayT *desktop);
static void dosBreakSet(uint16_t state);
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, ModeScoreFnT score, uint16_t *outMode, VbeModeT *outInfo, DisplayT *d);
-static bool hasCpuid(void);
static void installCritErrorHandler(void);
static void int8Handler(void);
static void int9Handler(void);
@@ -489,11 +485,11 @@ static int32_t mapLfb(DisplayT *d, uint32_t physAddr, LfbMapT *m
static int32_t modeStart(uint16_t mode, const VbeModeT *m, DisplayT *d, LfbMapT *map, BankT *bank);
static void modeXCopy(const DisplayT *d, const RectT *r, uint32_t pageOffset);
static void modeXSet(void);
+static int32_t mouseMissedEdges(int32_t buttons);
static uint32_t pagesToKb(unsigned long pages);
static uint32_t pitLatchElapsed(void);
static void platformCrashHandler(int sig);
int32_t platformDosRun(const char *command, const char *folder, bool pause, const DisplayT *desktop);
-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);
@@ -530,21 +526,15 @@ static __dpmi_raddr sCritVectorDos; // DOS's, given back while a DOS pr
static uint16_t sBreakDos = DOS_BREAK_OFF;
static bool sPlatformInitDone = false;
-// TSC availability for platformEntropyStamp, probed on first use.
-typedef enum {
- TscUnknownE = 0,
- TscAbsentE = 1,
- TscPresentE = 2
-} TscStateE;
-
-static TscStateE sTscState = TscUnknownE;
-
// Wheel state: set by platformMouseWheelInit, read by platformMousePoll
// and platformGetSystemInfo. Declared here (above all functions) so
// every function in the file can see them.
static bool sHasMouseWheel = false;
static int32_t sLastWheelDelta = 0;
+// The buttons platformMousePoll last reported (mouseMissedEdges).
+static int32_t sMouseLastButtons = 0;
+
// Software-tracked mouse cursor. sMouseRangeW/H are the clamp bounds, set to
// the VESA resolution in platformMouseInit. Position comes from one of two
// schemes selected at RUNTIME (sMouseMode):
@@ -603,8 +593,9 @@ static int32_t sInjectHead = 0;
static int32_t sInjectCount = 0;
static InjectMouseT sInjectLast;
-// System information static buffer
-static char sSysInfoBuf[PLATFORM_SYSINFO_MAX];
+// The system information text (platformGetSystemInfo): built in a
+// PLATFORM_SYSINFO_MAX buffer, then cut to its length
+static char *sSysInfoBuf = NULL;
static int32_t sSysInfoPos = 0;
// Per-app memory tracking (dvxMalloc & co.) lives in dvxMemTrack.c. The
@@ -772,29 +763,6 @@ static inline void copySpan(uint8_t *dst, const uint8_t *src, uint32_t bytes) {
}
-// Whether the CPU has a time-stamp counter (CPUID feature bit), probed
-// once.
-static bool cpuHasTsc(void) {
- if (sTscState == TscUnknownE) {
- sTscState = TscAbsentE;
-
- if (hasCpuid()) {
- uint32_t eax;
- uint32_t ebx;
- uint32_t ecx;
- uint32_t edx;
-
- __asm__ __volatile__ ("cpuid" : "=a"(eax), "=b"(ebx), "=c"(ecx), "=d"(edx) : "a"(CPUID_LEAF_FEATURES));
-
- if (edx & CPUID_FEAT_TSC) {
- sTscState = TscPresentE;
- }
- }
- }
-
- return sTscState == TscPresentE;
-}
-
// INT 24h: DVX's handler (critical errors fail the call), or DOS's own
// while a DOS program runs.
@@ -852,7 +820,7 @@ static uint32_t estimateClockMhz(void) {
// spin
}
- uint64_t tsc1 = readTsc();
+ uint64_t tsc1 = platformReadTsc();
uint32_t tickStart = _farpeekl(_dos_ds, BIOS_TICK_ADDR);
uint32_t now = tickStart;
@@ -866,7 +834,7 @@ static uint32_t estimateClockMhz(void) {
continue;
}
- uint64_t delta = readTsc() - tsc1;
+ uint64_t delta = platformReadTsc() - tsc1;
return (uint32_t)(delta / (CLOCK_MEAS_TICKS * BIOS_TICK_US));
}
@@ -962,41 +930,6 @@ static int32_t findBestMode(int32_t requestedW, int32_t requestedH, int32_t pref
}
-// Check if the CPUID instruction is available.
-// The CPUID instruction exists if bit 21 (ID flag) of EFLAGS can be
-// toggled. On a 386 this bit is hardwired to 0. On a 486 without
-// CPUID it's also hardwired. Only if we can flip it does CPUID exist.
-
-static bool hasCpuid(void) {
- uint32_t before;
- uint32_t after;
-
- __asm__ __volatile__ (
- "pushfl\n\t"
- "popl %%eax\n\t"
- "movl %%eax, %0\n\t"
- "xorl %2, %%eax\n\t"
- "pushl %%eax\n\t"
- "popfl\n\t"
- "pushfl\n\t"
- "popl %%eax\n\t"
- "movl %%eax, %1\n\t"
- : "=r"(before), "=r"(after)
- : "i"(EFLAGS_ID_BIT)
- : "eax"
- );
-
- __asm__ __volatile__ (
- "pushl %0\n\t"
- "popfl"
- :
- : "r"(before)
- );
-
- return ((before ^ after) & EFLAGS_ID_BIT) != 0;
-}
-
-
// Point the real-mode INT 24h vector at sCritStub, copied into a paragraph
// of DOS memory, so every critical error fails the DOS call. Given back
// to DOS's handler only while a DOS program runs (critErrorOwn): DOS
@@ -1037,7 +970,7 @@ static void int8Handler(void) {
TimerServiceT *t = &platformTimer;
if (t->tscPresent) {
- uint64_t now = readTsc();
+ uint64_t now = platformReadTsc();
if (t->tscAtIrq != 0) {
t->tscPeriod = now - t->tscAtIrq;
@@ -1246,6 +1179,45 @@ static void modeXSet(void) {
}
+// The input is polled once a frame of the shell, so a click can begin and
+// end between two polls, or a double-click's release and second press
+// can: the button state alone then misses them. The driver's counters
+// (fn 05h / 06h, cleared by each read) catch them, reported as one state
+// more -- the other half of the edge comes with the next poll.
+static int32_t mouseMissedEdges(int32_t buttons) {
+ __dpmi_regs r;
+
+ for (int32_t b = 0; b < MOUSE_BUTTONS; b++) {
+ int32_t bit = 1 << b;
+
+ memset(&r, 0, sizeof(r));
+ r.x.ax = MOUSE_FN_PRESS_INFO;
+ r.x.bx = (uint16_t)b;
+ __dpmi_int(INT_MOUSE, &r);
+
+ int32_t presses = r.x.bx;
+
+ memset(&r, 0, sizeof(r));
+ r.x.ax = MOUSE_FN_RELEASE_INFO;
+ r.x.bx = (uint16_t)b;
+ __dpmi_int(INT_MOUSE, &r);
+
+ int32_t releases = r.x.bx;
+ bool down = (buttons & bit) != 0;
+ bool wasDown = (sMouseLastButtons & bit) != 0;
+
+ if (!down && !wasDown && presses > 0) {
+ buttons |= bit;
+ } else if (down && wasDown && releases > 0) {
+ buttons &= ~bit;
+ }
+ }
+
+ sMouseLastButtons = buttons;
+ return buttons;
+}
+
+
// 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) {
@@ -1434,23 +1406,6 @@ int32_t platformDriveList(PlatformDriveT *out, int32_t max) {
}
-// High-resolution timestamp for input-timing entropy: the TSC when the CPU
-// has one, else the BIOS tick count with the live PIT channel 0 counter
-// (~0.84 us resolution) in the low 16 bits.
-uint64_t platformEntropyStamp(void) {
- if (cpuHasTsc()) {
- return readTsc();
- }
-
- outportb(PIT_CMD_PORT, PIT_LATCH_CH0);
-
- uint32_t countLo = inportb(PIT_CH0_PORT);
- uint32_t countHi = inportb(PIT_CH0_PORT);
- uint32_t ticks = _farpeekl(_dos_ds, BIOS_TICK_ADDR);
-
- return ((uint64_t)ticks << PIT_COUNT_BITS) | (countHi << BYTE_BITS) | countLo;
-}
-
// Copies a dirty rectangle from the system RAM backbuffer to video
// memory. This is the critical path for display updates -- the
@@ -1524,7 +1479,14 @@ const char *platformGetSystemInfo(const DisplayT *display) {
__dpmi_version_ret dpmiVer;
bool haveDpmiVer = (__dpmi_get_version(&dpmiVer) == 0);
- sSysInfoPos = 0;
+ free(sSysInfoBuf);
+ sSysInfoBuf = (char *)malloc(PLATFORM_SYSINFO_MAX);
+ sSysInfoPos = 0;
+
+ if (!sSysInfoBuf) {
+ return "";
+ }
+
sSysInfoBuf[0] = '\0';
sysInfoAppend("DVX System Information");
@@ -1538,7 +1500,7 @@ const char *platformGetSystemInfo(const DisplayT *display) {
sysInfoAppend("CPU Type: %d86", dpmiVer.cpu);
}
- if (!hasCpuid()) {
+ if (!platformHasCpuid()) {
sysInfoAppend("(no CPUID support)");
} else {
uint32_t maxFunc;
@@ -1774,6 +1736,9 @@ const char *platformGetSystemInfo(const DisplayT *display) {
sysInfoAppend("=== Disk Drives ===");
sysInfoDrives();
+ char *fit = (char *)realloc(sSysInfoBuf, (size_t)sSysInfoPos + 1);
+
+ sSysInfoBuf = fit ? fit : sSysInfoBuf;
return sSysInfoBuf;
}
@@ -1806,6 +1771,13 @@ void platformInit(void) {
// 4. Critical errors fail the call instead of prompting
installCritErrorHandler();
+
+ // stat() without what DVX never reads: inode numbers, execute bits (the
+ // C library opens a file of an unknown extension to read its magic),
+ // a folder's entry count as its size, the root's time stamp, and
+ // fstat's write bit. Its default does all of them, a few DOS calls
+ // more per stat on every listing.
+ _djstat_flags |= _STAT_INODE | _STAT_EXEC_EXT | _STAT_EXEC_MAGIC | _STAT_DIRSIZE | _STAT_ROOT_TIME | _STAT_WRITEBIT;
}
@@ -2060,7 +2032,7 @@ uint64_t platformMicros(void) {
uint32_t elapsed = 0;
if (t->tscPeriod != 0) {
- elapsed = timerTscElapsed(readTsc(), t->tscAtIrq, t->tscPeriod, t->tscPeriodDivisor, t->divisor);
+ elapsed = timerTscElapsed(platformReadTsc(), t->tscAtIrq, t->tscPeriod, t->tscPeriodDivisor, t->divisor);
} else {
elapsed = pitLatchElapsed();
}
@@ -2106,12 +2078,13 @@ uint64_t platformMicros(void) {
void platformMouseInit(int32_t screenW, int32_t screenH) {
__dpmi_regs r;
- sMouseRangeW = screenW;
- sMouseRangeH = screenH;
- sCurX = screenW / 2;
- sCurY = screenH / 2;
- sMouseMode = MouseModeMickeyE;
- sPromoteCounter = 0;
+ sMouseRangeW = screenW;
+ sMouseRangeH = screenH;
+ sCurX = screenW / 2;
+ sCurY = screenH / 2;
+ sMouseMode = MouseModeMickeyE;
+ sPromoteCounter = 0;
+ sMouseLastButtons = 0;
// Function 00h: reset driver, detect mouse hardware (BX = buttons).
memset(&r, 0, sizeof(r));
@@ -2231,6 +2204,10 @@ void platformMousePoll(int32_t *mx, int32_t *my, int32_t *buttons) {
sCurY += (int16_t)r.x.dx;
}
+ if (!sInjectMouse) {
+ *buttons = mouseMissedEdges(*buttons);
+ }
+
if (sInjectCount > 0) {
sInjectLast = sInjectQueue[sInjectHead];
sInjectHead = (sInjectHead + 1) % INJECT_MOUSE_MAX;
@@ -2380,6 +2357,13 @@ void platformRegisterSymOverrides(const PlatformSymOverrideT *entries) {
}
+// DJGPP's last resort for a name no registered table and no loaded
+// module has (dlopen asks it for each import, dlsym(NULL) for its name).
+void platformSetSymbolResolver(PlatformSymbolResolverFnT fn) {
+ _dlsymresolver = fn;
+}
+
+
// INT 21h AX=7160h CL=1, the long-name call for the short form of an
// existing path (CH=0: a SUBSTed drive as its true path). Not the C
// library's _truename_sfn: that asks with CL=0 (the fully qualified
@@ -2717,7 +2701,7 @@ void platformTimerInit(void) {
memset(&platformTimer, 0, sizeof(platformTimer));
platformTimer.divisor = PLATFORM_PIT_BIOS_DIVISOR;
- platformTimer.tscPresent = cpuHasTsc();
+ platformTimer.tscPresent = platformHasTsc();
uint32_t state = platformIrqSave();
@@ -2746,7 +2730,7 @@ void platformTimerProgram(uint32_t divisor) {
outportb(PIT_CH0_PORT, (divisor >> BYTE_BITS) & BYTE_MASK);
if (platformTimer.tscPresent) {
- platformTimer.tscAtIrq = readTsc();
+ platformTimer.tscAtIrq = platformReadTsc();
}
timerRestartPeriod(&platformTimer, elapsed, divisor);
@@ -2804,11 +2788,16 @@ void platformVideoEnumModes(void (*cb)(int32_t w, int32_t h, int32_t bpp, void *
return;
}
- // Static: this can run on an app task's small stack.
- static VbeModeT seen[MAX_VBE_MODES];
- uint16_t modes[MAX_VBE_MODES];
- int32_t seenCount = 0;
- int32_t modeCount = vbeReadModeList(modes);
+ // The shapes reported so far, on the heap: this can run on an app
+ // task's small stack
+ uint16_t modes[MAX_VBE_MODES];
+ int32_t seenCount = 0;
+ int32_t modeCount = vbeReadModeList(modes);
+ ModeKeyT *seen = (ModeKeyT *)malloc((size_t)(modeCount > 0 ? modeCount : 1) * sizeof(ModeKeyT));
+
+ if (!seen) {
+ return;
+ }
for (int32_t i = 0; i < modeCount; i++) {
VbeModeT m;
@@ -2830,9 +2819,14 @@ void platformVideoEnumModes(void (*cb)(int32_t w, int32_t h, int32_t bpp, void *
continue;
}
- seen[seenCount++] = m;
+ seen[seenCount].width = m.width;
+ seen[seenCount].height = m.height;
+ seen[seenCount].bpp = m.bpp;
+ seenCount++;
cb(m.width, m.height, m.bpp, userData);
}
+
+ free(seen);
}
@@ -3164,15 +3158,6 @@ void platformYield(void) {
}
-// Time-stamp counter (the caller knows the CPU has one: cpuHasTsc).
-ISR_INLINE uint64_t readTsc(void) {
- uint32_t lo;
- uint32_t hi;
-
- __asm__ __volatile__ ("rdtsc" : "=a"(lo), "=d"(hi));
- return ((uint64_t)hi << TSC_HI_SHIFT) | lo;
-}
-
// The linear region of a near pointer range in this program's CS (isCode)
// or DS space, for the DPMI lock/unlock calls.
@@ -3603,9 +3588,15 @@ static void videoError(const char *fmt, ...) {
// listed here and registered via dlregsym() before any dlopen().
//
// Three categories:
-// 1. Platform functions (all platform*() from this file)
+// 1. Platform functions (every platform*() a module may call: the
+// loader's own start-up ones, platformRegisterDxeExports,
+// platformRegisterSymOverrides, platformUserInit, platformSetLogPath,
+// platformSetLogMemory, platformBootNoteAdd, platformSetReadOnly,
+// platformSetModuleOpen, platformSetSymbolResolver, and
+// platformDxeExports, are not exported)
// 2. C runtime (libc, libm, libgcc, DJGPP internals)
-// 3. DXE3 runtime (dlopen/dlsym/dlclose/dlerror/dlregsym)
+// 3. DXE3 runtime (dlsym/dlclose/dlerror/dlregsym; not dlopen: a module
+// opens one with platformModuleOpen, which loads what it needs)
// DJGPP stdio internals
extern FILE __dj_stdin;
@@ -3668,62 +3659,39 @@ DXE_EXPORT_TABLE(sDxeExportTable)
DXE_EXPORT(dvxMemAppIdPtr)
DXE_EXPORT(dvxMemGetAppUsage)
DXE_EXPORT(dvxMemResetApp)
+ DXE_EXPORT(dvxMemSetApp)
DXE_EXPORT(dvxNextArg)
DXE_EXPORT(dvxReadDir)
DXE_EXPORT(dvxReadDirFree)
DXE_EXPORT(dvxRealloc)
DXE_EXPORT(dvxSkipWs)
DXE_EXPORT(dvxStrAppendf)
+ DXE_EXPORT(dvxStrAppendfCut)
DXE_EXPORT(dvxStrdup)
DXE_EXPORT(dvxTrimRight)
- // --- help compiler (dvxhlpc.c linked into dvx.exe; BASIC HelpCompile) ---
- DXE_EXPORT(hlpcCompile)
- DXE_EXPORT(hlpcSetMessageFn)
- // --- preferences (dvxPrefs.c linked into dvx.exe; the only resident copy) ---
- DXE_EXPORT(prefsAccelChoices)
- DXE_EXPORT(prefsChoiceIndex)
- DXE_EXPORT(prefsChoiceIndexOfValue)
- DXE_EXPORT(prefsChoiceLabels)
- DXE_EXPORT(prefsClose)
- DXE_EXPORT(prefsCreate)
- DXE_EXPORT(prefsGetBool)
- DXE_EXPORT(prefsGetChoice)
- DXE_EXPORT(prefsGetInt)
- DXE_EXPORT(prefsGetSecret)
- DXE_EXPORT(prefsGetString)
- DXE_EXPORT(prefsGetVideoMode)
- DXE_EXPORT(prefsKeyName)
- DXE_EXPORT(prefsLoad)
- DXE_EXPORT(prefsRemove)
- DXE_EXPORT(prefsSave)
- DXE_EXPORT(prefsSaveAs)
- DXE_EXPORT(prefsSetBool)
- DXE_EXPORT(prefsSetInt)
- DXE_EXPORT(prefsSetSecret)
- DXE_EXPORT(prefsSetString)
- DXE_EXPORT(prefsSetVideoMode)
- DXE_EXPORT(prefsWallpaperChoices)
- DXE_EXPORT(prefsWheelDirChoices)
// --- platform ---
DXE_EXPORT(platformAltScanToChar)
DXE_EXPORT(platformAppUserDir)
DXE_EXPORT(platformBiosScanCode)
+ DXE_EXPORT(platformBiosTicks)
DXE_EXPORT(platformBootDrive)
- DXE_EXPORT(platformBootNoteAdd)
DXE_EXPORT(platformBootNotes)
DXE_EXPORT(platformChdir)
DXE_EXPORT(platformClock)
DXE_EXPORT(platformClockHz)
DXE_EXPORT(platformCopyFile)
+ DXE_EXPORT(platformCopyFileWith)
+ DXE_EXPORT(platformDirEntryIsDir)
DXE_EXPORT(platformDirIsWritable)
DXE_EXPORT(platformDiskSpace)
DXE_EXPORT(platformDosRun)
DXE_EXPORT(platformDriveList)
DXE_EXPORT(platformDvxIniPath)
DXE_EXPORT(platformEntropyStamp)
+ DXE_EXPORT(platformFileStamp)
DXE_EXPORT(platformFilterMatch)
DXE_EXPORT(platformFlushRect)
DXE_EXPORT(platformGetMemoryInfo)
@@ -3735,6 +3703,8 @@ DXE_EXPORT_TABLE(sDxeExportTable)
DXE_EXPORT(platformInjectMouse)
DXE_EXPORT(platformInjectScan)
DXE_EXPORT(platformInstallCrashHandler)
+ DXE_EXPORT(platformInstalledChangeCount)
+ DXE_EXPORT(platformInstalledChanged)
DXE_EXPORT(platformIrqRestore)
DXE_EXPORT(platformIrqSave)
DXE_EXPORT(platformIsReadOnly)
@@ -3760,6 +3730,7 @@ DXE_EXPORT_TABLE(sDxeExportTable)
DXE_EXPORT(platformLongNames)
DXE_EXPORT(platformMicros)
DXE_EXPORT(platformMkdirRecursive)
+ DXE_EXPORT(platformModuleOpen)
DXE_EXPORT(platformMouseInit)
DXE_EXPORT(platformMousePoll)
DXE_EXPORT(platformMouseSetAccel)
@@ -3768,26 +3739,23 @@ DXE_EXPORT_TABLE(sDxeExportTable)
DXE_EXPORT(platformMouseWheelInit)
DXE_EXPORT(platformMouseWheelPoll)
DXE_EXPORT(platformNormalizeFKeyScan)
+ DXE_EXPORT(platformPathAbsolute)
DXE_EXPORT(platformPathBaseName)
DXE_EXPORT(platformPathDirEnd)
- DXE_EXPORT(platformPathAbsolute)
DXE_EXPORT(platformPathDirName)
DXE_EXPORT(platformPathIsAbsolute)
+ DXE_EXPORT(platformPathIsRoot)
DXE_EXPORT(platformPathIsSep)
DXE_EXPORT(platformPathIsUnder)
DXE_EXPORT(platformPathJoin)
DXE_EXPORT(platformPathNative)
+ DXE_EXPORT(platformPathParent)
DXE_EXPORT(platformPathSame)
DXE_EXPORT(platformPathSibling)
DXE_EXPORT(platformPathTidy)
DXE_EXPORT(platformReadFile)
- DXE_EXPORT(platformRegisterDxeExports)
- DXE_EXPORT(platformRegisterSymOverrides)
DXE_EXPORT(platformReplaceFile)
DXE_EXPORT(platformSetAppExitHook)
- DXE_EXPORT(platformSetLogMemory)
- DXE_EXPORT(platformSetLogPath)
- DXE_EXPORT(platformSetReadOnly)
DXE_EXPORT(platformShortPath)
DXE_EXPORT(platformShutdownHookAdd)
DXE_EXPORT(platformShutdownHookRemove)
@@ -3812,7 +3780,6 @@ DXE_EXPORT_TABLE(sDxeExportTable)
DXE_EXPORT(platformUnlockRegion)
DXE_EXPORT(platformUserDir)
DXE_EXPORT(platformUserDirOf)
- DXE_EXPORT(platformUserInit)
DXE_EXPORT(platformUserInTemp)
DXE_EXPORT(platformUserPath)
DXE_EXPORT(platformValidateFilename)
@@ -3829,6 +3796,7 @@ DXE_EXPORT_TABLE(sDxeExportTable)
DXE_EXPORT(platformVideoSetPalette)
DXE_EXPORT(platformVideoShutdown)
DXE_EXPORT(platformVideoWaitRetrace)
+ DXE_EXPORT(platformWalkDir)
DXE_EXPORT(platformWriteFile)
DXE_EXPORT(platformYield)
@@ -3944,6 +3912,7 @@ DXE_EXPORT_TABLE(sDxeExportTable)
DXE_EXPORT(remove)
DXE_EXPORT(rename)
DXE_EXPORT(rewind)
+ DXE_EXPORT(setvbuf)
DXE_EXPORT(tmpfile)
DXE_EXPORT(tmpnam)
@@ -3967,6 +3936,7 @@ DXE_EXPORT_TABLE(sDxeExportTable)
DXE_EXPORT(realpath)
DXE_EXPORT(stat)
DXE_EXPORT(unlink)
+ DXE_EXPORT(utime)
// --- time ---
DXE_EXPORT(asctime)
@@ -4067,7 +4037,6 @@ DXE_EXPORT_TABLE(sDxeExportTable)
// --- DXE3 runtime ---
DXE_EXPORT(dlclose)
DXE_EXPORT(dlerror)
- DXE_EXPORT(dlopen)
DXE_EXPORT(dlregsym)
DXE_EXPORT(dlsym)
diff --git a/src/libs/kpunch/libdvx/platform/dvxPlatformHost.c b/src/libs/kpunch/libdvx/platform/dvxPlatformHost.c
index 9e64e264..b55fd5c6 100644
--- a/src/libs/kpunch/libdvx/platform/dvxPlatformHost.c
+++ b/src/libs/kpunch/libdvx/platform/dvxPlatformHost.c
@@ -46,13 +46,14 @@
// Everything test-facing is declared in dvxPlatHost.h; everything DVX
// proper sees is the dvxPlat.h contract.
+#include "dvxApp.h"
+#include "dvxImgInt.h"
#include "dvxPlat.h"
#include "dvxPlatHost.h"
#include "dvxPlatInt.h"
#include "dvxPlatIsr.h"
#include "dvxPal.h"
-#include "thirdparty/stb_image_write_wrap.h"
#include
#include
@@ -91,9 +92,6 @@
// BIOS shift-state byte: only the low 8 bits are meaningful.
#define MODIFIER_MASK 0xFF
-// PNG output is 3 bytes per pixel (RGB).
-#define PNG_COMPONENTS 3
-
// Kilobytes per byte divisor for platformGetMemoryInfo.
#define BYTES_PER_KB 1024
@@ -166,6 +164,8 @@ static uint32_t sTicks = 0;
static uint64_t sMicros = 0;
static uint32_t sTickRemUs = 0;
static uint32_t sYieldStepUs = 0; // hostClockSetYieldStep
+static uint32_t sReadStepMs = 0; // hostClockSetReadStep
+static uint32_t sMicrosStepUs = 0; // hostClockSetMicrosStep
static bool sRealTime = false;
static uint64_t sRealTimeBase = WALL_CLOCK_UNSET; // wall us when real-time mode began
static uint32_t sRealTimeStart = 0; // fake ticks when real-time mode began
@@ -323,6 +323,16 @@ void hostClockSet(uint32_t ms) {
}
+void hostClockSetMicrosStep(uint32_t us) {
+ sMicrosStepUs = us;
+}
+
+
+void hostClockSetReadStep(uint32_t ms) {
+ sReadStepMs = ms;
+}
+
+
void hostClockSetRealTime(bool realTime) {
if (realTime == sRealTime) {
return;
@@ -402,56 +412,19 @@ bool hostSavePng(const char *path) {
return false;
}
- size_t rowBytes = (size_t)d->width * PNG_COMPONENTS;
- uint8_t *rgb = (uint8_t *)malloc(rowBytes * (size_t)d->height);
+ // In the palette the screen shows, which a full-screen program may
+ // have set (platformVideoSetPalette), not the desktop's
+ DisplayT shown = *d;
+
+ shown.palette = sPalette;
+
+ uint8_t *rgb = dvxBufferToRgb(&shown, d->lfb, d->width, d->height, d->pitch);
if (!rgb) {
return false;
}
- const PixelFormatT *f = &d->format;
-
- for (int32_t y = 0; y < d->height; y++) {
- const uint8_t *src = d->lfb + (size_t)y * (size_t)d->pitch;
- uint8_t *dst = rgb + (size_t)y * rowBytes;
-
- for (int32_t x = 0; x < d->width; x++) {
- uint32_t pixel;
-
- switch (f->bytesPerPixel) {
- case 1:
- pixel = src[x];
- break;
- case 2:
- pixel = ((const uint16_t *)src)[x];
- break;
- default:
- pixel = ((const uint32_t *)src)[x];
- break;
- }
-
- if (f->bitsPerPixel == HOST_BPP_8) {
- const uint8_t *entry = sPalette + (pixel & (PAL_ENTRIES - 1)) * PAL_CHANNELS;
-
- dst[0] = entry[0];
- dst[1] = entry[1];
- dst[2] = entry[2];
- } else {
- uint32_t r = (pixel & f->redMask) >> f->redShift;
- uint32_t g = (pixel & f->greenMask) >> f->greenShift;
- uint32_t b = (pixel & f->blueMask) >> f->blueShift;
-
- // Scale each field up to 8 bits by replicating its top bits.
- dst[0] = (uint8_t)((r << (8 - f->redBits)) | (r >> (2 * f->redBits - 8)));
- dst[1] = (uint8_t)((g << (8 - f->greenBits)) | (g >> (2 * f->greenBits - 8)));
- dst[2] = (uint8_t)((b << (8 - f->blueBits)) | (b >> (2 * f->blueBits - 8)));
- }
-
- dst += PNG_COMPONENTS;
- }
- }
-
- bool ok = stbi_write_png(path, d->width, d->height, PNG_COMPONENTS, rgb, (int)rowBytes) != 0;
+ bool ok = dvxImgWrite(path, false, d->width, d->height, rgb);
free(rgb);
return ok;
@@ -479,12 +452,20 @@ char platformBootDrive(void) {
}
+// The fake clock moves on by the read step after each read
+// (hostClockSetReadStep).
PlatformTicksT platformClock(void) {
if (sRealTime) {
return sRealTimeStart + (uint32_t)((wallClockUs() - sRealTimeBase) / US_PER_MS);
}
- return sTicks;
+ PlatformTicksT now = sTicks;
+
+ if (sReadStepMs > 0) {
+ hostClockAdvance((int32_t)sReadStepMs);
+ }
+
+ return now;
}
@@ -722,12 +703,19 @@ bool platformLongNames(const char *path) {
}
+// (hostClockSetMicrosStep).
uint64_t platformMicros(void) {
if (sRealTime) {
return sRealTimeMicros + (wallClockUs() - sRealTimeBase);
}
- return sMicros;
+ uint64_t now = sMicros;
+
+ if (sMicrosStepUs > 0) {
+ hostClockAdvanceUs(sMicrosStepUs);
+ }
+
+ return now;
}
@@ -796,6 +784,13 @@ void platformRegisterSymOverrides(const PlatformSymOverrideT *entries) {
}
+// The host's dynamic linker has no last resort to hook: a name it cannot
+// find stays missing.
+void platformSetSymbolResolver(PlatformSymbolResolverFnT fn) {
+ (void)fn;
+}
+
+
// Host file systems have no short names: path itself.
bool platformShortPath(const char *path, char *out, int32_t outSize) {
return snprintf(out, (size_t)outSize, "%s", path) < outSize;
diff --git a/src/libs/kpunch/libdvx/platform/dvxPlatformIrq.c b/src/libs/kpunch/libdvx/platform/dvxPlatformIrq.c
index 00821a4f..605a8c86 100644
--- a/src/libs/kpunch/libdvx/platform/dvxPlatformIrq.c
+++ b/src/libs/kpunch/libdvx/platform/dvxPlatformIrq.c
@@ -46,6 +46,7 @@ typedef void (*HookFnT)(void);
static bool hookAdd(HookFnT *table, int32_t count, HookFnT fn);
static void hookRemove(HookFnT *table, int32_t count, HookFnT fn);
+const volatile uint32_t *platformBiosTicks(void);
void platformInjectScan(uint8_t scan);
bool platformKeyEventRead(RawKeyEventT *evt);
bool platformKeyIsDown(int32_t rawCode);
@@ -111,6 +112,11 @@ static void hookRemove(HookFnT *table, int32_t count, HookFnT fn) {
}
+const volatile uint32_t *platformBiosTicks(void) {
+ return &platformTimer.biosTicks;
+}
+
+
void platformInjectScan(uint8_t scan) {
uint32_t state = platformIrqSave();
diff --git a/src/libs/kpunch/libdvx/platform/dvxPlatformUtil.c b/src/libs/kpunch/libdvx/platform/dvxPlatformUtil.c
index d1fc8e1f..4fc70284 100644
--- a/src/libs/kpunch/libdvx/platform/dvxPlatformUtil.c
+++ b/src/libs/kpunch/libdvx/platform/dvxPlatformUtil.c
@@ -30,10 +30,12 @@
#include "dvxPlat.h"
#include "dvxPlatInt.h"
+#include "dvxEndian.h"
#include "thirdparty/stb_ds_wrap.h"
#include
#include
+#include
#include
#include
#include
@@ -48,6 +50,7 @@
#include
#include
#include
+#include
#include
#else
#include
@@ -56,6 +59,17 @@
// Permission bits for directories created by platformMkdirRecursive (rwxr-xr-x).
#define MKDIR_MODE 0755
+// platformFileStamp: what makes a path a pattern, the files a DOS search
+// finds (not folders), and where DOS's date goes in the stamp
+#define STAMP_WILDCARDS "*?"
+#define STAMP_FIND_ATTR (FA_RDONLY | FA_HIDDEN | FA_SYSTEM | FA_ARCH)
+#define DOS_DATE_SHIFT 16
+
+// platformEntropyStamp on DOS: the PIT count goes in the TSC's top bits,
+// which stay 0 for years of uptime; the latched count reads low byte first
+#define ENTROPY_PIT_SHIFT 48
+#define PIT_BYTE_BITS 8
+
// platformDiskSpace on DOS: INT 21h AH=36h (at most 2 GB), and the FAT32
// call 7303h with its record (the version word at +2 goes in as 0) in the
// transfer buffer after the "X:\" root
@@ -130,6 +144,18 @@
#define NAME_CHAR_FIRST '!' // an 8.3 name holds '!'..'~' only
#define NAME_CHAR_LAST '~'
+// The DXE3 file header (DJGPP sys/dxe.h, dxe3_header): its size and the
+// fields platformDxeExports reads. An export entry is a 32-bit offset,
+// then the name and its NUL.
+#define DXE3_MAGIC 0x31455844u // "DXE1", which every DXE file starts with
+#define DXE3_HEADER_BYTES 80
+#define DXE3_EXP_COUNT_OFS 28
+#define DXE3_EXP_TABLE_OFS 32
+#define DXE3_EXP_SIZE_OFS 36
+#define DXE3_INIT_OFS 72
+#define DXE3_NO_INIT 0xFFFFFFFFu // _init of a module without constructors
+#define DXE3_SYM_OFFSET_SIZE 4
+#define DXE3_EXP_SIZE_MAX (1024u * 1024u)
// ============================================================
@@ -150,6 +176,23 @@ typedef struct {
int32_t higherMods;
} ScanFoldT;
+// A name platformWalkDir read, and whether it is a folder
+typedef struct {
+ char *name;
+ bool isDir;
+} WalkEntT;
+
+#ifdef __DJGPP__
+// The time stamp counter's presence, probed on first use (platformHasTsc)
+typedef enum {
+ TscUnknownE = 0,
+ TscAbsentE = 1,
+ TscPresentE = 2
+} TscStateE;
+
+static TscStateE sTscState = TscUnknownE;
+#endif
+
// ============================================================
// Prototypes
@@ -165,6 +208,7 @@ char **dvxReadDir(const char *dirPath);
void dvxReadDirFree(char **entries);
const char *dvxSkipWs(const char *s);
int32_t dvxStrAppendf(char *buf, int32_t bufSize, int32_t pos, const char *fmt, ...);
+int32_t dvxStrAppendfCut(char *buf, int32_t bufSize, int32_t pos, const char *fmt, ...);
int32_t dvxTrimRight(char *buf);
static const ScanFoldT *findScanFold(int32_t scancode);
static bool globMatchLen(const char *pat, int32_t patLen, const char *name);
@@ -184,11 +228,21 @@ void platformBootNoteAdd(const char *text);
const char *platformBootNotes(void);
int32_t platformChdir(const char *path);
bool platformCopyFile(const char *srcPath, const char *dstPath);
+bool platformCopyFileWith(const char *srcPath, const char *dstPath, bool (*goOn)(void *ctx), void *ctx);
bool platformDirIsWritable(const char *dir);
bool platformDiskSpace(const char *path, uint64_t *totalBytes, uint64_t *freeBytes);
const char *platformDvxIniPath(void);
+bool platformDxeExports(const char *path, PlatformDxeNameFnT fn, void *ctx, bool *runsAtLoad);
+#ifdef __DJGPP__
+uint64_t platformEntropyStamp(void);
+#endif
+bool platformFileStamp(const char *path, PlatformStampT *out);
bool platformFilterMatch(const char *name, const char *filter);
bool platformGlobMatch(const char *pattern, const char *name);
+#ifdef __DJGPP__
+bool platformHasCpuid(void);
+bool platformHasTsc(void);
+#endif
bool platformIsReadOnly(void);
bool platformKindBrowseDir(char *out, int32_t outSize, DvxKindE kind);
bool platformKindDir(char *out, int32_t outSize, DvxKindE kind, bool user);
@@ -199,6 +253,7 @@ char **platformKindReadDir(DvxKindE kind);
const char *platformLogMemory(void);
bool platformLongNameChar(char c);
int32_t platformMkdirRecursive(const char *path);
+void *platformModuleOpen(const char *path, int32_t mode);
int32_t platformNormalizeFKeyScan(int32_t scancode);
void platformNormalizeKeyEvent(PlatformKeyEventT *evt, int32_t liveMods);
void platformOutOfMemory(size_t bytes);
@@ -207,10 +262,12 @@ const char *platformPathBaseName(const char *path);
char *platformPathDirEnd(const char *path);
bool platformPathDirName(const char *path, char *out, int32_t outSize);
bool platformPathIsAbsolute(const char *path);
+bool platformPathIsRoot(const char *path);
bool platformPathIsSep(char c);
const char *platformPathIsUnder(const char *path, const char *dir);
bool platformPathJoin(char *out, int32_t outSize, const char *dir, const char *name);
char *platformPathNative(char *path);
+bool platformPathParent(const char *path, char *out, int32_t outSize);
bool platformPathSame(const char *a, const char *b);
bool platformPathSibling(char *out, int32_t outSize, const char *path, const char *ext);
char *platformPathTidy(char *path);
@@ -219,6 +276,7 @@ bool platformReplaceFile(const char *tmpPath, const char *pat
void platformSetAppExitHook(PlatformAppExitFnT fn);
void platformSetLogMemory(void);
void platformSetLogPath(const char *path);
+void platformSetModuleOpen(PlatformModuleOpenFnT fn);
void platformSetReadOnly(bool readOnly);
uint64_t platformStatSize(const struct stat *st);
int32_t platformStripLineEndings(char *buf, int32_t len);
@@ -230,7 +288,9 @@ bool platformUserInTemp(void);
bool platformUserPath(char *out, int32_t outSize, const char *rel);
const char *platformValidateLongName(const char *name);
const char *platformValidateShortName(const char *name);
+bool platformWalkDir(const char *dir, PlatformWalkFnT fn, void *ctx);
bool platformWriteFile(const char *path, const void *data, int32_t len);
+static int32_t strAppendV(char *buf, int32_t bufSize, int32_t pos, bool cut, const char *fmt, va_list args);
// ============================================================
@@ -241,13 +301,18 @@ bool platformWriteFile(const char *path, const void *data, in
// (platformSetLogMemory).
static const char *sLogPath = DVX_LOG_FILE;
-// The memory log and its length (without the NUL)
-static char sLogMem[PLATFORM_LOG_MEM_MAX];
+// The memory log (PLATFORM_LOG_MEM_MAX bytes, made by the first
+// platformSetLogMemory) and its length (without the NUL)
+static char *sLogMem = NULL;
static int32_t sLogMemLen = 0;
// Nothing can be saved: no writable user folder (platformUserInit)
static bool sReadOnly = false;
+// How often what is installed changed (platformInstalledChanged; wraps
+// around)
+static uint32_t sInstalledChanges = 0;
+
// The loader's notes for the shell, one per line, and whether they ran out
// of room (platformBootNoteAdd)
static char sBootNotes[PLATFORM_BOOT_NOTES_MAX];
@@ -273,6 +338,9 @@ static const char *const sDeviceNames[] = {
// Where exit() and abort() of loaded modules go first (platformAppExit).
static PlatformAppExitFnT sAppExitHook = NULL;
+// The loader's module opening (platformModuleOpen), set at its start
+static PlatformModuleOpenFnT sModuleOpen = NULL;
+
// Every modifier-specific scancode run the keyboard path folds.
static const ScanFoldT sScanFolds[] = {
{ FKEY_SCAN_SHIFT_F1, FKEY_LOW_COUNT, KEY_SCAN_F1, KEY_MOD_SHIFT, 0, KEY_MOD_CTRL | KEY_MOD_ALT },
@@ -491,23 +559,23 @@ const char *dvxSkipWs(const char *s) {
// as pos propagates, so a chain of appends collapses to a single
// truncation check on the final return value.
int32_t dvxStrAppendf(char *buf, int32_t bufSize, int32_t pos, const char *fmt, ...) {
- if (!buf || pos < 0 || pos >= bufSize) {
- return -1;
- }
-
va_list args;
- int32_t avail = bufSize - pos;
- int32_t written;
va_start(args, fmt);
- written = (int32_t)vsnprintf(buf + pos, (size_t)avail, fmt, args);
+ pos = strAppendV(buf, bufSize, pos, false, fmt, args);
va_end(args);
+ return pos;
+}
- if (written < 0 || written >= avail) {
- return -1;
- }
- return pos + written;
+// As dvxStrAppendf, but text that does not fit is cut to what does.
+int32_t dvxStrAppendfCut(char *buf, int32_t bufSize, int32_t pos, const char *fmt, ...) {
+ va_list args;
+
+ va_start(args, fmt);
+ pos = strAppendV(buf, bufSize, pos, true, fmt, args);
+ va_end(args);
+ return pos;
}
@@ -633,6 +701,10 @@ static void logMemAppend(const char *fmt, va_list ap) {
line[n++] = '\n';
+ if (!sLogMem) {
+ return;
+ }
+
int32_t over = sLogMemLen + n - (PLATFORM_LOG_MEM_MAX - 1);
if (over > 0) {
@@ -841,6 +913,11 @@ int32_t platformChdir(const char *path) {
bool platformCopyFile(const char *srcPath, const char *dstPath) {
+ return platformCopyFileWith(srcPath, dstPath, NULL, NULL);
+}
+
+
+bool platformCopyFileWith(const char *srcPath, const char *dstPath, bool (*goOn)(void *ctx), void *ctx) {
// Onto itself: opening the copy would empty the original first
if (platformPathSame(srcPath, dstPath)) {
return true;
@@ -864,7 +941,7 @@ bool platformCopyFile(const char *srcPath, const char *dstPath) {
bool ok = true;
while (ok && (n = fread(buf, 1, sizeof(buf), src)) > 0) {
- ok = (fwrite(buf, 1, n, dst) == n);
+ ok = (fwrite(buf, 1, n, dst) == n) && (!goOn || goOn(ctx));
}
ok = ok && !ferror(src);
@@ -879,6 +956,24 @@ bool platformCopyFile(const char *srcPath, const char *dstPath) {
}
+// DOS's readdir always knows (from the find data it already has); a host
+// file system may say unknown, and a link has to be followed.
+bool platformDirEntryIsDir(const char *dir, const char *name, int32_t type) {
+ char path[DVX_MAX_PATH];
+ struct stat st;
+
+ if (type == DT_DIR) {
+ return true;
+ }
+
+ if (type != DT_UNKNOWN && type != DT_LNK) {
+ return false;
+ }
+
+ return platformPathJoin(path, sizeof(path), dir, name) && stat(path, &st) == 0 && S_ISDIR(st.st_mode);
+}
+
+
// Make and remove a probe file in dir.
bool platformDirIsWritable(const char *dir) {
char path[DVX_MAX_PATH];
@@ -987,6 +1082,113 @@ const char *platformDvxIniPath(void) {
}
+// One read of the header and one of the export table; the names are
+// handed out as the file has them (DJGPP's leading underscore and all).
+bool platformDxeExports(const char *path, PlatformDxeNameFnT fn, void *ctx, bool *runsAtLoad) {
+ uint8_t hdr[DXE3_HEADER_BYTES];
+ uint8_t *table = NULL;
+ FILE *f = fopen(path, "rb");
+ uint32_t count = 0;
+ uint32_t size = 0;
+ uint32_t pos = 0;
+ bool ok;
+
+ if (!f) {
+ return false;
+ }
+
+ ok = fread(hdr, 1, sizeof(hdr), f) == sizeof(hdr) && dvxGetLe32(hdr) == DXE3_MAGIC;
+
+ if (ok) {
+ count = dvxGetLe32(hdr + DXE3_EXP_COUNT_OFS);
+ size = dvxGetLe32(hdr + DXE3_EXP_SIZE_OFS);
+ ok = size <= DXE3_EXP_SIZE_MAX && (table = (uint8_t *)malloc(size + 1)) != NULL;
+ }
+
+ ok = ok && fseek(f, (long)dvxGetLe32(hdr + DXE3_EXP_TABLE_OFS), SEEK_SET) == 0 && fread(table, 1, size, f) == size;
+ fclose(f);
+
+ for (uint32_t i = 0; ok && i < count; i++) {
+ const char *name = (const char *)table + pos + DXE3_SYM_OFFSET_SIZE;
+ size_t left = pos + DXE3_SYM_OFFSET_SIZE < size ? size - pos - DXE3_SYM_OFFSET_SIZE : 0;
+ size_t len = left ? strnlen(name, left) : 0;
+
+ // A name runs to its NUL inside the table, or the file is damaged
+ ok = len < left;
+
+ if (ok) {
+ fn(ctx, name);
+ pos += DXE3_SYM_OFFSET_SIZE + (uint32_t)len + 1;
+ }
+ }
+
+ if (ok) {
+ *runsAtLoad = dvxGetLe32(hdr + DXE3_INIT_OFS) != DXE3_NO_INIT;
+ }
+
+ free(table);
+ return ok;
+}
+
+
+#ifdef __DJGPP__
+// High-resolution timestamp for entropy: the live PIT channel 0 counter
+// (~0.84 us, its own crystal) with the TSC when the CPU has one, so a
+// sample carries the jitter between the two clocks; else with the BIOS
+// tick count above it. The host's is in dvxPlatformHost.c.
+uint64_t platformEntropyStamp(void) {
+ outportb(PIT_CMD_PORT, PIT_LATCH_CH0);
+
+ uint64_t pit = inportb(PIT_CH0_PORT);
+
+ pit |= (uint64_t)inportb(PIT_CH0_PORT) << PIT_BYTE_BITS;
+
+ if (platformHasTsc()) {
+ return platformReadTsc() ^ (pit << ENTROPY_PIT_SHIFT);
+ }
+
+ return ((uint64_t)_farpeekl(_dos_ds, BIOS_TICK_ADDR) << PIT_COUNT_BITS) | pit;
+}
+#endif
+
+
+// DOS: the directory search's own entry (DOS keeps the date in the high
+// half). A long-name search keeps a handle open until a findnext finds
+// nothing more, so one is made. Elsewhere the modification time's
+// seconds and nanoseconds folded together, so a file rewritten within a
+// second still changes its stamp.
+bool platformFileStamp(const char *path, PlatformStampT *out) {
+ if (!path || strpbrk(path, STAMP_WILDCARDS)) {
+ return false;
+ }
+
+#ifdef __DJGPP__
+ struct ffblk ff;
+
+ if (findfirst(path, &ff, STAMP_FIND_ATTR) != 0) {
+ return false;
+ }
+
+ out->size = (uint32_t)ff.ff_fsize;
+ out->time = ((uint32_t)ff.ff_fdate << DOS_DATE_SHIFT) | ff.ff_ftime;
+ findnext(&ff);
+ return true;
+#else
+ struct stat st;
+
+ if (stat(path, &st) != 0 || !S_ISREG(st.st_mode)) {
+ return false;
+ }
+
+ uint64_t size = platformStatSize(&st);
+
+ out->size = size > UINT32_MAX ? UINT32_MAX : (uint32_t)size;
+ out->time = (uint32_t)st.st_mtime ^ (uint32_t)st.st_mtim.tv_nsec;
+ return true;
+#endif
+}
+
+
// Tokens are the runs between FILTER_SEPARATORS; no copy of the filter is
// made, so its length is not limited.
bool platformFilterMatch(const char *name, const char *filter) {
@@ -1023,6 +1225,77 @@ bool platformGlobMatch(const char *pattern, const char *name) {
}
+#ifdef __DJGPP__
+// The CPUID instruction exists if bit 21 (ID flag) of EFLAGS can be
+// toggled. On a 386 this bit is hardwired to 0. On a 486 without
+// CPUID it's also hardwired. Only if we can flip it does CPUID exist.
+bool platformHasCpuid(void) {
+ uint32_t before;
+ uint32_t after;
+
+ __asm__ __volatile__ (
+ "pushfl\n\t"
+ "popl %%eax\n\t"
+ "movl %%eax, %0\n\t"
+ "xorl %2, %%eax\n\t"
+ "pushl %%eax\n\t"
+ "popfl\n\t"
+ "pushfl\n\t"
+ "popl %%eax\n\t"
+ "movl %%eax, %1\n\t"
+ : "=r"(before), "=r"(after)
+ : "i"(EFLAGS_ID_BIT)
+ : "eax"
+ );
+
+ __asm__ __volatile__ (
+ "pushl %0\n\t"
+ "popfl"
+ :
+ : "r"(before)
+ );
+
+ return ((before ^ after) & EFLAGS_ID_BIT) != 0;
+}
+#endif
+
+
+#ifdef __DJGPP__
+// RDTSC exists when CPUID's leaf 1 reports the time stamp counter. A 386
+// or an early 486 has neither.
+bool platformHasTsc(void) {
+ if (sTscState == TscUnknownE) {
+ sTscState = TscAbsentE;
+
+ if (platformHasCpuid()) {
+ uint32_t eax;
+ uint32_t ebx;
+ uint32_t ecx;
+ uint32_t edx;
+
+ __asm__ __volatile__ ("cpuid" : "=a"(eax), "=b"(ebx), "=c"(ecx), "=d"(edx) : "a"(CPUID_LEAF_FEATURES));
+
+ if (edx & CPUID_FEAT_TSC) {
+ sTscState = TscPresentE;
+ }
+ }
+ }
+
+ return sTscState == TscPresentE;
+}
+#endif
+
+
+uint32_t platformInstalledChangeCount(void) {
+ return sInstalledChanges;
+}
+
+
+void platformInstalledChanged(void) {
+ sInstalledChanges++;
+}
+
+
bool platformIsReadOnly(void) {
return sReadOnly;
}
@@ -1158,7 +1431,7 @@ char **platformKindReadDir(DvxKindE kind) {
const char *platformLogMemory(void) {
- return sLogMem;
+ return sLogMem ? sLogMem : "";
}
@@ -1214,6 +1487,13 @@ int32_t platformMkdirRecursive(const char *path) {
}
+// The loader's way once it has set it (platformSetModuleOpen); until then
+// (the loader itself starting, the host's tests) dlopen.
+void *platformModuleOpen(const char *path, int32_t mode) {
+ return sModuleOpen ? sModuleOpen(path, mode) : dlopen(path, (int)mode);
+}
+
+
// Fold a modifier-specific BIOS scancode (Shift/Ctrl/Alt+Fn, Alt+grey
// navigation key) onto the plain key's scancode.
int32_t platformNormalizeFKeyScan(int32_t scancode) {
@@ -1332,6 +1612,15 @@ bool platformPathIsAbsolute(const char *path) {
}
+bool platformPathIsRoot(const char *path) {
+ if (platformPathIsSep(path[0]) && path[1] == '\0') {
+ return true;
+ }
+
+ return path[0] && path[1] == ':' && (path[2] == '\0' || (platformPathIsSep(path[2]) && path[3] == '\0'));
+}
+
+
bool platformPathIsSep(char c) {
return c == '/' || c == '\\';
}
@@ -1384,6 +1673,22 @@ char *platformPathNative(char *path) {
}
+// The trimmed path's folder: platformPathDirName, once it is known to
+// have one.
+bool platformPathParent(const char *path, char *out, int32_t outSize) {
+ char dir[DVX_MAX_PATH];
+ int32_t len = pathTrimmedLen(path);
+
+ if (platformPathIsRoot(path) || len >= (int32_t)sizeof(dir)) {
+ return false;
+ }
+
+ memcpy(dir, path, (size_t)len);
+ dir[len] = '\0';
+ return platformPathDirEnd(dir) && platformPathDirName(dir, out, outSize);
+}
+
+
bool platformPathSame(const char *a, const char *b) {
return a && b && pathTrimmedLen(a) == pathTrimmedLen(b) && platformPathIsUnder(a, b) != NULL;
}
@@ -1467,7 +1772,10 @@ char *platformReadFile(const char *path, int32_t *outLen) {
long size = ftell(f);
- if (size < 0) {
+ // More than its length can say with room for the terminator, or a
+ // folder (on Linux it opens, and its end is LONG_MAX: INT32_MAX where
+ // long is 32 bits)
+ if (size < 0 || size >= INT32_MAX) {
fclose(f);
return NULL;
}
@@ -1546,6 +1854,10 @@ void platformSetAppExitHook(PlatformAppExitFnT fn) {
void platformSetLogMemory(void) {
+ if (!sLogMem) {
+ sLogMem = (char *)calloc(1, PLATFORM_LOG_MEM_MAX);
+ }
+
sLogPath = NULL;
}
@@ -1555,6 +1867,11 @@ void platformSetLogPath(const char *path) {
}
+void platformSetModuleOpen(PlatformModuleOpenFnT fn) {
+ sModuleOpen = fn;
+}
+
+
void platformSetReadOnly(bool readOnly) {
sReadOnly = readOnly;
}
@@ -1751,6 +2068,49 @@ const char *platformValidateShortName(const char *name) {
}
+bool platformWalkDir(const char *dir, PlatformWalkFnT fn, void *ctx) {
+ DIR *d = opendir(dir);
+ WalkEntT *names = NULL;
+ bool goOn = true;
+
+ if (!d) {
+ return true;
+ }
+
+ for (struct dirent *ent = readdir(d); ent; ent = readdir(d)) {
+ WalkEntT e;
+
+ if (ent->d_name[0] == '.') {
+ continue;
+ }
+
+ e.isDir = platformDirEntryIsDir(dir, ent->d_name, ent->d_type);
+ e.name = strdup(ent->d_name);
+
+ if (e.name) {
+ arrput(names, e);
+ }
+ }
+
+ closedir(d);
+
+ for (int32_t i = 0; goOn && i < (int32_t)arrlen(names); i++) {
+ char path[DVX_MAX_PATH];
+
+ if (platformPathJoin(path, sizeof(path), dir, names[i].name)) {
+ goOn = names[i].isDir ? platformWalkDir(path, fn, ctx) : fn(path, names[i].name, ctx);
+ }
+ }
+
+ for (int32_t i = 0; i < (int32_t)arrlen(names); i++) {
+ free(names[i].name);
+ }
+
+ arrfree(names);
+ return goOn;
+}
+
+
bool platformWriteFile(const char *path, const void *data, int32_t len) {
char tmpPath[DVX_MAX_PATH];
char bakPath[DVX_MAX_PATH];
@@ -1776,3 +2136,29 @@ bool platformWriteFile(const char *path, const void *data, int32_t len) {
return platformReplaceFile(tmpPath, path, bakPath);
}
+
+
+// The appends' shared core: fmt/args into buf at pos. Returns the new
+// position. Text that does not fit returns -1, or with cut the position
+// of the cut (bufSize - 1); a pos outside the buffer or a vsnprintf
+// failure returns -1, or with cut pos unchanged.
+static int32_t strAppendV(char *buf, int32_t bufSize, int32_t pos, bool cut, const char *fmt, va_list args) {
+ int32_t fail = cut ? pos : -1;
+
+ if (!buf || pos < 0 || pos >= bufSize) {
+ return fail;
+ }
+
+ int32_t avail = bufSize - pos;
+ int32_t written = (int32_t)vsnprintf(buf + pos, (size_t)avail, fmt, args);
+
+ if (written < 0) {
+ return fail;
+ }
+
+ if (written >= avail) {
+ return cut ? bufSize - 1 : -1;
+ }
+
+ return pos + written;
+}
diff --git a/src/libs/kpunch/libdvx/sdkGuide.dhs b/src/libs/kpunch/libdvx/sdkGuide.dhs
index 6ef0cdad..40d64a7b 100644
--- a/src/libs/kpunch/libdvx/sdkGuide.dhs
+++ b/src/libs/kpunch/libdvx/sdkGuide.dhs
@@ -301,7 +301,7 @@ The build makes mathx.lib and mathx.hlp (a preview of the help). The project hol
.h2 How the Library Works
-DVX loads every library when it starts, before any widget or program, in the order the .dep files require. Every function that is not static is exported: a program, widget, or library loaded later calls it directly, and DVX connects the call when it loads that module. A library that calls another library's functions lists it in its .dep file.
+DVX loads a library when something first needs it: a program, widget, or library that calls one of its functions (DVX loads the library just before that module), or a lookup by name (dlsym(NULL), a BASIC DECLARE LIBRARY call, dvxLibraryLoaded). Libraries with code that runs as they load (constructors) are loaded when DVX starts, so they can hook into the shell. Every function that is not static is exported: a program, widget, or library calls it directly, and DVX connects the call when it loads that module. A library that calls another library's functions lists it in its .dep file, which DVX loads first. A module that opens another module calls platformModuleOpen (dvxPlat.h), never dlopen, which dvx.exe does not export.
BASIC programs add mathx.bas to their project. DECLARE LIBRARY looks up each function by exactly the name it declares, without a trailing $ (a $ marks a function returning a string), so mathxGreet$ calls the C function mathxGreet. Keep the declarations and the C functions in step; strings are passed as const char * and returned as const char *.
@@ -451,7 +451,7 @@ A control project builds NAME.wgt, a file that holds only resources:
author, version ... The project's metadata; helpfile its help file's name
.endtable
-The loader tells such a file from a module by its first bytes: instead of loading it, it hands it, once every library and native widget is loaded, to the BASIC run-time library's basCtlLoadFile (found with dlsym; without the library the file is skipped and logged). basCtlLoadFile registers each control in the file for every program and the loader records the file as theirs, so the toolbox finds their icon24 and name.
+The loader tells such a file from a module by its first bytes: instead of loading it, it hands it, once every native widget is loaded, to the BASIC run-time library's basCtlLoadFile (found with dlsym; without the library the file is skipped and logged). basCtlLoadFile registers each control in the file for every program and the loader records the file as theirs, so the toolbox finds their icon24 and name.
.h2 The Bridge
@@ -593,7 +593,7 @@ Give acme.KEY to the people who install your packages (Software, File > Trust a
DVX is packages too: kpunch.dvx (the loader, SYSTEM, the shell, the core libraries and widgets, and the everyday programs), kpunch.basefont (the base fonts), and the optional parts Setup's "What to install" page offers. Each part is a bundle (kind bundle: a package with no files of its own that needs its members) and each member is one module in its own package: kpunch.basic (dvxbasic, basdev), kpunch.data (dvxsql, datactrl, dbgrid), kpunch.net (dvxnet, http, pkgnet), kpunch.comm (terminal, conn, xfer; it needs kpunch.net), kpunch.media (player, recorder), kpunch.devtools (iconed, resedit, helpedit, pkgbuild, widshow), and kpunch.demos (dvxdemo, basdemo). src/packages.ini lists them. The build (src/installer/mkPackages.py) works out what each member needs from the modules its code imports, the libraries its BASIC program declares, and the manifest's uses (a module reached by name), and stops when DVX itself, or a part, would need a part it does not name. A member's files outside its folder are its paths: the DOS tools in SYSTEM go with the part they serve (BASCOMP.EXE with kpunch.basdev, DVXRES.EXE with kpunch.resedit, and DVXHLPC.EXE with kpunch.helpedit, the Help Editor); DVXPKG.EXE stays in DVX itself, to install and remove packages at the DOS prompt.
-A module of yours that cannot work without a part's library names the library's package in PKG_NEEDS (PKG_NEEDS="kpunch.dvxnet=1.0.0"): Software then installs it only where that part is. One that can do without it does not import it: it asks dvxLibraryLoaded (BASIC: SysLibraryLoaded, system.bas) and reaches the library by name only when it is there, as Software does with pkgnet. BASIC's SQL functions do nothing without the Databases part (SQLOpen returns 0).
+A module of yours that cannot work without a part's library names the library's package in PKG_NEEDS (PKG_NEEDS="kpunch.dvxnet=1.0.0"): Software then installs it only where that part is. One that can do without it does not import it: it asks dvxLibraryLoaded (BASIC: SysLibraryLoaded, system.bas), which loads the library when it is installed, and reaches the library by name only when it is there, as Software does with pkgnet. Not importing a library also keeps DVX from loading it before it is needed. BASIC's SQL functions do nothing without the Databases part (SQLOpen returns 0).
.h2 Publishing on the Package Server
diff --git a/src/libs/kpunch/libdvx/widgetCore.c b/src/libs/kpunch/libdvx/widgetCore.c
index 1146301e..80470383 100644
--- a/src/libs/kpunch/libdvx/widgetCore.c
+++ b/src/libs/kpunch/libdvx/widgetCore.c
@@ -881,42 +881,8 @@ void widgetRemoveChild(WidgetT *parent, WidgetT *child) {
}
-// Calculates thumb position and size for a scrollbar track.
-// Used by both the WM-level scrollbars and widget-internal scrollbars
-// (ListBox, TreeView, etc.) to maintain consistent scrollbar behavior.
-//
-// The thumb size is proportional to visibleSize/totalSize -- a larger
-// visible area means a larger thumb, giving visual feedback about how
-// much content is scrollable. SB_MIN_THUMB prevents the thumb from
-// becoming too small to grab with a mouse.
-
void widgetScrollbarThumb(int32_t trackLen, int32_t totalSize, int32_t visibleSize, int32_t scrollPos, int32_t *thumbPos, int32_t *thumbSize) {
- // No track or no content: no thumb. Callers need no guard of their own.
- if (trackLen <= 0 || totalSize <= 0) {
- *thumbPos = 0;
- *thumbSize = 0;
- return;
- }
-
- // 64-bit products: pixel scroll ranges (a tree or text area of several
- // hundred thousand 16px rows) overflow int32 once multiplied by the track.
- *thumbSize = (int32_t)(((int64_t)trackLen * visibleSize) / totalSize);
-
- if (*thumbSize < SB_MIN_THUMB) {
- *thumbSize = SB_MIN_THUMB;
- }
-
- if (*thumbSize > trackLen) {
- *thumbSize = trackLen;
- }
-
- int32_t maxScroll = totalSize - visibleSize;
-
- if (maxScroll > 0) {
- *thumbPos = (int32_t)(((int64_t)(trackLen - *thumbSize) * clampInt(scrollPos, 0, maxScroll)) / maxScroll);
- } else {
- *thumbPos = 0;
- }
+ widgetScrollbarThumbMin(trackLen, totalSize, visibleSize, scrollPos, SB_MIN_THUMB, thumbPos, thumbSize);
}
@@ -949,6 +915,44 @@ int32_t widgetScrollbarThumbDragScroll(int32_t trackLen, int32_t total, int32_t
}
+// Calculates thumb position and size for a scrollbar track, for the
+// widget scrollbars (ListBox, TreeView, etc.) and the window manager's
+// alike, so every scrollbar behaves the same.
+//
+// The thumb size is proportional to visibleSize/totalSize -- a larger
+// visible area means a larger thumb, giving visual feedback about how
+// much content is scrollable. minThumb keeps the thumb from becoming too
+// small to grab with a mouse; it never spills past the track.
+void widgetScrollbarThumbMin(int32_t trackLen, int32_t totalSize, int32_t visibleSize, int32_t scrollPos, int32_t minThumb, int32_t *thumbPos, int32_t *thumbSize) {
+ // No track or no content: no thumb. Callers need no guard of their own.
+ if (trackLen <= 0 || totalSize <= 0) {
+ *thumbPos = 0;
+ *thumbSize = 0;
+ return;
+ }
+
+ // 64-bit products: pixel scroll ranges (a tree or text area of several
+ // hundred thousand 16px rows) overflow int32 once multiplied by the track.
+ *thumbSize = (int32_t)(((int64_t)trackLen * visibleSize) / totalSize);
+
+ if (*thumbSize < minThumb) {
+ *thumbSize = minThumb;
+ }
+
+ if (*thumbSize > trackLen) {
+ *thumbSize = trackLen;
+ }
+
+ int32_t maxScroll = totalSize - visibleSize;
+
+ if (maxScroll > 0) {
+ *thumbPos = (int32_t)(((int64_t)(trackLen - *thumbSize) * clampInt(scrollPos, 0, maxScroll)) / maxScroll);
+ } else {
+ *thumbPos = 0;
+ }
+}
+
+
// Moves keyboard focus to w (NULL clears focus). Clears the previous
// widget's selection, repaints both widgets, and fires the blur/focus
// callbacks via widgetFireFocusChange. A no-op when w already has focus.
diff --git a/src/libs/kpunch/libdvx/widgetEvent.c b/src/libs/kpunch/libdvx/widgetEvent.c
index 99b13635..ff3f2b9a 100644
--- a/src/libs/kpunch/libdvx/widgetEvent.c
+++ b/src/libs/kpunch/libdvx/widgetEvent.c
@@ -850,7 +850,6 @@ static void widgetOnMouseInner(WindowT *win, WidgetT *root, int32_t x, int32_t y
// the paint is needed (e.g. cursor blink, selection change).
void widgetOnPaint(WindowT *win, RectT *dirtyArea) {
- (void)dirtyArea;
WidgetT *root = win->widgetRoot;
if (!root) {
@@ -909,7 +908,21 @@ void widgetOnPaint(WindowT *win, RectT *dirtyArea) {
wgtPaint(root, &cd, &ctx->blitOps, &ctx->font, &ctx->colors, full);
// Paint overlay popups (dropdown/combobox)
- widgetPaintOverlays(root, &cd, &ctx->blitOps, &ctx->font, &ctx->colors);
+ bool overlay = widgetPaintOverlays(root, &cd, &ctx->blitOps, &ctx->font, &ctx->colors);
+
+ // A partial pass narrows the dirty area (when the caller gave one) to
+ // what it changed, so only that reaches the screen (an open popup may
+ // reach past its widget's box: then the whole of it)
+ RectT painted;
+
+ if (dirtyArea && !full && !overlay) {
+ if (!widgetPaintedArea(&painted) || !rectIntersect(dirtyArea, &painted, &painted)) {
+ painted.w = 0;
+ painted.h = 0;
+ }
+
+ *dirtyArea = painted;
+ }
}
diff --git a/src/libs/kpunch/libdvx/widgetIface.c b/src/libs/kpunch/libdvx/widgetIface.c
index c695f586..743116c3 100644
--- a/src/libs/kpunch/libdvx/widgetIface.c
+++ b/src/libs/kpunch/libdvx/widgetIface.c
@@ -294,7 +294,8 @@ static void coerceValue(WgtValueT *v, uint8_t type, char *buf, int32_t bufSize)
switch (type) {
case WGT_IFACE_STRING:
- out.s = wgtValueToText(v, buf, bufSize);
+ out.s = wgtValueToText(v, buf, bufSize);
+ out.len = (v->type == WGT_IFACE_STRING) ? v->len : 0;
break;
case WGT_IFACE_INT:
@@ -769,6 +770,14 @@ WgtValueT wgtValueBool(bool b) {
}
+WgtValueT wgtValueBytes(const char *s, int32_t len) {
+ WgtValueT v = wgtValueStr(s);
+
+ v.len = (s && len > 0) ? len : 0;
+ return v;
+}
+
+
WgtValueT wgtValueDouble(double f) {
WgtValueT v = zeroOf(WGT_IFACE_DOUBLE);
@@ -793,6 +802,15 @@ WgtValueT wgtValueInt(int32_t i) {
}
+int32_t wgtValueLen(const WgtValueT *v) {
+ if (v->type != WGT_IFACE_STRING) {
+ return 0;
+ }
+
+ return v->len > 0 ? v->len : (int32_t)strlen(v->s ? v->s : "");
+}
+
+
WgtValueT wgtValueStr(const char *s) {
WgtValueT v = zeroOf(WGT_IFACE_STRING);
diff --git a/src/libs/kpunch/libdvx/widgetOps.c b/src/libs/kpunch/libdvx/widgetOps.c
index 2d33e06b..88ac1734 100644
--- a/src/libs/kpunch/libdvx/widgetOps.c
+++ b/src/libs/kpunch/libdvx/widgetOps.c
@@ -41,6 +41,17 @@
static bool sFullRepaint = false;
+// What a partial paint pass changed (widgetPaintedArea): the union of the
+// boxes of the widgets it repainted, each widened by PAINTED_MARGIN for
+// what a widget draws at its very edge. A container that paints its
+// children itself counts as its own box (sPaintNesting: its children's
+// boxes may be in its own coordinates).
+static RectT sPainted;
+static bool sPaintedSome = false;
+static int32_t sPaintNesting = 0;
+
+#define PAINTED_MARGIN 2
+
// Knuth multiplicative hash constant (2^32 / golden ratio).
#define KNUTH_HASH_MUL 2654435761u
@@ -50,6 +61,7 @@ static bool sFullRepaint = false;
// ============================================================
static void debugContainerBorder(WidgetT *w, DisplayT *d, const BlitOpsT *ops);
+static void paintedAdd(const WidgetT *w);
static bool pressableHitTest(const WidgetT *w, const WidgetT *root, int32_t x, int32_t y);
static WidgetT *wgtFindImpl(WidgetT *w, const char *name);
static bool widgetDropFocusWithin(WidgetT *w);
@@ -89,6 +101,33 @@ static void debugContainerBorder(WidgetT *w, DisplayT *d, const BlitOpsT *ops) {
// Shared hit test for pressable widgets: true when (x,y) falls within
// the widget bounds and the drag originated in the same window.
+// A partial pass repainted w: its box (widened) joins what the pass
+// changed, unless a container that paints its children is painting it.
+static void paintedAdd(const WidgetT *w) {
+ if (sPaintNesting > 0) {
+ return;
+ }
+
+ int32_t x0 = w->x - PAINTED_MARGIN;
+ int32_t y0 = w->y - PAINTED_MARGIN;
+ int32_t x1 = w->x + w->w + PAINTED_MARGIN;
+ int32_t y1 = w->y + w->h + PAINTED_MARGIN;
+
+ if (sPaintedSome) {
+ x0 = DVX_MIN(x0, sPainted.x);
+ y0 = DVX_MIN(y0, sPainted.y);
+ x1 = DVX_MAX(x1, sPainted.x + sPainted.w);
+ y1 = DVX_MAX(y1, sPainted.y + sPainted.h);
+ }
+
+ sPainted.x = x0;
+ sPainted.y = y0;
+ sPainted.w = x1 - x0;
+ sPainted.h = y1 - y0;
+ sPaintedSome = true;
+}
+
+
static bool pressableHitTest(const WidgetT *w, const WidgetT *root, int32_t x, int32_t y) {
return w->window == root->window &&
x >= w->x && x < w->x + w->w &&
@@ -383,7 +422,9 @@ void wgtPaint(WidgetT *root, DisplayT *d, const BlitOpsT *ops, const BitmapFontT
return;
}
- sFullRepaint = fullRepaint;
+ sFullRepaint = fullRepaint;
+ sPaintedSome = false;
+ sPaintNesting = 0;
widgetPaintOne(root, d, ops, font, colors);
sFullRepaint = false;
}
@@ -475,13 +516,24 @@ void wgtSetReadOnly(WidgetT *w, bool readOnly) {
// Polymorphic text setter. Dispatches to the type-specific setText
// via vtable, then does a full invalidation because changing text
-// can change the widget's minimum size (triggering relayout).
-
+// can change the widget's minimum size (triggering relayout). A caption
+// (WCLASS_HAS_TEXT) the widget already has changes nothing, so a loop
+// that sets a caption on every pass does not relayout and repaint on
+// every frame; an editing widget's setText also resets its caret and
+// selection, so it always runs.
void wgtSetText(WidgetT *w, const char *text) {
if (!w) {
return;
}
+ if (w->wclass && (w->wclass->flags & WCLASS_HAS_TEXT) && wclsHas(w, WGT_METHOD_GET_TEXT)) {
+ const char *cur = wclsGetText(w);
+
+ if (cur && strcmp(cur, text ? text : "") == 0) {
+ return;
+ }
+ }
+
wclsSetText(w, text);
wgtInvalidate(w);
@@ -566,11 +618,22 @@ static void widgetNotifyChildChanged(WidgetT *w) {
#ifdef DVX_HOST_TEST
void widgetOpsTestReset(void) {
- sFullRepaint = false;
+ sFullRepaint = false;
+ sPaintedSome = false;
+ sPaintNesting = 0;
+ memset(&sPainted, 0, sizeof(sPainted));
}
#endif
+// What the last partial paint pass (wgtPaint) changed, in content
+// coordinates; false when it repainted nothing.
+bool widgetPaintedArea(RectT *out) {
+ *out = sPainted;
+ return sPaintedSome;
+}
+
+
// Recursive paint walker. For each visible widget:
// 1. Call the widget's paint function (if any) via vtable.
// 2. If the widget has WCLASS_PAINTS_CHILDREN, stop recursion --
@@ -620,11 +683,17 @@ void widgetPaintOne(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFo
// all descendants must repaint on the fresh background.
bool savedFull = sFullRepaint;
+ if (!savedFull) {
+ paintedAdd(w);
+ }
+
if (w->paintDirty) {
sFullRepaint = true;
}
+ sPaintNesting++;
wclsPaint(w, d, ops, font, colors);
+ sPaintNesting--;
sFullRepaint = savedFull;
w->paintDirty = false;
w->childDirty = false;
@@ -639,6 +708,10 @@ void widgetPaintOne(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFo
w->paintDirty = false;
if (dirty) {
+ if (!sFullRepaint) {
+ paintedAdd(w);
+ }
+
wclsPaint(w, d, ops, font, colors);
}
@@ -657,14 +730,15 @@ void widgetPaintOne(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFo
// Paints popup overlays (open dropdowns/comboboxes) on top of
// the widget tree. Called AFTER the main paint pass so popups
// always render above all other widgets regardless of tree position.
+// True when one was painted (it may reach past its widget's box).
//
// Only one popup can be open at a time (tracked by sOpenPopup).
// The tree-root ownership check prevents a popup from one window
// being painted into a different window's content buffer.
-void widgetPaintOverlays(WidgetT *root, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) {
+bool widgetPaintOverlays(WidgetT *root, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) {
if (!sOpenPopup) {
- return;
+ return false;
}
// Verify the popup belongs to this widget tree
@@ -675,10 +749,11 @@ void widgetPaintOverlays(WidgetT *root, DisplayT *d, const BlitOpsT *ops, const
}
if (check != root) {
- return;
+ return false;
}
wclsPaintOverlay(sOpenPopup, d, ops, font, colors);
+ return true;
}
diff --git a/src/libs/kpunch/listhelp/listHelp.c b/src/libs/kpunch/listhelp/listHelp.c
index 6526a1c7..783ec124 100644
--- a/src/libs/kpunch/listhelp/listHelp.c
+++ b/src/libs/kpunch/listhelp/listHelp.c
@@ -66,6 +66,7 @@ const char *listItemsGet(const ListItemsT *li, int32_t idx);
int32_t listItemsMaxWidth(ListItemsT *li, const BitmapFontT *font);
bool listItemsRemove(ListItemsT *li, const BitmapFontT *font, int32_t idx);
bool listItemsSetExternal(ListItemsT *li, const BitmapFontT *font, const char **items, int32_t count);
+bool listItemsSetSpec(ListItemsT *li, const BitmapFontT *font, const char *spec);
static void listItemsSync(ListItemsT *li);
void listPopupAddItem(WidgetT *w, ListPopupT *p, const char *text);
void listPopupCalcMinSize(WidgetT *w, const BitmapFontT *font, ListPopupT *p);
@@ -84,6 +85,7 @@ void listPopupOpen(WidgetT *w, ListPopupT *p);
void listPopupPaint(const WidgetT *w, const ListPopupT *p, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors);
void listPopupRemoveItem(WidgetT *w, ListPopupT *p, int32_t idx);
void listPopupSetItems(WidgetT *w, ListPopupT *p, const char **items, int32_t count);
+void listPopupSetItemsSpec(WidgetT *w, ListPopupT *p, const char *spec);
int32_t listPopupVisibleCount(int32_t itemCount);
void listReorderCancel(WidgetT *w, int32_t *pDragIdx, int32_t *pDropIdx);
int32_t listReorderDest(int32_t from, int32_t to, int32_t count);
@@ -591,6 +593,32 @@ bool listItemsSetExternal(ListItemsT *li, const BitmapFontT *font, const char **
}
+// Replace the items with the pieces of spec between LIST_ITEMS_SEP
+// characters (every one separates: "a|" is "a" and an empty item; "" or
+// NULL leaves the list empty): BASIC's SetItems "Red|Green|Blue".
+bool listItemsSetSpec(ListItemsT *li, const BitmapFontT *font, const char *spec) {
+ bool widthChanged = listItemsClear(li);
+ char *buf = (spec && *spec) ? strdup(spec) : NULL;
+
+ for (char *tok = buf; tok; ) {
+ char *sep = strchr(tok, LIST_ITEMS_SEP);
+
+ if (sep) {
+ *sep = '\0';
+ }
+
+ if (listItemsAdd(li, font, tok)) {
+ widthChanged = true;
+ }
+
+ tok = sep ? sep + 1 : NULL;
+ }
+
+ free(buf);
+ return widthChanged;
+}
+
+
// Repoint the active array at the owned storage after it changed (arrput
// may have reallocated it).
static void listItemsSync(ListItemsT *li) {
@@ -860,6 +888,17 @@ void listPopupSetItems(WidgetT *w, ListPopupT *p, const char **items, int32_t co
}
+// SetItems from a "Red|Green|Blue" spec (listItemsSetSpec): a new list,
+// so nothing is selected, as after listPopupClearItems.
+void listPopupSetItemsSpec(WidgetT *w, ListPopupT *p, const char *spec) {
+ bool widthChanged = listItemsSetSpec(&p->items, wgtFont(w), spec);
+
+ p->selectedIdx = -1;
+ p->hoverIdx = -1;
+ listPopupItemsChanged(w, p, widthChanged);
+}
+
+
// Rows the popup shows: the item count clamped to [1, DROPDOWN_MAX_VISIBLE].
int32_t listPopupVisibleCount(int32_t itemCount) {
return clampInt(itemCount, 1, DROPDOWN_MAX_VISIBLE);
diff --git a/src/libs/kpunch/listhelp/listHelp.h b/src/libs/kpunch/listhelp/listHelp.h
index 276c88bc..047b8520 100644
--- a/src/libs/kpunch/listhelp/listHelp.h
+++ b/src/libs/kpunch/listhelp/listHelp.h
@@ -45,6 +45,8 @@
#define POPUP_SCROLLBAR_W SCROLLBAR_WIDTH
// Thickness of the drag-reorder insertion line.
#define LIST_REORDER_LINE_H 2
+// Separates the items of a SetItems spec ("Red|Green|Blue").
+#define LIST_ITEMS_SEP '|'
// ============================================================
// Owned-string item list
@@ -78,6 +80,7 @@ const char *listItemsGet(const ListItemsT *li, int32_t idx);
int32_t listItemsMaxWidth(ListItemsT *li, const BitmapFontT *font);
bool listItemsRemove(ListItemsT *li, const BitmapFontT *font, int32_t idx);
bool listItemsSetExternal(ListItemsT *li, const BitmapFontT *font, const char **items, int32_t count);
+bool listItemsSetSpec(ListItemsT *li, const BitmapFontT *font, const char *spec);
// Rebuild an internally-owned stb_ds string array as a deep copy of an
// external source array. The copy is built before the old strings are
@@ -181,6 +184,7 @@ void listPopupOpen(WidgetT *w, ListPopupT *p);
void listPopupPaint(const WidgetT *w, const ListPopupT *p, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors);
void listPopupRemoveItem(WidgetT *w, ListPopupT *p, int32_t idx);
void listPopupSetItems(WidgetT *w, ListPopupT *p, const char **items, int32_t count);
+void listPopupSetItemsSpec(WidgetT *w, ListPopupT *p, const char *spec);
int32_t listPopupVisibleCount(int32_t itemCount);
// ============================================================
diff --git a/src/libs/kpunch/listhelp/listhelp.dhs b/src/libs/kpunch/listhelp/listhelp.dhs
index f743231c..c5b082ae 100644
--- a/src/libs/kpunch/listhelp/listhelp.dhs
+++ b/src/libs/kpunch/listhelp/listhelp.dhs
@@ -85,9 +85,10 @@ const char *listItemsGet(const ListItemsT *li, int32_t idx);
int32_t listItemsMaxWidth(ListItemsT *li, const BitmapFontT *font);
bool listItemsRemove(ListItemsT *li, const BitmapFontT *font, int32_t idx);
bool listItemsSetExternal(ListItemsT *li, const BitmapFontT *font, const char **items, int32_t count);
+bool listItemsSetSpec(ListItemsT *li, const BitmapFontT *font, const char *spec);
.endcode
-listItemsAdd and listItemsRemove first adopt (deep-copy) a caller's array, so they extend or shorten it without modifying it. listItemsSetExternal keeps the previously owned strings until the next adopt, clear, or free, because the new array may point into them, and never reads the caller's previous array (it may be freed already). The mutators measure in font, the widget's own (wgtFont), and return true when the widest item's width changed, i.e. when the widget's minimum width changed and it needs a relayout rather than just a repaint. listItemsGet returns "" for an index out of range.
+listItemsAdd and listItemsRemove first adopt (deep-copy) a caller's array, so they extend or shorten it without modifying it. listItemsSetExternal keeps the previously owned strings until the next adopt, clear, or free, because the new array may point into them, and never reads the caller's previous array (it may be freed already). The mutators measure in font, the widget's own (wgtFont), and return true when the widest item's width changed, i.e. when the widget's minimum width changed and it needs a relayout rather than just a repaint. listItemsGet returns "" for an index out of range. listItemsSetSpec replaces the items with the pieces of a "Red|Green|Blue" spec (LIST_ITEMS_SEP; every "|" separates, so "a|" is "a" and an empty item, and "" empties the list): BASIC's SetItems.
widgetAdoptOwnedStrings(owned, src, count) builds the deep copy that adoption uses (complete before the old strings are freed, so src may alias them) and widgetFreeOwnedStrings(owned) frees every string of an owned array and empties it. ListView uses both for its row-major cell copy.
@@ -155,6 +156,7 @@ typedef struct {
listPopupInit(p) No selection, no hover, closed.
listPopupAddItem / listPopupRemoveItem Item list edits; the selection and hover stay on the same item (-1 when it is removed).
listPopupClearItems / listPopupSetItems Replace the items; an out-of-range selection becomes -1.
+ listPopupSetItemsSpec(w, p, spec) Replace the items with a "Red|Green|Blue" spec (listItemsSetSpec); nothing is selected.
listPopupOpen(w, p) Open with the selection hovered and scrolled into view (keyboard, mouse, and accelerator all use it).
listPopupClose(w, p) WGT_METHOD_CLOSE_POPUP body.
listPopupCommit(p) Close and make the hovered item the selection; false when nothing valid is hovered.
diff --git a/src/libs/kpunch/music/Makefile b/src/libs/kpunch/music/Makefile
index a07bac2a..89224d59 100644
--- a/src/libs/kpunch/music/Makefile
+++ b/src/libs/kpunch/music/Makefile
@@ -22,13 +22,17 @@
# Music Library Makefile for DJGPP cross-compilation
#
-# music.lib (see music.h): libxmp-lite (thirdparty/libxmp-lite, with
-# DVX's patches from its dvx-patches folder, built with warnings off, as
-# its own options say below) and our own sources. The files that run at interrupt time (ISR_SRCS) are linked
-# last, so musicLock pins one span from their first function to the end
-# of the library's data; gcc must not move parts of them elsewhere or
-# turn loops into library calls. The OP2 instrument bank is copied
-# beside the library.
+# music.lib (see music.h) is our own sources but musicXmp.c. The files
+# that run at interrupt time (ISR_SRCS) are linked last, so musicLock pins
+# one span from their first function to the end of the library's data;
+# gcc must not move parts of them elsewhere or turn loops into library
+# calls. The OP2 instrument bank is copied beside the library.
+#
+# players/xmp.drv, the module player music.lib loads when a tracker
+# module is opened (musicXmpDrv.c), is musicXmp.c and libxmp-lite
+# (thirdparty/libxmp-lite, with DVX's patches from its dvx-patches
+# folder, built with warnings off, as its own options say below): only
+# what musicXmp.c reaches of libxmp is kept (DXE_GC).
include ../../../common.mk
@@ -36,10 +40,11 @@ XMP_DIR = thirdparty/libxmp-lite
XMP_SRCS = $(wildcard $(XMP_DIR)/src/*.c) $(wildcard $(XMP_DIR)/src/loaders/*.c)
XMP_HDRS = $(wildcard $(XMP_DIR)/include/*.h $(XMP_DIR)/src/*.h $(XMP_DIR)/src/loaders/*.h)
XMP_OPTS = -DLIBXMP_CORE_PLAYER -DLIBXMP_NO_PROWIZARD -DLIBXMP_NO_DEPACKERS -DBUILDING_STATIC -I$(XMP_DIR)/include -I$(XMP_DIR)/src
-XMP_CFLAGS = $(filter-out $(WARNFLAGS) $(DEPFLAGS),$(DOS_CFLAGS)) -w -fno-analyzer $(XMP_OPTS)
+XMP_CFLAGS = $(filter-out $(WARNFLAGS) $(DEPFLAGS),$(DOS_CFLAGS)) -w -fno-analyzer $(DXE_GC_CFLAGS) $(XMP_OPTS)
ISR_SRCS = musicCents.c musicFm.c musicModTick.c musicMpu.c musicWave.c musicSeq.c
-MAIN_SRCS = $(filter-out $(ISR_SRCS),$(wildcard *.c))
+PLAYER_SRC = musicXmp.c
+MAIN_SRCS = $(filter-out $(ISR_SRCS) $(PLAYER_SRC),$(wildcard *.c))
ISR_CFLAGS = -fno-reorder-blocks-and-partition -fno-reorder-functions -fno-tree-loop-distribute-patterns
CFLAGS = $(DOS_CFLAGS) $(ISR_CFLAGS) -I. -I$(XMP_DIR)/include -I../audio -I../libdvx -I../libdvx/platform -I../libdvx/thirdparty -I../libtasks -I../dvxshell
@@ -48,13 +53,17 @@ LIBSDIR = ../../../../bin/libs
TARGETDIR = $(LIBSDIR)/kpunch/music
TARGET = $(TARGETDIR)/music.lib
BANK = $(TARGETDIR)/genmidi.op2
+PLAYERDIR = $(TARGETDIR)/players
+PLAYER = $(PLAYERDIR)/xmp.drv
-XMP_OBJS = $(patsubst $(XMP_DIR)/%.c,$(OBJDIR)/xmp/%.o,$(XMP_SRCS))
-OBJS = $(XMP_OBJS) $(patsubst %.c,$(OBJDIR)/%.o,$(MAIN_SRCS) $(ISR_SRCS))
+XMP_OBJS = $(patsubst $(XMP_DIR)/%.c,$(OBJDIR)/xmp/%.o,$(XMP_SRCS))
+OBJS = $(patsubst %.c,$(OBJDIR)/%.o,$(MAIN_SRCS) $(ISR_SRCS))
+PLAYER_OBJ = $(OBJDIR)/$(PLAYER_SRC:.c=.o)
+GC_OBJ = $(OBJDIR)/xmpGc.o
.PHONY: all clean
-all: $(TARGET) $(TARGETDIR)/music.dep $(BANK)
+all: $(TARGET) $(TARGETDIR)/music.dep $(BANK) $(PLAYER)
$(TARGETDIR)/music.dep: music.dep | $(TARGETDIR)
sed 's/$$/\r/' $< > $@
@@ -66,6 +75,13 @@ $(TARGET): $(OBJS) | $(TARGETDIR)
$(DXE3GEN) -o $(TARGETDIR)/music.dxe -U $(OBJS)
mv $(TARGETDIR)/music.dxe $@
+$(GC_OBJ): $(PLAYER_OBJ) $(XMP_OBJS)
+ $(DXE_GC) $@ $(PLAYER_OBJ) -- $(PLAYER_OBJ) $(XMP_OBJS)
+
+$(PLAYER): $(GC_OBJ) | $(PLAYERDIR)
+ $(DXE3GEN) -o $(PLAYERDIR)/xmp.dxe -U $(GC_OBJ)
+ mv $(PLAYERDIR)/xmp.dxe $@
+
$(OBJDIR)/xmp/%.o: $(XMP_DIR)/%.c $(XMP_HDRS) $(DVX_MAKEFILES)
@mkdir -p $(dir $@)
$(CC) $(XMP_CFLAGS) -c -o $@ $<
@@ -79,7 +95,10 @@ $(OBJDIR):
$(TARGETDIR):
mkdir -p $(TARGETDIR)
--include $(patsubst %.c,$(OBJDIR)/%.d,$(MAIN_SRCS) $(ISR_SRCS))
+$(PLAYERDIR):
+ mkdir -p $(PLAYERDIR)
+
+-include $(patsubst %.c,$(OBJDIR)/%.d,$(MAIN_SRCS) $(ISR_SRCS) $(PLAYER_SRC))
clean:
rm -rf $(OBJDIR) $(TARGETDIR)
diff --git a/src/libs/kpunch/music/README.md b/src/libs/kpunch/music/README.md
new file mode 100644
index 00000000..d256c5c3
--- /dev/null
+++ b/src/libs/kpunch/music/README.md
@@ -0,0 +1,49 @@
+# music -- Music
+
+Music for DVX programs: tracker modules (MOD, S3M, XM, IT, through
+libxmp-lite), WAV and VOC files streamed as they play, Standard MIDI
+Files on the best synthesizer there is (an external MIDI synthesizer,
+the card's wavetable or FM, or DVX's own), and PLAY strings. BASIC
+reaches it through PLAY and src/include/basic/music.bas.
+
+## Documentation
+
+Full reference is in `music.dhs` (this directory), compiled into the DVX
+System Reference. Open via the Help Viewer app, or read
+`docs/dvx_system_reference.html` after `make`.
+
+## Source Files
+
+- `music.h`, `musicInt.h` -- public and internal API
+- `music.c` -- musics, where they play, programs, the public functions
+- `musicShell.c` -- music.lib's hooks into the shell
+- `musicXmpDrv.c` -- tracker modules: loads the module player when a
+ module is opened, and unloads it once none has been for a minute
+- `musicXmp.c` -- the module player, `players/xmp.drv`: tracker modules
+ (MOD, S3M, XM, IT) through libxmp-lite, not part of music.lib
+- `musicModHw.c`, `musicModTick.c` -- tracker modules on a wavetable
+ synthesizer's voices, played by the music timer
+- `musicSound.c` -- WAV and VOC files as music
+- `musicSmf.c`, `musicSeq.c` -- Standard MIDI Files; the sequencer, its
+ event ring, and the music timer
+- `musicMpu.c` -- MIDI out of the MPU-401 port
+- `musicFm.c`, `musicSynth.c`, `musicOp2.c` -- General MIDI on FM
+ voices, the software FM synthesizer, and OP2 (GENMIDI) banks
+- `musicWave.c`, `musicWaveLoad.c`, `musicWaveSoft.c` -- General MIDI on
+ wavetable voices (SoundFont), its samples in the synthesizer's memory,
+ and the same in software
+- `musicSf2.c`, `musicPat.c`, `musicFound.c` -- SoundFont 2 banks,
+ Gravis UltraSound patch sets, and the banks a card's own software
+ installed
+- `musicMml.c`, `musicMmlPlay.c` -- PLAY's music macro language and its
+ queues
+- `musicMidiRec.c` -- recording MIDI into a Standard MIDI File
+- `musicCents.c` -- cents and powers of two in fixed point, loudness as
+ attenuation
+- `musicBas.c` -- BASIC entry points (music.bas)
+- `thirdparty/libxmp-lite` -- module playback (MIT; local patches
+ tracked beside it)
+
+## Build
+
+`make -C src/libs/kpunch/music` (invoked by the top-level `make`).
diff --git a/src/libs/kpunch/music/music.c b/src/libs/kpunch/music/music.c
index a6a921b1..6930de2f 100644
--- a/src/libs/kpunch/music/music.c
+++ b/src/libs/kpunch/music/music.c
@@ -51,7 +51,9 @@
// from song to song (a song loads only what it lacks; a module's samples
// that do not fit beside them put them out); the library's context,
// which owns that memory, stays open FONT_KEEP_US after the last music
-// for the next one.
+// for the next one. The SoundFont's bank itself (a GUS patch set's
+// files, a SoundFont's headers: seconds to read) is kept for the session:
+// only its samples go with the context.
//
// The state a player is in lives in its IsrPlayerT for every kind of
// music, so there is one place to look: the timer, the fill functions
@@ -64,6 +66,7 @@
#include "musicInt.h"
#include "stb_ds.h"
+#include "taskSwch.h"
#include
#include
@@ -80,13 +83,13 @@ DVX_ABI_STAMP(music);
#define SW_VOICES SYNTH_VOICES_MAX
#define STREAM_BITS 16
#define STREAM_CHANNELS 2
-#define US_PER_SEC 1000000ULL
-#define US_PER_MS 1000
#define MS_PER_SEC 1000
#define GAIN_SHIFT 8
#define TIMER_NONE (-1)
#define ERROR_TEXT_UNKNOWN "Unknown error"
#define FONT_KEEP_US (MUSIC_FONT_KEEP_SEC * US_PER_SEC)
+#define LOAD_HEAD_BYTES 4096 // read to tell a MIDI file by its start
+#define LOAD_CHUNK_BYTES (64 * 1024) // a music file is read this much at a time
#define GM_BANK 0 // General MIDI's instruments; live MIDI starts every channel ...
#define GM_PROGRAM_FIRST 0 // ... on the first (and channel 10 on the standard kit)
@@ -99,12 +102,20 @@ typedef struct {
char name[DVX_MAX_PATH]; // as DVX.INI's soundFont names it
} WaveFontT;
-// The SoundFont software synthesizer's, one per music: its own copy of
-// the SoundFont's headers, the song's samples in memory, where each is
+// A SoundFont's bank parsed for the software synthesizer, shared by the
+// musics that use it (softBankHold); the last one read is kept (sSoftBank)
typedef struct {
- WaveBankT bank;
+ WaveBankT bank;
+ char name[DVX_MAX_PATH];
+ int32_t refs;
+} SoftBankT;
+
+// The SoundFont software synthesizer's, one per music: the SoundFont's
+// headers (shared), the song's samples in memory, where each is
+typedef struct {
+ SoftBankT *shared;
WaveSoftT soft;
- WaveLoadedT *loaded; // per sample of bank
+ WaveLoadedT *loaded; // per sample of the bank
} SoftFontT;
struct MusicT {
@@ -138,6 +149,7 @@ struct MusicT {
bool atEnd; // a module played to its end: its next play starts over
bool draining; // rendered to its end, not yet heard to it
bool fmChip; // hwSynth plays (the music, or the wavetable synthesizer's overflow)
+ bool waveLoading; // live programs' samples still loading (waveLiveStep, a musicPump each)
char soundFont[DVX_MAX_PATH]; // musicSetBank's SoundFont; "": DVX.INI's
};
@@ -161,6 +173,7 @@ static MusicBankT *sBank = NULL; // DVX.INI's choice as last loaded (a re
static MusicT *sMpuUser = NULL;
static int32_t sTimerId = TIMER_NONE;
static WaveFontT *sWave = NULL; // the SoundFont loaded for the wavetable synthesizer
+static SoftBankT *sSoftBank = NULL; // the SoundFont the software synthesizer read last (softBankHold)
static MusicT *sWaveUser = NULL; // the music on it
static uint64_t sQuietSince = 0; // when the last music went (sCtx kept for sWave)
static char sFontFailed[DVX_MAX_PATH]; // the SoundFont last found missing or damaged (said once)
@@ -196,6 +209,7 @@ void musicFree(MusicT *music);
void musicGetInfo(const MusicT *music, MusicInfoT *info);
uint32_t musicGetPosition(const MusicT *music);
int32_t musicGetVolume(const MusicT *music);
+void musicGiveWay(uint64_t *last);
void musicIdle(void *unused);
bool musicIsPlaying(const MusicT *music);
int32_t musicLoad(const char *path, MusicT **out);
@@ -224,9 +238,12 @@ static int32_t openSoftware(MusicT *m);
static int32_t openStream(MusicT *m, uint32_t bufferMs, AudioFillFnT fill);
static void outputClose(MusicT *m);
static int32_t outputOpen(MusicT *m);
+static char *readGivingWay(const char *path, int32_t *outLen);
static void rewindToStart(MusicT *m);
static int32_t sendMidi(MusicT *m, int32_t channel, uint8_t type, int32_t data1, int32_t data2);
static void silence(MusicT *m);
+static SoftBankT *softBankHold(const char *want, int32_t *err);
+static void softBankRelease(SoftBankT *sb);
static int32_t softLoad(MusicT *m, SoftFontT *sf, const char *want);
static int32_t softOpen(MusicT *m, const char *font);
static uint32_t soundFill(AudioStreamT *stream, void *buf, uint32_t frames, void *userData);
@@ -238,8 +255,10 @@ static void timerUpdate(void);
static bool waveBankLock(const WaveBankT *bank, bool lock);
static bool waveClaim(AudioWtInfoT *info);
static void waveDrop(MusicT *m);
+static void waveFontEmpty(void);
static void waveFontFree(WaveFontT *font);
static int32_t waveFontUse(const char *name);
+static void waveLiveStep(MusicT *m);
static void waveNeedsSong(const MusicT *m, const WaveBankT *bank, uint8_t *need);
static int32_t waveOpen(MusicT *m, const char *font, MidiSinkT *overflow);
static void waveProgram(MusicT *m, int32_t channel, int32_t program);
@@ -341,11 +360,12 @@ static MusicBankT *bankShared(const char *name, bool current, int32_t *err) {
}
-// The library's context closed, and the SoundFont with it (its samples
-// are in the context's memory).
+// The library's context closed, and the SoundFont's samples with it
+// (they are in the context's memory); its bank, read (a GUS patch set's
+// files, a SoundFont's headers) at the cost of seconds, stays for the next
+// song.
static void ctxClose(void) {
- waveFontFree(sWave);
- sWave = NULL;
+ waveFontEmpty();
audioClose(sCtx);
sCtx = NULL;
}
@@ -368,7 +388,7 @@ static int32_t ctxEnsure(void) {
// instrument bank only the MPU-401 port can play it. The banks are the
// ones musicSetBank chose, else DVX.INI's choices as they are now.
static int32_t decideMidi(MusicT *m) {
- PrefsHandleT *prefs = prefsLoad(platformDvxIniPath());
+ PrefsHandleT *prefs = prefsDvxIniShared();
const char *want = audioMidiChoices[audioSettingMidi(prefs)].iniName;
int32_t mode = mpuModeChoices[prefsGetChoice(prefs, AUDIO_INI_SECTION, MUSIC_INI_MPU_MODE, mpuModeChoices, MPU_MODE_COUNT, mpuModeChoices[MPU_MODE_GM].iniName)].value;
bool mpu = strcasecmp(want, AUDIO_MIDI_MPU401) == 0;
@@ -383,7 +403,6 @@ static int32_t decideMidi(MusicT *m) {
snprintf(bank, sizeof(bank), "%s", prefsGetString(prefs, AUDIO_INI_SECTION, MUSIC_INI_BANK, ""));
snprintf(font, sizeof(font), "%s", m->soundFont[0] ? m->soundFont : prefsGetString(prefs, AUDIO_INI_SECTION, MUSIC_INI_SOUND_FONT, ""));
- prefsClose(prefs);
if (mpu && !sMpuUser && isrSlotFree() && audioMidiAcquire(sCtx) == AUDIO_OK) {
sMpuUser = m;
@@ -631,12 +650,10 @@ static uint32_t moduleFill(AudioStreamT *stream, void *buf, uint32_t frames, voi
// (moduleWave) and DVX.INI [audio] modules does not say software, else on
// a stream mixed in software.
static int32_t moduleOpen(MusicT *m) {
- PrefsHandleT *prefs = prefsLoad(platformDvxIniPath());
+ PrefsHandleT *prefs = prefsDvxIniShared();
const char *want = audioModulesChoices[audioSettingModules(prefs)].iniName;
bool soft = strcasecmp(want, AUDIO_MODULES_SOFTWARE) == 0;
- prefsClose(prefs);
-
if (!soft && moduleWave(m) == MUSIC_OK) {
return MUSIC_OK;
}
@@ -660,9 +677,8 @@ static int32_t moduleWave(MusicT *m) {
int32_t rc = modHwOpen(sCtx, m->xmp, gainOf(m), &m->isr.mod);
if (rc == MODHW_ERR_ROOM && sWave) {
- waveFontFree(sWave);
- sWave = NULL;
- rc = modHwOpen(sCtx, m->xmp, gainOf(m), &m->isr.mod);
+ waveFontEmpty();
+ rc = modHwOpen(sCtx, m->xmp, gainOf(m), &m->isr.mod);
}
if (rc != MUSIC_OK) {
@@ -852,11 +868,23 @@ int32_t musicGetVolume(const MusicT *music) {
}
-// After a while with no music the SoundFont's samples are let go.
+// tsYield: on a program's own task DVX's frames run (none of the
+// program's code); on the shell's, the other tasks.
+void musicGiveWay(uint64_t *last) {
+ if (platformMicros() - *last >= MUSIC_GIVE_WAY_US) {
+ tsYield();
+ *last = platformMicros();
+ }
+}
+
+
+// After a while with no music the SoundFont's samples are let go, and
+// the module player after a while unused.
void musicIdle(void *unused) {
(void)unused;
musicMmlIdle();
musicPump();
+ xmpIdle();
if (!sMusics && sCtx && platformMicros() - sQuietSince >= FONT_KEEP_US) {
ctxClose();
@@ -869,7 +897,9 @@ bool musicIsPlaying(const MusicT *music) {
}
-// A WAV is read as it plays (soundOpenFile); anything else is read whole.
+// A WAV is read as it plays (soundOpenFile); a MIDI file or a module is
+// read whole, giving way as it is, once its head says it is one (a file
+// of another kind is refused without being read).
int32_t musicLoad(const char *path, MusicT **out) {
SoundSrcT *src = NULL;
int32_t len = 0;
@@ -891,7 +921,19 @@ int32_t musicLoad(const char *path, MusicT **out) {
return rc;
}
- char *data = platformReadFile(path, &len);
+ uint8_t head[LOAD_HEAD_BYTES];
+ FILE *fp = fopen(path, "rb");
+ size_t got = fp ? fread(head, 1, sizeof(head), fp) : 0;
+
+ if (fp) {
+ fclose(fp);
+ }
+
+ if (!midiIsFile(head, (uint32_t)got) && !xmpIsModule(path)) {
+ return fp ? MUSIC_ERR_FORMAT : MUSIC_ERR_OPEN;
+ }
+
+ char *data = readGivingWay(path, &len);
if (!data) {
return MUSIC_ERR_OPEN;
@@ -960,7 +1002,9 @@ int32_t musicLoadMem(const void *data, uint32_t size, MusicT **out) {
return rc == MUSIC_OK ? soundMusic(src, out) : rc;
}
- bool midi = midiIsFile(bytes, size);
+ bool midi = midiIsFile(bytes, size);
+ XmpPlayerT *xmp = NULL;
+ AudioInfoT info;
rc = ctxEnsure();
@@ -968,9 +1012,21 @@ int32_t musicLoadMem(const void *data, uint32_t size, MusicT **out) {
return rc;
}
+ // A module loads (giving way) before its music is made: no other
+ // task meets a music that is half there
+ if (!midi) {
+ audioGetInfo(&info);
+ rc = xmpOpen(bytes, size, info.rate, STREAM_CHANNELS, &xmp);
+
+ if (rc != MUSIC_OK) {
+ return rc;
+ }
+ }
+
MusicT *m = musicNew(midi ? MUSIC_KIND_MIDI : MUSIC_KIND_MODULE);
if (!m) {
+ xmpClose(xmp);
rc = MUSIC_ERR_NO_MEMORY;
} else if (midi) {
rc = midiParse(bytes, size, &m->file);
@@ -984,11 +1040,8 @@ int32_t musicLoadMem(const void *data, uint32_t size, MusicT **out) {
m->isr.seq.count = m->file.count;
m->isr.seq.length = m->file.length;
} else {
- AudioInfoT info;
-
- audioGetInfo(&info);
m->rate = info.rate;
- rc = xmpOpen(bytes, size, m->rate, STREAM_CHANNELS, &m->xmp);
+ m->xmp = xmp;
}
if (m && rc != MUSIC_OK) {
@@ -1178,6 +1231,10 @@ void musicPump(void) {
if (m->isr.mod && m->isr.state == PLAY_PLAYING) {
modHwFill(m->isr.mod, m->xmp, m->loop);
}
+
+ if (m->waveLoading) {
+ waveLiveStep(m);
+ }
}
while (again) {
@@ -1321,8 +1378,8 @@ int32_t musicSetVolume(MusicT *music, int32_t volume) {
}
-// The library is being unloaded: free every music, the banks, the
-// context.
+// The library is being unloaded: free every music, the module player,
+// the banks, the context.
void musicShutdown(void) {
musicMmlShutdown();
@@ -1330,12 +1387,22 @@ void musicShutdown(void) {
musicFree(sMusics);
}
+ xmpUnload();
+
if (sCtx) {
ctxClose();
}
+ waveFontFree(sWave);
bankRelease(sBank);
+ sWave = NULL;
sBank = NULL;
+
+ if (sSoftBank) {
+ waveBankFree(&sSoftBank->bank);
+ free(sSoftBank);
+ sSoftBank = NULL;
+ }
}
@@ -1497,6 +1564,37 @@ static int32_t outputOpen(MusicT *m) {
}
+// A file read whole, LOAD_CHUNK_BYTES at a time, giving way between
+// (musicGiveWay); NULL when it cannot be read. Free with free().
+static char *readGivingWay(const char *path, int32_t *outLen) {
+ FILE *fp = fopen(path, "rb");
+ long size = fp && fseek(fp, 0, SEEK_END) == 0 ? ftell(fp) : -1;
+ char *data = size > 0 && size < INT32_MAX && fseek(fp, 0, SEEK_SET) == 0 ? (char *)malloc((size_t)size) : NULL;
+ long done = 0;
+ uint64_t last = platformMicros();
+
+ while (data && done < size) {
+ size_t want = (size_t)DVX_MIN((long)LOAD_CHUNK_BYTES, size - done);
+
+ if (fread(data + done, 1, want, fp) != want) {
+ free(data);
+ data = NULL;
+ break;
+ }
+
+ done += (long)want;
+ musicGiveWay(&last);
+ }
+
+ if (fp) {
+ fclose(fp);
+ }
+
+ *outLen = (int32_t)done;
+ return data;
+}
+
+
// A module or a sound back to its start (a module's frames queued for the
// synthesizer dropped).
static void rewindToStart(MusicT *m) {
@@ -1556,28 +1654,82 @@ static void silence(MusicT *m) {
}
+// The SoundFont want names, read once: the bank kept (sSoftBank) is
+// shared while it is the one wanted; another is read in its place (a kept
+// one no music holds goes; one still held goes with its last music).
+// NULL, *err set, when it cannot be read.
+static SoftBankT *softBankHold(const char *want, int32_t *err) {
+ if (sSoftBank && strcasecmp(sSoftBank->name, want) == 0) {
+ sSoftBank->refs++;
+ *err = MUSIC_OK;
+ return sSoftBank;
+ }
+
+ SoftBankT *sb = (SoftBankT *)calloc(1, sizeof(SoftBankT));
+
+ *err = sb ? fontLoad(want, &sb->bank) : MUSIC_ERR_NO_MEMORY;
+
+ if (*err != MUSIC_OK) {
+ if (sb) {
+ waveBankFree(&sb->bank);
+ }
+
+ free(sb);
+ return NULL;
+ }
+
+ snprintf(sb->name, sizeof(sb->name), "%s", want);
+ sb->refs = 1;
+
+ if (sSoftBank && sSoftBank->refs == 0) {
+ waveBankFree(&sSoftBank->bank);
+ free(sSoftBank);
+ }
+
+ sSoftBank = sb;
+ return sb;
+}
+
+
+// A music's hold given up; a bank no longer kept goes with its last.
+static void softBankRelease(SoftBankT *sb) {
+ if (!sb) {
+ return;
+ }
+
+ sb->refs--;
+
+ if (sb->refs == 0 && sb != sSoftBank) {
+ waveBankFree(&sb->bank);
+ free(sb);
+ }
+}
+
+
// softOpen's SoftFontT sf filled: the SoundFont want names, the music's
// renderer, the song's samples, the backend.
static int32_t softLoad(MusicT *m, SoftFontT *sf, const char *want) {
- int32_t rc = fontLoad(want, &sf->bank);
+ int32_t rc;
WaveBackendT be;
+ sf->shared = softBankHold(want, &rc);
+
if (rc == MUSIC_OK) {
- rc = waveSynthNew(m, sf->bank.sampleCount);
+ rc = waveSynthNew(m, sf->shared->bank.sampleCount);
}
if (rc == MUSIC_OK) {
- sf->loaded = (WaveLoadedT *)calloc(sf->bank.sampleCount ? sf->bank.sampleCount : 1, sizeof(WaveLoadedT));
+ sf->loaded = (WaveLoadedT *)calloc(sf->shared->bank.sampleCount ? sf->shared->bank.sampleCount : 1, sizeof(WaveLoadedT));
rc = sf->loaded ? MUSIC_OK : MUSIC_ERR_NO_MEMORY;
}
if (rc == MUSIC_OK) {
- for (uint32_t i = 0; i < sf->bank.sampleCount; i++) {
+ for (uint32_t i = 0; i < sf->shared->bank.sampleCount; i++) {
sf->loaded[i].addr = WAVE_NOT_LOADED;
}
- waveNeedsSong(m, &sf->bank, m->waveNeed);
- rc = waveSoftLoad(&sf->soft, &sf->bank, sf->loaded, m->waveNeed);
+ waveNeedsSong(m, &sf->shared->bank, m->waveNeed);
+ rc = waveSoftLoad(&sf->soft, &sf->shared->bank, sf->loaded, m->waveNeed);
}
if (rc != MUSIC_OK) {
@@ -1585,7 +1737,7 @@ static int32_t softLoad(MusicT *m, SoftFontT *sf, const char *want) {
}
waveSoftBackend(&sf->soft, m->rate, &be);
- waveSynthInit(m->wave, &be, &sf->bank, sf->loaded, &m->swSynth->sink, MUSIC_VOLUME_MAX);
+ waveSynthInit(m->wave, &be, &sf->shared->bank, sf->loaded, &m->swSynth->sink, MUSIC_VOLUME_MAX);
return MUSIC_OK;
}
@@ -1796,16 +1948,34 @@ static void waveDrop(MusicT *m) {
if (m->softFont) {
waveSoftFree(&m->softFont->soft);
- waveBankFree(&m->softFont->bank);
+ softBankRelease(m->softFont->shared);
free(m->softFont->loaded);
}
free(m->softFont);
free(m->wave);
free(m->waveNeed);
- m->softFont = NULL;
- m->wave = NULL;
- m->waveNeed = NULL;
+ m->softFont = NULL;
+ m->wave = NULL;
+ m->waveNeed = NULL;
+ m->waveLoading = false;
+}
+
+
+// The SoundFont's samples out of the synthesizer's memory; its bank stays
+// (sWave: the next song that uses it loads only its samples), unless an
+// empty cache cannot be made for it.
+static void waveFontEmpty(void) {
+ if (!sWave) {
+ return;
+ }
+
+ waveCacheFree(sCtx, &sWave->cache);
+
+ if (waveCacheInit(&sWave->cache, &sWave->bank) != MUSIC_OK) {
+ waveFontFree(sWave);
+ sWave = NULL;
+ }
}
@@ -1868,6 +2038,23 @@ static int32_t waveFontUse(const char *name) {
}
+// A step of a live program's samples loading on the wavetable
+// synthesizer (waveProgram), one sample a musicPump: a song's worth taken
+// at once would hold DVX up for half a second.
+static void waveLiveStep(MusicT *m) {
+ AudioWtInfoT info;
+ bool more = false;
+
+ audioWtGetInfo(&info);
+
+ if (waveLoadStep(sCtx, &sWave->bank, &sWave->cache, m->waveNeed, info.flags, &more) != MUSIC_OK) {
+ dvxLog("Music: a sample of a program chosen does not fit in the synthesizer's memory (or could not be read); its notes play on the FM synthesizer");
+ }
+
+ m->waveLoading = more;
+}
+
+
// The samples the music needs from bank: the song's, or for live MIDI
// the first program and the standard kit (the rest as they are chosen).
static void waveNeedsSong(const MusicT *m, const WaveBankT *bank, uint8_t *need) {
@@ -1927,38 +2114,25 @@ static int32_t waveOpen(MusicT *m, const char *font, MidiSinkT *overflow) {
// A program chosen live on a wavetable synthesizer: its samples loaded
-// when some are not there yet (on the chip the notes sounding are
-// silenced first, as samples may move to make room).
+// when some are not there yet, a sample a musicPump (waveLiveStep; its
+// notes play on FM until they are). In software they join the others in
+// memory at once.
static void waveProgram(MusicT *m, int32_t channel, int32_t program) {
SoftFontT *sf = m->softFont;
- const WaveBankT *bank = sf ? &sf->bank : &sWave->bank;
+ const WaveBankT *bank = sf ? &sf->shared->bank : &sWave->bank;
uint16_t bankNum = channel == MIDI_PERCUSSION ? SF_DRUM_BANK : m->wave->channels[channel].bankMsb;
- AudioWtInfoT info;
- uint32_t missing = 0;
if (waveNeedsPreset(bank, bankNum, (uint8_t)program, m->waveNeed) == 0) {
return;
}
- // In software the samples join the others in memory
- if (sf) {
- if (waveSoftLoad(&sf->soft, bank, sf->loaded, m->waveNeed) != MUSIC_OK) {
- dvxLog("Music: program %" PRId32 "'s samples could not be read; its notes play on the FM synthesizer", program);
- }
-
+ if (!sf) {
+ m->waveLoading = true;
return;
}
- // Its notes off before samples move in the synthesizer's memory (the
- // timer may be sending)
- uint32_t flags = platformIrqSave();
-
- m->wave->sink.silence(&m->wave->sink);
- platformIrqRestore(flags);
- audioWtGetInfo(&info);
-
- if (waveLoadNeeded(sCtx, &sWave->bank, &sWave->cache, m->waveNeed, info.flags, &missing) != MUSIC_OK || missing > 0) {
- dvxLog("Music: not every sample of program %" PRId32 " fits in the synthesizer's memory; those notes play on the FM synthesizer", program);
+ if (waveSoftLoad(&sf->soft, bank, sf->loaded, m->waveNeed) != MUSIC_OK) {
+ dvxLog("Music: program %" PRId32 "'s samples could not be read; its notes play on the FM synthesizer", program);
}
}
diff --git a/src/libs/kpunch/music/music.dhs b/src/libs/kpunch/music/music.dhs
index b6a5a279..ce8c6f1d 100644
--- a/src/libs/kpunch/music/music.dhs
+++ b/src/libs/kpunch/music/music.dhs
@@ -85,7 +85,7 @@
Music for DVX programs on whatever sound hardware the computer has: tracker modules (MOD, S3M, XM, IT), Standard MIDI Files (type 0 and 1), WAV and VOC sound files, and live MIDI a program sends itself. Every music belongs to the program that loaded it and is stopped and freed when the program ends, however it ends; any number of programs may play music at the same time.
-Header: music/music.h. Loaded as: bin/libs/kpunch/music/music.lib (needs audio.lib); the General MIDI instrument bank for FM is bin/libs/kpunch/music/genmidi.op2, and the General MIDI SoundFont for wavetable music is an add-on (SHARED\BANKS\KPUNCH\DVXGM.SF2).
+Header: music/music.h. Loaded as: bin/libs/kpunch/music/music.lib (needs audio.lib); the tracker module player (libxmp-lite) is bin/libs/kpunch/music/players/xmp.drv, which music.lib loads when a module is opened and lets go a minute after the last one closes; the General MIDI instrument bank for FM is bin/libs/kpunch/music/genmidi.op2, and the General MIDI SoundFont for wavetable music is an add-on (SHARED\BANKS\KPUNCH\DVXGM.SF2).
.h2 Playing a File
@@ -141,7 +141,7 @@ When there is no FM chip (or it is busy), MIDI plays on DVX's own software FM sy
A wavetable synthesizer (audio.lib's gus and guswt drivers for a Gravis UltraSound, awe for the EMU8000 of a Sound Blaster AWE32 or AWE64) plays recorded instruments from its own memory. Its instruments come from a SoundFont, DVX.INI [audio] soundFont (MUSIC_INI_SOUND_FONT), named as midiBank names an OP2 bank; not set, the standard one (MUSIC_SOUND_FONT_DEFAULT, KPUNCH\DVXGM.SF2: DVX's General MIDI SoundFont add-on, made from FluidR3_GM by mksf, about 3.4 MB). SoundFont 2 files are read in full; SoundFont 1 (.SBK, the AWE32's own) as far as it is known (it has no public specification). soundFont may also name a Gravis UltraSound's ULTRASND.INI by its path, which plays that folder's .PAT patches as instruments; and where the standard SoundFont is not installed, the patches in the folder ULTRADIR names play (musicPatchSetPath), else the AWE's own bank for its sample ROM (musicRomBankPath: SFBANK\SYNTHGM.SF2 or SYNTHGM.SBK in the folder SOUND names, as Creative's drivers installed it). Control Panel's Sounds page lists all of these.
-A song loads only the samples its notes play (the bank selects, programs, and notes are looked at first), each with the frames a chip's interpolation reads after it, into memory the synthesizer gives every program a share of; what a song loaded stays for the next while it fits, and room is made by giving up the samples used longest ago. When the samples still do not fit they are all loaded again smaller: 8-bit where the chip plays it, then at half the rate, then a quarter; at the least, the ones that do not fit are left out. Notes the bank lacks, or whose samples were left out, play on the FM chip or the software synthesizer, as do the notes beyond the synthesizer's voices. One music plays on the wavetable synthesizer at a time (another meanwhile plays on FM); its SoundFont and samples are kept 60 seconds after the last music (MUSIC_FONT_KEEP_SEC). Live MIDI loads a program's samples as it is chosen. A SoundFont made for an AWE's General MIDI ROM (ROM samples) plays from the card's ROM with nothing loaded; on any other synthesizer its notes go to FM.
+A song loads only the samples its notes play (the bank selects, programs, and notes are looked at first), each with the frames a chip's interpolation reads after it, into memory the synthesizer gives every program a share of; what a song loaded stays for the next while it fits, and room is made by giving up the samples used longest ago. When the samples still do not fit (a size they cannot fit at, even in the whole of the synthesizer's memory, is not tried) they are all loaded again smaller: 8-bit where the chip plays it, then at half the rate, then a quarter; at the least, the ones that do not fit are left out. Notes the bank lacks, or whose samples were left out, play on the FM chip or the software synthesizer, as do the notes beyond the synthesizer's voices. One music plays on the wavetable synthesizer at a time (another meanwhile plays on FM); its samples are kept 60 seconds after the last music (MUSIC_FONT_KEEP_SEC), and the SoundFont itself (its headers, a GUS patch set's files) for the whole session, so a later song does not read them again. Live MIDI loads a program's samples once it is chosen, one at a time in the idle time after (a whole instrument at once would hold DVX up), at the best quality that fits beside the samples already loaded; until they are in, its notes play on the FM chip. A SoundFont made for an AWE's General MIDI ROM (ROM samples) plays from the card's ROM with nothing loaded; on any other synthesizer its notes go to FM.
The AWE runs the SoundFont's envelopes, LFOs, filter, and reverb and chorus sends itself; on the GUS the library runs the envelopes and LFOs (its volume and pitch changed as the note goes on), without a filter or effects.
@@ -199,7 +199,7 @@ A MIDI recording keeps channel messages with their times (musicMidiRecordAdd(rec
MOD (ProTracker and compatibles), S3M, XM, and IT through libxmp-lite 4.7.3 (thirdparty/libxmp-lite with DVX's patches, listed in its DVX-VERSION.md; formats only, no depackers or ProWizard). Stereo separation is 50 percent (softer than the Amiga's hard left and right). The whole module is held in memory. libxmp works out pitches with floating point, so it plays modules in the shell's idle time, never at interrupt time; on a 486SX (no maths coprocessor) that goes through floating point emulation and is slow. On a stream libxmp mixes every channel in software (linear interpolation, three multiplies a channel a frame).
-On a wavetable synthesizer (musicModHw.c) libxmp mixes nothing: DVX's hardware voice patch has it report each of its voices as every tick starts (the sample, where it is, its rate, volume, pan, and the loop it is in), and only move them on. Those ticks are made up to 400 ms ahead in the idle time, as the synthesizer's notes, and the music timer plays them on time (500 times a second: a note starts within 2 ms of its time). A voice starts on the chip where libxmp starts or moves it (a note, a sample offset, a retrigger, another sample or loop); otherwise its pitch, volume, and pan follow libxmp's each tick, and the chip's own interpolation plays it. The samples are stored as they are (an 8-bit one 8-bit where the chip plays those, as a GUS does), else all 8-bit. The chips loop only forward, so a ping-pong loop is stored unrolled: its way back after its end, each frame where libxmp reads it (an Impulse Tracker module's way back a frame shorter, as Impulse Tracker plays it). What the chips cannot do plays near enough: a ping-pong sustain loop goes forward, a sample played backwards with no loop goes forward, and Impulse Tracker's filters are not heard; a voice the chip has none free for (a program holds the rest) is not heard. A voice at full volume plays 8 dB down, with the chip's level (AudioWtInfoT) taken off too, so a module is about as loud as libxmp mixes it. A pause stops the voices, and the play goes on from the tick that was playing; musicGetPosition is the tick being heard.
+On a wavetable synthesizer (musicModHw.c) libxmp mixes nothing: DVX's hardware voice patch has it report each of its voices as every tick starts (the sample, where it is, its rate, volume, pan, and the loop it is in), and only move them on. Those ticks are made up to 400 ms ahead in the idle time, as the synthesizer's notes, and the music timer plays them on time (500 times a second: a note starts within 2 ms of its time). A voice starts on the chip where libxmp starts or moves it (a note, a sample offset, a retrigger, another sample or loop); otherwise its pitch, volume, and pan follow libxmp's each tick, and the chip's own interpolation plays it. The samples are stored as they are (an 8-bit one 8-bit where the chip plays those, as a GUS does), else all 8-bit; a way of storing them that cannot fit in the synthesizer's free memory is not tried (nothing is uploaded for nothing), and a module that fits in neither plays mixed in software. A big module loads giving way to DVX's other tasks between its samples (a DVX patch to libxmp, 0002-load-hook), and a file of no music kind is refused by its first bytes, without being read whole. A WAV or VOC file plays as it is read. The chips loop only forward, so a ping-pong loop is stored unrolled: its way back after its end, each frame where libxmp reads it (an Impulse Tracker module's way back a frame shorter, as Impulse Tracker plays it). What the chips cannot do plays near enough: a ping-pong sustain loop goes forward, a sample played backwards with no loop goes forward, and Impulse Tracker's filters are not heard; a voice the chip has none free for (a program holds the rest) is not heard. A voice at full volume plays 8 dB down, with the chip's level (AudioWtInfoT) taken off too, so a module is about as loud as libxmp mixes it. A pause stops the voices, and the play goes on from the tick that was playing; musicGetPosition is the tick being heard.
Measured on an emulated 486DX2-66 at 22,050 Hz, a 4-channel MOD with all four channels sounding takes about 49 percent of the CPU mixed in software and about 14 percent on a Gravis UltraSound's voices, most of that libxmp's own work each tick (about 1.3 ms of the 20).
@@ -212,9 +212,9 @@ musicMmlOpen(&tune);
musicMmlPlay(tune, "MB T140 O3 L8 CDEC CDEC EFG4 EFG4");
.endcode
-A macro queue plays tunes written in QBasic's music macro language (the commands are listed in music.h and in the BASIC language reference: notes A-G with sharps and flats, N, O, > and <, L, P, dots, T, MN / ML / MS, MF / MB). musicMmlPlay parses the whole string first; a mistake anywhere returns MUSIC_ERR_MACRO and queues nothing. The settings last from one string to the next. musicMmlTone queues a square-wave tone of milliHz thousandths of a hertz (0: silence) for us microseconds (QBasic's SOUND). musicMmlQueued counts the notes not yet played, musicMmlStop silences the queue, musicMmlBusy says whether a caller that follows QBasic should still wait (foreground mode: until everything has played; background: while more than MUSIC_MML_BACKGROUND_MAX notes wait) and moves the queue along. A queue belongs to the program that opened it and is freed when that program ends.
+A macro queue plays tunes written in QBasic's music macro language (the commands are listed in music.h and in the BASIC language reference: notes A-G with sharps and flats, N, O, > and <, L, P, dots, T, MN / ML / MS, MF / MB, and QB64's V (volume) and @ (waveform: 1 square, 3 triangle, 5 noise)). musicMmlPlay parses the whole string first; a mistake anywhere returns MUSIC_ERR_MACRO and queues nothing. The settings last from one string to the next. musicMmlTone queues a square-wave tone of milliHz thousandths of a hertz (0: silence) for us microseconds (QBasic's SOUND). musicMmlQueued counts the notes not yet played, musicMmlStop silences the queue, musicMmlBusy says whether a caller that follows QBasic should still wait (foreground mode: until everything has played; background: while more than MUSIC_MML_BACKGROUND_MAX notes wait) and moves the queue along. A queue belongs to the program that opened it and is freed when that program ends.
-Timing is QBasic's: a whole note is four beats of 60 / T seconds, each dot adds half again, and a note sounds for 7/8 (MN), all (ML), or 3/4 (MS) of its length. Octave 3 starts at middle C; octave 3's A is 440 Hz. The notes play as square waves on an audio.lib note queue, each to the exact sample (on the PC speaker as tones). DVX's own I command (I1 to I128) plays the notes that follow on that General MIDI instrument: each run of such notes becomes a small MIDI file that plays like any MIDI music (on the synthesizer DVX.INI [audio] midi chooses); I0 returns to tones. Runs play one after the other; when the instruments cannot play (no instrument bank, no device) the run plays as tones.
+Timing is QBasic's: a whole note is four beats of 60 / T seconds, each dot adds half again, and a note sounds for 7/8 (MN), all (ML), or 3/4 (MS) of its length. Octave 3 starts at middle C; octave 3's A is 440 Hz. The notes play on an audio.lib note queue, each to the exact sample in its own waveform and volume (audioNotesSetTone; on the PC speaker as square tones, V0 silent); V also scales an instrument's velocity. DVX's own I command (I1 to I128) plays the notes that follow on that General MIDI instrument: each run of such notes becomes a small MIDI file that plays like any MIDI music (on the synthesizer DVX.INI [audio] midi chooses); I0 returns to tones. Runs play one after the other; when the instruments cannot play (no instrument bank, no device) the run plays as tones.
.h2 Several Programs at Once
@@ -222,7 +222,7 @@ Every music plays through one audio context of the library's own; audio.lib mixe
.h2 BASIC
-src/include/basic/music.bas (DECLARE LIBRARY "music"): MusicLoad(fileName) and MusicMidiOpen() return a music number (0 on failure: MusicError$ says why); MusicPlay(music, loopIt), MusicStop, MusicPause, MusicResume, MusicPlaying, MusicVolume(music, percent), MusicPosition, MusicLength, MusicSeek(music, ms), MusicFree; live MIDI MusicNoteOn(music, channel, note, velocity), MusicNoteOff, MusicProgram, MusicControl, MusicPitchBend with channels 1 to 16 (10 for drums), and MusicMidiSend(music, midiMsg) (musicMidiSend of a message packed as MidiInMessage gives it, AUDIO_MIDI_PACKED_*). MIDI recordings: MidiRecordNew (a recording number, 0 when all 8 are in use), MidiRecordAdd(recording, milliseconds, midiMsg), MidiRecordCount, MidiRecordLength, MidiRecordSave(recording, fileName) (True / False with MusicError$), MidiRecordFree. A number only works for the program that got it. MusicLoadMacro(macro) (musicLoadMacro, a music number) and MusicSetBank(music, bank) (musicSetBank, "" the standard bank, a SoundFont or an ULTRASND.INI for wavetable music; MusicError$ says when it is missing or damaged); MusicPatchSet$ and MusicRomBank$ (musicPatchSetPath, musicRomBankPath: "" when there is none). MusicGetVolume(music) (musicGetVolume in percent) and MusicInfo$(music, field) (musicGetInfo: "title", "output", "kind" = "module" / "midi" / "live" / "sound" for MusicKindE, "where" = "" / "stream" / "mpu401" / "fm" / "software" / "wave" / "softwave" for MusicOutputE, "paused" and "looping" as "-1" / "0"; any case, "" for another field). music.bas also declares the raw MIDI port calls of audio.lib (MidiPortInfo$, MidiAcquire, MidiPut, MidiRelease) and its MIDI input (MidiInOpen, MidiInClose, MidiInMessage, MidiInTime, MidiInClock, MidiInLost, and the MIDI_* CONSTs); see the sound library.
+src/include/basic/music.bas (DECLARE LIBRARY "music"): MusicLoad(fileName), MusicLoadData(bytes AS BINARY STRING) (musicLoadMem), and MusicMidiOpen() return a music number (0 on failure: MusicError$ says why); MusicPlay(music, loopIt), MusicStop, MusicPause, MusicResume, MusicPlaying, MusicEnded (True once after the music played to its end: each number's musicSetEndFn sets a flag that MusicPlay and MusicEnded clear), MusicVolume(music, percent), MusicPosition, MusicLength, MusicSeek(music, ms), MusicFree; live MIDI MusicNoteOn(music, channel, note, velocity), MusicNoteOff, MusicProgram, MusicControl, MusicPitchBend with channels 1 to 16 (10 for drums), and MusicMidiSend(music, midiMsg) (musicMidiSend of a message packed as MidiInMessage gives it, AUDIO_MIDI_PACKED_*). MIDI recordings: MidiRecordNew (a recording number, 0 when all 8 are in use), MidiRecordAdd(recording, milliseconds, midiMsg), MidiRecordCount, MidiRecordLength, MidiRecordSave(recording, fileName) (True / False with MusicError$), MidiRecordFree. A number only works for the program that got it. MusicLoadMacro(macro) (musicLoadMacro, a music number) and MusicSetBank(music, bank) (musicSetBank, "" the standard bank, a SoundFont or an ULTRASND.INI for wavetable music; MusicError$ says when it is missing or damaged); MusicPatchSet$ and MusicRomBank$ (musicPatchSetPath, musicRomBankPath: "" when there is none). MusicGetVolume(music) (musicGetVolume in percent) and MusicInfo$(music, field) (musicGetInfo: "title", "output", "kind" = "module" / "midi" / "live" / "sound" for MusicKindE, "where" = "" / "stream" / "mpu401" / "fm" / "software" / "wave" / "softwave" for MusicOutputE, "paused" and "looping" as "-1" / "0"; any case, "" for another field). music.bas also declares the raw MIDI port calls of audio.lib (MidiPortInfo$, MidiAcquire, MidiPut, MidiRelease) and its MIDI input (MidiInOpen, MidiInClose, MidiInMessage, MidiInTime, MidiInClock, MidiInLost, and the MIDI_* CONSTs); see the sound library.
The BASIC runtime itself calls musicBasBeep, musicBasSound(hz, ticks), musicBasPlay(macro), musicBasPlayCount, and musicBasBusy for the statements BEEP, SOUND, and PLAY and the PLAY(n) function; each BASIC program has its own macro queue. QBasic's X command and "=" numbers (given with VARPTR$) are filled in by the BASIC runtime before musicBasPlay is called, so the macro language itself has neither. SOUND's duration is in clock ticks (18.2 a second) and 0 stops the queue; BEEP is 800 Hz for a quarter second.
diff --git a/src/libs/kpunch/music/music.h b/src/libs/kpunch/music/music.h
index a8e71bb0..d9ba699b 100644
--- a/src/libs/kpunch/music/music.h
+++ b/src/libs/kpunch/music/music.h
@@ -332,7 +332,11 @@ void musicMidiRecordFree(MusicMidiRecordT *rec);
// may wait)
// I n DVX's own: n 1-128 plays the notes that follow on General
// MIDI instrument n (1 is the piano) on the music synthesizer,
-// I0 (the default) square-wave tones again
+// I0 (the default) tones again
+// V n QB64's: volume of the notes that follow, 0-100 percent
+// (default 100; an instrument's velocity too)
+// @ n QB64's: the tones' waveform, 1 square (the default), 3
+// triangle, 5 noise
// The X (execute a string) command and VARPTR$ parameters are not part
// of the language here: BASIC's PLAY fills them in before the string
// reaches the library. The settings stay from one call to the next.
@@ -390,7 +394,8 @@ int32_t musicBasSound(double hz, double ticks);
// Each BASIC program's musics are numbered from 1 (0 = MUSIC_FAILED;
// MusicError$ says why), and its MIDI recordings the same way. Volumes
// are percent (0..100). A MIDI message is packed as MidiInMessage gives
-// it (AUDIO_MIDI_PACKED_*).
+// it (AUDIO_MIDI_PACKED_*). MusicLoadData takes BINARY STRING bytes;
+// MusicEnded is True once after a music played to its end.
void MidiRecordAdd(int32_t handle, int32_t milliseconds, int32_t midiMsg);
int32_t MidiRecordCount(int32_t handle);
void MidiRecordFree(int32_t handle);
@@ -398,12 +403,14 @@ int32_t MidiRecordLength(int32_t handle);
int32_t MidiRecordNew(void);
int32_t MidiRecordSave(int32_t handle, const char *fileName);
void MusicControl(int32_t handle, int32_t channel, int32_t controller, int32_t value);
+int32_t MusicEnded(int32_t handle);
const char *MusicError(void);
void MusicFree(int32_t handle);
int32_t MusicGetVolume(int32_t handle);
const char *MusicInfo(int32_t handle, const char *field);
int32_t MusicLength(int32_t handle);
int32_t MusicLoad(const char *fileName);
+int32_t MusicLoadData(const uint8_t *data, int32_t len);
int32_t MusicLoadMacro(const char *macro);
int32_t MusicMidiOpen(void);
void MusicMidiSend(int32_t handle, int32_t midiMsg);
diff --git a/src/libs/kpunch/music/musicBas.c b/src/libs/kpunch/music/musicBas.c
index 91d02f89..225be3a6 100644
--- a/src/libs/kpunch/music/musicBas.c
+++ b/src/libs/kpunch/music/musicBas.c
@@ -45,7 +45,6 @@
#define BYTE_MASK 0xFF
#define BAS_FIRST_CHANNEL 1
#define PERCENT_MAX 100
-#define BAS_INFO_MAX (MUSIC_TITLE_MAX + MUSIC_DESC_MAX)
// QBasic's clock tick (SOUND durations): 65536 PIT clocks of 1,193,182 Hz
#define QB_TICK_US (65536.0 * 1000000.0 / 1193182.0)
@@ -56,6 +55,7 @@
typedef struct {
MusicT *music;
int32_t appId;
+ bool ended; // played to its end since MusicEnded or MusicPlay last asked
} BasSlotT;
typedef struct {
@@ -83,7 +83,7 @@ static const char *const sWhereWords[] = { "", "stream", "mpu401", "fm"
static BasSlotT sSlots[BAS_SLOTS];
static BasRecordSlotT sRecords[BAS_RECORD_SLOTS];
static int32_t sLastError = MUSIC_OK;
-static char sInfo[BAS_INFO_MAX];
+static BasTextT sText; // the last string result
// ============================================================
// Prototypes
@@ -91,10 +91,12 @@ static char sInfo[BAS_INFO_MAX];
static int32_t basAdd(MusicT *music, int32_t rc);
static int32_t basAppId(void);
+static void basEnded(MusicT *music, void *userData);
static MusicT *basGet(int32_t handle);
static uint8_t basPacked(int32_t midiMsg, int32_t shift);
static MusicMidiRecordT *basRecord(int32_t handle);
static int32_t basResult(int32_t rc);
+static BasSlotT *basSlot(int32_t handle);
static const char *basWord(const char *const *words, int32_t count, int32_t value);
void MidiRecordAdd(int32_t handle, int32_t milliseconds, int32_t midiMsg);
int32_t MidiRecordCount(int32_t handle);
@@ -109,12 +111,14 @@ int32_t musicBasPlay(const char *macro);
int32_t musicBasPlayCount(void);
int32_t musicBasSound(double hz, double ticks);
void MusicControl(int32_t handle, int32_t channel, int32_t controller, int32_t value);
+int32_t MusicEnded(int32_t handle);
const char *MusicError(void);
void MusicFree(int32_t handle);
int32_t MusicGetVolume(int32_t handle);
const char *MusicInfo(int32_t handle, const char *field);
int32_t MusicLength(int32_t handle);
int32_t MusicLoad(const char *fileName);
+int32_t MusicLoadData(const uint8_t *data, int32_t len);
int32_t MusicLoadMacro(const char *macro);
int32_t MusicMidiOpen(void);
void MusicMidiSend(int32_t handle, int32_t midiMsg);
@@ -147,7 +151,9 @@ static int32_t basAdd(MusicT *music, int32_t rc) {
if (!sSlots[i].music) {
sSlots[i].music = music;
sSlots[i].appId = appId;
+ sSlots[i].ended = false;
sLastError = MUSIC_OK;
+ musicSetEndFn(music, basEnded, &sSlots[i]);
return i + 1;
}
}
@@ -163,15 +169,19 @@ static int32_t basAppId(void) {
}
+// A music of a BASIC program played to its end (MusicEnded).
+static void basEnded(MusicT *music, void *userData) {
+ (void)music;
+ ((BasSlotT *)userData)->ended = true;
+}
+
+
// The calling program's music with this number, else NULL (and the error
// is noted).
static MusicT *basGet(int32_t handle) {
- if (handle < 1 || handle > BAS_SLOTS || !sSlots[handle - 1].music || sSlots[handle - 1].appId != basAppId()) {
- sLastError = MUSIC_ERR_ARG;
- return NULL;
- }
+ BasSlotT *s = basSlot(handle);
- return sSlots[handle - 1].music;
+ return s ? s->music : NULL;
}
@@ -200,6 +210,18 @@ static int32_t basResult(int32_t rc) {
}
+// The calling program's music with this number, else NULL (and the
+// error is noted).
+static BasSlotT *basSlot(int32_t handle) {
+ if (handle < 1 || handle > BAS_SLOTS || !sSlots[handle - 1].music || sSlots[handle - 1].appId != basAppId()) {
+ sLastError = MUSIC_ERR_ARG;
+ return NULL;
+ }
+
+ return &sSlots[handle - 1];
+}
+
+
// The word for an enum value ("" out of range).
static const char *basWord(const char *const *words, int32_t count, int32_t value) {
return (value >= 0 && value < count) ? words[value] : "";
@@ -318,6 +340,20 @@ void MusicControl(int32_t handle, int32_t channel, int32_t controller, int32_t v
}
+// True once after the music played to its end (not when stopped), since
+// MusicPlay started it or MusicEnded last said so.
+int32_t MusicEnded(int32_t handle) {
+ BasSlotT *s = basSlot(handle);
+ bool ended = s && s->ended;
+
+ if (s) {
+ s->ended = false;
+ }
+
+ return ended ? BAS_TRUE : BAS_FALSE;
+}
+
+
const char *MusicError(void) {
return musicErrorText(sLastError);
}
@@ -365,12 +401,10 @@ const char *MusicInfo(int32_t handle, const char *field) {
switch ((MusicInfoFieldE)idx) {
case InfoTitleE:
- snprintf(sInfo, sizeof(sInfo), "%s", info.title);
- return sInfo;
+ return basTextSet(&sText, info.title);
case InfoOutputE:
- snprintf(sInfo, sizeof(sInfo), "%s", info.output);
- return sInfo;
+ return basTextSet(&sText, info.output);
case InfoKindE:
return basWord(sKindWords, DVX_ARRAY_LEN(sKindWords), info.kind);
@@ -379,12 +413,10 @@ const char *MusicInfo(int32_t handle, const char *field) {
return basWord(sWhereWords, DVX_ARRAY_LEN(sWhereWords), info.where);
case InfoPausedE:
- snprintf(sInfo, sizeof(sInfo), "%d", info.paused ? BAS_TRUE : BAS_FALSE);
- return sInfo;
+ return basTextFormat(&sText, "%d", info.paused ? BAS_TRUE : BAS_FALSE);
case InfoLoopingE:
- snprintf(sInfo, sizeof(sInfo), "%d", info.looping ? BAS_TRUE : BAS_FALSE);
- return sInfo;
+ return basTextFormat(&sText, "%d", info.looping ? BAS_TRUE : BAS_FALSE);
default:
return "";
@@ -408,6 +440,15 @@ int32_t MusicLoad(const char *fileName) {
}
+// A music from the bytes of its file (a resource, an HTTP reply).
+int32_t MusicLoadData(const uint8_t *data, int32_t len) {
+ MusicT *m = NULL;
+ int32_t rc = musicLoadMem(data, len > 0 ? (uint32_t)len : 0, &m);
+
+ return basAdd(m, rc);
+}
+
+
int32_t MusicLoadMacro(const char *macro) {
MusicT *m = NULL;
int32_t rc = musicLoadMacro(macro, &m);
@@ -442,13 +483,13 @@ void MusicNoteOn(int32_t handle, int32_t channel, int32_t note, int32_t velocity
// The UltraSound's own patches' ULTRASND.INI (musicPatchSetPath), or "".
const char *MusicPatchSet(void) {
- static char path[DVX_MAX_PATH];
+ char path[DVX_MAX_PATH];
if (!musicPatchSetPath(path, sizeof(path))) {
path[0] = '\0';
}
- return path;
+ return basTextSet(&sText, path);
}
@@ -463,7 +504,13 @@ void MusicPitchBend(int32_t handle, int32_t channel, int32_t bend) {
void MusicPlay(int32_t handle, int32_t loopIt) {
- sLastError = musicPlay(basGet(handle), loopIt != BAS_FALSE);
+ BasSlotT *s = basSlot(handle);
+
+ if (s) {
+ s->ended = false;
+ }
+
+ sLastError = musicPlay(s ? s->music : NULL, loopIt != BAS_FALSE);
}
@@ -489,13 +536,13 @@ void MusicResume(int32_t handle) {
// The AWE's own ROM bank (musicRomBankPath), or "".
const char *MusicRomBank(void) {
- static char path[DVX_MAX_PATH];
+ char path[DVX_MAX_PATH];
if (!musicRomBankPath(path, sizeof(path))) {
path[0] = '\0';
}
- return path;
+ return basTextSet(&sText, path);
}
diff --git a/src/libs/kpunch/music/musicInt.h b/src/libs/kpunch/music/musicInt.h
index 90c3c056..34f5cc46 100644
--- a/src/libs/kpunch/music/musicInt.h
+++ b/src/libs/kpunch/music/musicInt.h
@@ -47,7 +47,9 @@
// musicWaveLoad.c a SoundFont's samples in the synthesizer's memory
// musicWaveSoft.c the SoundFont software synthesizer (WaveSoftT)
// musicCents.c cents, powers of two, and attenuation in fixed point
-// musicXmp.c tracker modules through libxmp-lite
+// musicXmpDrv.c tracker modules: the player module, loaded when one
+// is opened
+// musicXmp.c ... that module (xmp.drv): libxmp-lite behind it
// musicSound.c WAV and VOC files (SoundSrcT): a WAV read from the
// disk as it plays, a VOC decoded into memory
// musicModHw.c a module on a wavetable synthesizer's voices: its
@@ -75,6 +77,10 @@
#include "music.h"
#include "audioDrv.h"
+// Microseconds
+#define US_PER_SEC 1000000ULL
+#define US_PER_MS 1000
+
// ============================================================
// MIDI
// ============================================================
@@ -450,10 +456,14 @@ int32_t patLoadSet(const char *iniPath, WaveBankT *bank);
// A file or folder is at path (musicFound.c).
bool musicFileThere(const char *path);
+// The file the bank reads last read (waveBankRead keeps it open for the
+// next: a song's samples come from one or a few files) closed; a load
+// that read samples calls it once done.
+void waveBankClose(void);
+
// The bank's memory given back (the bank cleared).
void waveBankFree(WaveBankT *bank);
-
// frames frames of sample data from frame first into out (16-bit);
// false when they cannot be read.
bool waveBankRead(const WaveBankT *bank, uint32_t first, uint32_t frames, int16_t *out);
@@ -727,7 +737,8 @@ void waveSoftRender(WaveSoftT *soft, int16_t *out, uint32_t frames, uint32_t
// ============================================================
// Seconds the library keeps the SoundFont's samples in the synthesizer
-// after the last music goes (music.c), for the next one.
+// after the last music goes (music.c), for the next one (its bank is
+// kept for the session).
#define MUSIC_FONT_KEEP_SEC 60
// The bank's samples in the synthesizer's memory, kept from song to song
@@ -760,6 +771,13 @@ uint32_t waveNeedsPreset(const WaveBankT *bank, uint16_t bankNum, uint8_t progra
// cannot be read); MUSIC_ERR_NO_MEMORY.
int32_t waveLoadNeeded(AudioCtxT *ctx, const WaveBankT *bank, WaveCacheT *cache, const uint8_t *need, uint32_t flags, uint32_t *missing);
+// One needed sample that is missing loaded (the ROM ones first), at the
+// best quality that fits beside the loaded ones, which stay where they
+// are; *more when others are still missing. One that fits nowhere (or
+// cannot be read) is cleared from need: MUSIC_ERR_NO_DEVICE (or the
+// read's error).
+int32_t waveLoadStep(AudioCtxT *ctx, const WaveBankT *bank, WaveCacheT *cache, uint8_t *need, uint32_t flags, bool *more);
+
// ============================================================
// The MPU-401 port (musicMpu.c)
// ============================================================
@@ -864,7 +882,7 @@ void synthRender(SynthChipT *chip, int16_t *out, uint32_t frames, uint32_t ch
void synthWrite(SynthChipT *chip, int32_t voice, uint8_t reg, uint8_t value);
// ============================================================
-// Tracker modules (musicXmp.c)
+// Tracker modules (musicXmpDrv.c; the player module, musicXmp.c)
// ============================================================
typedef struct XmpPlayerT XmpPlayerT;
@@ -911,14 +929,17 @@ typedef void (*XmpHwFnT)(void *user, const XmpHwVoiceT *voice);
// module is ready to render at rate (Hz) in channels (1 or 2) 16-bit
// frames. xmpRender renders frames frames; false once a song that does
// not loop has ended (the rest of out is silence). Times in ms.
+// xmpIsModule: the file at path is a module libxmp plays (its head read
+// only). xmpOpen gives way (musicGiveWay) between the samples it loads.
void xmpClose(XmpPlayerT *player);
+bool xmpIsModule(const char *path);
uint32_t xmpLength(XmpPlayerT *player);
int32_t xmpOpen(const uint8_t *data, uint32_t size, uint32_t rate, uint32_t channels, XmpPlayerT **out);
uint32_t xmpPosition(XmpPlayerT *player);
bool xmpRender(XmpPlayerT *player, int16_t *out, uint32_t frames, bool loop);
void xmpRestart(XmpPlayerT *player);
void xmpSeek(XmpPlayerT *player, uint32_t ms);
-const char *xmpTitle(XmpPlayerT *player);
+const char *xmpTitle(const XmpPlayerT *player);
// The module's tracks and samples (false: no sample there), and the
// frames its ping-pong loops are shortened by on the way back (Impulse
@@ -938,6 +959,41 @@ bool xmpHwFrame(XmpPlayerT *player, bool loop, uint32_t *us);
void xmpHwOff(XmpPlayerT *player);
bool xmpHwOn(XmpPlayerT *player, uint32_t voices, XmpHwFnT fn, void *user);
+// The player module (xmp.drv, the only one in MUSIC_XMP_DIR: musicXmp.c
+// and libxmp-lite) is loaded by the first xmpIsModule or xmpOpen; without
+// it no file is a module (xmpOpen: MUSIC_ERR_FORMAT). xmpIdle
+// (musicIdle) unloads it MUSIC_XMP_KEEP_SEC after its last use with no
+// player open, so songs in a row load it once; xmpUnload (musicShutdown,
+// every player closed) unloads it now.
+void xmpIdle(void);
+void xmpUnload(void);
+
+#define MUSIC_XMP_DIR "libs" DVX_PATH_SEP "kpunch" DVX_PATH_SEP "music" DVX_PATH_SEP "players"
+#define MUSIC_XMP_SYMBOL "musicXmpDriver"
+#define MUSIC_XMP_KEEP_SEC 60
+
+// What the module exports as MUSIC_XMP_SYMBOL: the functions behind the
+// xmp* calls above, which musicXmpDrv.c makes through it.
+typedef struct {
+ DvxDriverHeaderT header;
+ __typeof__(xmpBidiAdjust) *bidiAdjust;
+ __typeof__(xmpChannels) *channels;
+ __typeof__(xmpClose) *close;
+ __typeof__(xmpHwFrame) *hwFrame;
+ __typeof__(xmpHwOff) *hwOff;
+ __typeof__(xmpHwOn) *hwOn;
+ __typeof__(xmpIsModule) *isModule;
+ __typeof__(xmpLength) *length;
+ __typeof__(xmpOpen) *open;
+ __typeof__(xmpPosition) *position;
+ __typeof__(xmpRender) *render;
+ __typeof__(xmpRestart) *restart;
+ __typeof__(xmpSample) *sample;
+ __typeof__(xmpSampleCount) *sampleCount;
+ __typeof__(xmpSeek) *seek;
+ __typeof__(xmpTitle) *title;
+} XmpDriverT;
+
// ============================================================
// Sound files (musicSound.c)
// ============================================================
@@ -945,10 +1001,10 @@ bool xmpHwOn(XmpPlayerT *player, uint32_t voices, XmpHwFnT fn, void *user
typedef struct SoundSrcT SoundSrcT;
// A WAV or VOC file as the frames of a sound music. soundOpenFile reads
-// a WAV named by its path as it plays (only its start now); soundOpenMem
-// decodes a WAV or VOC file's bytes into memory. MUSIC_OK,
-// MUSIC_ERR_FORMAT (not a file the call takes: soundOpenFile takes only
-// WAV), _OPEN, _CORRUPT, _NO_MEMORY. soundRead gives up to frames 16-bit
+// a WAV or VOC named by its path as it plays (only its start now, and a
+// VOC's block headers); soundOpenMem decodes a WAV or VOC file's bytes
+// into memory. MUSIC_OK, MUSIC_ERR_FORMAT (not a WAV or VOC file),
+// _OPEN, _CORRUPT, _NO_MEMORY. soundRead gives up to frames 16-bit
// frames from the place it is at (fewer at the end), soundSeek moves it.
// Program time only.
uint32_t soundChannels(const SoundSrcT *s);
@@ -1134,6 +1190,9 @@ bool musicLock(bool lock);
#define MML_PROGRAM_PIANO 1 // the acoustic grand piano, General MIDI's first
#define MML_PROGRAMS 128
+// Volume (V): percent of the full level (an instrument's velocity too)
+#define MML_VOLUME_MAX 100
+
// The settings a macro changes, which last from one string to the next.
typedef struct {
int32_t octave;
@@ -1141,6 +1200,8 @@ typedef struct {
int32_t tempo; // quarter notes a minute
int32_t onEighths;
int32_t program;
+ int32_t volume; // 0..MML_VOLUME_MAX (V)
+ int32_t wave; // AudioWaveE of the tones (@)
bool background;
} MmlStateT;
@@ -1151,9 +1212,11 @@ typedef struct {
uint32_t soundUs; // the part that sounds
int32_t midiNote; // MIDI note number (instruments)
int32_t program; // MML_PROGRAM_TONE or the instrument
+ int32_t volume; // 0..MML_VOLUME_MAX
+ int32_t wave; // AudioWaveE (tones)
} MmlNoteT;
-// The defaults (O4 L4 T120 MN MF I0).
+// The defaults (O4 L4 T120 MN MF I0 V100 @1).
void mmlStateInit(MmlStateT *st);
// Append the notes of macro to *notes (stb_ds array) and update st;
@@ -1180,6 +1243,12 @@ int32_t musicAppCurrent(void);
void musicAppEnded(int32_t appId);
int32_t musicAppEnter(int32_t appId);
void musicBasForget(int32_t appId);
+
+// Long work at program time (a big module loading, a file read) gives
+// way: DVX's other tasks run once MUSIC_GIVE_WAY_US has passed since
+// *last (a platformMicros time the caller started at; set again).
+#define MUSIC_GIVE_WAY_US 20000
+void musicGiveWay(uint64_t *last);
void musicIdle(void *unused);
void musicShutdown(void);
void musicSuspend(bool suspending);
diff --git a/src/libs/kpunch/music/musicMml.c b/src/libs/kpunch/music/musicMml.c
index 4eea5993..e720e14d 100644
--- a/src/libs/kpunch/music/musicMml.c
+++ b/src/libs/kpunch/music/musicMml.c
@@ -27,7 +27,8 @@
// a note of length n a 1/n of that, each dot half as long again; the
// sounding part is 7/8 (MN), all (ML) or 3/4 (MS) of it, the rest is
// silence. Octave n's C is MIDI note 24 + 12n (octave 3 starts at middle
-// C), and its A (octave 3) is 440 Hz.
+// C), and its A (octave 3) is 440 Hz. QB64's V (volume, percent) and @
+// (the tones' waveform: 1 square, 3 triangle, 5 noise) are taken too.
#include "musicInt.h"
#include "stb_ds.h"
@@ -65,6 +66,17 @@ static const uint32_t sOctave0MilliHz[MML_SEMITONES] = {
// Semitone of A..G within the octave
static const int32_t sLetterSemitone[] = { 9, 11, 0, 2, 4, 5, 7 };
+// The waveforms @n names (QB64's numbers; 2 sawtooth and 4 sine DVX does
+// not make)
+static const struct {
+ int32_t number;
+ AudioWaveE wave;
+} sMmlWaves[] = {
+ { 1, AUDIO_WAVE_SQUARE },
+ { 3, AUDIO_WAVE_TRIANGLE },
+ { 5, AUDIO_WAVE_NOISE }
+};
+
// MIDI file: 0.1 ms ticks (10,000 ticks a quarter note of 1,000,000 us)
#define SMF_DIVISION 10000
#define SMF_TEMPO_US 1000000
@@ -80,6 +92,7 @@ static int32_t countDots(const char **p);
int32_t mmlParse(MmlStateT *st, const char *macro, MmlNoteT **notes);
int32_t mmlSmfBuild(const MmlNoteT *notes, int32_t count, uint8_t **out, uint32_t *size);
void mmlStateInit(MmlStateT *st);
+static bool mmlWave(int32_t number, int32_t *wave);
static uint32_t pitchMilliHz(int32_t pitch);
static bool readNumber(const char **p, int32_t *out);
static bool readRanged(const char **p, int32_t lo, int32_t hi, int32_t *out);
@@ -100,6 +113,8 @@ static void addNote(MmlNoteT **notes, const MmlStateT *st, int32_t pitch, int32_
n.milliHz = pitch < 0 ? 0 : pitchMilliHz(pitch);
n.midiNote = pitch < 0 ? 0 : MML_MIDI_BASE + pitch;
n.program = st->program;
+ n.volume = st->volume;
+ n.wave = st->wave;
arrput(*notes, n);
}
@@ -206,6 +221,14 @@ int32_t mmlParse(MmlStateT *st, const char *macro, MmlNoteT **notes) {
ok = readRanged(&p, MML_PROGRAM_TONE, MML_PROGRAMS, &s.program);
break;
+ case 'V':
+ ok = readRanged(&p, 0, MML_VOLUME_MAX, &s.volume);
+ break;
+
+ case '@':
+ ok = readNumber(&p, &n) && mmlWave(n, &s.wave);
+ break;
+
case 'M':
c = toupper((unsigned char)*p);
p = *p ? p + 1 : p;
@@ -278,11 +301,12 @@ int32_t mmlSmfBuild(const MmlNoteT *notes, int32_t count, uint8_t **out, uint32_
lastTick = onAt;
}
- if (n->milliHz) {
+ // V scales the velocity (a note at V0 is silent)
+ if (n->milliHz && n->volume > 0) {
midiPutVlq(&trk, onAt - lastTick);
arrput(trk, MIDI_NOTE_ON);
arrput(trk, (uint8_t)n->midiNote);
- arrput(trk, SMF_VELOCITY);
+ arrput(trk, (uint8_t)DVX_MAX(1, SMF_VELOCITY * n->volume / MML_VOLUME_MAX));
midiPutVlq(&trk, offAt - onAt);
arrput(trk, MIDI_NOTE_OFF);
arrput(trk, (uint8_t)n->midiNote);
@@ -308,10 +332,25 @@ void mmlStateInit(MmlStateT *st) {
st->tempo = MML_TEMPO_DEFAULT;
st->onEighths = MML_ON_NORMAL;
st->program = MML_PROGRAM_TONE;
+ st->volume = MML_VOLUME_MAX;
+ st->wave = AUDIO_WAVE_SQUARE;
st->background = false;
}
+// The waveform QB64's @number names, false for one DVX does not make.
+static bool mmlWave(int32_t number, int32_t *wave) {
+ for (int32_t i = 0; i < DVX_ARRAY_LEN(sMmlWaves); i++) {
+ if (sMmlWaves[i].number == number) {
+ *wave = (int32_t)sMmlWaves[i].wave;
+ return true;
+ }
+ }
+
+ return false;
+}
+
+
static uint32_t pitchMilliHz(int32_t pitch) {
return sOctave0MilliHz[pitch % MML_SEMITONES] << (pitch / MML_SEMITONES);
}
diff --git a/src/libs/kpunch/music/musicMmlPlay.c b/src/libs/kpunch/music/musicMmlPlay.c
index 38581fbe..7d0e9f78 100644
--- a/src/libs/kpunch/music/musicMmlPlay.c
+++ b/src/libs/kpunch/music/musicMmlPlay.c
@@ -46,7 +46,6 @@
// Note queue entries one note may take (the tone and the silence after it)
#define MML_ENTRIES_NOTE 2
-#define US_PER_MS 1000
// A run of notes played the same way.
typedef struct {
@@ -325,6 +324,8 @@ int32_t musicMmlTone(MusicMmlT *mml, uint32_t milliHz, uint32_t us) {
n.us = us;
n.soundUs = milliHz ? us : 0;
n.program = MML_PROGRAM_TONE;
+ n.volume = MML_VOLUME_MAX;
+ n.wave = AUDIO_WAVE_SQUARE;
partAdd(mml, &n, 1, false);
pump(mml);
return MUSIC_OK;
@@ -402,6 +403,8 @@ static bool tonesFeed(MusicMmlT *m, MmlPartT *part) {
while (m->tones && part->next < count && audioNotesSpace(m->tones) >= MML_ENTRIES_NOTE) {
const MmlNoteT *n = &part->notes[part->next++];
+ audioNotesSetTone(m->tones, (AudioWaveE)n->wave, n->volume * AUDIO_VOLUME_MAX / MML_VOLUME_MAX);
+
if (n->milliHz && audioNotesAdd(m->tones, n->milliHz, n->soundUs) == AUDIO_OK) {
if (n->us > n->soundUs) {
audioNotesAdd(m->tones, 0, n->us - n->soundUs);
diff --git a/src/libs/kpunch/music/musicModHw.c b/src/libs/kpunch/music/musicModHw.c
index a2691957..b8a2e131 100644
--- a/src/libs/kpunch/music/musicModHw.c
+++ b/src/libs/kpunch/music/musicModHw.c
@@ -55,7 +55,6 @@
#define RATE_FIX 16 // a rate's sixteenths count in its pitch
#define VOL_SHIFT 3 // XMP_HW_VOL_MAX >> VOL_SHIFT = WAVE_STEP_MAX
#define LEVEL_CB 80 // a voice at full volume 8 dB down: as loud as libxmp mixes it (measured on a GUS alone)
-#define US_PER_MS 1000
// ============================================================
// Prototypes
@@ -71,6 +70,7 @@ uint32_t modHwPosition(const ModHwT *mod);
void modHwSetGain(ModHwT *mod, int32_t gain);
void modHwSilence(ModHwT *mod);
static void modHwVoice(void *user, const XmpHwVoiceT *v);
+static uint32_t sampleFrames(const ModHwT *mod, const XmpSampleT *xs, uint32_t *body, uint32_t *back);
static int32_t sampleLoad(ModHwT *mod, const XmpSampleT *xs, uint32_t i, uint32_t bits);
static void samplesFree(ModHwT *mod);
static int32_t samplesLoad(ModHwT *mod, XmpPlayerT *xmp, uint32_t flags);
@@ -288,16 +288,26 @@ static void modHwVoice(void *user, const XmpHwVoiceT *v) {
}
+// The frames sample xs is stored as (sampleLoad): *body of its own (to a
+// ping-pong loop's end), then *back of the loop's way back.
+static uint32_t sampleFrames(const ModHwT *mod, const XmpSampleT *xs, uint32_t *body, uint32_t *back) {
+ uint32_t span = xs->loopEnd - xs->loopStart;
+
+ *back = xs->bidi && span > mod->bidiAdjust ? span - mod->bidiAdjust : 0;
+ *body = *back ? xs->loopEnd : xs->frames;
+ return *body + *back;
+}
+
+
// Sample i into the synthesizer's memory at bits: its frames (a ping-pong
// loop unrolled), then WAVE_GUARD_FRAMES the chip's interpolation reads
// past the end (the loop again, where the stored frames end with it;
// else silence). MODHW_ERR_ROOM when it does not fit.
static int32_t sampleLoad(ModHwT *mod, const XmpSampleT *xs, uint32_t i, uint32_t bits) {
ModHwSampleT *s = &mod->samples[i];
- uint32_t span = xs->loopEnd - xs->loopStart;
- uint32_t back = xs->bidi && span > mod->bidiAdjust ? span - mod->bidiAdjust : 0;
- uint32_t body = back ? xs->loopEnd : xs->frames;
- uint32_t frames = body + back;
+ uint32_t body;
+ uint32_t back;
+ uint32_t frames = sampleFrames(mod, xs, &body, &back);
uint32_t repeat = xs->loops && (back || xs->loopEnd == xs->frames) ? frames - xs->loopStart : 0;
int16_t *pcm = (int16_t *)malloc((frames + WAVE_GUARD_FRAMES) * sizeof(int16_t));
uint32_t addr = 0;
@@ -358,13 +368,30 @@ static void samplesFree(ModHwT *mod) {
// Every sample as it is (8-bit ones 8-bit where the chip plays those),
// else (some do not fit) all 8-bit where it does; a sample libxmp has no
-// data for is left out.
+// data for is left out. A pass whose samples cannot fit in the memory
+// free (all of it, blocks apart or not) is passed over, nothing uploaded.
static int32_t samplesLoad(ModHwT *mod, XmpPlayerT *xmp, uint32_t flags) {
- bool chip8 = (flags & AUDIO_WT_8BIT) != 0;
- int32_t rc = MODHW_ERR_ROOM;
+ bool chip8 = (flags & AUDIO_WT_8BIT) != 0;
+ int32_t rc = MODHW_ERR_ROOM;
+ AudioWtInfoT info;
+
+ audioWtGetInfo(&info);
for (uint32_t pass = 0; pass < (chip8 ? 2U : 1U) && rc == MODHW_ERR_ROOM; pass++) {
XmpSampleT xs;
+ uint64_t bytes = 0;
+ uint32_t body;
+ uint32_t back;
+
+ for (uint32_t i = 0; i < mod->sampleCount; i++) {
+ if (xmpSample(xmp, i, &xs)) {
+ bytes += (uint64_t)(sampleFrames(mod, &xs, &body, &back) + WAVE_GUARD_FRAMES) * ((chip8 && (pass > 0 || !xs.bits16) ? BITS_8 : BITS_16) / BYTE_BITS);
+ }
+ }
+
+ if (bytes > info.freeTotal) {
+ continue;
+ }
rc = MUSIC_OK;
samplesFree(mod);
diff --git a/src/libs/kpunch/music/musicOp2.c b/src/libs/kpunch/music/musicOp2.c
index e429c4f2..2ac19d5d 100644
--- a/src/libs/kpunch/music/musicOp2.c
+++ b/src/libs/kpunch/music/musicOp2.c
@@ -32,6 +32,7 @@
// All numbers little-endian.
#include "musicInt.h"
+#include "dvxEndian.h"
#include
@@ -43,7 +44,6 @@
#define OP2_FINE_TUNE_AT 2
#define OP2_FIXED_NOTE_AT 3
#define OP2_NOTE_OFFSET_AT 14
-#define BYTE_BITS 8
// Byte offsets within a voice
enum {
@@ -79,7 +79,7 @@ int32_t op2Parse(const uint8_t *data, uint32_t size, Op2BankT *bank) {
const uint8_t *p = data + OP2_SIGNATURE_BYTES + i * OP2_INSTR_BYTES;
Op2InstrumentT *in = &bank->instruments[i];
- in->flags = (uint16_t)(p[0] | (p[1] << BYTE_BITS));
+ in->flags = dvxGetLe16(p);
in->fineTune = p[OP2_FINE_TUNE_AT];
in->fixedNote = p[OP2_FIXED_NOTE_AT];
@@ -104,5 +104,5 @@ static void readVoice(const uint8_t *p, Op2VoiceT *v) {
v->carSustain = p[VOICE_CAR_SUSTAIN];
v->carWave = p[VOICE_CAR_WAVE];
v->carLevel = (uint8_t)(p[VOICE_CAR_KSL] | p[VOICE_CAR_LEVEL]);
- v->noteOffset = (int16_t)(p[OP2_NOTE_OFFSET_AT] | (p[OP2_NOTE_OFFSET_AT + 1] << BYTE_BITS));
+ v->noteOffset = (int16_t)dvxGetLe16(p + OP2_NOTE_OFFSET_AT);
}
diff --git a/src/libs/kpunch/music/musicPat.c b/src/libs/kpunch/music/musicPat.c
index 49a5c959..ed9ef818 100644
--- a/src/libs/kpunch/music/musicPat.c
+++ b/src/libs/kpunch/music/musicPat.c
@@ -54,6 +54,7 @@
// pan balance 0..15
#include "musicInt.h"
+#include "dvxEndian.h"
#include "stb_ds.h"
#include
@@ -129,7 +130,6 @@
#define RATE_RANGE_STEP 8 // each range step: 8 times slower
#define FRAME_NS 38873 // a GF1 frame at 24 voices
#define NS_PER_US 1000
-#define US_PER_SEC 1000000
#define LFO_BASE_MILLIHZ 8176 // 0 absolute cents
#define LFO_MILLIHZ_BASE 50 // 0.05 Hz
#define LFO_MILLIHZ_NUM 2333 // + 0.02333 Hz a step
@@ -179,8 +179,6 @@ typedef struct {
// ============================================================
static int32_t keyCents(uint32_t milliHz);
-static uint32_t le16(const uint8_t *p);
-static uint32_t le32(const uint8_t *p);
static int32_t lfoCents(uint8_t rate);
static void patAddWave(PatBuildT *b, const PatWaveT *w, uint16_t file, uint8_t keyLo, uint8_t keyHi);
static bool patFind(PrefsHandleT *ini, const char *section, const char *dir, int32_t key, char *path);
@@ -195,16 +193,6 @@ static int32_t keyCents(uint32_t milliHz) {
}
-static uint32_t le16(const uint8_t *p) {
- return (uint32_t)p[0] | (uint32_t)p[1] << 8;
-}
-
-
-static uint32_t le32(const uint8_t *p) {
- return le16(p) | le16(p + 2) << 16;
-}
-
-
// An LFO rate byte (0.05 Hz + 0.0233 Hz a step) in absolute cents.
static int32_t lfoCents(uint8_t rate) {
return waveRatioCents(LFO_MILLIHZ_BASE + rate * LFO_MILLIHZ_NUM / LFO_MILLIHZ_DEN, LFO_BASE_MILLIHZ);
@@ -481,7 +469,7 @@ static int32_t patRead(const char *path, PatWaveT **waves) {
return MUSIC_ERR_BANK;
}
- uint32_t count = le16(head + PAT_WAVES_AT);
+ uint32_t count = dvxGetLe16(head + PAT_WAVES_AT);
long at = PAT_HEADER_BYTES + PAT_INSTR_BYTES + PAT_LAYER_BYTES;
for (uint32_t i = 0; i < count && rc == MUSIC_OK; i++) {
@@ -494,13 +482,13 @@ static int32_t patRead(const char *path, PatWaveT **waves) {
memset(&w, 0, sizeof(w));
w.dataAt = (uint32_t)(at + PAT_WAVE_BYTES);
- w.size = le32(wh + W_SIZE);
- w.loopStart = le32(wh + W_LOOP_START);
- w.loopEnd = le32(wh + W_LOOP_END);
- w.rate = (uint16_t)le16(wh + W_RATE);
- w.low = le32(wh + W_LOW);
- w.high = le32(wh + W_HIGH);
- w.root = le32(wh + W_ROOT);
+ w.size = dvxGetLe32(wh + W_SIZE);
+ w.loopStart = dvxGetLe32(wh + W_LOOP_START);
+ w.loopEnd = dvxGetLe32(wh + W_LOOP_END);
+ w.rate = dvxGetLe16(wh + W_RATE);
+ w.low = dvxGetLe32(wh + W_LOW);
+ w.high = dvxGetLe32(wh + W_HIGH);
+ w.root = dvxGetLe32(wh + W_ROOT);
w.balance = wh[W_BALANCE] > BALANCE_MAX ? BALANCE_MAX : wh[W_BALANCE];
w.tremRate = wh[W_TREM_RATE];
w.tremDepth = wh[W_TREM_DEPTH];
@@ -508,8 +496,8 @@ static int32_t patRead(const char *path, PatWaveT **waves) {
w.vibRate = wh[W_VIB_RATE];
w.vibDepth = wh[W_VIB_DEPTH];
w.modes = wh[W_MODES];
- w.scaleKey = (uint16_t)le16(wh + W_SCALE_FREQ);
- w.scaleFactor = (uint16_t)le16(wh + W_SCALE_FACTOR);
+ w.scaleKey = dvxGetLe16(wh + W_SCALE_FREQ);
+ w.scaleFactor = dvxGetLe16(wh + W_SCALE_FACTOR);
memcpy(w.envRate, wh + W_ENV_RATE, ENV_POINTS);
memcpy(w.envLevel, wh + W_ENV_OFFSET, ENV_POINTS);
at += PAT_WAVE_BYTES + (long)w.size;
diff --git a/src/libs/kpunch/music/musicSf2.c b/src/libs/kpunch/music/musicSf2.c
index 90e9def7..0b1fb6b5 100644
--- a/src/libs/kpunch/music/musicSf2.c
+++ b/src/libs/kpunch/music/musicSf2.c
@@ -56,6 +56,7 @@
// specification is not public; this follows what is known of the files.
#include "musicInt.h"
+#include "dvxEndian.h"
#include
#include
@@ -251,6 +252,11 @@ static const GenRuleT sGenRules[SF_GEN_COUNT] = {
[60] = { 0, 0, 0, false }
};
+// The file waveBankRead read last, kept open for the next read
+// (waveBankClose closes it)
+static FILE *sReadFp = NULL;
+static char sReadPath[DVX_MAX_PATH];
+
// ============================================================
// Prototypes
// ============================================================
@@ -260,8 +266,7 @@ static bool bagRange(const HydraT *h, int32_t bagArray, uint32_t bag, ui
static int32_t buildRegions(const HydraT *h, WaveBankT *bank);
static bool findHydra(const uint8_t *pdta, uint32_t size, bool sf1, HydraT *h);
static int32_t instrumentRegions(const HydraT *h, const WaveBankT *bank, uint32_t inst, const ZoneT *preset, uint32_t *count, WaveRegionT **regions);
-static uint32_t le16(const uint8_t *p);
-static uint32_t le32(const uint8_t *p);
+static FILE *readFile(const char *path);
static int32_t readHydra(SourceT *src, uint32_t offset, uint32_t size, bool sf1, WaveBankT *bank);
static int32_t readSamples(const HydraT *h, WaveBankT *bank);
static void regionGens(bool sf1, const ZoneT *inst, const ZoneT *preset, int16_t *out);
@@ -275,6 +280,7 @@ int32_t sf2Load(const char *path, WaveBankT *bank);
int32_t sf2Parse(const uint8_t *data, uint32_t size, WaveBankT *bank);
static bool sourcesRead(const WaveBankT *bank, uint32_t first, uint32_t frames, int16_t *out);
static bool srcRead(SourceT *src, uint32_t offset, void *out, uint32_t len);
+void waveBankClose(void);
void waveBankFree(WaveBankT *bank);
bool waveBankRead(const WaveBankT *bank, uint32_t first, uint32_t frames, int16_t *out);
static bool zoneHas(const HydraT *h, int32_t genArray, uint32_t first, uint32_t last, uint32_t oper);
@@ -285,10 +291,10 @@ static bool zoneHas(const HydraT *h, int32_t genArray, uint32_t first, u
static void applyGens(const HydraT *h, int32_t genArray, uint32_t first, uint32_t last, ZoneT *zone) {
for (uint32_t g = first; g < last; g++) {
const uint8_t *rec = h->at[genArray] + g * GEN_BYTES;
- uint32_t oper = le16(rec + GEN_OPER);
+ uint32_t oper = dvxGetLe16(rec + GEN_OPER);
if (oper < SF_GEN_COUNT) {
- zone->gen[oper] = (int16_t)le16(rec + GEN_AMOUNT);
+ zone->gen[oper] = (int16_t)dvxGetLe16(rec + GEN_AMOUNT);
zone->set[oper] = 1;
}
}
@@ -300,8 +306,8 @@ static void applyGens(const HydraT *h, int32_t genArray, uint32_t first, uint32_
static bool bagRange(const HydraT *h, int32_t bagArray, uint32_t bag, uint32_t *first, uint32_t *last) {
int32_t genArray = bagArray == H_PBAG ? H_PGEN : H_IGEN;
- *first = le16(h->at[bagArray] + bag * BAG_BYTES + BAG_GEN);
- *last = le16(h->at[bagArray] + (bag + 1) * BAG_BYTES + BAG_GEN);
+ *first = dvxGetLe16(h->at[bagArray] + bag * BAG_BYTES + BAG_GEN);
+ *last = dvxGetLe16(h->at[bagArray] + (bag + 1) * BAG_BYTES + BAG_GEN);
return *first <= *last && *last <= h->count[genArray];
}
@@ -318,15 +324,15 @@ static int32_t buildRegions(const HydraT *h, WaveBankT *bank) {
for (uint32_t p = 0; p < presets; p++) {
const uint8_t *rec = h->at[H_PHDR] + p * PHDR_BYTES;
- uint32_t bagFirst = le16(rec + PHDR_BAG);
- uint32_t bagLast = le16(rec + PHDR_BYTES + PHDR_BAG);
+ uint32_t bagFirst = dvxGetLe16(rec + PHDR_BAG);
+ uint32_t bagLast = dvxGetLe16(rec + PHDR_BYTES + PHDR_BAG);
WavePresetT *out = &bank->presets[p];
ZoneT global;
memcpy(out->name, rec, SF_NAME_LEN);
out->name[SF_NAME_LEN] = '\0';
- out->program = (uint16_t)le16(rec + PHDR_PRESET);
- out->bank = (uint16_t)le16(rec + PHDR_BANK);
+ out->program = dvxGetLe16(rec + PHDR_PRESET);
+ out->bank = dvxGetLe16(rec + PHDR_BANK);
out->first = bank->regionCount;
if (bagFirst > bagLast || bagLast >= h->count[H_PBAG]) {
@@ -386,7 +392,7 @@ static bool findHydra(const uint8_t *pdta, uint32_t size, bool sf1, HydraT *h) {
h->sf1 = sf1;
for (uint32_t at = 0; at + RIFF_HEADER_BYTES <= size;) {
- uint32_t len = le32(pdta + at + RIFF_ID_BYTES);
+ uint32_t len = dvxGetLe32(pdta + at + RIFF_ID_BYTES);
uint32_t body = at + RIFF_HEADER_BYTES;
if (len > size - body) {
@@ -423,8 +429,8 @@ static bool findHydra(const uint8_t *pdta, uint32_t size, bool sf1, HydraT *h) {
// its ranges), appended to *regions.
static int32_t instrumentRegions(const HydraT *h, const WaveBankT *bank, uint32_t inst, const ZoneT *preset, uint32_t *count, WaveRegionT **regions) {
const uint8_t *rec = h->at[H_INST] + inst * INST_BYTES;
- uint32_t bagFirst = le16(rec + INST_BAG);
- uint32_t bagLast = le16(rec + INST_BYTES + INST_BAG);
+ uint32_t bagFirst = dvxGetLe16(rec + INST_BAG);
+ uint32_t bagLast = dvxGetLe16(rec + INST_BYTES + INST_BAG);
ZoneT global;
if (bagFirst > bagLast || bagLast >= h->count[H_IBAG]) {
@@ -502,13 +508,17 @@ static int32_t instrumentRegions(const HydraT *h, const WaveBankT *bank, uint32_
}
-static uint32_t le16(const uint8_t *p) {
- return (uint32_t)p[0] | ((uint32_t)p[1] << 8);
-}
+// path opened for waveBankRead, or the file already open when it is path
+// (one is kept until waveBankClose).
+static FILE *readFile(const char *path) {
+ if (sReadFp && strcmp(sReadPath, path) == 0) {
+ return sReadFp;
+ }
-
-static uint32_t le32(const uint8_t *p) {
- return (uint32_t)p[0] | ((uint32_t)p[1] << 8) | ((uint32_t)p[2] << 16) | ((uint32_t)p[3] << 24);
+ waveBankClose();
+ sReadFp = fopen(path, "rb");
+ snprintf(sReadPath, sizeof(sReadPath), "%s", path);
+ return sReadFp;
}
@@ -553,11 +563,11 @@ static int32_t readSamples(const HydraT *h, WaveBankT *bank) {
if (h->sf1) {
const uint8_t *rec = h->at[H_SHDR] + i * SHDR1_BYTES;
- rom = rom && le32(rec + SHDR1_START) != 0;
- s->start = le32(rec + SHDR1_START);
- s->end = le32(rec + SHDR1_END);
- s->loopStart = le32(rec + SHDR1_LOOP_START) + SF1_LOOP_START_ADD;
- s->loopEnd = le32(rec + SHDR1_LOOP_END) + SF1_LOOP_END_ADD;
+ rom = rom && dvxGetLe32(rec + SHDR1_START) != 0;
+ s->start = dvxGetLe32(rec + SHDR1_START);
+ s->end = dvxGetLe32(rec + SHDR1_END);
+ s->loopStart = dvxGetLe32(rec + SHDR1_LOOP_START) + SF1_LOOP_START_ADD;
+ s->loopEnd = dvxGetLe32(rec + SHDR1_LOOP_END) + SF1_LOOP_END_ADD;
s->rate = SF1_RATE;
s->rootKey = SF1_ROOT;
s->correction = 0;
@@ -565,14 +575,14 @@ static int32_t readSamples(const HydraT *h, WaveBankT *bank) {
} else {
const uint8_t *rec = h->at[H_SHDR] + i * SHDR_BYTES;
- s->start = le32(rec + SHDR_START);
- s->end = le32(rec + SHDR_END);
- s->loopStart = le32(rec + SHDR_LOOP_START);
- s->loopEnd = le32(rec + SHDR_LOOP_END);
- s->rate = le32(rec + SHDR_RATE);
+ s->start = dvxGetLe32(rec + SHDR_START);
+ s->end = dvxGetLe32(rec + SHDR_END);
+ s->loopStart = dvxGetLe32(rec + SHDR_LOOP_START);
+ s->loopEnd = dvxGetLe32(rec + SHDR_LOOP_END);
+ s->rate = dvxGetLe32(rec + SHDR_RATE);
s->rootKey = rec[SHDR_PITCH] > MIDI_TOP ? UNPITCHED_ROOT : rec[SHDR_PITCH];
s->correction = (int8_t)rec[SHDR_CORRECTION];
- s->type = (uint16_t)le16(rec + SHDR_TYPE);
+ s->type = dvxGetLe16(rec + SHDR_TYPE);
}
if (!(s->type & SF_SAMPLE_ROM) && (s->start > s->end || s->end > bank->dataFrames)) {
@@ -766,7 +776,7 @@ static int32_t sf2From(SourceT *src, WaveBankT *bank) {
return MUSIC_ERR_BANK;
}
- uint32_t end = RIFF_HEADER_BYTES + le32(head + RIFF_ID_BYTES);
+ uint32_t end = RIFF_HEADER_BYTES + dvxGetLe32(head + RIFF_ID_BYTES);
end = end < src->size ? end : src->size;
@@ -778,7 +788,7 @@ static int32_t sf2From(SourceT *src, WaveBankT *bank) {
return MUSIC_ERR_BANK;
}
- len = le32(list + RIFF_ID_BYTES);
+ len = dvxGetLe32(list + RIFF_ID_BYTES);
if (len > end - at - RIFF_HEADER_BYTES) {
return MUSIC_ERR_BANK;
@@ -797,7 +807,7 @@ static int32_t sf2From(SourceT *src, WaveBankT *bank) {
return MUSIC_ERR_BANK;
}
- ckLen = le32(ck + RIFF_ID_BYTES);
+ ckLen = dvxGetLe32(ck + RIFF_ID_BYTES);
if (ckLen > bodyEnd - sub - RIFF_HEADER_BYTES) {
return MUSIC_ERR_BANK;
@@ -806,7 +816,7 @@ static int32_t sf2From(SourceT *src, WaveBankT *bank) {
if (memcmp(list + RIFF_HEADER_BYTES, "INFO", RIFF_TYPE_BYTES) == 0 && memcmp(ck, "ifil", RIFF_ID_BYTES) == 0 && ckLen >= IFIL_BYTES) {
uint8_t ver[IFIL_BYTES];
- version = srcRead(src, sub + RIFF_HEADER_BYTES, ver, sizeof(ver)) ? le16(ver) : 0;
+ version = srcRead(src, sub + RIFF_HEADER_BYTES, ver, sizeof(ver)) ? dvxGetLe16(ver) : 0;
} else if (memcmp(list + RIFF_HEADER_BYTES, "sdta", RIFF_TYPE_BYTES) == 0 && memcmp(ck, "smpl", RIFF_ID_BYTES) == 0) {
bank->dataOffset = sub + RIFF_HEADER_BYTES;
bank->dataFrames = ckLen / SAMPLE_BYTES;
@@ -908,16 +918,12 @@ static bool sourcesRead(const WaveBankT *bank, uint32_t first, uint32_t frames,
return false;
}
- FILE *f = fopen(bank->files[src->file], "rb");
+ FILE *f = readFile(bank->files[src->file]);
long at = (long)src->offset + (long)(from - src->first) * (long)bytes;
bool ok = f && fseek(f, at, SEEK_SET) == 0 && fread(raw, bytes, count, f) == count;
- if (f) {
- fclose(f);
- }
-
for (uint32_t k = 0; ok && k < count; k++) {
- uint32_t v = bytes == SAMPLE_BYTES ? le16(raw + k * SAMPLE_BYTES) : (uint32_t)raw[k] << BYTE_BITS;
+ uint32_t v = bytes == SAMPLE_BYTES ? dvxGetLe16(raw + k * SAMPLE_BYTES) : (uint32_t)raw[k] << BYTE_BITS;
out[from - first + k] = (int16_t)(src->format & WAVE_SRC_UNSIGNED ? v ^ SIGN_FLIP : v);
}
@@ -947,6 +953,16 @@ static bool srcRead(SourceT *src, uint32_t offset, void *out, uint32_t len) {
}
+void waveBankClose(void) {
+ if (sReadFp) {
+ fclose(sReadFp);
+ }
+
+ sReadFp = NULL;
+ sReadPath[0] = '\0';
+}
+
+
void waveBankFree(WaveBankT *bank) {
free(bank->presets);
free(bank->regions);
@@ -970,25 +986,20 @@ bool waveBankRead(const WaveBankT *bank, uint32_t first, uint32_t frames, int16_
if (bank->memory) {
for (uint32_t i = 0; i < frames; i++) {
- out[i] = (int16_t)le16(bank->memory + offset + i * SAMPLE_BYTES);
+ out[i] = (int16_t)dvxGetLe16(bank->memory + offset + i * SAMPLE_BYTES);
}
return true;
}
- FILE *f = fopen(bank->path, "rb");
- bool ok = f && fseek(f, (long)offset, SEEK_SET) == 0 && fread(out, SAMPLE_BYTES, frames, f) == frames;
+ FILE *f = readFile(bank->path);
- if (f) {
- fclose(f);
- }
-
- return ok;
+ return f && fseek(f, (long)offset, SEEK_SET) == 0 && fread(out, SAMPLE_BYTES, frames, f) == frames;
}
// Whether generators first .. last - 1 hold oper (a zone's instrument or
// sample: its last generator, by the specification).
static bool zoneHas(const HydraT *h, int32_t genArray, uint32_t first, uint32_t last, uint32_t oper) {
- return last > first && le16(h->at[genArray] + (last - 1) * GEN_BYTES + GEN_OPER) == oper;
+ return last > first && dvxGetLe16(h->at[genArray] + (last - 1) * GEN_BYTES + GEN_OPER) == oper;
}
diff --git a/src/libs/kpunch/music/musicSmf.c b/src/libs/kpunch/music/musicSmf.c
index 8b4ade1d..837eec02 100644
--- a/src/libs/kpunch/music/musicSmf.c
+++ b/src/libs/kpunch/music/musicSmf.c
@@ -44,6 +44,7 @@
// recordings, the events put into a byte array by midiPut....
#include "musicInt.h"
+#include "dvxEndian.h"
#include "stb_ds.h"
#include
@@ -71,7 +72,6 @@
#define META_END_OF_TRACK 0x2F
#define META_TEMPO_BYTES 3
#define TEMPO_DEFAULT_US 500000 // 120 beats a minute
-#define US_PER_SEC 1000000ULL
#define BYTE_BITS 8
#define BYTE_MASK 0xFF
#define CHUNK_LEN_BYTES 4
@@ -99,8 +99,6 @@ typedef struct {
// Prototypes
// ============================================================
-static uint32_t be16(const uint8_t *p);
-static uint32_t be32(const uint8_t *p);
static bool findSmf(const uint8_t *data, uint32_t size, const uint8_t **smf, uint32_t *smfSize);
static bool idIs(const uint8_t *p, const char *id);
int32_t midiFileBuild(const uint8_t *trk, uint32_t trkLen, uint32_t division, uint8_t **out, uint32_t *size);
@@ -117,14 +115,6 @@ static uint32_t readTrack(SmfCursorT c, uint32_t track, RawEventT **raw, uint8_t
static bool readVlq(SmfCursorT *c, uint32_t *value);
-static uint32_t be16(const uint8_t *p) {
- return ((uint32_t)p[0] << BYTE_BITS) | p[1];
-}
-
-
-static uint32_t be32(const uint8_t *p) {
- return (be16(p) << (BYTE_BITS * 2)) | be16(p + 2);
-}
// The SMF inside data: the data itself, or a RIFF RMID file's "data"
@@ -143,7 +133,7 @@ static bool findSmf(const uint8_t *data, uint32_t size, const uint8_t **smf, uin
uint32_t at = RIFF_HEADER_BYTES;
while (at + CHUNK_HEADER_BYTES <= size) {
- uint32_t len = data[at + 4] | ((uint32_t)data[at + 5] << BYTE_BITS) | ((uint32_t)data[at + 6] << (BYTE_BITS * 2)) | ((uint32_t)data[at + 7] << (BYTE_BITS * 3));
+ uint32_t len = dvxGetLe32(data + at + CHUNK_ID_BYTES);
if (idIs(data + at, "data")) {
uint32_t avail = size - at - CHUNK_HEADER_BYTES;
@@ -223,14 +213,14 @@ int32_t midiParse(const uint8_t *data, uint32_t size, MidiFileT *out) {
return MUSIC_ERR_FORMAT;
}
- uint32_t hdrLen = be32(smf + CHUNK_ID_BYTES);
+ uint32_t hdrLen = dvxGetBe32(smf + CHUNK_ID_BYTES);
if (hdrLen < SMF_HEADER_BYTES || smfSize < CHUNK_HEADER_BYTES + SMF_HEADER_BYTES) {
return MUSIC_ERR_CORRUPT;
}
- uint32_t type = be16(smf + CHUNK_HEADER_BYTES);
- uint32_t division = be16(smf + CHUNK_HEADER_BYTES + 4);
+ uint32_t type = dvxGetBe16(smf + CHUNK_HEADER_BYTES);
+ uint32_t division = dvxGetBe16(smf + CHUNK_HEADER_BYTES + 4);
if (type > SMF_TYPE_MAX) {
return MUSIC_ERR_FORMAT;
@@ -258,7 +248,7 @@ int32_t midiParse(const uint8_t *data, uint32_t size, MidiFileT *out) {
uint32_t endTick = 0;
while (at + CHUNK_HEADER_BYTES <= smfSize) {
- uint32_t len = be32(smf + at + CHUNK_ID_BYTES);
+ uint32_t len = dvxGetBe32(smf + at + CHUNK_ID_BYTES);
uint32_t avail = smfSize - at - CHUNK_HEADER_BYTES;
if (len > avail) {
diff --git a/src/libs/kpunch/music/musicSound.c b/src/libs/kpunch/music/musicSound.c
index 43d15666..ff394ad1 100644
--- a/src/libs/kpunch/music/musicSound.c
+++ b/src/libs/kpunch/music/musicSound.c
@@ -27,9 +27,12 @@
// its first SOUND_HEAD_MAX bytes (audioWavParse finds its format and
// where its sound is), and the stream's fill reads the frames it needs, a
// buffer at a time, at program time; a long recording needs no memory.
-// A VOC file (its blocks may hold silences and continue one another), or
-// a sound file given as bytes, is decoded into 16-bit frames in memory
-// (audioDecode; not pinned, as nothing reads it at interrupt time).
+// A VOC file named by its path is read as it plays too: its blocks (which
+// may hold silences and continue one another) are walked once at loading
+// to count its frames (audioVocNext, their headers only), then again as
+// it plays. A sound file given as bytes is decoded into 16-bit frames in
+// memory (audioDecode; not pinned, as nothing reads it at interrupt
+// time).
#include "musicInt.h"
@@ -42,13 +45,18 @@
#define BYTE_BITS 8
struct SoundSrcT {
- FILE *fp; // a WAV read as it plays (NULL: pcm)
- int16_t *pcm; // decoded frames (NULL: fp)
- uint8_t *raw; // fp's frames as the file has them, SOUND_READ_FRAMES at a time
- AudioFormatT fmt; // as the file has it (bits 8 or 16)
- uint32_t dataAt; // fp: where the frames start
- uint32_t frames;
- uint32_t pos; // the next frame read
+ FILE *fp; // a WAV or VOC read as it plays (NULL: pcm)
+ int16_t *pcm; // decoded frames (NULL: fp)
+ uint8_t *raw; // fp's frames as the file has them, SOUND_READ_FRAMES at a time
+ AudioFormatT fmt; // as the file has it (bits 8 or 16)
+ uint32_t dataAt; // a WAV: where the frames start
+ uint32_t frames;
+ uint32_t pos; // the next frame read
+ AudioVocT voc; // a VOC's blocks, walked as it plays
+ AudioVocPartE part; // ... the part being read
+ uint32_t partAt; // ... its next sample's byte (data)
+ uint32_t partLeft; // ... its samples not read yet
+ bool isVoc;
};
// ============================================================
@@ -65,8 +73,12 @@ int32_t soundOpenMem(const uint8_t *data, uint32_t size, SoundSrcT **out
uint32_t soundRate(const SoundSrcT *s);
uint32_t soundRead(SoundSrcT *s, int16_t *out, uint32_t frames);
static uint32_t soundReadFile(SoundSrcT *s, int16_t *out, uint32_t frames);
+static uint32_t soundReadVoc(SoundSrcT *s, int16_t *out, uint32_t frames);
void soundSeek(SoundSrcT *s, uint32_t frame);
uint32_t soundTell(const SoundSrcT *s);
+static int32_t vocOpen(SoundSrcT *s);
+static uint32_t vocRead(void *ctx, uint32_t at, uint8_t *buf, uint32_t len);
+static bool vocSkip(SoundSrcT *s, uint32_t samples);
uint32_t soundChannels(const SoundSrcT *s) {
@@ -152,6 +164,13 @@ int32_t soundOpenFile(const char *path, SoundSrcT **out) {
rc = fread(head, 1, headBytes, s->fp) == headBytes ? audioWavParse(head, headBytes, (uint32_t)size, &s->fmt, &s->dataAt, &dataBytes) : AUDIO_ERR_CORRUPT;
free(head);
+ // Not a WAV: a VOC, maybe
+ if (rc == AUDIO_ERR_FORMAT) {
+ s->voc.size = (uint32_t)size;
+ rc = vocOpen(s);
+ dataBytes = s->frames * soundFrameBytes(s);
+ }
+
if (rc != AUDIO_OK) {
soundClose(s);
return soundError(rc);
@@ -201,6 +220,10 @@ uint32_t soundRate(const SoundSrcT *s) {
uint32_t soundRead(SoundSrcT *s, int16_t *out, uint32_t frames) {
uint32_t n = frames < s->frames - s->pos ? frames : s->frames - s->pos;
+ if (s->isVoc) {
+ return soundReadVoc(s, out, n);
+ }
+
if (s->fp) {
return soundReadFile(s, out, n);
}
@@ -233,10 +256,58 @@ static uint32_t soundReadFile(SoundSrcT *s, int16_t *out, uint32_t frames) {
}
+// A VOC's parts in turn: sound read from the file, silence as 0s; a file
+// that ends (or fails) before the frames its walk counted ends there.
+static uint32_t soundReadVoc(SoundSrcT *s, int16_t *out, uint32_t frames) {
+ uint32_t chans = s->fmt.channels;
+ uint32_t want = frames * chans;
+ uint32_t done = 0;
+ uint32_t bytesOf = s->fmt.bits / BYTE_BITS;
+
+ while (done < want) {
+ if (s->partLeft == 0) {
+ if (audioVocNext(&s->voc, &s->part, &s->partAt, &s->partLeft) != AUDIO_OK || s->part == AudioVocEndE) {
+ break;
+ }
+
+ continue;
+ }
+
+ uint32_t n = DVX_MIN(DVX_MIN(want - done, s->partLeft), SOUND_READ_FRAMES * chans);
+
+ if (s->part == AudioVocSilenceE) {
+ memset(out + done, 0, n * sizeof(int16_t));
+ } else if (vocRead(s, s->partAt, s->raw, n * bytesOf) == n * bytesOf) {
+ audioConvert(s->raw, out + done, n, s->fmt.bits);
+ s->partAt += n * bytesOf;
+ } else {
+ break;
+ }
+
+ s->partLeft -= n;
+ done += n;
+ }
+
+ if (done < want) {
+ s->frames = s->pos + done / chans;
+ }
+
+ s->pos += done / chans;
+ return done / chans;
+}
+
+
+// A VOC's blocks are walked from the start to the frame.
void soundSeek(SoundSrcT *s, uint32_t frame) {
s->pos = frame < s->frames ? frame : s->frames;
- if (s->fp) {
+ if (s->isVoc) {
+ s->partLeft = 0;
+
+ if (audioVocBegin(&s->voc) != AUDIO_OK || !vocSkip(s, s->pos * s->fmt.channels)) {
+ s->frames = s->pos;
+ }
+ } else if (s->fp) {
uint32_t at = s->dataAt + s->pos * soundFrameBytes(s);
fseek(s->fp, (long)at, SEEK_SET);
@@ -247,3 +318,69 @@ void soundSeek(SoundSrcT *s, uint32_t frame) {
uint32_t soundTell(const SoundSrcT *s) {
return s->pos;
}
+
+
+// A VOC file (s->fp, s->voc.size its bytes): its blocks walked once, their
+// headers only, for its format and frame count.
+static int32_t vocOpen(SoundSrcT *s) {
+ AudioVocPartE kind = AudioVocDataE;
+ uint32_t at;
+ uint32_t count;
+ uint32_t samples = 0;
+
+ s->isVoc = true;
+ s->voc.read = vocRead;
+ s->voc.ctx = s;
+
+ int32_t rc = audioVocBegin(&s->voc);
+
+ while (rc == AUDIO_OK && (rc = audioVocNext(&s->voc, &kind, &at, &count)) == AUDIO_OK && kind != AudioVocEndE) {
+ samples += count;
+ }
+
+ if (rc == AUDIO_OK && (!s->voc.haveFmt || samples < s->voc.fmt.channels)) {
+ rc = AUDIO_ERR_CORRUPT;
+ }
+
+ s->fmt = s->voc.fmt;
+ s->frames = rc == AUDIO_OK ? samples / s->fmt.channels : 0;
+ return rc;
+}
+
+
+// audioVoc's read: from the file (ctx: the SoundSrcT).
+static uint32_t vocRead(void *ctx, uint32_t at, uint8_t *buf, uint32_t len) {
+ SoundSrcT *s = (SoundSrcT *)ctx;
+
+ if (fseek(s->fp, (long)at, SEEK_SET) != 0) {
+ return 0;
+ }
+
+ return (uint32_t)fread(buf, 1, len, s->fp);
+}
+
+
+// Pass samples samples of a VOC walked from its start (the part reached
+// left part-read); false when it ends first.
+static bool vocSkip(SoundSrcT *s, uint32_t samples) {
+ while (samples > 0) {
+ if (s->partLeft == 0) {
+ if (audioVocNext(&s->voc, &s->part, &s->partAt, &s->partLeft) != AUDIO_OK || s->part == AudioVocEndE) {
+ return false;
+ }
+
+ continue;
+ }
+
+ uint32_t n = DVX_MIN(samples, s->partLeft);
+
+ if (s->part == AudioVocDataE) {
+ s->partAt += n * (s->fmt.bits / BYTE_BITS);
+ }
+
+ s->partLeft -= n;
+ samples -= n;
+ }
+
+ return true;
+}
diff --git a/src/libs/kpunch/music/musicWaveLoad.c b/src/libs/kpunch/music/musicWaveLoad.c
index 917ef442..2e521bb3 100644
--- a/src/libs/kpunch/music/musicWaveLoad.c
+++ b/src/libs/kpunch/music/musicWaveLoad.c
@@ -77,12 +77,14 @@ static int32_t loadAt(AudioCtxT *ctx, const WaveBankT *bank, WaveCacheT *cache,
static int32_t loadOne(AudioCtxT *ctx, const WaveBankT *bank, WaveCacheT *cache, const uint8_t *need, uint32_t sample, const QualityT *q);
static uint32_t loadRom(const WaveBankT *bank, WaveCacheT *cache, const uint8_t *need, uint32_t flags);
static uint32_t markPreset(const WaveBankT *bank, const WavePresetT *preset, int32_t key, int32_t velocity, uint8_t *need);
+static bool qualityFits(const WaveBankT *bank, const WaveCacheT *cache, const uint8_t *need, const QualityT *q);
static bool qualityPlays(const QualityT *q, uint32_t flags);
static uint32_t storedFrames(const WaveBankT *bank, uint32_t sample);
static void unload(AudioCtxT *ctx, WaveCacheT *cache, uint32_t sample);
void waveCacheFree(AudioCtxT *ctx, WaveCacheT *cache);
int32_t waveCacheInit(WaveCacheT *cache, const WaveBankT *bank);
int32_t waveLoadNeeded(AudioCtxT *ctx, const WaveBankT *bank, WaveCacheT *cache, const uint8_t *need, uint32_t flags, uint32_t *missing);
+int32_t waveLoadStep(AudioCtxT *ctx, const WaveBankT *bank, WaveCacheT *cache, uint8_t *need, uint32_t flags, bool *more);
void waveNeedsFile(const WaveBankT *bank, const MidiFileT *file, uint8_t *need);
uint32_t waveNeedsPreset(const WaveBankT *bank, uint16_t bankNum, uint8_t program, uint8_t *need);
@@ -243,6 +245,35 @@ static uint32_t markPreset(const WaveBankT *bank, const WavePresetT *preset, int
// The chip plays samples at q's bits.
+// The needed samples that are not in memory at quality q fit beside the
+// ones that are (all of the synthesizer's memory else free: the most it
+// could have).
+static bool qualityFits(const WaveBankT *bank, const WaveCacheT *cache, const uint8_t *need, const QualityT *q) {
+ AudioWtInfoT info;
+ uint64_t want = 0;
+ uint64_t kept = 0;
+
+ audioWtGetInfo(&info);
+
+ for (uint32_t i = 0; i < cache->count; i++) {
+ const WaveLoadedT *ld = &cache->loaded[i];
+ bool here = ld->addr != WAVE_NOT_LOADED && !(ld->addr & AUDIO_WT_ADDR_ROM) && ld->bits == q->bits && ld->shift == q->shift;
+
+ if (!need[i] || (bank->samples[i].type & SF_SAMPLE_ROM)) {
+ continue;
+ }
+
+ if (here) {
+ kept += (uint64_t)ld->frames * (q->bits / BYTE_BITS);
+ } else {
+ want += (uint64_t)(storedFrames(bank, i) >> q->shift) * (q->bits / BYTE_BITS);
+ }
+ }
+
+ return kept + want <= info.memory;
+}
+
+
static bool qualityPlays(const QualityT *q, uint32_t flags) {
return q->bits == BITS_16 || (flags & AUDIO_WT_8BIT) != 0;
}
@@ -330,12 +361,16 @@ int32_t waveLoadNeeded(AudioCtxT *ctx, const WaveBankT *bank, WaveCacheT *cache,
last--;
}
+ // A quality whose samples cannot fit even in an empty synthesizer is
+ // passed over (not read and loaded for nothing); the least is always
+ // tried (it leaves out what does not fit)
for (int32_t qi = 0; qi <= last && rc == MUSIC_ERR_NO_DEVICE; qi++) {
- if (qualityPlays(&sQualities[qi], flags)) {
+ if (qualityPlays(&sQualities[qi], flags) && (qi == last || qualityFits(bank, cache, need, &sQualities[qi]))) {
rc = loadAt(ctx, bank, cache, need, &sQualities[qi], qi == last, missing);
}
}
+ waveBankClose();
*missing += romLeft;
// None fitted even at the least
@@ -343,6 +378,49 @@ int32_t waveLoadNeeded(AudioCtxT *ctx, const WaveBankT *bank, WaveCacheT *cache,
}
+// The ROM samples, then one missing sample at the best quality the chip
+// plays that it fits at (room made of samples not needed; the loaded ones
+// stay where they are, so nothing sounding moves). One that fits at none,
+// or cannot be read, is no longer needed: MUSIC_ERR_NO_DEVICE (or the
+// read's error), and its notes play on FM.
+int32_t waveLoadStep(AudioCtxT *ctx, const WaveBankT *bank, WaveCacheT *cache, uint8_t *need, uint32_t flags, bool *more) {
+ int32_t next = -1;
+ bool left = false;
+ int32_t rc = MUSIC_ERR_NO_DEVICE;
+
+ loadRom(bank, cache, need, flags);
+
+ for (uint32_t i = 0; i < cache->count; i++) {
+ if (need[i] && !(bank->samples[i].type & SF_SAMPLE_ROM) && cache->loaded[i].addr == WAVE_NOT_LOADED) {
+ left = left || next >= 0;
+ next = next >= 0 ? next : (int32_t)i;
+ }
+ }
+
+ *more = left;
+
+ if (next < 0) {
+ return MUSIC_OK;
+ }
+
+ cache->lastUse[next] = cache->songs;
+
+ for (int32_t qi = 0; qi < QUALITY_COUNT && rc == MUSIC_ERR_NO_DEVICE; qi++) {
+ if (qualityPlays(&sQualities[qi], flags)) {
+ rc = loadOne(ctx, bank, cache, need, (uint32_t)next, &sQualities[qi]);
+ }
+ }
+
+ waveBankClose();
+
+ if (rc != MUSIC_OK) {
+ need[next] = 0;
+ }
+
+ return rc;
+}
+
+
void waveNeedsFile(const WaveBankT *bank, const MidiFileT *file, uint8_t *need) {
uint8_t program[MUSIC_MIDI_CHANNELS];
uint8_t bankMsb[MUSIC_MIDI_CHANNELS];
diff --git a/src/libs/kpunch/music/musicWaveSoft.c b/src/libs/kpunch/music/musicWaveSoft.c
index f77df3bd..98a61717 100644
--- a/src/libs/kpunch/music/musicWaveSoft.c
+++ b/src/libs/kpunch/music/musicWaveSoft.c
@@ -199,6 +199,7 @@ int32_t waveSoftLoad(WaveSoftT *soft, const WaveBankT *bank, WaveLoadedT *loaded
soft->arena = grown;
if (!waveBankRead(bank, s->start, frames, soft->arena + soft->frames)) {
+ waveBankClose();
return MUSIC_ERR_OPEN;
}
@@ -210,6 +211,7 @@ int32_t waveSoftLoad(WaveSoftT *soft, const WaveBankT *bank, WaveLoadedT *loaded
soft->frames += frames;
}
+ waveBankClose();
return MUSIC_OK;
}
diff --git a/src/libs/kpunch/music/musicXmp.c b/src/libs/kpunch/music/musicXmp.c
index b092866f..b0487347 100644
--- a/src/libs/kpunch/music/musicXmp.c
+++ b/src/libs/kpunch/music/musicXmp.c
@@ -22,6 +22,9 @@
// musicXmp.c -- tracker modules (MOD, S3M, XM, IT) through libxmp-lite
//
+// The player module, xmp.drv, with libxmp-lite (most of what music.lib
+// would otherwise hold): music.lib loads it when a module is opened
+// (musicXmpDrv.c) and calls it through the descriptor it exports.
// libxmp-lite (thirdparty/libxmp-lite, with DVX's patches) loads the
// module from memory and renders 16-bit frames at the stream's rate with
// linear interpolation; the stream's fill function calls xmpRender at
@@ -48,7 +51,7 @@ struct XmpPlayerT {
xmp_context ctx;
uint32_t channels;
uint32_t rate;
- int32_t voices; // libxmp's voices before xmpHwOn
+ int32_t voices; // libxmp's voices before playerHwOn
XmpHwFnT hwFn; // NULL: rendering
void *hwUser;
char title[MUSIC_TITLE_MAX];
@@ -58,31 +61,54 @@ struct XmpPlayerT {
// Prototypes
// ============================================================
-uint32_t xmpBidiAdjust(XmpPlayerT *player);
-uint32_t xmpChannels(XmpPlayerT *player);
-void xmpClose(XmpPlayerT *player);
-bool xmpHwFrame(XmpPlayerT *player, bool loop, uint32_t *us);
-static void xmpHwHook(void *user, const struct xmp_hw_voice *v);
-void xmpHwOff(XmpPlayerT *player);
-bool xmpHwOn(XmpPlayerT *player, uint32_t voices, XmpHwFnT fn, void *user);
-uint32_t xmpLength(XmpPlayerT *player);
-int32_t xmpOpen(const uint8_t *data, uint32_t size, uint32_t rate, uint32_t channels, XmpPlayerT **out);
-uint32_t xmpPosition(XmpPlayerT *player);
-bool xmpRender(XmpPlayerT *player, int16_t *out, uint32_t frames, bool loop);
-void xmpRestart(XmpPlayerT *player);
-bool xmpSample(XmpPlayerT *player, uint32_t i, XmpSampleT *out);
-uint32_t xmpSampleCount(XmpPlayerT *player);
-void xmpSeek(XmpPlayerT *player, uint32_t ms);
-static bool xmpStart(XmpPlayerT *player);
-const char *xmpTitle(XmpPlayerT *player);
+static uint32_t playerBidiAdjust(XmpPlayerT *player);
+static uint32_t playerChannels(XmpPlayerT *player);
+static void playerClose(XmpPlayerT *player);
+static bool playerHwFrame(XmpPlayerT *player, bool loop, uint32_t *us);
+static void playerHwHook(void *user, const struct xmp_hw_voice *v);
+static void playerHwOff(XmpPlayerT *player);
+static bool playerHwOn(XmpPlayerT *player, uint32_t voices, XmpHwFnT fn, void *user);
+static bool playerIsModule(const char *path);
+static uint32_t playerLength(XmpPlayerT *player);
+static void playerLoadHook(void *user);
+static int32_t playerOpen(const uint8_t *data, uint32_t size, uint32_t rate, uint32_t channels, XmpPlayerT **out);
+static uint32_t playerPosition(XmpPlayerT *player);
+static bool playerRender(XmpPlayerT *player, int16_t *out, uint32_t frames, bool loop);
+static void playerRestart(XmpPlayerT *player);
+static bool playerSample(XmpPlayerT *player, uint32_t i, XmpSampleT *out);
+static uint32_t playerSampleCount(XmpPlayerT *player);
+static void playerSeek(XmpPlayerT *player, uint32_t ms);
+static bool playerStart(XmpPlayerT *player);
+static const char *playerTitle(const XmpPlayerT *player);
+
+const XmpDriverT musicXmpDriver = {
+ DVX_DRIVER_HDR(XmpDriverT, "xmp", 0),
+ .bidiAdjust = playerBidiAdjust,
+ .channels = playerChannels,
+ .close = playerClose,
+ .hwFrame = playerHwFrame,
+ .hwOff = playerHwOff,
+ .hwOn = playerHwOn,
+ .isModule = playerIsModule,
+ .length = playerLength,
+ .open = playerOpen,
+ .position = playerPosition,
+ .render = playerRender,
+ .restart = playerRestart,
+ .sample = playerSample,
+ .sampleCount = playerSampleCount,
+ .seek = playerSeek,
+ .title = playerTitle
+};
-uint32_t xmpBidiAdjust(XmpPlayerT *player) {
+
+static uint32_t playerBidiAdjust(XmpPlayerT *player) {
return (uint32_t)xmp_hw_bidir_adjust(player->ctx);
}
-uint32_t xmpChannels(XmpPlayerT *player) {
+static uint32_t playerChannels(XmpPlayerT *player) {
struct xmp_module_info mi;
xmp_get_module_info(player->ctx, &mi);
@@ -90,7 +116,7 @@ uint32_t xmpChannels(XmpPlayerT *player) {
}
-void xmpClose(XmpPlayerT *player) {
+static void playerClose(XmpPlayerT *player) {
if (!player) {
return;
}
@@ -102,7 +128,7 @@ void xmpClose(XmpPlayerT *player) {
}
-bool xmpHwFrame(XmpPlayerT *player, bool loop, uint32_t *us) {
+static bool playerHwFrame(XmpPlayerT *player, bool loop, uint32_t *us) {
struct xmp_frame_info fi;
int rc = xmp_play_frame(player->ctx);
@@ -114,7 +140,7 @@ bool xmpHwFrame(XmpPlayerT *player, bool loop, uint32_t *us) {
// libxmp's voice as XmpHwVoiceT, to the music library's function.
-static void xmpHwHook(void *user, const struct xmp_hw_voice *v) {
+static void playerHwHook(void *user, const struct xmp_hw_voice *v) {
XmpPlayerT *p = (XmpPlayerT *)user;
XmpHwVoiceT hv;
@@ -135,43 +161,48 @@ static void xmpHwHook(void *user, const struct xmp_hw_voice *v) {
// Back to rendering, where the song was.
-void xmpHwOff(XmpPlayerT *player) {
- uint32_t ms = xmpPosition(player);
+static void playerHwOff(XmpPlayerT *player) {
+ uint32_t ms = playerPosition(player);
player->hwFn = NULL;
xmp_set_hw_voices(player->ctx, NULL, NULL);
xmp_end_player(player->ctx);
xmp_set_player(player->ctx, XMP_PLAYER_VOICES, player->voices);
- xmpStart(player);
- xmpSeek(player, ms);
+ playerStart(player);
+ playerSeek(player, ms);
}
// Started again (where the song was) with the voices and the hook: the
// voice count is set before libxmp starts playing.
-bool xmpHwOn(XmpPlayerT *player, uint32_t voices, XmpHwFnT fn, void *user) {
- uint32_t ms = xmpPosition(player);
+static bool playerHwOn(XmpPlayerT *player, uint32_t voices, XmpHwFnT fn, void *user) {
+ uint32_t ms = playerPosition(player);
player->hwFn = fn;
player->hwUser = user;
xmp_end_player(player->ctx);
xmp_set_player(player->ctx, XMP_PLAYER_VOICES, (int)voices);
- xmp_set_hw_voices(player->ctx, xmpHwHook, player);
+ xmp_set_hw_voices(player->ctx, playerHwHook, player);
- if (!xmpStart(player)) {
- xmpHwOff(player);
+ if (!playerStart(player)) {
+ playerHwOff(player);
return false;
}
// Nothing is mixed: no interpolation spares libxmp copying each
// loop's ends at every turn of it
xmp_set_player(player->ctx, XMP_PLAYER_INTERP, XMP_INTERP_NEAREST);
- xmpSeek(player, ms);
+ playerSeek(player, ms);
return true;
}
-uint32_t xmpLength(XmpPlayerT *player) {
+static bool playerIsModule(const char *path) {
+ return xmp_test_module(path, NULL) == 0;
+}
+
+
+static uint32_t playerLength(XmpPlayerT *player) {
struct xmp_frame_info fi;
xmp_get_frame_info(player->ctx, &fi);
@@ -179,7 +210,13 @@ uint32_t xmpLength(XmpPlayerT *player) {
}
-int32_t xmpOpen(const uint8_t *data, uint32_t size, uint32_t rate, uint32_t channels, XmpPlayerT **out) {
+// libxmp's load hook (xmp_set_load_hook, DVX patch): before each sample.
+static void playerLoadHook(void *user) {
+ musicGiveWay((uint64_t *)user);
+}
+
+
+static int32_t playerOpen(const uint8_t *data, uint32_t size, uint32_t rate, uint32_t channels, XmpPlayerT **out) {
*out = NULL;
XmpPlayerT *p = (XmpPlayerT *)calloc(1, sizeof(XmpPlayerT));
@@ -199,8 +236,14 @@ int32_t xmpOpen(const uint8_t *data, uint32_t size, uint32_t rate, uint32_t chan
xmp_set_player(p->ctx, XMP_PLAYER_DEFPAN, XMP_DEFAULT_PAN);
+ uint64_t last = platformMicros();
+
+ xmp_set_load_hook(p->ctx, playerLoadHook, &last);
+
int rc = xmp_load_module_from_memory(p->ctx, data, (long)size);
+ xmp_set_load_hook(p->ctx, NULL, NULL);
+
if (rc != 0) {
xmp_free_context(p->ctx);
free(p);
@@ -212,7 +255,7 @@ int32_t xmpOpen(const uint8_t *data, uint32_t size, uint32_t rate, uint32_t chan
return rc == -XMP_ERROR_SYSTEM ? MUSIC_ERR_NO_MEMORY : MUSIC_ERR_CORRUPT;
}
- if (!xmpStart(p)) {
+ if (!playerStart(p)) {
xmp_release_module(p->ctx);
xmp_free_context(p->ctx);
free(p);
@@ -230,7 +273,7 @@ int32_t xmpOpen(const uint8_t *data, uint32_t size, uint32_t rate, uint32_t chan
}
-uint32_t xmpPosition(XmpPlayerT *player) {
+static uint32_t playerPosition(XmpPlayerT *player) {
struct xmp_frame_info fi;
xmp_get_frame_info(player->ctx, &fi);
@@ -238,7 +281,7 @@ uint32_t xmpPosition(XmpPlayerT *player) {
}
-bool xmpRender(XmpPlayerT *player, int16_t *out, uint32_t frames, bool loop) {
+static bool playerRender(XmpPlayerT *player, int16_t *out, uint32_t frames, bool loop) {
int bytes = (int)(frames * player->channels * XMP_BYTES_16);
int rc = xmp_play_buffer(player->ctx, out, bytes, loop ? XMP_LOOP_FOREVER : XMP_LOOP_ONCE);
@@ -247,13 +290,13 @@ bool xmpRender(XmpPlayerT *player, int16_t *out, uint32_t frames, bool loop) {
// Back to the start, with the loop count and the render buffer reset.
-void xmpRestart(XmpPlayerT *player) {
+static void playerRestart(XmpPlayerT *player) {
xmp_restart_module(player->ctx);
xmp_play_buffer(player->ctx, NULL, 0, 0);
}
-bool xmpSample(XmpPlayerT *player, uint32_t i, XmpSampleT *out) {
+static bool playerSample(XmpPlayerT *player, uint32_t i, XmpSampleT *out) {
struct xmp_module_info mi;
xmp_get_module_info(player->ctx, &mi);
@@ -280,7 +323,7 @@ bool xmpSample(XmpPlayerT *player, uint32_t i, XmpSampleT *out) {
}
-uint32_t xmpSampleCount(XmpPlayerT *player) {
+static uint32_t playerSampleCount(XmpPlayerT *player) {
struct xmp_module_info mi;
xmp_get_module_info(player->ctx, &mi);
@@ -288,7 +331,7 @@ uint32_t xmpSampleCount(XmpPlayerT *player) {
}
-void xmpSeek(XmpPlayerT *player, uint32_t ms) {
+static void playerSeek(XmpPlayerT *player, uint32_t ms) {
xmp_seek_time(player->ctx, (int)ms);
xmp_play_buffer(player->ctx, NULL, 0, 0);
}
@@ -296,7 +339,7 @@ void xmpSeek(XmpPlayerT *player, uint32_t ms) {
// The player started at the stream's rate and format, linear
// interpolation (what xmp_start_player resets is set again).
-static bool xmpStart(XmpPlayerT *player) {
+static bool playerStart(XmpPlayerT *player) {
if (xmp_start_player(player->ctx, (int)player->rate, player->channels == 1 ? XMP_FORMAT_MONO : 0) != 0) {
return false;
}
@@ -306,6 +349,6 @@ static bool xmpStart(XmpPlayerT *player) {
}
-const char *xmpTitle(XmpPlayerT *player) {
+static const char *playerTitle(const XmpPlayerT *player) {
return player->title;
}
diff --git a/src/libs/kpunch/music/musicXmpDrv.c b/src/libs/kpunch/music/musicXmpDrv.c
new file mode 100644
index 00000000..6260c31c
--- /dev/null
+++ b/src/libs/kpunch/music/musicXmpDrv.c
@@ -0,0 +1,226 @@
+// 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.
+
+// musicXmpDrv.c -- tracker modules: the player module, loaded on demand
+//
+// libxmp-lite and musicXmp.c, most of what the library would otherwise
+// hold, are a module of their own (xmp.drv in MUSIC_XMP_DIR, a driver
+// module: dvxDrv.h) that exports one XmpDriverT. The xmp* calls the rest
+// of the library makes are here, through it: the first xmpIsModule or
+// xmpOpen loads it, and xmpIdle unloads it once no player is open and it
+// has not been used for MUSIC_XMP_KEEP_SEC. A build that links
+// musicXmp.c in (the host tests) has the descriptor already. A player
+// exists only while the module is loaded, so its calls need no check.
+
+#include "musicInt.h"
+#include "dvxPlat.h"
+#include "stb_ds_wrap.h"
+
+#include
+
+#define XMP_KEEP_US (MUSIC_XMP_KEEP_SEC * US_PER_SEC)
+
+// ============================================================
+// Prototypes
+// ============================================================
+
+static bool driverValid(const DvxDriverHeaderT *desc, int32_t kind);
+uint32_t xmpBidiAdjust(XmpPlayerT *player);
+uint32_t xmpChannels(XmpPlayerT *player);
+void xmpClose(XmpPlayerT *player);
+static const XmpDriverT *xmpDriver(void);
+bool xmpHwFrame(XmpPlayerT *player, bool loop, uint32_t *us);
+void xmpHwOff(XmpPlayerT *player);
+bool xmpHwOn(XmpPlayerT *player, uint32_t voices, XmpHwFnT fn, void *user);
+void xmpIdle(void);
+bool xmpIsModule(const char *path);
+uint32_t xmpLength(XmpPlayerT *player);
+int32_t xmpOpen(const uint8_t *data, uint32_t size, uint32_t rate, uint32_t channels, XmpPlayerT **out);
+uint32_t xmpPosition(XmpPlayerT *player);
+bool xmpRender(XmpPlayerT *player, int16_t *out, uint32_t frames, bool loop);
+void xmpRestart(XmpPlayerT *player);
+bool xmpSample(XmpPlayerT *player, uint32_t i, XmpSampleT *out);
+uint32_t xmpSampleCount(XmpPlayerT *player);
+void xmpSeek(XmpPlayerT *player, uint32_t ms);
+const char *xmpTitle(const XmpPlayerT *player);
+void xmpUnload(void);
+
+static const char *const sDrvSymbols[] = { DVX_SYM(MUSIC_XMP_SYMBOL) };
+static const uint32_t sDrvSizes[] = { sizeof(XmpDriverT) };
+static const DvxDriverClassT sDrvClass = { sDrvSymbols, sDrvSizes, 1, driverValid };
+
+static DvxDriverModT sMod = { NULL, NULL, 0 }; // desc NULL: not loaded
+static const XmpDriverT *sDrv = NULL; // sMod's descriptor
+static int32_t sPlayers = 0; // open players
+static uint64_t sLastUse = 0; // platformMicros of the last xmpIsModule / xmpOpen / xmpClose
+
+
+static bool driverValid(const DvxDriverHeaderT *desc, int32_t kind) {
+ (void)kind;
+
+ const XmpDriverT *d = (const XmpDriverT *)desc;
+
+ return d->bidiAdjust && d->channels && d->close && d->hwFrame && d->hwOff && d->hwOn && d->isModule && d->length && d->open && d->position && d->render && d->restart && d->sample && d->sampleCount && d->seek && d->title;
+}
+
+
+uint32_t xmpBidiAdjust(XmpPlayerT *player) {
+ return sDrv->bidiAdjust(player);
+}
+
+
+uint32_t xmpChannels(XmpPlayerT *player) {
+ return sDrv->channels(player);
+}
+
+
+void xmpClose(XmpPlayerT *player) {
+ if (!player) {
+ return;
+ }
+
+ sDrv->close(player);
+ sPlayers--;
+ sLastUse = platformMicros();
+}
+
+
+// The player module, loaded if it is not (NULL: it is not there).
+static const XmpDriverT *xmpDriver(void) {
+ sLastUse = platformMicros();
+
+ if (sDrv) {
+ return sDrv;
+ }
+
+ const DvxDriverHeaderT *linked = (const DvxDriverHeaderT *)dlsym(NULL, sDrvSymbols[0]);
+
+ if (linked) {
+ sMod.desc = linked;
+ } else {
+ DvxDriverModT *mods = dvxDriversLoad(MUSIC_XMP_DIR, &sDrvClass);
+
+ if (arrlen(mods) > 0) {
+ sMod = dvxDriverKeep(mods, 0);
+ }
+
+ dvxDriversFree(mods);
+ }
+
+ sDrv = (const XmpDriverT *)sMod.desc;
+ return sDrv;
+}
+
+
+bool xmpHwFrame(XmpPlayerT *player, bool loop, uint32_t *us) {
+ return sDrv->hwFrame(player, loop, us);
+}
+
+
+void xmpHwOff(XmpPlayerT *player) {
+ sDrv->hwOff(player);
+}
+
+
+bool xmpHwOn(XmpPlayerT *player, uint32_t voices, XmpHwFnT fn, void *user) {
+ return sDrv->hwOn(player, voices, fn, user);
+}
+
+
+void xmpIdle(void) {
+ if (sPlayers == 0 && platformMicros() - sLastUse >= XMP_KEEP_US) {
+ xmpUnload();
+ }
+}
+
+
+bool xmpIsModule(const char *path) {
+ const XmpDriverT *drv = xmpDriver();
+
+ return drv && drv->isModule(path);
+}
+
+
+uint32_t xmpLength(XmpPlayerT *player) {
+ return sDrv->length(player);
+}
+
+
+int32_t xmpOpen(const uint8_t *data, uint32_t size, uint32_t rate, uint32_t channels, XmpPlayerT **out) {
+ const XmpDriverT *drv = xmpDriver();
+
+ *out = NULL;
+
+ if (!drv) {
+ return MUSIC_ERR_FORMAT;
+ }
+
+ int32_t rc = drv->open(data, size, rate, channels, out);
+
+ if (rc == MUSIC_OK) {
+ sPlayers++;
+ }
+
+ return rc;
+}
+
+
+uint32_t xmpPosition(XmpPlayerT *player) {
+ return sDrv->position(player);
+}
+
+
+bool xmpRender(XmpPlayerT *player, int16_t *out, uint32_t frames, bool loop) {
+ return sDrv->render(player, out, frames, loop);
+}
+
+
+void xmpRestart(XmpPlayerT *player) {
+ sDrv->restart(player);
+}
+
+
+bool xmpSample(XmpPlayerT *player, uint32_t i, XmpSampleT *out) {
+ return sDrv->sample(player, i, out);
+}
+
+
+uint32_t xmpSampleCount(XmpPlayerT *player) {
+ return sDrv->sampleCount(player);
+}
+
+
+void xmpSeek(XmpPlayerT *player, uint32_t ms) {
+ sDrv->seek(player, ms);
+}
+
+
+const char *xmpTitle(const XmpPlayerT *player) {
+ return sDrv->title(player);
+}
+
+
+// Unloaded now (a linked-in player stays; it is found again).
+void xmpUnload(void) {
+ dvxDriverUnload(&sMod);
+ sDrv = NULL;
+}
diff --git a/src/libs/kpunch/music/thirdparty/libxmp-lite/DVX-VERSION.md b/src/libs/kpunch/music/thirdparty/libxmp-lite/DVX-VERSION.md
index f33df121..3554a61a 100644
--- a/src/libs/kpunch/music/thirdparty/libxmp-lite/DVX-VERSION.md
+++ b/src/libs/kpunch/music/thirdparty/libxmp-lite/DVX-VERSION.md
@@ -8,7 +8,7 @@
| Tarball SHA-256 | e199da3f7552f5ac688a091b8d28ea3c9ffd2beb845d15aac17d4d264f851d71 (262,671 bytes) |
| Files kept | src/*.c, src/*.h, src/loaders/*.c, src/loaders/*.h, include/libxmp-lite/xmp.h (as include/xmp.h), README (it holds the licence), test/test.it and test/test.xm (used by the host tests) -- byte for byte as in the tarball but for the patches below. Not kept: the build files (configure, CMake, the Makefiles, including src/Makefile and src/loaders/Makefile), jni, the rest of test/. |
| Licence | MIT ("Extended Module Player Lite, Copyright (C) 1996-2026 Claudio Matsuoka and Hipolito Carraro Jr", in README). Keep the notice with the source; reproduced in the music library's developer help (topic lib.music) for binaries. |
-| Patches | dvx-patches/0001-hardware-voices.patch (see Patches) |
+| Patches | dvx-patches/0001-hardware-voices.patch, dvx-patches/0002-load-hook.patch (see Patches) |
## Build options
@@ -51,6 +51,12 @@ patch" in the sources.
frames the loaded module's ping-pong loops are shortened by on the way
back (Impulse Tracker's 1). music.lib (musicModHw.c) plays modules on
a wavetable synthesizer's voices that way.
+- `0002-load-hook.patch` -- `xmp_set_load_hook(ctx, fn, user)`
+ (include/xmp.h, src/control.c, src/common.h: `load_hook` and
+ `load_user` in `struct module_data`): src/loaders/sample.c's
+ `libxmp_load_sample` calls the hook before each sample loads.
+ music.lib (musicXmp.c) gives way to DVX's other tasks from it while a
+ big module loads.
## Updating
diff --git a/src/libs/kpunch/music/thirdparty/libxmp-lite/dvx-patches/0002-load-hook.patch b/src/libs/kpunch/music/thirdparty/libxmp-lite/dvx-patches/0002-load-hook.patch
new file mode 100644
index 00000000..97454f5c
--- /dev/null
+++ b/src/libs/kpunch/music/thirdparty/libxmp-lite/dvx-patches/0002-load-hook.patch
@@ -0,0 +1,66 @@
+--- a/include/xmp.h
++++ b/include/xmp.h
+@@ -368,6 +368,10 @@
+
+ typedef void (*xmp_hw_voice_fn)(void *, const struct xmp_hw_voice *);
+
++/* DVX patch (load hook): called before each sample of a module loads, so
++ * a caller can let other work run while a big module loads. */
++typedef void (*xmp_load_hook_fn)(void *);
++
+ struct xmp_callbacks {
+ unsigned long (*read_func)(void *dest, unsigned long len,
+ unsigned long nmemb, void *priv);
+@@ -424,6 +428,7 @@
+ LIBXMP_EXPORT int xmp_set_player (xmp_context, int, int);
+ LIBXMP_EXPORT int xmp_set_hw_voices (xmp_context, xmp_hw_voice_fn, void *); /* DVX patch */
+ LIBXMP_EXPORT int xmp_hw_bidir_adjust (xmp_context); /* DVX patch */
++LIBXMP_EXPORT int xmp_set_load_hook (xmp_context, xmp_load_hook_fn, void *); /* DVX patch */
+ LIBXMP_EXPORT int xmp_get_player (xmp_context, int);
+ LIBXMP_EXPORT int xmp_set_instrument_path (xmp_context, const char *);
+
+--- a/src/common.h
++++ b/src/common.h
+@@ -552,6 +552,8 @@
+ uint8 **scan_cnt; /* scan counters */
+ struct extra_sample_data *xtra;
+ struct midi_macro_data *midi;
++ xmp_load_hook_fn load_hook; /* DVX patch: before each sample loads (NULL: none) */
++ void *load_user;
+ int compare_vblank;
+ };
+
+--- a/src/control.c
++++ b/src/control.c
+@@ -423,6 +423,17 @@
+ return 0;
+ }
+
++/* DVX patch: the load hook (see xmp.h); a NULL fn is none. */
++int xmp_set_load_hook(xmp_context opaque, xmp_load_hook_fn fn, void *user)
++{
++ struct context_data *ctx = (struct context_data *)opaque;
++ struct module_data *m = &ctx->m;
++
++ m->load_hook = fn;
++ m->load_user = user;
++ return 0;
++}
++
+ /* DVX patch: the frames a ping-pong loop's way back is shortened by in
+ * the loaded module (Impulse Tracker's 1, else 0): a loop of n frames
+ * takes 2n minus this to play there and back. */
+--- a/src/loaders/sample.c
++++ b/src/loaders/sample.c
+@@ -186,6 +186,11 @@
+ int framelen;
+ int bytelen, extralen, i;
+
++ /* DVX patch: the load hook (xmp_set_load_hook) */
++ if (m->load_hook) {
++ m->load_hook(m->load_user);
++ }
++
+ #ifndef LIBXMP_CORE_PLAYER
+ /* Adlib FM patches */
+ if (flags & SAMPLE_FLAG_ADLIB) {
diff --git a/src/libs/kpunch/music/thirdparty/libxmp-lite/include/xmp.h b/src/libs/kpunch/music/thirdparty/libxmp-lite/include/xmp.h
index 65d7fef4..ac728eb6 100644
--- a/src/libs/kpunch/music/thirdparty/libxmp-lite/include/xmp.h
+++ b/src/libs/kpunch/music/thirdparty/libxmp-lite/include/xmp.h
@@ -368,6 +368,10 @@ struct xmp_hw_voice {
typedef void (*xmp_hw_voice_fn)(void *, const struct xmp_hw_voice *);
+/* DVX patch (load hook): called before each sample of a module loads, so
+ * a caller can let other work run while a big module loads. */
+typedef void (*xmp_load_hook_fn)(void *);
+
struct xmp_callbacks {
unsigned long (*read_func)(void *dest, unsigned long len,
unsigned long nmemb, void *priv);
@@ -424,6 +428,7 @@ LIBXMP_EXPORT int xmp_channel_vol (xmp_context, int, int);
LIBXMP_EXPORT int xmp_set_player (xmp_context, int, int);
LIBXMP_EXPORT int xmp_set_hw_voices (xmp_context, xmp_hw_voice_fn, void *); /* DVX patch */
LIBXMP_EXPORT int xmp_hw_bidir_adjust (xmp_context); /* DVX patch */
+LIBXMP_EXPORT int xmp_set_load_hook (xmp_context, xmp_load_hook_fn, void *); /* DVX patch */
LIBXMP_EXPORT int xmp_get_player (xmp_context, int);
LIBXMP_EXPORT int xmp_set_instrument_path (xmp_context, const char *);
diff --git a/src/libs/kpunch/music/thirdparty/libxmp-lite/src/common.h b/src/libs/kpunch/music/thirdparty/libxmp-lite/src/common.h
index 042bafaf..3139b31a 100644
--- a/src/libs/kpunch/music/thirdparty/libxmp-lite/src/common.h
+++ b/src/libs/kpunch/music/thirdparty/libxmp-lite/src/common.h
@@ -552,6 +552,8 @@ struct module_data {
uint8 **scan_cnt; /* scan counters */
struct extra_sample_data *xtra;
struct midi_macro_data *midi;
+ xmp_load_hook_fn load_hook; /* DVX patch: before each sample loads (NULL: none) */
+ void *load_user;
int compare_vblank;
};
diff --git a/src/libs/kpunch/music/thirdparty/libxmp-lite/src/control.c b/src/libs/kpunch/music/thirdparty/libxmp-lite/src/control.c
index 9412de9b..6c6e5289 100644
--- a/src/libs/kpunch/music/thirdparty/libxmp-lite/src/control.c
+++ b/src/libs/kpunch/music/thirdparty/libxmp-lite/src/control.c
@@ -423,6 +423,17 @@ int xmp_set_hw_voices(xmp_context opaque, xmp_hw_voice_fn fn, void *user)
return 0;
}
+/* DVX patch: the load hook (see xmp.h); a NULL fn is none. */
+int xmp_set_load_hook(xmp_context opaque, xmp_load_hook_fn fn, void *user)
+{
+ struct context_data *ctx = (struct context_data *)opaque;
+ struct module_data *m = &ctx->m;
+
+ m->load_hook = fn;
+ m->load_user = user;
+ return 0;
+}
+
/* DVX patch: the frames a ping-pong loop's way back is shortened by in
* the loaded module (Impulse Tracker's 1, else 0): a loop of n frames
* takes 2n minus this to play there and back. */
diff --git a/src/libs/kpunch/music/thirdparty/libxmp-lite/src/loaders/sample.c b/src/libs/kpunch/music/thirdparty/libxmp-lite/src/loaders/sample.c
index d7a4c0c9..786f5485 100644
--- a/src/libs/kpunch/music/thirdparty/libxmp-lite/src/loaders/sample.c
+++ b/src/libs/kpunch/music/thirdparty/libxmp-lite/src/loaders/sample.c
@@ -186,6 +186,11 @@ int libxmp_load_sample(struct module_data *m, HIO_HANDLE *f, int flags, struct x
int framelen;
int bytelen, extralen, i;
+ /* DVX patch: the load hook (xmp_set_load_hook) */
+ if (m->load_hook) {
+ m->load_hook(m->load_user);
+ }
+
#ifndef LIBXMP_CORE_PLAYER
/* Adlib FM patches */
if (flags & SAMPLE_FLAG_ADLIB) {
diff --git a/src/libs/kpunch/pkg/Makefile b/src/libs/kpunch/pkg/Makefile
index 58ece387..76b58e63 100644
--- a/src/libs/kpunch/pkg/Makefile
+++ b/src/libs/kpunch/pkg/Makefile
@@ -26,52 +26,82 @@
# writer, manifests, keys and Ed25519 signatures, .DPK files, the
# installed-package records, the keyring, planning, and the stepwise
# install and remove operations. miniz (inflate and
-# deflate, no archive API) and Monocypher (with its Ed25519 file) are
-# built in; SETUP.EXE and the package tool compile the same sources.
+# deflate, no archive API) is built in; Ed25519 is the cryptography
+# library's (secCurve.h). SETUP.EXE and the package tool compile the
+# same sources, and crypto's with Monocypher.
+# The ZIP writer and the key and signature making (deflate and Ed25519
+# signing) are writer/writer.drv, the package writer module pkg.lib loads
+# when a package is made (pkgWriter.h, pkgWriterMod.c).
# Also SYSTEM\DVXPKG.EXE, the package tool on DOS (pkgDosMain.c over
# pkgCli.c), a program of its own like DVXRES.EXE.
include ../../../common.mk
MINIZ = thirdparty/miniz
-MONO = thirdparty/monocypher/src
-CFLAGS = $(DOS_CFLAGS) -I. -I.. -I../libdvx -I../libdvx/platform -I../libdvx/thirdparty -I$(MINIZ) -I$(MONO) $(PKG_MINIZ_FLAGS)
+MONO = ../crypto/thirdparty/monocypher/src
+CFLAGS = $(DOS_CFLAGS) $(DXE_GC_CFLAGS) -I. -I.. -I../libdvx -I../libdvx/platform -I../libdvx/thirdparty -I$(MINIZ) -I$(MONO) $(PKG_MINIZ_FLAGS)
OBJDIR = ../../../../obj/pkg
LIBSDIR = ../../../../bin/libs
-SRCS = pkgBas.c pkgDb.c pkgDpk.c pkgFormat.c pkgIni.c pkgManifest.c pkgOp.c pkgPlan.c pkgSign.c pkgTrust.c pkgZip.c pkgZipWrite.c $(MINIZ)/miniz.c $(MONO)/monocypher.c $(MONO)/optional/monocypher-ed25519.c
-OBJS = $(patsubst %.c,$(OBJDIR)/%.o,$(notdir $(SRCS)))
+# What the library, the writer module, and the tool share
+CORE_SRCS = pkgDb.c pkgDpk.c pkgFormat.c pkgIni.c pkgManifest.c pkgOp.c pkgPlan.c pkgSign.c pkgTrust.c pkgZip.c pkgZipJob.c
+THIRD_SRCS = $(MINIZ)/miniz.c
+WRITER_SRCS = pkgSignMake.c pkgZipWrite.c
+SRCS = pkgBas.c pkgWriterMod.c $(CORE_SRCS) $(THIRD_SRCS)
+OBJS = $(patsubst %.c,$(OBJDIR)/%.o,$(notdir $(SRCS)))
+# pkg's own objects: what they reach of miniz is all pkg.lib keeps
+# (DXE_GC)
+OWN_OBJS = $(filter $(OBJDIR)/pkg%.o,$(OBJS))
+GC_OBJ = $(OBJDIR)/pkgGc.o
TARGETDIR = $(LIBSDIR)/kpunch/pkg
TARGET = $(TARGETDIR)/pkg.lib
+# The writer module: its descriptor is its only root (DXE_GC), so it keeps
+# what the writer reaches of miniz and exports nothing else
+WRITERDIR = $(TARGETDIR)/writer
+WRITER = $(WRITERDIR)/writer.drv
+WRITER_DESC = $(OBJDIR)/pkgWriterDrv.o
+WRITER_OBJS = $(WRITER_DESC) $(patsubst %.c,$(OBJDIR)/%.o,$(WRITER_SRCS) $(notdir $(THIRD_SRCS)))
+WRITER_GC = $(OBJDIR)/writerGc.o
+
SYSTEMDIR = ../../../../bin/system
TOOL = $(SYSTEMDIR)/DVXPKG.EXE
TOOLOBJDIR = $(OBJDIR)/tool
-TOOL_SRCS = pkgDosMain.c pkgCli.c $(filter-out pkgBas.c,$(SRCS)) ../serial/security/secHash.c ../serial/security/security.c ../libdvx/platform/dvxPlatformUtil.c ../libdvx/thirdparty/stb_ds_impl.c
+TOOL_SRCS = pkgDosMain.c pkgCli.c $(CORE_SRCS) $(WRITER_SRCS) $(THIRD_SRCS) ../crypto/secHash.c ../crypto/secSign.c ../crypto/security.c $(MONO)/monocypher.c $(MONO)/optional/monocypher-ed25519.c ../libdvx/platform/dvxPlatformUtil.c ../libdvx/thirdparty/stb_ds_impl.c
TOOL_OBJS = $(patsubst %.c,$(TOOLOBJDIR)/%.o,$(notdir $(TOOL_SRCS)))
-vpath %.c . $(MINIZ) $(MONO) $(MONO)/optional ../serial/security ../libdvx/platform ../libdvx/thirdparty
+vpath %.c . $(MINIZ) $(MONO) $(MONO)/optional ../crypto ../libdvx/platform ../libdvx/thirdparty
.PHONY: all clean
-all: $(TARGET) $(TARGETDIR)/pkg.dep $(TOOL)
+all: $(TARGET) $(TARGETDIR)/pkg.dep $(WRITER) $(TOOL)
$(TARGETDIR)/pkg.dep: pkg.dep | $(TARGETDIR)
sed 's/$$/\r/' $< > $@
-$(TARGET): $(OBJS) | $(TARGETDIR)
- $(DXE3GEN) -o $(TARGETDIR)/pkg.dxe -U $(OBJS)
+$(GC_OBJ): $(OBJS)
+ $(DXE_GC) $@ $(OWN_OBJS) -- $(OBJS)
+
+$(TARGET): $(GC_OBJ) | $(TARGETDIR)
+ $(DXE3GEN) -o $(TARGETDIR)/pkg.dxe -U $(GC_OBJ)
mv $(TARGETDIR)/pkg.dxe $@
+$(WRITER_GC): $(WRITER_OBJS)
+ $(DXE_GC) $@ $(WRITER_DESC) -- $(WRITER_OBJS)
+
+$(WRITER): $(WRITER_GC) | $(WRITERDIR)
+ $(DXE3GEN) -o $(WRITERDIR)/writer.dxe -U $(WRITER_GC)
+ mv $(WRITERDIR)/writer.dxe $@
+
$(OBJDIR)/%.o: %.c $(DVX_MAKEFILES) | $(OBJDIR)
$(CC) $(CFLAGS) -c -o $@ $<
-# DOS executable: link (stripped: it ships on the floppies), convert to
-# COFF, prepend the DPMI stub
+# DOS executable: link (stripped, and only what it reaches: it ships on
+# the floppies), convert to COFF, prepend the DPMI stub
$(TOOL): $(TOOL_OBJS) | $(SYSTEMDIR)
- $(CC) -s -o $(SYSTEMDIR)/dvxpkg.exe $^ -lm
+ $(CC) -s -Wl,--gc-sections -o $(SYSTEMDIR)/dvxpkg.exe $^ -lm
$(EXE2COFF) $(SYSTEMDIR)/dvxpkg.exe
cat $(CWSDSTUB) $(SYSTEMDIR)/dvxpkg > $@
rm -f $(SYSTEMDIR)/dvxpkg $(SYSTEMDIR)/dvxpkg.exe
@@ -85,10 +115,10 @@ $(TOOLOBJDIR) $(SYSTEMDIR):
$(OBJDIR):
mkdir -p $(OBJDIR)
-$(TARGETDIR):
- mkdir -p $(TARGETDIR)
+$(TARGETDIR) $(WRITERDIR):
+ mkdir -p $@
--include $(OBJS:.o=.d) $(TOOL_OBJS:.o=.d)
+-include $(OBJS:.o=.d) $(WRITER_OBJS:.o=.d) $(TOOL_OBJS:.o=.d)
clean:
- rm -rf $(OBJS) $(OBJS:.o=.d) $(TARGET) $(TARGETDIR)/pkg.dep $(TARGETDIR) $(TOOLOBJDIR) $(TOOL)
+ rm -rf $(OBJS) $(OBJS:.o=.d) $(WRITER_OBJS) $(WRITER_OBJS:.o=.d) $(GC_OBJ) $(WRITER_GC) $(TARGET) $(TARGETDIR)/pkg.dep $(TARGETDIR) $(TOOLOBJDIR) $(TOOL)
diff --git a/src/libs/kpunch/pkg/pkg.dep b/src/libs/kpunch/pkg/pkg.dep
index ffb0ed6e..0063f87e 100644
--- a/src/libs/kpunch/pkg/pkg.dep
+++ b/src/libs/kpunch/pkg/pkg.dep
@@ -1,2 +1,2 @@
libdvx
-serial
+crypto
diff --git a/src/libs/kpunch/pkg/pkg.dhs b/src/libs/kpunch/pkg/pkg.dhs
index d34fe636..f7c9b118 100644
--- a/src/libs/kpunch/pkg/pkg.dhs
+++ b/src/libs/kpunch/pkg/pkg.dhs
@@ -43,7 +43,7 @@ Add-on packages: the .DPK file format, manifests, Ed25519 keys and signatures, t
Header: pkg/pkg.h (it includes the others below)
-Loaded as: bin/libs/kpunch/pkg/pkg.lib
+Loaded as: bin/libs/kpunch/pkg/pkg.lib, and bin/libs/kpunch/pkg/writer/writer.drv when a package is made (below)
BASIC: pkg.bas (sessions, offers, plans, install and remove jobs, building, signing, and keys; PkgFileOwner$ is pkgDbOwner).
@@ -124,7 +124,8 @@ A key pair is NAME.KEY (public: id, name, public key) and NAME.SEC (secret: id,
pkgDpkBuildFiles(&templ, files, count, out, secret, err, size) The same from a list of PkgDpkFileT (the name in the package, the file it comes from).
pkgDpkFolderFiles(folder, prefix, &files) Every file under folder as that list, named prefix/...; pkgDpkFilesFree frees it.
pkgDpkReadInfo(path, &info, err, size) The manifest and PACKAGE.SIG, checked; pkgDpkInfoFree.
- pkgDpkCheckFiles(path, &info, err, size) Every file's size and SHA-256 against the manifest.
+ pkgDpkCheckFiles(path, &info, err, size) Every file's size and SHA-256 against the manifest, all at once.
+ pkgDpkCheckStart(path, &info, err, size) The same as a job: pkgDpkJobStep a piece at a time, pkgDpkJobPercent by bytes read (BASIC's PkgVerifyStart).
pkgDpkSign(path, &secret, detached, err, size) Sign into the package, or beside it as NAME.SIG.
pkgDpkSigner(&info, detached, keys, count) Which of keys made the signature (or the detached one).
.endtable
@@ -145,7 +146,30 @@ pkgManifestFree(&templ);
Each installed package has a record, SYSTEM/PACKAGES/VENDOR/NAME.INI: its manifest as installed, whether it was chosen or only needed by another, and the key that signed it. pkgDbLoad reads them all; pkgDbFind, pkgDbOwner (who has a file), pkgDbPut, and pkgDbDrop keep them. Setup writes DVX's own record (kpunch.dvx, with no files).
-pkgKeyringLoad reads the keys DVX trusts: SYSTEM/KEYS (DVX's own) and the user's KEYS folder (publishers the user chose); pkgKeyringAdd trusts another. pkgTrustOf says how far a package is trusted (PkgTrustSystemE, PkgTrustUserE, PkgTrustNoneE). The rules the planner keeps: kpunch.* packages must be signed by a SYSTEM key; an upgrade must be signed by the key that signed the installed version (or a SYSTEM key); an unsigned package from a file is installed only after a warning.
+pkgKeyringLoad reads the keys DVX trusts: SYSTEM/KEYS (DVX's own) and the user's KEYS folder (publishers the user chose); pkgKeyringAdd trusts another, and pkgKeyringRemove(ring, userDir, idText, err, size) stops trusting one of the user's (its files in KEYS are deleted; a SYSTEM key stays). pkgTrustOf says how far a package is trusted (PkgTrustSystemE, PkgTrustUserE, PkgTrustNoneE). The rules the planner keeps: kpunch.* packages must be signed by a SYSTEM key; an upgrade must be signed by the key that signed the installed version (or a SYSTEM key); an unsigned package from a file is installed only after a warning.
+
+.h2 ZIP Files (pkgZip.h)
+
+The reader goes front to back through a ZIP from a read callback (pkgZipFileRead over a FILE *), never seeking; each entry's data goes, inflated, to a write callback in steps, and its size and CRC-32 are checked at its end. pkgZipProblemText says why a step failed, as the end of a sentence. The writer makes a ZIP the reader takes: pkgZipCreate, pkgZipBeginFile or pkgZipBeginMem, pkgZipStep until each is in, pkgZipFinish (pkgZipAbort removes a half-made one). pkgZipDosTime and pkgZipDosStamp turn ZIP dates into times and back.
+
+A plain ZIP job (PkgZipJobT, pkgZipJob.c) does a whole archive a step at a time, for a program that keeps drawing:
+
+.table
+ Function Description
+ -------- -----------
+ pkgZipJobRead(path, dir, err, size) Read a ZIP to its end, every entry checked; with dir, unpack each under it (folders made, files dated as in the ZIP; a name with a drive, a leading '/', or ".." fails it).
+ pkgZipJobCreate(path, names, sources, count, err, size) Make a ZIP of count files: names[i] in the ZIP from the file sources[i].
+ pkgZipJobStep(job, maxWork) PkgZipJobMoreE, PkgZipJobDoneE, or PkgZipJobFailedE (pkgZipJobMessage says why).
+ pkgZipJobEntries(job, &count) The entries read or written so far.
+ pkgZipJobCurrent, pkgZipJobPercent The entry it is on; how far it is (bytes of the ZIP read, or of the files added).
+ pkgZipJobFree(job) Stop and free it: a ZIP being made, or a file being unpacked, is removed.
+.endtable
+
+pkgHexEncode and pkgHexDecode (pkgFormat.h) turn bytes into hex digits and back, as manifests, keys, and the package server write SHA-256s and key ids.
+
+.h2 The Package Writer Module
+
+Writing ZIPs (miniz's deflate) and making keys and signatures (Ed25519 signing) are used only when a package is made, so in DVX they are a module of their own, writer.drv in LIBS\KPUNCH\PKG\WRITER, which pkg.lib loads the first time a program makes a ZIP, a key, or a signature (pkgZipCreate, pkgKeyMake, or pkgSign, directly or through pkgKeyWrite, the .DPK and ZIP jobs, and BASIC's PkgBuildStart, PkgSignStart, PkgKeygen, PkgSignData, and PkgZipCreateStart); it stays until pkg.lib is unloaded. Nothing changes for a program: the calls keep their names (pkgWriterMod.c passes them on; pkgWriter.h has the module's descriptor). Without the module (a damaged installation) pkgZipCreate gives NULL and a key or signature made is all zeros, which nothing accepts; DVX.LOG says the writer is missing. Reading, checking, and installing packages never load it. Programs that link the library in whole (DVXPKG.EXE, the build machine's dvxpkg, the tests) compile pkgZipWrite.c and pkgSignMake.c instead of the module.
.h2 Planning (pkgPlan.h)
diff --git a/src/libs/kpunch/pkg/pkgBas.c b/src/libs/kpunch/pkg/pkgBas.c
index 96ef0eb0..0b0592d5 100644
--- a/src/libs/kpunch/pkg/pkgBas.c
+++ b/src/libs/kpunch/pkg/pkgBas.c
@@ -25,36 +25,47 @@
//
// A session (PkgOpen) holds the packages installed in the DVX folder, the
// keys DVX trusts, the packages read from a folder (offers), the last
-// install plan, and one running job (installing the plan, or removing
-// packages). Lists come back as ListView rows: cells separated by TAB,
-// each row ending in LF. Making packages (PkgBuild, PkgSign, PkgKeygen)
-// needs a session only for PkgError$.
+// install plan, and one running job (reading packages as the offers,
+// installing the plan, removing packages, building, signing, or checking
+// a package, or listing, unpacking, or making a plain ZIP). Lists come
+// back as ListView rows: cells separated by TAB, each row ending in LF.
+// Making a key (PkgKeygen) needs a session only for PkgError$.
#include "pkgBas.h"
+#include "pkgInt.h"
#include "pkg.h"
#include "dvxBas.h"
#include "dvxPlat.h"
-#include "monocypher.h"
+#include "crypto/security.h"
#include "stb_ds_wrap.h"
#include
#include
#include
#include
+#include
-#define ROW_ENDS "\n\r" // what may end a row PkgBuild is given
+#define ROW_ENDS "\n\r" // what may end a row PkgBuildStart is given
#define ID_SEP "\n" // between the ids a function takes or gives
#define OFFER_SEP " ," // between PkgPlan's offer numbers
#define NUMBER_TEXT_MAX 24
#define PERCENT_MAX 100
-#define YES_TEXT "yes"
-#define NO_TEXT "no"
+#define KEY_SYSTEM_TEXT "system" // PkgKeysRows$: DVX's own key
+#define KEY_USER_TEXT "user" // one the user trusts
+#define ZIP_FOLDER_TEXT "folder" // PkgZipRows$: an entry that is a folder
+#define ZIP_DATE_FORMAT "%Y-%m-%d %H:%M"
+#define ZIP_DATE_MAX 20
#define PKG_SEP '/' // between the parts of a name in a package
typedef enum {
JobNoneE,
JobInstallE,
- JobRemoveE
+ JobRemoveE,
+ JobBuildE, // dpk: building a package
+ JobSignE, // dpk: signing one
+ JobReadE, // scan: reading packages as the offers
+ JobVerifyE, // dpk: checking a package's files
+ JobZipE // zip: a plain ZIP listed, unpacked, or made
} JobE;
typedef struct {
@@ -66,6 +77,11 @@ typedef struct {
char *staleRows; // the plan's stale add-ons, as rows (malloc'd)
JobE job;
PkgOpT *op;
+ PkgDpkJobT *dpk; // building or signing
+ PkgOfferScanT scan; // reading
+ PkgZipJobT *zip; // a ZIP job
+ PkgZipEntryT *zipEntries; // stb_ds: what the last ZIP job listed, unpacked, or packed
+ bool cancelAsked; // PkgRunCancel: the job ends once its operation can stop
int32_t jobAt; // the plan step or removal running
char **removeIds; // stb_ds of strdup'd ids
bool removeChanged;
@@ -74,6 +90,7 @@ typedef struct {
uint32_t jobBytes; // bytes of the whole job (estimated for removal)
PkgRunE result;
char current[PKG_TEXT_MAX];
+ char userDir[DVX_MAX_PATH]; // the user folder of the session's DVX folder: its trusted keys
char err[PKG_ERR_MAX];
} SessionT;
@@ -86,12 +103,14 @@ static uint32_t filesBytes(const PkgManifestT *m);
static bool filesParse(SessionT *ss, const char *rows, PkgDpkFileT **files);
static void idsFree(char **ids);
static char **idsSplit(const char *text);
+static void jobBegin(SessionT *ss, JobE job, uint32_t bytes);
static void jobEnd(SessionT *ss, PkgRunE result);
static bool jobNext(SessionT *ss);
static char *nameCopy(const char *text, size_t len);
static const PkgOfferT *offerOf(const SessionT *ss, int32_t offer);
static void offersFree(SessionT *ss);
-int32_t PkgBuild(int32_t s, const char *templatePath, const char *files, const char *outPath, const char *secretPath);
+int32_t PkgBuildStart(int32_t s, const char *templatePath, const char *files, const char *outPath, const char *secretPath);
+int32_t PkgCheckData(int32_t s, const uint8_t *data, int32_t len, const char *signature, const char *keyPath);
void PkgClose(int32_t s);
const char *PkgDependents(int32_t s, const char *ids);
const char *PkgDependsRows(int32_t s, const char *id);
@@ -108,17 +127,19 @@ const char *PkgInstalledRows(int32_t s);
int32_t PkgInstallStart(int32_t s);
int32_t PkgKeygen(int32_t s, const char *keyName, const char *basePath);
const char *PkgKeyInfo(int32_t s, const char *path);
+const char *PkgKeysRows(int32_t s);
const char *PkgKindFolder(const char *kind, const char *id);
const char *PkgKindName(int32_t kind);
const char *PkgNeededBy(int32_t s, const char *id);
int32_t PkgOfferCount(int32_t s);
const char *PkgOfferDependsRows(int32_t s, int32_t offer);
const char *PkgOfferField(int32_t s, int32_t offer, int32_t field);
-int32_t PkgOfferFile(int32_t s, const char *path);
const char *PkgOfferFilesRows(int32_t s, int32_t offer);
-int32_t PkgOfferFolder(int32_t s, const char *dir);
+int32_t PkgOfferFileStart(int32_t s, const char *path);
+int32_t PkgOfferFolderStart(int32_t s, const char *dir);
int32_t PkgOfferIndex(int32_t s, const char *id);
int32_t PkgOpen(void);
+int32_t PkgOpenIn(const char *root);
const char *PkgOrphans(int32_t s, const char *ids);
int32_t PkgPlan(int32_t s, const char *offers);
double PkgPlanBytes(int32_t s);
@@ -132,19 +153,33 @@ int32_t PkgRunFor(int32_t s, int32_t milliseconds);
const char *PkgRunKept(int32_t s);
const char *PkgRunMessage(int32_t s);
int32_t PkgRunPercent(int32_t s);
-int32_t PkgSign(int32_t s, const char *dpkPath, const char *secretPath, int32_t detached);
+const char *PkgSignData(int32_t s, const uint8_t *data, int32_t len, const char *secretPath);
+int32_t PkgSignStart(int32_t s, const char *dpkPath, const char *secretPath, int32_t detached);
int32_t PkgTrustKey(int32_t s, const char *keyPath);
+int32_t PkgUntrustKey(int32_t s, const char *keyId);
+int32_t PkgVerifyStart(int32_t s, const char *dpkPath, const char *keyPath);
+int32_t PkgVersionCompare(const char *a, const char *b);
int32_t PkgVersionValid(const char *text);
+int32_t PkgZipCreateStart(int32_t s, const char *zipPath, const char *files);
+int32_t PkgZipExtractStart(int32_t s, const char *zipPath, const char *folder);
+int32_t PkgZipListStart(int32_t s, const char *zipPath);
+const char *PkgZipRows(int32_t s);
static void planForget(SessionT *ss);
static bool restartKind(PkgKindE kind);
static int32_t resultOf(SessionT *ss, bool ok);
static void rowsDepends(const SessionT *ss, const PkgManifestT *m);
static void rowsFiles(const PkgManifestT *m);
static void rowsPackages(const PkgInstalledT *const *pkgs);
+static void runDpk(SessionT *ss);
+static void runOp(SessionT *ss);
+static void runRead(SessionT *ss);
+static void runZip(SessionT *ss);
static void sessionFree(void *obj);
static SessionT *sessionOf(int32_t s);
+static bool signedBy(SessionT *ss, const char *dpkPath, const PkgDpkInfoT *info, const char *keyPath);
static const PkgKeyT *signerKey(const SessionT *ss, const uint8_t *id);
static char *textCopy(const char *text, size_t len);
+static bool zipStart(SessionT *ss, PkgZipJobT *job);
// ============================================================
// Module state
@@ -237,7 +272,7 @@ static const char *fieldOf(const SessionT *ss, const PkgManifestT *m, int32_t fi
return basTextSet(&sText, key ? key->name : "");
case PkgFieldExplicitE:
- return basTextSet(&sText, inst ? (inst->explicitly ? YES_TEXT : NO_TEXT) : "");
+ return basTextSet(&sText, inst ? (inst->explicitly ? PKG_BAS_YES : PKG_BAS_NO) : "");
case PkgFieldTrustE:
return basTextSet(&sText, offer ? trustNames[offer->trust] : "");
@@ -268,7 +303,7 @@ static uint32_t filesBytes(const PkgManifestT *m) {
}
-// PkgBuild's files: rows of name TAB file (blank rows skipped) onto *files
+// PkgBuildStart's files: rows of name TAB file (blank rows skipped) onto *files
// (pkgDpkFilesFree). False with the reason in ss->err.
static bool filesParse(SessionT *ss, const char *rows, PkgDpkFileT **files) {
const char *p = rows ? rows : "";
@@ -292,7 +327,7 @@ static bool filesParse(SessionT *ss, const char *rows, PkgDpkFileT **files) {
if (!f.name || !f.source) {
free(f.name);
free(f.source);
- snprintf(ss->err, sizeof(ss->err), "There is not enough memory.");
+ snprintf(ss->err, sizeof(ss->err), PKG_ERR_NO_MEMORY);
return false;
}
@@ -336,12 +371,44 @@ static char **idsSplit(const char *text) {
}
+// Start a job of bytes (for its progress; a package job counts its own).
+static void jobBegin(SessionT *ss, JobE job, uint32_t bytes) {
+ idsFree(ss->kept);
+ ss->kept = NULL;
+ ss->err[0] = '\0';
+ ss->current[0] = '\0';
+ ss->job = job;
+ ss->jobAt = 0;
+ ss->doneBefore = 0;
+ ss->jobBytes = bytes;
+ ss->cancelAsked = false;
+ ss->result = PkgRunBusyE;
+}
+
+
// The job is over (done, or failed with ss->err said): an operation still
-// open is stopped (or finished, once its files are in), and an install's
-// plan is used up.
+// open is stopped (or finished, once its files are in), a package being
+// built or signed is removed, packages being read are dropped, and an
+// install's plan is used up.
static void jobEnd(SessionT *ss, PkgRunE result) {
+ if (ss->zip) {
+ int32_t count = 0;
+ const PkgZipEntryT *entries = pkgZipJobEntries(ss->zip, &count);
+
+ arrsetlen(ss->zipEntries, count);
+
+ if (count > 0) {
+ memcpy(ss->zipEntries, entries, (size_t)count * sizeof(*entries));
+ }
+ }
+
pkgOpFree(ss->op);
- ss->op = NULL;
+ pkgDpkJobFree(ss->dpk);
+ pkgZipJobFree(ss->zip);
+ pkgOfferScanFree(&ss->scan);
+ ss->op = NULL;
+ ss->dpk = NULL;
+ ss->zip = NULL;
if (ss->job == JobInstallE) {
planForget(ss);
@@ -364,7 +431,7 @@ static bool jobNext(SessionT *ss) {
const PkgOfferT *o = &ss->offers[st->offer];
name = o->manifest.name;
- ss->op = pkgOpInstall(&ss->db, o->where, &ss->ring, st->explicitly);
+ ss->op = pkgOpInstall(&ss->db, o, st->explicitly);
} else if (ss->job == JobRemoveE && ss->jobAt < (int32_t)arrlen(ss->removeIds)) {
const PkgInstalledT *inst = pkgDbFind(&ss->db, ss->removeIds[ss->jobAt]);
@@ -379,7 +446,7 @@ static bool jobNext(SessionT *ss) {
snprintf(ss->current, sizeof(ss->current), "%s", name);
if (!ss->op) {
- snprintf(ss->err, sizeof(ss->err), "There is not enough memory.");
+ snprintf(ss->err, sizeof(ss->err), PKG_ERR_NO_MEMORY);
}
return ss->op != NULL;
@@ -419,28 +486,70 @@ static void offersFree(SessionT *ss) {
}
-// Build the package outPath from the template templatePath
+// Start building the package outPath from the template templatePath
// (pkgTemplateRead; code with no abi gets this DVX's) and files: rows of
// a name in the package (from the DVX folder) TAB the file it is made
-// from (PkgFolderRows$). Signed with the secret key file secretPath
-// unless it is "". True, or False with the reason in PkgError$.
-int32_t PkgBuild(int32_t s, const char *templatePath, const char *files, const char *outPath, const char *secretPath) {
+// from (PkgFolderRows$); run it with PkgRunFor. Signed with the secret
+// key file secretPath unless it is "". False with the reason in
+// PkgError$ (none when a job is running).
+int32_t PkgBuildStart(int32_t s, const char *templatePath, const char *files, const char *outPath, const char *secretPath) {
SessionT *ss = sessionOf(s);
PkgDpkFileT *list = NULL;
+ PkgDpkJobT *job = NULL;
bool keyed = secretPath && secretPath[0];
PkgManifestT templ;
PkgSecretT secret;
- bool ok;
- if (!ss || !templatePath || !outPath || !pkgTemplateRead(templatePath, &templ, ss->err, sizeof(ss->err))) {
+ if (!ss || ss->job != JobNoneE || !templatePath || !outPath || !pkgTemplateRead(templatePath, &templ, ss->err, sizeof(ss->err))) {
return BAS_FALSE;
}
- ok = filesParse(ss, files, &list) && (!keyed || pkgSecretRead(secretPath, &secret, ss->err, sizeof(ss->err)));
- ok = ok && pkgDpkBuildFiles(&templ, list, (int32_t)arrlen(list), outPath, keyed ? &secret : NULL, ss->err, sizeof(ss->err));
+ if (filesParse(ss, files, &list) && (!keyed || pkgSecretRead(secretPath, &secret, ss->err, sizeof(ss->err)))) {
+ job = pkgDpkBuildStart(&templ, list, (int32_t)arrlen(list), outPath, keyed ? &secret : NULL, ss->err, sizeof(ss->err));
+ }
+
pkgSecretWipe(&secret);
pkgDpkFilesFree(list);
pkgManifestFree(&templ);
+
+ if (job) {
+ jobBegin(ss, JobBuildE, 0);
+ ss->dpk = job;
+ }
+
+ return resultOf(ss, job != NULL);
+}
+
+
+// True when signature (PkgSignData$'s text, or a .SIG file's) was made
+// over data by the key in the key file keyPath, or with keyPath "" by a
+// key DVX trusts. False with the reason in PkgError$.
+int32_t PkgCheckData(int32_t s, const uint8_t *data, int32_t len, const char *signature, const char *keyPath) {
+ SessionT *ss = sessionOf(s);
+ PkgSigT sig;
+ PkgKeyT key;
+ bool ok = ss && signature && pkgSigParse(signature, (int32_t)strlen(signature), &sig, ss->err, sizeof(ss->err));
+
+ if (ok && keyPath && keyPath[0]) {
+ ok = pkgKeyRead(keyPath, &key, ss->err, sizeof(ss->err));
+
+ if (ok && !pkgSigCheck(&sig, &key, data, len)) {
+ snprintf(ss->err, sizeof(ss->err), "The signature was not made by %s over these bytes.", keyPath);
+ ok = false;
+ }
+ } else if (ok) {
+ bool trusted = false;
+
+ for (int32_t i = 0; i < (int32_t)arrlen(ss->ring.keys) && !trusted; i++) {
+ trusted = pkgSigCheck(&sig, &ss->ring.keys[i], data, len);
+ }
+
+ if (!trusted) {
+ snprintf(ss->err, sizeof(ss->err), "The signature was not made over these bytes by a key DVX trusts.");
+ ok = false;
+ }
+ }
+
return resultOf(ss, ok);
}
@@ -539,7 +648,7 @@ const char *PkgFilesRows(int32_t s, const char *id) {
// Every file under folder (but a PACKAGE.INI or PACKAGE.SIG at its top) as
-// PkgBuild's files: rows of its name in the package (prefix, then its name
+// PkgBuildStart's files: rows of its name in the package (prefix, then its name
// under folder; from folder itself when prefix is "") and the file, in
// name order. "" with the reason in PkgError$ when folder cannot be read.
const char *PkgFolderRows(int32_t s, const char *folder, const char *prefix) {
@@ -612,14 +721,7 @@ int32_t PkgInstallStart(int32_t s) {
return BAS_FALSE;
}
- idsFree(ss->kept);
- ss->kept = NULL;
- ss->err[0] = '\0';
- ss->job = JobInstallE;
- ss->jobAt = 0;
- ss->doneBefore = 0;
- ss->jobBytes = ss->plan.bytes;
- ss->result = PkgRunBusyE;
+ jobBegin(ss, JobInstallE, ss->plan.bytes);
if (!jobNext(ss)) {
jobEnd(ss, ss->err[0] ? PkgRunFailedE : PkgRunDoneE);
@@ -648,7 +750,7 @@ int32_t PkgKeygen(int32_t s, const char *keyName, const char *basePath) {
}
ok = ok && pkgKeyWrite(seed, keyName, basePath, NULL, ss->err, sizeof(ss->err));
- crypto_wipe(seed, sizeof(seed));
+ secSecureZero(seed, sizeof(seed));
return resultOf(ss, ok);
}
@@ -689,6 +791,25 @@ const char *PkgKeyInfo(int32_t s, const char *path) {
}
+// The keys DVX trusts, as rows: name, id (hex), and "system" (DVX's own,
+// in SYSTEM\KEYS) or "user" (one the user chose to trust).
+const char *PkgKeysRows(int32_t s) {
+ const SessionT *ss = sessionOf(s);
+
+ basTextStart(&sText);
+
+ for (int32_t i = 0; ss && i < (int32_t)arrlen(ss->ring.keys); i++) {
+ char id[PKG_KEY_ID_TEXT];
+ const char *cells[] = { ss->ring.keys[i].name, id, i < ss->ring.systemCount ? KEY_SYSTEM_TEXT : KEY_USER_TEXT };
+
+ pkgKeyIdText(ss->ring.keys[i].id, id);
+ basTextRow(&sText, cells, DVX_ARRAY_LEN(cells));
+ }
+
+ return basTextEnd(&sText);
+}
+
+
// The folder a package id of kind ("app", ...) keeps its files in, from
// the DVX folder (APPS\\VENDOR\\NAME for a program); "" for bundle and
// system (no folder of their own), or a bad kind or id.
@@ -761,24 +882,6 @@ const char *PkgOfferField(int32_t s, int32_t offer, int32_t field) {
}
-// Read the package at path, and every other package in its folder (what
-// it may need), as the offers (the ones before are gone, and the plan).
-// The number of path's offer (0), or -1: PkgError$ says why.
-int32_t PkgOfferFile(int32_t s, const char *path) {
- SessionT *ss = sessionOf(s);
-
- // A running install reads the plan's offers
- if (!ss || !path || ss->job != JobNoneE) {
- return -1;
- }
-
- offersFree(ss);
- planForget(ss);
- ss->offers = pkgOffersFor(path, &ss->ring, ss->err, sizeof(ss->err));
- return ss->offers ? 0 : -1;
-}
-
-
// The files of offer (the rows of PkgFilesRows).
const char *PkgOfferFilesRows(int32_t s, int32_t offer) {
const PkgOfferT *o = offerOf(sessionOf(s), offer);
@@ -793,18 +896,41 @@ const char *PkgOfferFilesRows(int32_t s, int32_t offer) {
}
-// Every package in dir as the offers: how many, or -1 while a job runs.
-int32_t PkgOfferFolder(int32_t s, const char *dir) {
+// Start reading the package at path, and every other package in its
+// folder (what it may need), as the offers: a job (run with PkgRunFor;
+// the ones before are gone, and the plan); path's offer is 0. It fails
+// when path cannot be read (PkgRunMessage$ says why). False while a job
+// runs (an install reads the plan's offers).
+int32_t PkgOfferFileStart(int32_t s, const char *path) {
SessionT *ss = sessionOf(s);
+ char dir[DVX_MAX_PATH];
- if (!ss || !dir || ss->job != JobNoneE) {
- return -1;
+ if (!ss || !path || ss->job != JobNoneE) {
+ return BAS_FALSE;
}
offersFree(ss);
planForget(ss);
- ss->offers = pkgOffersInFolder(dir, &ss->ring);
- return (int32_t)arrlen(ss->offers);
+ jobBegin(ss, JobReadE, 0);
+ pkgOfferScanStart(&ss->scan, platformPathDirName(path, dir, sizeof(dir)) ? dir : NULL, path, &ss->ring);
+ return BAS_TRUE;
+}
+
+
+// Start reading every package in dir as the offers, as
+// PkgOfferFileStart; PkgOfferCount says how many there were.
+int32_t PkgOfferFolderStart(int32_t s, const char *dir) {
+ SessionT *ss = sessionOf(s);
+
+ if (!ss || !dir || ss->job != JobNoneE) {
+ return BAS_FALSE;
+ }
+
+ offersFree(ss);
+ planForget(ss);
+ jobBegin(ss, JobReadE, 0);
+ pkgOfferScanStart(&ss->scan, dir, NULL, &ss->ring);
+ return BAS_TRUE;
}
@@ -828,20 +954,36 @@ int32_t PkgOfferIndex(int32_t s, const char *id) {
// A session of the DVX folder's packages and the keys DVX trusts: its
// handle, or 0 when out of memory.
int32_t PkgOpen(void) {
+ return PkgOpenIn("");
+}
+
+
+// A session on the DVX folder root ("" = this DVX's; another one, such as
+// a copy being made ready on another disk, as DVXPKG's -r does), with the
+// keys its user folder trusts. 0 when its records cannot be read.
+int32_t PkgOpenIn(const char *root) {
SessionT *ss = (SessionT *)calloc(1, sizeof(SessionT));
int32_t handle;
+ if (!root) {
+ root = "";
+ }
+
if (!ss) {
return BAS_NO_HANDLE;
}
- if (!pkgDbLoad(&ss->db, "")) {
+ if (!pkgDbLoad(&ss->db, root) || (root[0] && !platformUserDirOf(ss->userDir, sizeof(ss->userDir), root))) {
pkgDbFree(&ss->db);
free(ss);
return BAS_NO_HANDLE;
}
- pkgKeyringLoad(&ss->ring, "", platformUserDir());
+ if (!root[0]) {
+ snprintf(ss->userDir, sizeof(ss->userDir), "%s", platformUserDir());
+ }
+
+ pkgKeyringLoad(&ss->ring, root, ss->userDir);
ss->result = PkgRunDoneE;
handle = basHandleAdd(&sSessions, ss);
@@ -953,7 +1095,7 @@ const char *PkgPlanRows(int32_t s) {
const PkgOfferT *o = &ss->offers[st->offer];
const PkgManifestT *m = &o->manifest;
char version[PKG_VERSION_MAX];
- const char *cells[] = { m->id, m->name, version, actNames[st->act], st->explicitly ? YES_TEXT : NO_TEXT, trustNames[o->trust] };
+ const char *cells[] = { m->id, m->name, version, actNames[st->act], st->explicitly ? PKG_BAS_YES : PKG_BAS_NO, trustNames[o->trust] };
pkgVersionFormat(m->version, version, sizeof(version));
basTextRow(&sText, cells, DVX_ARRAY_LEN(cells));
@@ -982,16 +1124,9 @@ int32_t PkgRemoveStart(int32_t s, const char *ids, int32_t removeChanged) {
return BAS_FALSE;
}
- idsFree(ss->kept);
- ss->kept = NULL;
- ss->err[0] = '\0';
+ jobBegin(ss, JobRemoveE, 0);
ss->removeIds = idsSplit(ids);
ss->removeChanged = removeChanged != BAS_FALSE;
- ss->job = JobRemoveE;
- ss->jobAt = 0;
- ss->doneBefore = 0;
- ss->jobBytes = 0;
- ss->result = PkgRunBusyE;
for (int32_t i = 0; i < (int32_t)arrlen(ss->removeIds); i++) {
const PkgInstalledT *inst = pkgDbFind(&ss->db, ss->removeIds[i]);
@@ -1007,19 +1142,31 @@ int32_t PkgRemoveStart(int32_t s, const char *ids, int32_t removeChanged) {
}
-// Stop the job after the package it is on (one being unpacked is left as
-// it was; one whose files are in is finished).
+// Stop the job: reading packages, or a package being built or signed, at
+// once (nothing read; nothing left behind); an install or removal after
+// the package it is on (one being
+// unpacked is left as it was; one whose files are in is finished, by
+// PkgRunFor, which says PKG_RUN_BUSY until then).
void PkgRunCancel(int32_t s) {
SessionT *ss = sessionOf(s);
- if (ss && ss->job != JobNoneE) {
- snprintf(ss->err, sizeof(ss->err), "Stopped before it was done.");
+ if (!ss || ss->job == JobNoneE) {
+ return;
+ }
+
+ snprintf(ss->err, sizeof(ss->err), "Stopped before it was done.");
+ ss->cancelAsked = true;
+
+ if (ss->op) {
+ pkgOpStop(ss->op);
+ } else {
jobEnd(ss, PkgRunFailedE);
}
}
-// The name of the package the job is on.
+// The name of the package the job is on (building or signing: of the file
+// in it).
const char *PkgRunCurrent(int32_t s) {
SessionT *ss = sessionOf(s);
@@ -1039,33 +1186,14 @@ int32_t PkgRunFor(int32_t s, int32_t milliseconds) {
}
while (ss->job != JobNoneE) {
- PkgOpStateE st = pkgOpStep(ss->op);
- uint32_t done;
- uint32_t total;
-
- if (st == PkgOpFailedE) {
- snprintf(ss->err, sizeof(ss->err), "%s: %s", ss->current, pkgOpMessage(ss->op));
- jobEnd(ss, PkgRunFailedE);
- } else if (st == PkgOpDoneE) {
- char *const *kept = pkgOpKept(ss->op);
-
- for (int32_t i = 0; i < (int32_t)arrlen(kept); i++) {
- char *copy = strdup(kept[i]);
-
- if (copy) {
- arrput(ss->kept, copy);
- }
- }
-
- pkgOpProgress(ss->op, &done, &total);
- ss->doneBefore += total;
- pkgOpFree(ss->op);
- ss->op = NULL;
- ss->jobAt++;
-
- if (!jobNext(ss)) {
- jobEnd(ss, ss->err[0] ? PkgRunFailedE : PkgRunDoneE);
- }
+ if (ss->job == JobReadE) {
+ runRead(ss);
+ } else if (ss->dpk) {
+ runDpk(ss);
+ } else if (ss->zip) {
+ runZip(ss);
+ } else {
+ runOp(ss);
}
if (platformClock() >= end) {
@@ -1113,6 +1241,18 @@ int32_t PkgRunPercent(int32_t s) {
return ss->result == PkgRunDoneE ? PERCENT_MAX : 0;
}
+ if (ss->job == JobReadE) {
+ return pkgOfferScanPercent(&ss->scan);
+ }
+
+ if (ss->dpk) {
+ return pkgDpkJobPercent(ss->dpk);
+ }
+
+ if (ss->zip) {
+ return pkgZipJobPercent(ss->zip);
+ }
+
if (ss->op) {
pkgOpProgress(ss->op, &done, &total);
}
@@ -1125,20 +1265,64 @@ int32_t PkgRunPercent(int32_t s) {
}
-// Sign the package dpkPath with the secret key file secretPath: into it,
-// or beside it (NAME.SIG) when detached. True, or False with the reason
-// in PkgError$.
-int32_t PkgSign(int32_t s, const char *dpkPath, const char *secretPath, int32_t detached) {
- SessionT *ss = sessionOf(s);
+// Sign data with the secret key file secretPath: the signature as a
+// signature file's text (save it, or send it with the data); "" with the
+// reason in PkgError$.
+const char *PkgSignData(int32_t s, const uint8_t *data, int32_t len, const char *secretPath) {
+ SessionT *ss = sessionOf(s);
PkgSecretT secret;
- bool ok;
+ PkgSigT sig;
+ char *text = NULL;
+ int32_t textLen;
- if (!ss || !dpkPath || !secretPath) {
+ if (ss && secretPath && pkgSecretRead(secretPath, &secret, ss->err, sizeof(ss->err))) {
+ pkgSign(&secret, data, len, &sig);
+ pkgSecretWipe(&secret);
+ text = pkgSigFormat(&sig, &textLen);
+ }
+
+ const char *out = basTextSet(&sText, text ? text : "");
+
+ resultOf(ss, text != NULL);
+ free(text);
+ return out;
+}
+
+
+// Start signing the package dpkPath with the secret key file secretPath:
+// into it (the package rewritten), or beside it (NAME.SIG) when detached
+// (done at once); run it with PkgRunFor. False with the reason in
+// PkgError$ (none when a job is running).
+int32_t PkgSignStart(int32_t s, const char *dpkPath, const char *secretPath, int32_t detached) {
+ SessionT *ss = sessionOf(s);
+ PkgDpkJobT *job = NULL;
+ PkgSecretT secret;
+ bool ok;
+
+ if (!ss || ss->job != JobNoneE || !dpkPath || !secretPath) {
return BAS_FALSE;
}
- ok = pkgSecretRead(secretPath, &secret, ss->err, sizeof(ss->err)) && pkgDpkSign(dpkPath, &secret, detached != BAS_FALSE, ss->err, sizeof(ss->err));
+ ok = pkgSecretRead(secretPath, &secret, ss->err, sizeof(ss->err));
+
+ if (ok && detached != BAS_FALSE) {
+ ok = pkgDpkSign(dpkPath, &secret, true, ss->err, sizeof(ss->err));
+ } else if (ok) {
+ job = pkgDpkSignStart(dpkPath, &secret, ss->err, sizeof(ss->err));
+ ok = job != NULL;
+ }
+
pkgSecretWipe(&secret);
+
+ if (ok) {
+ jobBegin(ss, JobSignE, 0);
+ ss->dpk = job;
+
+ if (!job) {
+ jobEnd(ss, PkgRunDoneE);
+ }
+ }
+
return resultOf(ss, ok);
}
@@ -1148,7 +1332,72 @@ int32_t PkgSign(int32_t s, const char *dpkPath, const char *secretPath, int32_t
int32_t PkgTrustKey(int32_t s, const char *keyPath) {
SessionT *ss = sessionOf(s);
- return resultOf(ss, ss && keyPath && pkgKeyringAdd(&ss->ring, platformUserDir(), keyPath, ss->err, sizeof(ss->err)));
+ return resultOf(ss, ss && keyPath && pkgKeyringAdd(&ss->ring, ss->userDir, keyPath, ss->err, sizeof(ss->err)));
+}
+
+
+// Stop trusting the key with id keyId (hex, as PkgKeysRows$ gives it):
+// its files leave the user's KEYS folder. DVX's own keys stay. Offers
+// read before keep the trust they had: read them again.
+int32_t PkgUntrustKey(int32_t s, const char *keyId) {
+ SessionT *ss = sessionOf(s);
+
+ return resultOf(ss, ss && pkgKeyringRemove(&ss->ring, ss->userDir, keyId, ss->err, sizeof(ss->err)));
+}
+
+
+// Start checking the package at dpkPath without installing it: every file
+// against its manifest (size and SHA-256), a job run with PkgRunFor. With
+// keyPath (a key file) it must also be signed by that key, in it or
+// beside it (NAME.SIG), or nothing starts. False with the reason in
+// PkgError$ (none when a job is running).
+int32_t PkgVerifyStart(int32_t s, const char *dpkPath, const char *keyPath) {
+ SessionT *ss = sessionOf(s);
+ PkgDpkInfoT info;
+ PkgDpkJobT *job = NULL;
+
+ if (!ss || ss->job != JobNoneE || !dpkPath) {
+ return BAS_FALSE;
+ }
+
+ if (!pkgDpkReadInfo(dpkPath, &info, ss->err, sizeof(ss->err))) {
+ return BAS_FALSE;
+ }
+
+ bool ok = !keyPath || !keyPath[0] || signedBy(ss, dpkPath, &info, keyPath);
+
+ if (ok) {
+ job = pkgDpkCheckStart(dpkPath, &info, ss->err, sizeof(ss->err));
+ ok = job != NULL;
+ }
+
+ pkgDpkInfoFree(&info);
+
+ if (ok) {
+ jobBegin(ss, JobVerifyE, 0);
+ ss->dpk = job;
+ }
+
+ return resultOf(ss, ok);
+}
+
+
+// -1 when version a is older than b, 0 when the same, 1 when newer
+// (1.10.0 is newer than 1.9.2). A text that is not a version counts as
+// older than any version.
+int32_t PkgVersionCompare(const char *a, const char *b) {
+ PkgVersionT va;
+ PkgVersionT vb;
+ bool okA = a && pkgVersionParse(a, &va);
+ bool okB = b && pkgVersionParse(b, &vb);
+
+ if (!okA || !okB) {
+ return (okA ? 1 : 0) - (okB ? 1 : 0);
+ }
+
+ int32_t cmp = pkgVersionCompare(va, vb);
+
+ return (cmp > 0) - (cmp < 0);
}
@@ -1161,6 +1410,97 @@ int32_t PkgVersionValid(const char *text) {
}
+// Start making a plain ZIP at zipPath from files: rows of a name in the
+// ZIP and the file it is made from, TAB between them (PkgFolderRows$
+// gives them for a folder), a job run with PkgRunFor. False with the
+// reason in PkgError$ (none while a job is running).
+int32_t PkgZipCreateStart(int32_t s, const char *zipPath, const char *files) {
+ SessionT *ss = sessionOf(s);
+ PkgDpkFileT *list = NULL;
+ PkgZipJobT *job = NULL;
+
+ if (!ss || ss->job != JobNoneE || !zipPath) {
+ return BAS_FALSE;
+ }
+
+ if (filesParse(ss, files, &list)) {
+ const char **names = NULL;
+ const char **sources = NULL;
+
+ for (int32_t i = 0; i < (int32_t)arrlen(list); i++) {
+ arrput(names, list[i].name);
+ arrput(sources, list[i].source);
+ }
+
+ job = pkgZipJobCreate(zipPath, names, sources, (int32_t)arrlen(list), ss->err, sizeof(ss->err));
+ arrfree(names);
+ arrfree(sources);
+ }
+
+ pkgDpkFilesFree(list);
+ return resultOf(ss, zipStart(ss, job));
+}
+
+
+// Start unpacking the plain ZIP at zipPath into folder (made as needed;
+// a file already there is replaced), a job run with PkgRunFor: each
+// entry's size and CRC are checked, and a name that would leave folder
+// stops it. False with the reason in PkgError$.
+int32_t PkgZipExtractStart(int32_t s, const char *zipPath, const char *folder) {
+ SessionT *ss = sessionOf(s);
+
+ if (!ss || ss->job != JobNoneE || !zipPath || !folder) {
+ return BAS_FALSE;
+ }
+
+ return resultOf(ss, zipStart(ss, pkgZipJobRead(zipPath, folder, ss->err, sizeof(ss->err))));
+}
+
+
+// Start reading the plain ZIP at zipPath to its end, every entry checked
+// (PkgZipRows$ lists them after), a job run with PkgRunFor. False with
+// the reason in PkgError$.
+int32_t PkgZipListStart(int32_t s, const char *zipPath) {
+ SessionT *ss = sessionOf(s);
+
+ if (!ss || ss->job != JobNoneE || !zipPath) {
+ return BAS_FALSE;
+ }
+
+ return resultOf(ss, zipStart(ss, pkgZipJobRead(zipPath, NULL, ss->err, sizeof(ss->err))));
+}
+
+
+// What the last ZIP job listed, unpacked, or packed (as far as it got),
+// as rows: name ('/' between parts), size, date (YYYY-MM-DD HH:MM), and
+// "folder" for a folder entry.
+const char *PkgZipRows(int32_t s) {
+ const SessionT *ss = sessionOf(s);
+
+ basTextStart(&sText);
+
+ for (int32_t i = 0; ss && i < (int32_t)arrlen(ss->zipEntries); i++) {
+ const PkgZipEntryT *e = &ss->zipEntries[i];
+ time_t when = pkgZipDosTime(e->dosDate, e->dosTime);
+ struct tm *t = localtime(&when);
+ char size[NUMBER_TEXT_MAX];
+ char date[ZIP_DATE_MAX];
+
+ snprintf(size, sizeof(size), "%lu", (unsigned long)e->size);
+
+ if (!t || strftime(date, sizeof(date), ZIP_DATE_FORMAT, t) == 0) {
+ date[0] = '\0';
+ }
+
+ const char *cells[] = { e->name, size, date, e->isDir ? ZIP_FOLDER_TEXT : "" };
+
+ basTextRow(&sText, cells, DVX_ARRAY_LEN(cells));
+ }
+
+ return basTextEnd(&sText);
+}
+
+
static void planForget(SessionT *ss) {
pkgPlanFree(&ss->plan);
free(ss->staleRows);
@@ -1224,6 +1564,95 @@ static void rowsPackages(const PkgInstalledT *const *pkgs) {
// A session (the handles' freeFn).
+// One step of the package being built or signed.
+static void runDpk(SessionT *ss) {
+ PkgDpkStepE st = pkgDpkJobStep(ss->dpk, PKG_DPK_STEP);
+
+ snprintf(ss->current, sizeof(ss->current), "%s", pkgDpkJobCurrent(ss->dpk));
+
+ if (st == PkgDpkFailedE) {
+ snprintf(ss->err, sizeof(ss->err), "%s", pkgDpkJobMessage(ss->dpk));
+ jobEnd(ss, PkgRunFailedE);
+ } else if (st == PkgDpkDoneE) {
+ jobEnd(ss, PkgRunDoneE);
+ }
+}
+
+
+// One step of the install or removal's operation; once it is over, the
+// next one starts (none when the job was asked to stop: ss->err says so).
+static void runOp(SessionT *ss) {
+ PkgOpStateE st = pkgOpStep(ss->op);
+ uint32_t done;
+ uint32_t total;
+
+ if (st == PkgOpFailedE) {
+ if (!ss->cancelAsked) {
+ snprintf(ss->err, sizeof(ss->err), "%s: %s", ss->current, pkgOpMessage(ss->op));
+ }
+
+ jobEnd(ss, PkgRunFailedE);
+ return;
+ }
+
+ if (st != PkgOpDoneE) {
+ return;
+ }
+
+ char *const *kept = pkgOpKept(ss->op);
+
+ for (int32_t i = 0; i < (int32_t)arrlen(kept); i++) {
+ char *copy = strdup(kept[i]);
+
+ if (copy) {
+ arrput(ss->kept, copy);
+ }
+ }
+
+ pkgOpProgress(ss->op, &done, &total);
+ ss->doneBefore += total;
+ pkgOpFree(ss->op);
+ ss->op = NULL;
+ ss->jobAt++;
+
+ if (ss->cancelAsked) {
+ jobEnd(ss, PkgRunFailedE);
+ } else if (!jobNext(ss)) {
+ jobEnd(ss, ss->err[0] ? PkgRunFailedE : PkgRunDoneE);
+ }
+}
+
+
+// One package read as an offer; once all are, they are the offers.
+static void runRead(SessionT *ss) {
+ PkgScanStepE st = pkgOfferScanStep(&ss->scan, ss->err, sizeof(ss->err));
+
+ snprintf(ss->current, sizeof(ss->current), "%s", ss->scan.current);
+
+ if (st == PkgScanFailedE) {
+ jobEnd(ss, PkgRunFailedE);
+ } else if (st == PkgScanDoneE) {
+ ss->offers = ss->scan.offers;
+ ss->scan.offers = NULL;
+ jobEnd(ss, PkgRunDoneE);
+ }
+}
+
+
+static void runZip(SessionT *ss) {
+ PkgZipJobStepE st = pkgZipJobStep(ss->zip, PKG_DPK_STEP);
+
+ snprintf(ss->current, sizeof(ss->current), "%s", pkgZipJobCurrent(ss->zip));
+
+ if (st == PkgZipJobFailedE) {
+ snprintf(ss->err, sizeof(ss->err), "%s", pkgZipJobMessage(ss->zip));
+ jobEnd(ss, PkgRunFailedE);
+ } else if (st == PkgZipJobDoneE) {
+ jobEnd(ss, PkgRunDoneE);
+ }
+}
+
+
static void sessionFree(void *obj) {
SessionT *ss = (SessionT *)obj;
@@ -1233,6 +1662,7 @@ static void sessionFree(void *obj) {
idsFree(ss->kept);
pkgKeyringFree(&ss->ring);
pkgDbFree(&ss->db);
+ arrfree(ss->zipEntries);
free(ss);
}
@@ -1242,6 +1672,30 @@ static SessionT *sessionOf(int32_t s) {
}
+// True when the package at dpkPath (info its manifest and own signature)
+// is signed by the key in the key file keyPath, in it or beside it
+// (NAME.SIG); else false with the reason in ss->err.
+static bool signedBy(SessionT *ss, const char *dpkPath, const PkgDpkInfoT *info, const char *keyPath) {
+ PkgKeyT key;
+ PkgSigT detached;
+ char sigPath[DVX_MAX_PATH];
+ char why[PKG_ERR_MAX];
+
+ if (!pkgKeyRead(keyPath, &key, ss->err, sizeof(ss->err))) {
+ return false;
+ }
+
+ bool hasDetached = platformPathSibling(sigPath, sizeof(sigPath), dpkPath, PKG_SIG_EXT) && pkgSigRead(sigPath, &detached, why, sizeof(why));
+
+ if (!pkgDpkSigner(info, hasDetached ? &detached : NULL, &key, 1)) {
+ snprintf(ss->err, sizeof(ss->err), "%s is not signed by %s.", dpkPath, keyPath);
+ return false;
+ }
+
+ return true;
+}
+
+
// The key DVX trusts with id, or NULL.
static const PkgKeyT *signerKey(const SessionT *ss, const uint8_t *id) {
for (int32_t i = 0; ss && i < (int32_t)arrlen(ss->ring.keys); i++) {
@@ -1266,3 +1720,17 @@ static char *textCopy(const char *text, size_t len) {
return copy;
}
+
+
+// A ZIP job started (job NULL: it could not, ss->err says why): the last
+// one's rows forgotten.
+static bool zipStart(SessionT *ss, PkgZipJobT *job) {
+ if (!job) {
+ return false;
+ }
+
+ arrsetlen(ss->zipEntries, 0);
+ jobBegin(ss, JobZipE, 0);
+ ss->zip = job;
+ return true;
+}
diff --git a/src/libs/kpunch/pkg/pkgBas.h b/src/libs/kpunch/pkg/pkgBas.h
index 2c73bd94..12d0bd25 100644
--- a/src/libs/kpunch/pkg/pkgBas.h
+++ b/src/libs/kpunch/pkg/pkgBas.h
@@ -49,9 +49,17 @@ typedef enum {
PkgFieldTrustE, // offer: system / user / none
PkgFieldWhereE, // offer: its file
PkgFieldSizeE, // the bytes of its files
+ PkgFieldSha256E, // catalogue: its package file's SHA-256 (hex), else ""
+ PkgFieldRepoSignedE, // catalogue: yes when the repository signed it, else no ("" elsewhere)
PkgFieldCountE
} PkgFieldE;
+// The yes / no of PkgFieldExplicitE, PkgFieldRepoSignedE, and the plan's
+// rows
+#define PKG_BAS_YES "yes"
+#define PKG_BAS_NO "no"
+
+
// PKG_RUN_*
typedef enum {
PkgRunBusyE,
@@ -59,7 +67,8 @@ typedef enum {
PkgRunFailedE
} PkgRunE;
-int32_t PkgBuild(int32_t s, const char *templatePath, const char *files, const char *outPath, const char *secretPath);
+int32_t PkgBuildStart(int32_t s, const char *templatePath, const char *files, const char *outPath, const char *secretPath);
+int32_t PkgCheckData(int32_t s, const uint8_t *data, int32_t len, const char *signature, const char *keyPath);
void PkgClose(int32_t s);
const char *PkgDependents(int32_t s, const char *ids);
const char *PkgDependsRows(int32_t s, const char *id);
@@ -76,17 +85,19 @@ const char *PkgInstalledRows(int32_t s);
int32_t PkgInstallStart(int32_t s);
int32_t PkgKeygen(int32_t s, const char *keyName, const char *basePath);
const char *PkgKeyInfo(int32_t s, const char *path);
+const char *PkgKeysRows(int32_t s);
const char *PkgKindFolder(const char *kind, const char *id);
const char *PkgKindName(int32_t kind);
const char *PkgNeededBy(int32_t s, const char *id);
int32_t PkgOfferCount(int32_t s);
const char *PkgOfferDependsRows(int32_t s, int32_t offer);
const char *PkgOfferField(int32_t s, int32_t offer, int32_t field);
-int32_t PkgOfferFile(int32_t s, const char *path);
const char *PkgOfferFilesRows(int32_t s, int32_t offer);
-int32_t PkgOfferFolder(int32_t s, const char *dir);
+int32_t PkgOfferFileStart(int32_t s, const char *path);
+int32_t PkgOfferFolderStart(int32_t s, const char *dir);
int32_t PkgOfferIndex(int32_t s, const char *id);
int32_t PkgOpen(void);
+int32_t PkgOpenIn(const char *root);
const char *PkgOrphans(int32_t s, const char *ids);
int32_t PkgPlan(int32_t s, const char *offers);
double PkgPlanBytes(int32_t s);
@@ -100,8 +111,16 @@ int32_t PkgRunFor(int32_t s, int32_t milliseconds);
const char *PkgRunKept(int32_t s);
const char *PkgRunMessage(int32_t s);
int32_t PkgRunPercent(int32_t s);
-int32_t PkgSign(int32_t s, const char *dpkPath, const char *secretPath, int32_t detached);
+const char *PkgSignData(int32_t s, const uint8_t *data, int32_t len, const char *secretPath);
+int32_t PkgSignStart(int32_t s, const char *dpkPath, const char *secretPath, int32_t detached);
int32_t PkgTrustKey(int32_t s, const char *keyPath);
+int32_t PkgUntrustKey(int32_t s, const char *keyId);
+int32_t PkgVerifyStart(int32_t s, const char *dpkPath, const char *keyPath);
+int32_t PkgVersionCompare(const char *a, const char *b);
int32_t PkgVersionValid(const char *text);
+int32_t PkgZipCreateStart(int32_t s, const char *zipPath, const char *files);
+int32_t PkgZipExtractStart(int32_t s, const char *zipPath, const char *folder);
+int32_t PkgZipListStart(int32_t s, const char *zipPath);
+const char *PkgZipRows(int32_t s);
#endif // PKG_BAS_H
diff --git a/src/libs/kpunch/pkg/pkgCli.c b/src/libs/kpunch/pkg/pkgCli.c
index 6f139bc6..b7eb5b88 100644
--- a/src/libs/kpunch/pkg/pkgCli.c
+++ b/src/libs/kpunch/pkg/pkgCli.c
@@ -28,7 +28,7 @@
#include "pkgOp.h"
#include "pkgPlan.h"
#include "dvxPlat.h"
-#include "monocypher.h"
+#include "crypto/security.h"
#include "stb_ds_wrap.h"
#include
@@ -286,7 +286,7 @@ static int cmdInstall(const CliOptsT *o) {
for (int32_t i = 0; planned && i < (int32_t)arrlen(plan.steps); i++) {
const PkgOfferT *offer = &offers[plan.steps[i].offer];
- planned = runOp(pkgOpInstall(&db, offer->where, &ring, plan.steps[i].explicitly), offer->manifest.id) == EXIT_OK;
+ planned = runOp(pkgOpInstall(&db, offer, plan.steps[i].explicitly), offer->manifest.id) == EXIT_OK;
rc = planned ? EXIT_OK : EXIT_FAILED;
}
@@ -315,7 +315,7 @@ static int cmdKeygen(const char *name, const char *base, PkgRandomFnT random) {
}
ok = pkgKeyWrite(seed, name, base, &key, err, sizeof(err));
- crypto_wipe(seed, sizeof(seed));
+ secSecureZero(seed, sizeof(seed));
if (!ok) {
fprintf(stderr, "dvxpkg: %s\n", err);
diff --git a/src/libs/kpunch/pkg/pkgDb.c b/src/libs/kpunch/pkg/pkgDb.c
index c74f7d39..639e2359 100644
--- a/src/libs/kpunch/pkg/pkgDb.c
+++ b/src/libs/kpunch/pkg/pkgDb.c
@@ -330,7 +330,7 @@ bool pkgDbPut(PkgDbT *db, const PkgManifestT *m, bool explicitly, const uint8_t
text = pkgDbFormat(m, explicitly, signer, &len);
if (!text) {
- return fail(err, errSize, "There is not enough memory.");
+ return fail(err, errSize, PKG_ERR_NO_MEMORY);
}
// Kept as read back, so the entry is the record exactly
diff --git a/src/libs/kpunch/pkg/pkgDpk.c b/src/libs/kpunch/pkg/pkgDpk.c
index 1cfc3c68..e9f830d3 100644
--- a/src/libs/kpunch/pkg/pkgDpk.c
+++ b/src/libs/kpunch/pkg/pkgDpk.c
@@ -27,10 +27,11 @@
#include "pkgInt.h"
#include "dvxPlat.h"
#include "dvxTypes.h"
-#include "serial/security/secHash.h"
+#include "crypto/secHash.h"
#include "stb_ds_wrap.h"
#include
+#include
#include
#include
#include
@@ -39,12 +40,12 @@
#include
#include
-#define READ_CHUNK 16384 // bytes hashed at a time
#define ENTRY_STEP 32768 // work per pkgZipData call (no one waits here)
+#define HASH_SHARE 10 // of a build's progress, the hashing (deflating takes far longer)
+#define PERCENT_MAX 100
#define SIG_TEXT_MAX 1024 // largest PACKAGE.SIG read
#define TEMP_EXT ".$PK" // the package being rewritten by pkgDpkSign
#define PATH_SEP_TEXT "/"
-#define DOT '.'
// A ZIP entry's data into memory
typedef struct {
@@ -53,32 +54,180 @@ typedef struct {
uint32_t cap;
} MemSinkT;
+typedef enum {
+ DpkHashE, // building: each file's size and SHA-256
+ DpkManifestE, // building: the manifest made (and signed), the package started
+ DpkHeadE, // the manifest (and signature) into the package
+ DpkFilesE, // building: each file into the package
+ DpkCopyE, // signing: each file of the package signed copied in
+ DpkCheckE, // checking: each file of the package hashed against the manifest
+ DpkFinishE, // the central directory (signing: the package replaced)
+ DpkDoneE,
+ DpkFailedE
+} DpkStageE;
+
+struct PkgDpkJobT {
+ PkgManifestT m; // building: the manifest made; checking: the one checked against
+ bool *seen; // checking: each of m's files found (malloc'd)
+ PkgDpkFileT *files; // building: copies, in name order (stb_ds)
+ PkgSecretT secret;
+ char *text; // the manifest as written
+ char *sigText; // its signature, when signed
+ char *path; // the package written
+ char *signing; // signing: the package signed (path replaces it)
+ char *outReal; // building: path's real path, when it is there already
+ FILE *src; // the file being hashed, or the package being signed
+ PkgZipWriterT *w;
+ PkgZipT *zip; // signing: the package signed, read
+ SecSha256T sha;
+ uint64_t bytes; // building: bytes of the files hashed
+ uint64_t added; // building: bytes of the files added
+ uint32_t hashed; // bytes of the file being hashed so far
+ uint32_t total; // signing, checking: the size of the package read
+ int32_t textLen;
+ int32_t sigLen;
+ int32_t at; // building: the file being hashed or added
+ DpkStageE stage;
+ bool keyed;
+ bool entryOpen; // an entry begun: being added, copied, or passed over
+ bool entryKeep; // signing: the entry is copied
+ char current[PKG_NAME_MAX];
+ char err[PKG_ERR_MAX];
+ uint8_t buf[PKG_DPK_STEP];
+};
+
// ============================================================
// Prototypes
// ============================================================
+static void checkEnd(PkgDpkJobT *job);
+static void checkStep(PkgDpkJobT *job, int32_t maxWork);
static int compareFiles(const void *a, const void *b);
+static void copyStep(PkgDpkJobT *job, int32_t maxWork);
static bool fail(char *err, int32_t errSize, const char *fmt, ...) __attribute__((format(printf, 3, 4)));
-static int32_t fileRead(void *ctx, uint8_t *buf, int32_t len);
-static bool hashFile(const char *path, uint32_t *size, uint8_t sha[PKG_SHA256_SIZE]);
+static void filesStep(PkgDpkJobT *job, int32_t maxWork);
+static void finishStep(PkgDpkJobT *job);
+static void hashEnd(PkgDpkJobT *job);
+static void hashStep(PkgDpkJobT *job, int32_t maxWork);
static bool hashWrite(void *ctx, const uint8_t *data, int32_t len);
+static void headBegin(PkgDpkJobT *job);
+static void headStep(PkgDpkJobT *job, int32_t maxWork);
+static bool isOutput(const PkgDpkJobT *job, const char *source);
+static void jobFail(PkgDpkJobT *job, const char *fmt, ...) __attribute__((format(printf, 2, 3)));
+static bool jobRun(PkgDpkJobT *job, char *err, int32_t errSize);
static bool joinName(char *out, int32_t outSize, const char *dir, const char *name);
+static void manifestStep(PkgDpkJobT *job);
static bool memWrite(void *ctx, const uint8_t *data, int32_t len);
bool pkgDpkBuild(const PkgManifestT *templ, const char *folder, const char *outPath, const PkgSecretT *secret, char *err, int32_t errSize);
bool pkgDpkBuildFiles(const PkgManifestT *templ, const PkgDpkFileT *files, int32_t count, const char *outPath, const PkgSecretT *secret, char *err, int32_t errSize);
+PkgDpkJobT *pkgDpkBuildStart(const PkgManifestT *templ, const PkgDpkFileT *files, int32_t count, const char *outPath, const PkgSecretT *secret, char *err, int32_t errSize);
bool pkgDpkCheckFiles(const char *path, const PkgDpkInfoT *info, char *err, int32_t errSize);
+PkgDpkJobT *pkgDpkCheckStart(const char *path, const PkgDpkInfoT *info, char *err, int32_t errSize);
void pkgDpkFilesFree(PkgDpkFileT *files);
bool pkgDpkFolderFiles(const char *folder, const char *prefix, PkgDpkFileT **files);
void pkgDpkInfoFree(PkgDpkInfoT *info);
+const char *pkgDpkJobCurrent(const PkgDpkJobT *job);
+void pkgDpkJobFree(PkgDpkJobT *job);
+const char *pkgDpkJobMessage(const PkgDpkJobT *job);
+int32_t pkgDpkJobPercent(const PkgDpkJobT *job);
+PkgDpkStepE pkgDpkJobStep(PkgDpkJobT *job, int32_t maxWork);
bool pkgDpkReadInfo(const char *path, PkgDpkInfoT *info, char *err, int32_t errSize);
bool pkgDpkSign(const char *path, const PkgSecretT *secret, bool detached, char *err, int32_t errSize);
const PkgKeyT *pkgDpkSigner(const PkgDpkInfoT *info, const PkgSigT *detached, const PkgKeyT *keys, int32_t count);
+PkgDpkJobT *pkgDpkSignStart(const char *path, const PkgSecretT *secret, char *err, int32_t errSize);
static bool readEntry(PkgZipT *zip, const PkgZipEntryT *e, uint32_t max, MemSinkT *sink);
-static bool sameFile(const char *a, const char *b);
-static bool siblingPath(const char *path, const char *ext, char *out, int32_t outSize);
static void sortFiles(PkgDpkFileT *files);
static bool walk(const char *folder, const char *rel, const char *prefix, PkgDpkFileT **files);
-static const char *zipReason(const PkgZipT *zip);
+
+
+// Checking: the package read to its end; a file the manifest lists and
+// the package does not have fails it.
+static void checkEnd(PkgDpkJobT *job) {
+ for (int32_t i = 0; i < (int32_t)arrlen(job->m.files); i++) {
+ if (!job->seen[i]) {
+ jobFail(job, "%s is in the manifest but not in the package.", job->m.files[i].name);
+ return;
+ }
+ }
+
+ job->stage = DpkDoneE;
+}
+
+
+// Checking: the next entry's header, or a piece of its data (hashed when
+// it is one of the package's files, else passed over: the manifest and
+// signature); at its end its size and SHA-256 against the manifest's.
+static void checkStep(PkgDpkJobT *job, int32_t maxWork) {
+ if (!job->entryOpen) {
+ PkgZipEntryT e;
+ PkgZipStepE step = pkgZipNext(job->zip, &e);
+
+ if (step == PkgZipEndE) {
+ checkEnd(job);
+ return;
+ }
+
+ if (step != PkgZipEntryE) {
+ jobFail(job, "%s", pkgZipProblemText(job->zip));
+ return;
+ }
+
+ if (e.isDir) {
+ return;
+ }
+
+ snprintf(job->current, sizeof(job->current), "%s", e.name);
+ job->entryKeep = strcasecmp(e.name, PKG_MANIFEST_NAME) != 0 && strcasecmp(e.name, PKG_SIG_NAME) != 0;
+ job->entryOpen = true;
+
+ if (!job->entryKeep) {
+ return;
+ }
+
+ const PkgFileT *file = pkgManifestFindFile(&job->m, e.name);
+
+ if (!file) {
+ jobFail(job, "%s is in the package but not in its manifest.", e.name);
+ return;
+ }
+
+ job->at = (int32_t)(file - job->m.files);
+
+ if (job->seen[job->at]) {
+ jobFail(job, "%s is in the package twice.", e.name);
+ return;
+ }
+
+ job->seen[job->at] = true;
+ job->hashed = e.size;
+ secSha256Init(&job->sha);
+ return;
+ }
+
+ PkgZipStepE data = job->entryKeep ? pkgZipData(job->zip, hashWrite, &job->sha, maxWork) : pkgZipData(job->zip, NULL, NULL, maxWork);
+
+ if (data == PkgZipMoreE) {
+ return;
+ }
+
+ job->entryOpen = false;
+
+ if (data != PkgZipFileDoneE) {
+ jobFail(job, "%s: %s", job->current, pkgZipProblemText(job->zip));
+ return;
+ }
+
+ if (job->entryKeep) {
+ const PkgFileT *file = &job->m.files[job->at];
+ uint8_t sha[PKG_SHA256_SIZE];
+
+ secSha256Final(&job->sha, sha);
+
+ if (job->hashed != file->size || memcmp(sha, file->sha256, PKG_SHA256_SIZE) != 0) {
+ jobFail(job, "%s is not the file the manifest lists (it was changed or damaged).", job->current);
+ }
+ }
+}
// Two files to build by name.
@@ -87,6 +236,49 @@ static int compareFiles(const void *a, const void *b) {
}
+// Signing: each file of the package signed copied in as it is packed
+// (its manifest and an old signature passed over: the new ones are in).
+static void copyStep(PkgDpkJobT *job, int32_t maxWork) {
+ PkgZipStepE step;
+
+ if (!job->entryOpen) {
+ PkgZipEntryT e;
+
+ step = pkgZipNext(job->zip, &e);
+
+ if (step == PkgZipEndE) {
+ job->stage = DpkFinishE;
+ return;
+ }
+
+ if (step != PkgZipEntryE) {
+ jobFail(job, "%s cannot be rewritten: %s", job->signing, pkgZipProblemText(job->zip));
+ return;
+ }
+
+ snprintf(job->current, sizeof(job->current), "%s", e.name);
+ job->entryKeep = !e.isDir && strcasecmp(e.name, PKG_MANIFEST_NAME) != 0 && strcasecmp(e.name, PKG_SIG_NAME) != 0;
+ job->entryOpen = true;
+
+ if (job->entryKeep && !pkgZipBeginCopy(job->w, &e)) {
+ jobFail(job, "Cannot write %s.", job->path);
+ }
+
+ return;
+ }
+
+ step = job->entryKeep ? pkgZipDataPacked(job->zip, pkgZipCopyWrite, job->w, maxWork) : pkgZipData(job->zip, NULL, NULL, maxWork);
+
+ if (step == PkgZipFailE && pkgZipProblem(job->zip) == PkgZipWriteFailedE) {
+ jobFail(job, "Cannot write %s.", job->path);
+ } else if (step == PkgZipFailE) {
+ jobFail(job, "%s cannot be rewritten: %s", job->signing, pkgZipProblemText(job->zip));
+ }
+
+ job->entryOpen = step == PkgZipMoreE;
+}
+
+
static bool fail(char *err, int32_t errSize, const char *fmt, ...) {
va_list ap;
@@ -97,38 +289,137 @@ static bool fail(char *err, int32_t errSize, const char *fmt, ...) {
}
-static int32_t fileRead(void *ctx, uint8_t *buf, int32_t len) {
- FILE *f = (FILE *)ctx;
- size_t n = fread(buf, 1, (size_t)len, f);
+// Building: each file into the package, a piece at a time.
+static void filesStep(PkgDpkJobT *job, int32_t maxWork) {
+ if (job->at == (int32_t)arrlen(job->files)) {
+ job->stage = DpkFinishE;
+ return;
+ }
- return (n == 0 && ferror(f)) ? -1 : (int32_t)n;
+ const PkgDpkFileT *file = &job->files[job->at];
+
+ if (!job->entryOpen) {
+ snprintf(job->current, sizeof(job->current), "%s", file->name);
+ job->entryOpen = pkgZipBeginFile(job->w, file->name, file->source);
+
+ if (!job->entryOpen) {
+ jobFail(job, "Cannot add %s to %s.", file->source, job->path);
+ }
+
+ return;
+ }
+
+ PkgZipStepE step = pkgZipStep(job->w, maxWork);
+
+ if (step == PkgZipFailE) {
+ jobFail(job, "Cannot add %s to %s.", file->source, job->path);
+ } else if (step == PkgZipFileDoneE) {
+ const PkgFileT *listed = pkgManifestFindFile(&job->m, file->name);
+
+ job->added += listed ? listed->size : 0;
+ job->entryOpen = false;
+ job->at++;
+ }
}
-static bool hashFile(const char *path, uint32_t *size, uint8_t sha[PKG_SHA256_SIZE]) {
- uint8_t buf[READ_CHUNK];
- SecSha256T ctx;
- uint64_t total = 0;
- size_t n;
- FILE *f = fopen(path, "rb");
+// The central directory written (signing: the package signed replaced by
+// the copy).
+static void finishStep(PkgDpkJobT *job) {
+ PkgZipWriterT *w = job->w;
- if (!f) {
- return false;
+ job->w = NULL;
+
+ if (!pkgZipFinish(w)) {
+ jobFail(job, "Cannot write %s.", job->path);
+ return;
}
- secSha256Init(&ctx);
+ if (job->signing) {
+ pkgZipClose(job->zip);
+ fclose(job->src);
+ job->zip = NULL;
+ job->src = NULL;
- while ((n = fread(buf, 1, sizeof(buf), f)) > 0) {
- secSha256Update(&ctx, buf, (int32_t)n);
- total += n;
+ if (remove(job->signing) != 0 || rename(job->path, job->signing) != 0) {
+ jobFail(job, "Cannot replace %s (the signed copy is %s).", job->signing, job->path);
+ return;
+ }
}
- bool ok = !ferror(f) && total <= UINT32_MAX;
+ job->stage = DpkDoneE;
+}
- fclose(f);
- secSha256Final(&ctx, sha);
- *size = (uint32_t)total;
- return ok;
+
+// The file being hashed is read through: its size and SHA-256 into the
+// manifest.
+static void hashEnd(PkgDpkJobT *job) {
+ const PkgDpkFileT *file = &job->files[job->at];
+ bool bad = ferror(job->src) != 0;
+ uint8_t sha[PKG_SHA256_SIZE];
+
+ fclose(job->src);
+ job->src = NULL;
+ secSha256Final(&job->sha, sha);
+
+ if (bad) {
+ jobFail(job, "Cannot read %s.", file->source);
+ } else if (!pkgManifestAddFile(&job->m, file->name, job->hashed, sha)) {
+ jobFail(job, PKG_ERR_NO_MEMORY);
+ } else {
+ job->bytes += job->hashed;
+ job->at++;
+ }
+}
+
+
+// Building: each file's size and SHA-256, a piece at a time (the package
+// being written, when it is there already, is not one of them).
+static void hashStep(PkgDpkJobT *job, int32_t maxWork) {
+ if (!job->src) {
+ if (job->at == (int32_t)arrlen(job->files)) {
+ job->at = 0;
+ job->stage = DpkManifestE;
+ return;
+ }
+
+ PkgDpkFileT *file = &job->files[job->at];
+
+ if (isOutput(job, file->source)) {
+ free(file->name);
+ free(file->source);
+ arrdel(job->files, job->at);
+ return;
+ }
+
+ snprintf(job->current, sizeof(job->current), "%s", file->name);
+ job->src = fopen(file->source, "rb");
+ job->hashed = 0;
+ secSha256Init(&job->sha);
+
+ if (!job->src) {
+ jobFail(job, "Cannot read %s.", file->source);
+ return;
+ }
+ }
+
+ for (int32_t work = 0; work == 0 || work < maxWork; ) {
+ size_t n = fread(job->buf, 1, sizeof(job->buf), job->src);
+
+ if (n == 0) {
+ hashEnd(job);
+ return;
+ }
+
+ if (n > UINT32_MAX - job->hashed) {
+ jobFail(job, "%s is too big for a package.", job->files[job->at].source);
+ return;
+ }
+
+ secSha256Update(&job->sha, job->buf, (int32_t)n);
+ job->hashed += (uint32_t)n;
+ work += (int32_t)n;
+ }
}
@@ -138,6 +429,90 @@ static bool hashWrite(void *ctx, const uint8_t *data, int32_t len) {
}
+// The package started: the signature made (when there is a key) and the
+// manifest's entry begun (headStep adds them).
+static void headBegin(PkgDpkJobT *job) {
+ if (job->keyed) {
+ PkgSigT sig;
+
+ pkgSign(&job->secret, (const uint8_t *)job->text, job->textLen, &sig);
+ job->sigText = pkgSigFormat(&sig, &job->sigLen);
+
+ if (!job->sigText) {
+ jobFail(job, PKG_ERR_NO_MEMORY);
+ return;
+ }
+ }
+
+ job->w = pkgZipCreate(job->path);
+
+ if (!job->w) {
+ jobFail(job, "Cannot create %s.", job->path);
+ } else if (!pkgZipBeginMem(job->w, PKG_MANIFEST_NAME, job->text, (uint32_t)job->textLen, time(NULL))) {
+ jobFail(job, "Cannot write %s.", job->path);
+ } else {
+ job->stage = DpkHeadE;
+ }
+}
+
+
+// The manifest into the package, a piece at a time, then the signature
+// (small: at once).
+static void headStep(PkgDpkJobT *job, int32_t maxWork) {
+ PkgZipStepE step = pkgZipStep(job->w, maxWork);
+
+ if (step == PkgZipMoreE) {
+ return;
+ }
+
+ if (step == PkgZipFailE || (job->sigText && !pkgZipAddMem(job->w, PKG_SIG_NAME, job->sigText, (uint32_t)job->sigLen, time(NULL)))) {
+ jobFail(job, "Cannot write %s.", job->path);
+ return;
+ }
+
+ job->stage = job->signing ? DpkCopyE : DpkFilesE;
+}
+
+
+// True when source is the package being written (only when that is there
+// already; only a file of its name is looked at closely). Not by inode
+// numbers: DVX's stat does not compute them (platformInit).
+static bool isOutput(const PkgDpkJobT *job, const char *source) {
+ char real[PATH_MAX];
+
+ return job->outReal && platformPathSame(platformPathBaseName(source), platformPathBaseName(job->outReal)) && realpath(source, real) && platformPathSame(real, job->outReal);
+}
+
+
+static void jobFail(PkgDpkJobT *job, const char *fmt, ...) {
+ va_list ap;
+
+ va_start(ap, fmt);
+ vsnprintf(job->err, sizeof(job->err), fmt, ap);
+ va_end(ap);
+ job->stage = DpkFailedE;
+}
+
+
+// Run job (freed) to its end at once: false with the reason in err (a
+// NULL job did not start: err says why already).
+static bool jobRun(PkgDpkJobT *job, char *err, int32_t errSize) {
+ PkgDpkStepE step;
+
+ if (!job) {
+ return false;
+ }
+
+ while ((step = pkgDpkJobStep(job, INT32_MAX)) == PkgDpkMoreE) {
+ }
+
+ bool ok = step == PkgDpkDoneE || fail(err, errSize, "%s", job->err);
+
+ pkgDpkJobFree(job);
+ return ok;
+}
+
+
// dir/name ('/' between them; name alone when dir is "").
static bool joinName(char *out, int32_t outSize, const char *dir, const char *name) {
int32_t n = dir[0] ? snprintf(out, (size_t)outSize, "%s" PATH_SEP_TEXT "%s", dir, name) : snprintf(out, (size_t)outSize, "%s", name);
@@ -146,6 +521,24 @@ static bool joinName(char *out, int32_t outSize, const char *dir, const char *na
}
+// Building: the manifest checked and made, and the package started.
+static void manifestStep(PkgDpkJobT *job) {
+ if (!pkgManifestCheck(&job->m, job->err, sizeof(job->err))) {
+ job->stage = DpkFailedE;
+ return;
+ }
+
+ job->text = pkgManifestFormat(&job->m, &job->textLen);
+
+ if (!job->text) {
+ jobFail(job, PKG_ERR_NO_MEMORY);
+ return;
+ }
+
+ headBegin(job);
+}
+
+
static bool memWrite(void *ctx, const uint8_t *data, int32_t len) {
MemSinkT *sink = (MemSinkT *)ctx;
@@ -171,174 +564,101 @@ bool pkgDpkBuild(const PkgManifestT *templ, const char *folder, const char *outP
bool pkgDpkBuildFiles(const PkgManifestT *templ, const PkgDpkFileT *files, int32_t count, const char *outPath, const PkgSecretT *secret, char *err, int32_t errSize) {
- PkgManifestT m;
- PkgDpkFileT *sorted = NULL; // the files' entries (not their text) copied
- char *text = NULL;
- int32_t len = 0;
- bool ok = true;
+ return jobRun(pkgDpkBuildStart(templ, files, count, outPath, secret, err, errSize), err, errSize);
+}
- // The template's fields and dependencies (code made for this DVX when
- // it does not say); the files are these
- m = *templ;
- m.format = PKG_MANIFEST_FORMAT;
- m.abi = (pkgKindIsCode(m.kind) && m.abi <= 0) ? DVX_ABI_VERSION : m.abi;
- m.files = NULL;
- m.depends = NULL;
- for (int32_t i = 0; i < (int32_t)arrlen(templ->depends); i++) {
- arrput(m.depends, templ->depends[i]);
- }
+PkgDpkJobT *pkgDpkBuildStart(const PkgManifestT *templ, const PkgDpkFileT *files, int32_t count, const char *outPath, const PkgSecretT *secret, char *err, int32_t errSize) {
+ PkgDpkJobT *job = (PkgDpkJobT *)calloc(1, sizeof(PkgDpkJobT));
+ char real[PATH_MAX];
+ bool ok = job != NULL;
- // In name order (the package being written is never one of them)
- for (int32_t i = 0; i < count; i++) {
- if (!sameFile(files[i].source, outPath)) {
- arrput(sorted, files[i]);
- }
- }
+ if (ok) {
+ // The template's fields and dependencies (code made for this DVX
+ // when it does not say); the files are these
+ job->m = *templ;
+ job->m.format = PKG_MANIFEST_FORMAT;
+ job->m.abi = (pkgKindIsCode(job->m.kind) && job->m.abi <= 0) ? DVX_ABI_VERSION : job->m.abi;
+ job->m.files = NULL;
+ job->m.depends = NULL;
+ job->stage = DpkHashE;
+ job->keyed = secret != NULL;
+ job->path = strdup(outPath);
+ ok = job->path != NULL;
- sortFiles(sorted);
-
- for (int32_t i = 0; ok && i < (int32_t)arrlen(sorted); i++) {
- uint8_t sha[PKG_SHA256_SIZE];
- uint32_t size = 0;
-
- if (!hashFile(sorted[i].source, &size, sha)) {
- ok = fail(err, errSize, "Cannot read %s.", sorted[i].source);
- } else if (!pkgManifestAddFile(&m, sorted[i].name, size, sha)) {
- ok = fail(err, errSize, "There is not enough memory.");
- }
- }
-
- ok = ok && pkgManifestCheck(&m, err, errSize);
- text = ok ? pkgManifestFormat(&m, &len) : NULL;
-
- if (ok && !text) {
- ok = fail(err, errSize, "There is not enough memory.");
- }
-
- PkgZipWriterT *w = ok ? pkgZipCreate(outPath) : NULL;
-
- if (ok && !w) {
- ok = fail(err, errSize, "Cannot create %s.", outPath);
- }
-
- if (ok && !pkgZipAddMem(w, PKG_MANIFEST_NAME, text, (uint32_t)len, time(NULL))) {
- ok = fail(err, errSize, "Cannot write %s.", outPath);
- }
-
- if (ok && secret) {
- PkgSigT sig;
- int32_t sigLen = 0;
- char *sigText = NULL;
-
- pkgSign(secret, (const uint8_t *)text, len, &sig);
- sigText = pkgSigFormat(&sig, &sigLen);
-
- if (!sigText || !pkgZipAddMem(w, PKG_SIG_NAME, sigText, (uint32_t)sigLen, time(NULL))) {
- ok = fail(err, errSize, "Cannot write %s.", outPath);
+ if (secret) {
+ job->secret = *secret;
}
- free(sigText);
- }
-
- for (int32_t i = 0; ok && i < (int32_t)arrlen(sorted); i++) {
- if (!pkgZipAddFile(w, sorted[i].name, sorted[i].source)) {
- ok = fail(err, errSize, "Cannot add %s to %s.", sorted[i].source, outPath);
+ if (ok && realpath(outPath, real)) {
+ job->outReal = strdup(real);
+ ok = job->outReal != NULL;
}
+
+ for (int32_t i = 0; i < (int32_t)arrlen(templ->depends); i++) {
+ arrput(job->m.depends, templ->depends[i]);
+ }
+
+ for (int32_t i = 0; ok && i < count; i++) {
+ PkgDpkFileT f = { strdup(files[i].name), strdup(files[i].source) };
+
+ ok = f.name && f.source;
+
+ if (ok) {
+ arrput(job->files, f);
+ } else {
+ free(f.name);
+ free(f.source);
+ }
+ }
+
+ sortFiles(job->files);
}
- if (w && ok && !pkgZipFinish(w)) {
- ok = fail(err, errSize, "Cannot write %s.", outPath);
- } else if (w && !ok) {
- pkgZipAbort(w);
+ if (!ok) {
+ pkgDpkJobFree(job);
+ fail(err, errSize, PKG_ERR_NO_MEMORY);
+ return NULL;
}
- free(text);
- arrfree(sorted);
- pkgManifestFree(&m);
- return ok;
+ return job;
}
bool pkgDpkCheckFiles(const char *path, const PkgDpkInfoT *info, char *err, int32_t errSize) {
- const PkgManifestT *m = &info->manifest;
- int32_t count = (int32_t)arrlen(m->files);
- bool *seen = (bool *)calloc((size_t)count + 1, sizeof(bool));
- FILE *f = seen ? fopen(path, "rb") : NULL;
- PkgZipT *zip = f ? pkgZipOpen(fileRead, f) : NULL;
- bool ok = zip != NULL;
- PkgZipEntryT e;
- PkgZipStepE step = PkgZipFailE;
+ return jobRun(pkgDpkCheckStart(path, info, err, errSize), err, errSize);
+}
- if (!ok) {
+
+PkgDpkJobT *pkgDpkCheckStart(const char *path, const PkgDpkInfoT *info, char *err, int32_t errSize) {
+ PkgDpkJobT *job = (PkgDpkJobT *)calloc(1, sizeof(PkgDpkJobT));
+ struct stat st;
+
+ if (!job) {
+ fail(err, errSize, PKG_ERR_NO_MEMORY);
+ return NULL;
+ }
+
+ job->seen = (bool *)calloc((size_t)arrlen(info->manifest.files) + 1, sizeof(bool));
+
+ if (!job->seen || !pkgManifestCopy(&job->m, &info->manifest)) {
+ fail(err, errSize, PKG_ERR_NO_MEMORY);
+ pkgDpkJobFree(job);
+ return NULL;
+ }
+
+ job->total = stat(path, &st) == 0 ? (uint32_t)st.st_size : 0;
+ job->src = fopen(path, "rb");
+ job->zip = job->src ? pkgZipOpen(pkgZipFileRead, job->src) : NULL;
+
+ if (!job->zip) {
fail(err, errSize, "Cannot read %s.", path);
+ pkgDpkJobFree(job);
+ return NULL;
}
- while (ok && (step = pkgZipNext(zip, &e)) == PkgZipEntryE) {
- if (e.isDir) {
- continue;
- }
-
- // The manifest and signature: not files of the package
- if (strcasecmp(e.name, PKG_MANIFEST_NAME) == 0 || strcasecmp(e.name, PKG_SIG_NAME) == 0) {
- ok = pkgZipData(zip, NULL, NULL, INT32_MAX) == PkgZipFileDoneE || fail(err, errSize, "%s", zipReason(zip));
- continue;
- }
-
- const PkgFileT *file = pkgManifestFindFile(m, e.name);
-
- if (!file) {
- ok = fail(err, errSize, "%s is in the package but not in its manifest.", e.name);
- break;
- }
-
- int32_t index = (int32_t)(file - m->files);
-
- if (seen[index]) {
- ok = fail(err, errSize, "%s is in the package twice.", e.name);
- break;
- }
-
- seen[index] = true;
-
- SecSha256T ctx;
- uint8_t sha[PKG_SHA256_SIZE];
- PkgZipStepE data;
-
- secSha256Init(&ctx);
-
- while ((data = pkgZipData(zip, hashWrite, &ctx, ENTRY_STEP)) == PkgZipMoreE) {
- }
-
- secSha256Final(&ctx, sha);
-
- if (data != PkgZipFileDoneE) {
- ok = fail(err, errSize, "%s: %s", e.name, zipReason(zip));
- } else if (e.size != file->size || memcmp(sha, file->sha256, PKG_SHA256_SIZE) != 0) {
- ok = fail(err, errSize, "%s is not the file the manifest lists (it was changed or damaged).", e.name);
- }
- }
-
- if (ok && step == PkgZipFailE) {
- ok = fail(err, errSize, "%s", zipReason(zip));
- }
-
- for (int32_t i = 0; ok && i < count; i++) {
- if (!seen[i]) {
- ok = fail(err, errSize, "%s is in the manifest but not in the package.", m->files[i].name);
- }
- }
-
- if (zip) {
- pkgZipClose(zip);
- }
-
- if (f) {
- fclose(f);
- }
-
- free(seen);
- return ok;
+ job->stage = DpkCheckE;
+ return job;
}
@@ -368,9 +688,113 @@ void pkgDpkInfoFree(PkgDpkInfoT *info) {
}
+const char *pkgDpkJobCurrent(const PkgDpkJobT *job) {
+ return job->current;
+}
+
+
+void pkgDpkJobFree(PkgDpkJobT *job) {
+ if (!job) {
+ return;
+ }
+
+ pkgZipAbort(job->w);
+
+ if (job->zip) {
+ pkgZipClose(job->zip);
+ }
+
+ if (job->src) {
+ fclose(job->src);
+ }
+
+ pkgSecretWipe(&job->secret);
+ pkgManifestFree(&job->m);
+ free(job->seen);
+ pkgDpkFilesFree(job->files);
+ free(job->text);
+ free(job->sigText);
+ free(job->path);
+ free(job->signing);
+ free(job->outReal);
+ free(job);
+}
+
+
+const char *pkgDpkJobMessage(const PkgDpkJobT *job) {
+ return job->err;
+}
+
+
+// Building: the hashing by files, then the rest by bytes added; signing
+// and checking: by bytes of the package read.
+int32_t pkgDpkJobPercent(const PkgDpkJobT *job) {
+ int32_t count = (int32_t)arrlen(job->files);
+
+ if (job->stage == DpkDoneE) {
+ return PERCENT_MAX;
+ }
+
+ if (job->signing || job->seen) {
+ return job->total && job->zip ? (int32_t)((uint64_t)pkgZipOffset(job->zip) * PERCENT_MAX / job->total) : 0;
+ }
+
+ if (job->stage == DpkHashE) {
+ return count ? job->at * HASH_SHARE / count : 0;
+ }
+
+ uint64_t done = job->added + (job->stage == DpkFilesE ? pkgZipAdded(job->w) : 0);
+
+ return HASH_SHARE + (job->bytes ? (int32_t)(done * (PERCENT_MAX - HASH_SHARE) / job->bytes) : 0);
+}
+
+
+PkgDpkStepE pkgDpkJobStep(PkgDpkJobT *job, int32_t maxWork) {
+ switch (job->stage) {
+ case DpkHashE:
+ hashStep(job, maxWork);
+ break;
+
+ case DpkManifestE:
+ manifestStep(job);
+ break;
+
+ case DpkHeadE:
+ headStep(job, maxWork);
+ break;
+
+ case DpkFilesE:
+ filesStep(job, maxWork);
+ break;
+
+ case DpkCopyE:
+ copyStep(job, maxWork);
+ break;
+
+ case DpkCheckE:
+ checkStep(job, maxWork);
+ break;
+
+ case DpkFinishE:
+ finishStep(job);
+ break;
+
+ case DpkDoneE:
+ case DpkFailedE:
+ break;
+ }
+
+ if (job->stage == DpkDoneE) {
+ return PkgDpkDoneE;
+ }
+
+ return job->stage == DpkFailedE ? PkgDpkFailedE : PkgDpkMoreE;
+}
+
+
bool pkgDpkReadInfo(const char *path, PkgDpkInfoT *info, char *err, int32_t errSize) {
FILE *f = fopen(path, "rb");
- PkgZipT *zip = f ? pkgZipOpen(fileRead, f) : NULL;
+ PkgZipT *zip = f ? pkgZipOpen(pkgZipFileRead, f) : NULL;
MemSinkT text = { NULL, 0, 0 };
PkgZipEntryT e;
bool ok = zip != NULL;
@@ -382,7 +806,7 @@ bool pkgDpkReadInfo(const char *path, PkgDpkInfoT *info, char *err, int32_t errS
} else if (pkgZipNext(zip, &e) != PkgZipEntryE || e.isDir || strcasecmp(e.name, PKG_MANIFEST_NAME) != 0) {
ok = fail(err, errSize, "%s is not a DVX package (it does not start with %s).", path, PKG_MANIFEST_NAME);
} else if (!readEntry(zip, &e, PKG_MANIFEST_MAX, &text)) {
- ok = fail(err, errSize, "The manifest of %s cannot be read: %s", path, zipReason(zip));
+ ok = fail(err, errSize, "The manifest of %s cannot be read: %s", path, pkgZipProblemText(zip));
} else if (!pkgManifestParse((const char *)text.buf, (int32_t)text.len, &info->manifest, err, errSize)) {
ok = false;
} else if (!pkgManifestCheck(&info->manifest, err, errSize)) {
@@ -436,6 +860,10 @@ bool pkgDpkSign(const char *path, const PkgSecretT *secret, bool detached, char
char other[DVX_MAX_PATH];
bool ok;
+ if (!detached) {
+ return jobRun(pkgDpkSignStart(path, secret, err, errSize), err, errSize);
+ }
+
if (!pkgDpkReadInfo(path, &info, err, errSize)) {
return false;
}
@@ -445,55 +873,18 @@ bool pkgDpkSign(const char *path, const PkgSecretT *secret, bool detached, char
ok = sigText != NULL;
if (!ok) {
- fail(err, errSize, "There is not enough memory.");
+ fail(err, errSize, PKG_ERR_NO_MEMORY);
}
- if (ok && !siblingPath(path, detached ? PKG_SIG_EXT : TEMP_EXT, other, sizeof(other))) {
+ if (ok && !platformPathSibling(other, sizeof(other), path, PKG_SIG_EXT)) {
ok = fail(err, errSize, "The path of %s is too long.", path);
}
- if (ok && detached) {
+ if (ok) {
FILE *f = fopen(other, "wb");
ok = f && fwrite(sigText, 1, (size_t)sigLen, f) == (size_t)sigLen;
ok = (f && fclose(f) == 0 && ok) || fail(err, errSize, "Cannot write %s.", other);
- } else if (ok) {
- // Rewrite the package: the manifest, the new signature, then every
- // file as it was (an old signature left out)
- FILE *f = fopen(path, "rb");
- PkgZipT *zip = f ? pkgZipOpen(fileRead, f) : NULL;
- PkgZipWriterT *w = zip ? pkgZipCreate(other) : NULL;
- PkgZipEntryT e;
- PkgZipStepE step = PkgZipFailE;
-
- ok = w && pkgZipAddMem(w, PKG_MANIFEST_NAME, info.text, (uint32_t)info.textLen, time(NULL)) && pkgZipAddMem(w, PKG_SIG_NAME, sigText, (uint32_t)sigLen, time(NULL));
-
- while (ok && (step = pkgZipNext(zip, &e)) == PkgZipEntryE) {
- MemSinkT data = { NULL, 0, 0 };
- bool keep = !e.isDir && strcasecmp(e.name, PKG_MANIFEST_NAME) != 0 && strcasecmp(e.name, PKG_SIG_NAME) != 0;
-
- ok = readEntry(zip, &e, e.size, &data) && (!keep || pkgZipAddMem(w, e.name, data.buf, data.len, pkgZipDosTime(e.dosDate, e.dosTime)));
- free(data.buf);
- }
-
- ok = ok && step == PkgZipEndE;
-
- if (zip) {
- pkgZipClose(zip);
- }
-
- if (f) {
- fclose(f);
- }
-
- if (!ok) {
- pkgZipAbort(w);
- ok = fail(err, errSize, "Cannot rewrite %s.", path);
- } else if (!pkgZipFinish(w)) {
- ok = fail(err, errSize, "Cannot write %s.", other);
- } else {
- ok = (remove(path) == 0 && rename(other, path) == 0) || fail(err, errSize, "Cannot replace %s (the signed copy is %s).", path, other);
- }
}
free(sigText);
@@ -520,6 +911,61 @@ const PkgKeyT *pkgDpkSigner(const PkgDpkInfoT *info, const PkgSigT *detached, co
}
+PkgDpkJobT *pkgDpkSignStart(const char *path, const PkgSecretT *secret, char *err, int32_t errSize) {
+ PkgDpkJobT *job = (PkgDpkJobT *)calloc(1, sizeof(PkgDpkJobT));
+ PkgDpkInfoT info;
+ char other[DVX_MAX_PATH];
+ struct stat st;
+
+ if (!job) {
+ fail(err, errSize, PKG_ERR_NO_MEMORY);
+ return NULL;
+ }
+
+ if (!pkgDpkReadInfo(path, &info, err, errSize)) {
+ free(job);
+ return NULL;
+ }
+
+ // The manifest's bytes as they were signed; the rest of what was read
+ // goes
+ job->text = info.text;
+ job->textLen = info.textLen;
+ info.text = NULL;
+ pkgDpkInfoFree(&info);
+
+ job->secret = *secret;
+ job->keyed = true;
+ job->signing = strdup(path);
+ job->total = stat(path, &st) == 0 ? (uint32_t)st.st_size : 0;
+ job->src = fopen(path, "rb");
+ job->zip = job->src ? pkgZipOpen(pkgZipFileRead, job->src) : NULL;
+
+ if (!job->signing || !job->zip) {
+ fail(err, errSize, "Cannot read %s.", path);
+ } else if (!platformPathSibling(other, sizeof(other), path, TEMP_EXT)) {
+ fail(err, errSize, "The path of %s is too long.", path);
+ } else {
+ job->path = strdup(other);
+
+ if (!job->path) {
+ fail(err, errSize, PKG_ERR_NO_MEMORY);
+ } else {
+ headBegin(job);
+
+ if (job->stage == DpkHeadE) {
+ return job;
+ }
+
+ fail(err, errSize, "%s", job->err);
+ }
+ }
+
+ pkgDpkJobFree(job);
+ return NULL;
+}
+
+
// The whole of entry e (at most max bytes) into a new buffer in sink (the
// caller frees sink->buf).
static bool readEntry(PkgZipT *zip, const PkgZipEntryT *e, uint32_t max, MemSinkT *sink) {
@@ -544,26 +990,6 @@ static bool readEntry(PkgZipT *zip, const PkgZipEntryT *e, uint32_t max, MemSink
}
-// True when a and b name the same existing file.
-static bool sameFile(const char *a, const char *b) {
- struct stat sa;
- struct stat sb;
-
- return stat(a, &sa) == 0 && stat(b, &sb) == 0 && sa.st_ino == sb.st_ino && sa.st_dev == sb.st_dev && sa.st_ino != 0;
-}
-
-
-// path with its extension (if any) replaced by ext.
-static bool siblingPath(const char *path, const char *ext, char *out, int32_t outSize) {
- const char *base = platformPathBaseName(path);
- const char *dot = strrchr(base, DOT);
- int32_t keep = dot ? (int32_t)(dot - path) : (int32_t)strlen(path);
- int32_t n = snprintf(out, (size_t)outSize, "%.*s%s", (int)keep, path, ext);
-
- return n > 0 && n < outSize;
-}
-
-
// files (stb_ds) in name order (qsort is not given an empty one: it
// would be NULL).
static void sortFiles(PkgDpkFileT *files) {
@@ -627,20 +1053,3 @@ static bool walk(const char *folder, const char *rel, const char *prefix, PkgDpk
closedir(d);
return ok;
}
-
-
-static const char *zipReason(const PkgZipT *zip) {
- switch (pkgZipProblem(zip)) {
- case PkgZipReadFailedE:
- return "it cannot be read.";
-
- case PkgZipUnreadableE:
- return "it uses a form DVX cannot read.";
-
- case PkgZipWriteFailedE:
- return "there is not enough memory, or a part is too big.";
-
- default:
- return "it is damaged.";
- }
-}
diff --git a/src/libs/kpunch/pkg/pkgDpk.h b/src/libs/kpunch/pkg/pkgDpk.h
index 0b2110dc..7cbedb1f 100644
--- a/src/libs/kpunch/pkg/pkgDpk.h
+++ b/src/libs/kpunch/pkg/pkgDpk.h
@@ -43,6 +43,19 @@
#define PKG_MANIFEST_MAX (256 * 1024) // largest manifest read
+#define PKG_DPK_STEP 4096 // work a job step: bytes hashed, deflated, or copied
+
+// A package being built or signed a step at a time (pkgDpkBuildStart,
+// pkgDpkSignStart), so a program can draw progress and stay responsive
+// between steps
+typedef struct PkgDpkJobT PkgDpkJobT;
+
+typedef enum {
+ PkgDpkMoreE, // more to do
+ PkgDpkDoneE, // the package is written
+ PkgDpkFailedE // it is not (pkgDpkJobMessage says why; nothing is left behind)
+} PkgDpkStepE;
+
// What a package says about itself (its first entries)
typedef struct {
PkgManifestT manifest;
@@ -62,19 +75,27 @@ typedef struct {
// Build outPath from a template manifest (its [files], if any, is
// replaced; a program, library, or widget with no abi gets this DVX's,
// DVX_ABI_VERSION) and the files under folder (pkgDpkFolderFiles, no
-// prefix), signed with secret when not NULL. False with the reason in
-// err (and no file left behind).
+// prefix), signed with secret when not NULL, all at once. False with the
+// reason in err (and no file left behind).
bool pkgDpkBuild(const PkgManifestT *templ, const char *folder, const char *outPath, const PkgSecretT *secret, char *err, int32_t errSize);
// The same from files (count of them, any order; one that is outPath is
// left out).
bool pkgDpkBuildFiles(const PkgManifestT *templ, const PkgDpkFileT *files, int32_t count, const char *outPath, const PkgSecretT *secret, char *err, int32_t errSize);
+// Start building as pkgDpkBuildFiles (what it is given is copied); run it
+// with pkgDpkJobStep. NULL with the reason in err.
+PkgDpkJobT *pkgDpkBuildStart(const PkgManifestT *templ, const PkgDpkFileT *files, int32_t count, const char *outPath, const PkgSecretT *secret, char *err, int32_t errSize);
+
// Check every file of the package at path against info's manifest: each
-// entry listed, of its size and SHA-256, and each listed file there.
-// False with the reason in err.
+// entry listed, of its size and SHA-256, and each listed file there, all
+// at once (pkgDpkCheckStart run to its end). False with the reason in err.
bool pkgDpkCheckFiles(const char *path, const PkgDpkInfoT *info, char *err, int32_t errSize);
+// Start checking as pkgDpkCheckFiles (info's manifest is copied); run it
+// with pkgDpkJobStep. NULL with the reason in err.
+PkgDpkJobT *pkgDpkCheckStart(const char *path, const PkgDpkInfoT *info, char *err, int32_t errSize);
+
void pkgDpkFilesFree(PkgDpkFileT *files);
// Every file under folder (but a PACKAGE.INI or PACKAGE.SIG at its top)
@@ -86,16 +107,38 @@ bool pkgDpkFolderFiles(const char *folder, const char *prefix, PkgDpkF
void pkgDpkInfoFree(PkgDpkInfoT *info);
+// The name (in the package) of the file the job is on ("" when none).
+const char *pkgDpkJobCurrent(const PkgDpkJobT *job);
+
+// Stop the job if it is not done (the package it was writing removed),
+// and free it. NULL is ignored.
+void pkgDpkJobFree(PkgDpkJobT *job);
+
+// Why the job failed ("" when it did not).
+const char *pkgDpkJobMessage(const PkgDpkJobT *job);
+
+// How far the job is, 0 to 100.
+int32_t pkgDpkJobPercent(const PkgDpkJobT *job);
+
+// Run the job for about maxWork bytes of work (at least one piece):
+// PkgDpkMoreE, PkgDpkDoneE, or PkgDpkFailedE; then the same again.
+PkgDpkStepE pkgDpkJobStep(PkgDpkJobT *job, int32_t maxWork);
+
// Read the manifest (and the publisher's signature, if any) of the
// package at path, and check the manifest (pkgManifestCheck). False with
// the reason in err (nothing to free).
bool pkgDpkReadInfo(const char *path, PkgDpkInfoT *info, char *err, int32_t errSize);
-// Sign the package at path with secret: into it (PACKAGE.SIG, replacing
-// one that is there; the package is rewritten) or, when detached, as
-// NAME.SIG beside it. False with the reason in err.
+// Sign the package at path with secret, all at once: into it
+// (PACKAGE.SIG, replacing one that is there; the package is rewritten,
+// its files copied as they are packed) or, when detached, as NAME.SIG
+// beside it. False with the reason in err.
bool pkgDpkSign(const char *path, const PkgSecretT *secret, bool detached, char *err, int32_t errSize);
+// Start signing the package at path into it, as pkgDpkSign; run it with
+// pkgDpkJobStep. NULL with the reason in err.
+PkgDpkJobT *pkgDpkSignStart(const char *path, const PkgSecretT *secret, char *err, int32_t errSize);
+
// The one of keys (count of them) that made info's signature or the
// detached one (NULL: none), or NULL when neither is by any of them.
const PkgKeyT *pkgDpkSigner(const PkgDpkInfoT *info, const PkgSigT *detached, const PkgKeyT *keys, int32_t count);
diff --git a/src/libs/kpunch/pkg/pkgFormat.c b/src/libs/kpunch/pkg/pkgFormat.c
index e566598f..769d9b36 100644
--- a/src/libs/kpunch/pkg/pkgFormat.c
+++ b/src/libs/kpunch/pkg/pkgFormat.c
@@ -49,10 +49,6 @@ _Static_assert(PKG_HEADER_SIZE == HDR_CRC + HDR_CRC_SIZE, "PKG_HEADER_SIZE is th
#define TIER_BYTES 8
#define TIER_BYTES_HI 12
-#define BYTE_BITS 8
-#define BYTE_MASK 0xFF
-#define HALF_BITS 16
-#define HALF_MASK 0xFFFFu
#define WORD_BITS 32
// The separator of local paths (DVX_PATH_SEP, without needing dvxPlat.h:
@@ -71,8 +67,6 @@ _Static_assert(PKG_HEADER_SIZE == HDR_CRC + HDR_CRC_SIZE, "PKG_HEADER_SIZE is th
// ============================================================
static bool dosPartIsValid(const char *part, int32_t len);
-uint16_t pkgGet16(const uint8_t *p);
-uint32_t pkgGet32(const uint8_t *p);
bool pkgHeaderDecode(const uint8_t *in, PkgHeaderT *h);
void pkgHeaderEncode(const PkgHeaderT *h, uint8_t *out);
bool pkgHeaderRead(FILE *f, PkgHeaderT *h, uint32_t *dataStart);
@@ -80,8 +74,6 @@ bool pkgLocalPath(const char *dir, const char *relName, char *out, in
bool pkgNameIsValid(const char *name);
bool pkgPartName(int32_t disk, char *out, int32_t outSize);
int32_t pkgPartNumber(const char *name);
-void pkgPut16(uint8_t *p, uint16_t v);
-void pkgPut32(uint8_t *p, uint32_t v);
// One name part: base of 1-8 characters, optional '.' and 1-3 more.
@@ -110,31 +102,21 @@ static bool dosPartIsValid(const char *part, int32_t len) {
}
-uint16_t pkgGet16(const uint8_t *p) {
- return (uint16_t)(p[0] | (p[1] << BYTE_BITS));
-}
-
-
-uint32_t pkgGet32(const uint8_t *p) {
- return (uint32_t)pkgGet16(p) | ((uint32_t)pkgGet16(p + sizeof(uint16_t)) << HALF_BITS);
-}
-
-
bool pkgHeaderDecode(const uint8_t *in, PkgHeaderT *h) {
- if (memcmp(in, PKG_MAGIC, PKG_MAGIC_SIZE) != 0 || pkgGet16(in + HDR_VERSION) != PKG_FORMAT_VERSION) {
+ if (memcmp(in, PKG_MAGIC, PKG_MAGIC_SIZE) != 0 || dvxGetLe16(in + HDR_VERSION) != PKG_FORMAT_VERSION) {
return false;
}
- if ((uint32_t)mz_crc32(MZ_CRC32_INIT, in, HDR_CRC) != pkgGet32(in + HDR_CRC)) {
+ if ((uint32_t)mz_crc32(MZ_CRC32_INIT, in, HDR_CRC) != dvxGetLe32(in + HDR_CRC)) {
return false;
}
- h->disk = pkgGet16(in + HDR_DISK);
- h->diskCount = pkgGet16(in + HDR_DISK_COUNT);
- h->setId = pkgGet32(in + HDR_SET_ID);
- h->partOffset = pkgGet32(in + HDR_PART_OFFSET);
- h->partLength = pkgGet32(in + HDR_PART_LENGTH);
- h->zipSize = pkgGet32(in + HDR_ZIP_SIZE);
+ h->disk = dvxGetLe16(in + HDR_DISK);
+ h->diskCount = dvxGetLe16(in + HDR_DISK_COUNT);
+ h->setId = dvxGetLe32(in + HDR_SET_ID);
+ h->partOffset = dvxGetLe32(in + HDR_PART_OFFSET);
+ h->partLength = dvxGetLe32(in + HDR_PART_LENGTH);
+ h->zipSize = dvxGetLe32(in + HDR_ZIP_SIZE);
// Each tier ends where the one before it does or later, inside the ZIP
bool ok = h->disk >= 1 && h->disk <= h->diskCount && h->diskCount <= PKG_MAX_DISKS && h->partOffset <= h->zipSize && h->partLength <= h->zipSize - h->partOffset;
@@ -144,9 +126,9 @@ bool pkgHeaderDecode(const uint8_t *in, PkgHeaderT *h) {
const uint8_t *p = in + HDR_TIERS + t * HDR_TIER_SIZE;
PkgTierT *tier = &h->tiers[t];
- tier->end = pkgGet32(p + TIER_END);
- tier->fileCount = pkgGet32(p + TIER_FILES);
- tier->bytes = (uint64_t)pkgGet32(p + TIER_BYTES) | ((uint64_t)pkgGet32(p + TIER_BYTES_HI) << WORD_BITS);
+ tier->end = dvxGetLe32(p + TIER_END);
+ tier->fileCount = dvxGetLe32(p + TIER_FILES);
+ tier->bytes = (uint64_t)dvxGetLe32(p + TIER_BYTES) | ((uint64_t)dvxGetLe32(p + TIER_BYTES_HI) << WORD_BITS);
ok = ok && tier->end >= prev.end && tier->end <= h->zipSize && tier->fileCount >= prev.fileCount && tier->bytes >= prev.bytes;
prev = *tier;
}
@@ -158,25 +140,25 @@ bool pkgHeaderDecode(const uint8_t *in, PkgHeaderT *h) {
void pkgHeaderEncode(const PkgHeaderT *h, uint8_t *out) {
memset(out, 0, PKG_HEADER_SIZE);
memcpy(out, PKG_MAGIC, PKG_MAGIC_SIZE);
- pkgPut16(out + HDR_VERSION, PKG_FORMAT_VERSION);
- pkgPut16(out + HDR_DISK, h->disk);
- pkgPut16(out + HDR_DISK_COUNT, h->diskCount);
- pkgPut32(out + HDR_SET_ID, h->setId);
- pkgPut32(out + HDR_PART_OFFSET, h->partOffset);
- pkgPut32(out + HDR_PART_LENGTH, h->partLength);
- pkgPut32(out + HDR_ZIP_SIZE, h->zipSize);
+ dvxPutLe16(out + HDR_VERSION, PKG_FORMAT_VERSION);
+ dvxPutLe16(out + HDR_DISK, h->disk);
+ dvxPutLe16(out + HDR_DISK_COUNT, h->diskCount);
+ dvxPutLe32(out + HDR_SET_ID, h->setId);
+ dvxPutLe32(out + HDR_PART_OFFSET, h->partOffset);
+ dvxPutLe32(out + HDR_PART_LENGTH, h->partLength);
+ dvxPutLe32(out + HDR_ZIP_SIZE, h->zipSize);
for (int32_t t = 0; t < PKG_TIERS; t++) {
uint8_t *p = out + HDR_TIERS + t * HDR_TIER_SIZE;
const PkgTierT *tier = &h->tiers[t];
- pkgPut32(p + TIER_END, tier->end);
- pkgPut32(p + TIER_FILES, tier->fileCount);
- pkgPut32(p + TIER_BYTES, (uint32_t)tier->bytes);
- pkgPut32(p + TIER_BYTES_HI, (uint32_t)(tier->bytes >> WORD_BITS));
+ dvxPutLe32(p + TIER_END, tier->end);
+ dvxPutLe32(p + TIER_FILES, tier->fileCount);
+ dvxPutLe32(p + TIER_BYTES, (uint32_t)tier->bytes);
+ dvxPutLe32(p + TIER_BYTES_HI, (uint32_t)(tier->bytes >> WORD_BITS));
}
- pkgPut32(out + HDR_CRC, (uint32_t)mz_crc32(MZ_CRC32_INIT, out, HDR_CRC));
+ dvxPutLe32(out + HDR_CRC, (uint32_t)mz_crc32(MZ_CRC32_INIT, out, HDR_CRC));
}
@@ -276,13 +258,3 @@ int32_t pkgPartNumber(const char *name) {
}
-void pkgPut16(uint8_t *p, uint16_t v) {
- p[0] = (uint8_t)(v & BYTE_MASK);
- p[1] = (uint8_t)(v >> BYTE_BITS);
-}
-
-
-void pkgPut32(uint8_t *p, uint32_t v) {
- pkgPut16(p, (uint16_t)(v & HALF_MASK));
- pkgPut16(p + sizeof(uint16_t), (uint16_t)(v >> HALF_BITS));
-}
diff --git a/src/libs/kpunch/pkg/pkgFormat.h b/src/libs/kpunch/pkg/pkgFormat.h
index 06b78490..bf0cde07 100644
--- a/src/libs/kpunch/pkg/pkgFormat.h
+++ b/src/libs/kpunch/pkg/pkgFormat.h
@@ -58,6 +58,8 @@
#include
#include
+#include "dvxEndian.h"
+
// Part header: magic, format version, then the fields of PkgHeaderT and
// a CRC-32 of everything before it. Little-endian.
#define PKG_HEADER_SIZE 68
@@ -172,10 +174,13 @@ bool pkgPartName(int32_t disk, char *out, int32_t outSize);
// one.
int32_t pkgPartNumber(const char *name);
-// Little-endian field access (shared with the ZIP reader and writer)
-uint16_t pkgGet16(const uint8_t *p);
-uint32_t pkgGet32(const uint8_t *p);
-void pkgPut16(uint8_t *p, uint16_t v);
-void pkgPut32(uint8_t *p, uint32_t v);
+// Exactly n bytes from n * PKG_HEX_PER_BYTE hex digits (either case) and
+// nothing more (a manifest's SHA-256, a key's id); bytes as lower-case hex
+// digits (out holds n * PKG_HEX_PER_BYTE + 1).
+#define PKG_HEX_PER_BYTE 2
+
+bool pkgHexDecode(const char *text, uint8_t *bytes, int32_t n);
+void pkgHexEncode(const uint8_t *bytes, int32_t n, char *out);
+
#endif // PKG_FORMAT_H
diff --git a/src/libs/kpunch/pkg/pkgIni.c b/src/libs/kpunch/pkg/pkgIni.c
index 335fdd06..05e2b8c8 100644
--- a/src/libs/kpunch/pkg/pkgIni.c
+++ b/src/libs/kpunch/pkg/pkgIni.c
@@ -23,17 +23,12 @@
// pkgIni.c -- the package library's INI reader and hex (see pkgInt.h)
#include "pkgInt.h"
+#include "dvxHash.h"
#include
#include
-#define HEX_DIGITS "0123456789abcdef"
-#define HEX_RADIX 16
-#define HEX_PER_BYTE 2
-#define HEX_LETTER_BASE 10 // the value of 'a'
#define SECTION_BRACKETS 2 // '[' and ']'
-#define NIBBLE_BITS 4
-#define NIBBLE_MASK 0x0F
#define COMMENT_SEMICOLON ';'
#define COMMENT_HASH '#'
#define SECTION_OPEN '['
@@ -44,7 +39,6 @@
// Prototypes
// ============================================================
-static int32_t hexValue(char c);
bool pkgHexDecode(const char *text, uint8_t *bytes, int32_t n);
void pkgHexEncode(const uint8_t *bytes, int32_t n, char *out);
PkgIniStepE pkgIniNext(PkgIniT *ini);
@@ -52,30 +46,21 @@ void pkgIniStart(PkgIniT *ini, const char *text, int32_t len);
static bool trimCopy(char *out, int32_t outSize, const char *from, int32_t len);
-static int32_t hexValue(char c) {
- if (c >= '0' && c <= '9') {
- return c - '0';
- }
-
- c = (char)tolower((unsigned char)c);
- return (c >= 'a' && c <= 'f') ? c - 'a' + HEX_LETTER_BASE : -1;
-}
-
bool pkgHexDecode(const char *text, uint8_t *bytes, int32_t n) {
- if ((int32_t)strlen(text) != n * HEX_PER_BYTE) {
+ if ((int32_t)strlen(text) != n * PKG_HEX_PER_BYTE) {
return false;
}
for (int32_t i = 0; i < n; i++) {
- int32_t hi = hexValue(text[i * HEX_PER_BYTE]);
- int32_t lo = hexValue(text[i * HEX_PER_BYTE + 1]);
+ int32_t hi = dvxHexValue((uint8_t)text[i * PKG_HEX_PER_BYTE]);
+ int32_t lo = dvxHexValue((uint8_t)text[i * PKG_HEX_PER_BYTE + 1]);
if (hi < 0 || lo < 0) {
return false;
}
- bytes[i] = (uint8_t)(hi * HEX_RADIX + lo);
+ bytes[i] = (uint8_t)(hi * DVX_HEX_RADIX + lo);
}
return true;
@@ -84,11 +69,11 @@ bool pkgHexDecode(const char *text, uint8_t *bytes, int32_t n) {
void pkgHexEncode(const uint8_t *bytes, int32_t n, char *out) {
for (int32_t i = 0; i < n; i++) {
- out[i * HEX_PER_BYTE] = HEX_DIGITS[bytes[i] >> NIBBLE_BITS];
- out[i * HEX_PER_BYTE + 1] = HEX_DIGITS[bytes[i] & NIBBLE_MASK];
+ out[i * PKG_HEX_PER_BYTE] = DVX_HEX_DIGITS[bytes[i] >> DVX_NIBBLE_BITS];
+ out[i * PKG_HEX_PER_BYTE + 1] = DVX_HEX_DIGITS[bytes[i] & DVX_NIBBLE_MASK];
}
- out[n * HEX_PER_BYTE] = '\0';
+ out[n * PKG_HEX_PER_BYTE] = '\0';
}
diff --git a/src/libs/kpunch/pkg/pkgInt.h b/src/libs/kpunch/pkg/pkgInt.h
index be7bf04e..6c9bd57b 100644
--- a/src/libs/kpunch/pkg/pkgInt.h
+++ b/src/libs/kpunch/pkg/pkgInt.h
@@ -22,12 +22,13 @@
// pkgInt.h -- the package library's own helpers: reading the small INI
// texts of manifests, keys, and signatures (one strict reader for all of
-// them), and hex.
+// them), and the messages its parts share.
#ifndef PKG_INT_H
#define PKG_INT_H
#include "pkgFormat.h"
+#include "pkgSign.h"
#include
#include
@@ -35,6 +36,8 @@
#define PKG_INI_SECTION_MAX 32
#define PKG_INI_VALUE_MAX 160
+#define PKG_ERR_NO_MEMORY "There is not enough memory."
+
typedef enum {
PkgIniSectionE, // a [section] line: ini->section
PkgIniKeyE, // a key = value line: ini->key, ini->value (trimmed)
@@ -52,16 +55,14 @@ typedef struct {
char value[PKG_INI_VALUE_MAX];
} PkgIniT;
-// Bytes as lower-case hex digits (out holds 2 * n + 1).
-void pkgHexEncode(const uint8_t *bytes, int32_t n, char *out);
-
-// Exactly n bytes from 2 * n hex digits (either case) and nothing more.
-bool pkgHexDecode(const char *text, uint8_t *bytes, int32_t n);
-
// The next section or key line (blank lines and lines starting with ';'
// or '#' are skipped; a line ends at CR LF or LF).
PkgIniStepE pkgIniNext(PkgIniT *ini);
void pkgIniStart(PkgIniT *ini, const char *text, int32_t len);
+// A key's id: the start of the SHA-256 of its public key (pkgSign.c;
+// pkgSignMake.c uses it too).
+void pkgKeyIdOf(const uint8_t publicKey[PKG_PUBLIC_SIZE], uint8_t id[PKG_KEY_ID_SIZE]);
+
#endif // PKG_INT_H
diff --git a/src/libs/kpunch/pkg/pkgManifest.c b/src/libs/kpunch/pkg/pkgManifest.c
index 309f7573..7d5080e9 100644
--- a/src/libs/kpunch/pkg/pkgManifest.c
+++ b/src/libs/kpunch/pkg/pkgManifest.c
@@ -51,7 +51,6 @@ DVX_ABI_STAMP(pkg);
#define TEXT_FIRST 512 // the formatted manifest's first capacity
#define LINE_MAX (PKG_NAME_MAX + PKG_SHA256_SIZE * 2 + 32)
#define EOL "\r\n"
-#define SHA_HEX_SIZE (PKG_SHA256_SIZE * 2 + 1)
#define PATH_SEP_TEXT "/"
// The keys of [package] (sPackageKeys order)
@@ -120,12 +119,14 @@ static const char *const sSystemOwnDirs[] = { PKG_DB_DIR, PKG_KEYS_DIR };
// Prototypes
// ============================================================
+static int compareNames(const void *a, const void *b);
static void copyText(char *out, int32_t outSize, const char *value, int32_t line, bool *ok, char *err, int32_t errSize);
static bool fail(char *err, int32_t errSize, const char *fmt, ...) __attribute__((format(printf, 3, 4)));
static int32_t folderText(char *out, int32_t outSize, const char *root, const char *vendor, const char *pkgName);
static bool idCharValid(char c);
static bool idPartValid(const char *part, int32_t len);
static bool inFolder(const char *name, const char *root, const char *vendor, const char *pkgName);
+static bool listedTwice(const PkgManifestT *m, const char **twice);
static bool parseFileLine(PkgManifestT *m, const PkgIniT *ini, char *err, int32_t errSize);
static bool parsePackageKey(PkgManifestT *m, const PkgIniT *ini, uint32_t *seen, char *err, int32_t errSize);
static bool parseText(const char *text, int32_t len, bool template, PkgManifestT *m, char *err, int32_t errSize);
@@ -142,6 +143,7 @@ bool pkgKindParse(const char *text, PkgKindE *kind);
void pkgManifestAddDepend(PkgManifestT *m, const char *id, PkgVersionT min);
bool pkgManifestAddFile(PkgManifestT *m, const char *name, uint32_t size, const uint8_t sha256[PKG_SHA256_SIZE]);
bool pkgManifestCheck(const PkgManifestT *m, char *err, int32_t errSize);
+bool pkgManifestCopy(PkgManifestT *dst, const PkgManifestT *src);
const PkgFileT *pkgManifestFindFile(const PkgManifestT *m, const char *name);
char *pkgManifestFormat(const PkgManifestT *m, int32_t *outLen);
void pkgManifestFree(PkgManifestT *m);
@@ -156,6 +158,12 @@ static void textAdd(TextT *t, const char *fmt, ...) __attribute__((format
// A text value into its field; a value too long fails the parse.
+// Two file names, as DOS compares them (for qsort).
+static int compareNames(const void *a, const void *b) {
+ return strcasecmp(*(const char *const *)a, *(const char *const *)b);
+}
+
+
static void copyText(char *out, int32_t outSize, const char *value, int32_t line, bool *ok, char *err, int32_t errSize) {
if ((int32_t)strlen(value) >= outSize) {
*ok = fail(err, errSize, "Line %d: the value is too long (at most %d characters).", (int)line, (int)outSize - 1);
@@ -225,8 +233,37 @@ static bool inFolder(const char *name, const char *root, const char *vendor, con
// A [files] line: name = size sha256.
+// *twice: a name m lists twice (as DOS compares names), or NULL: the names
+// sorted, so the same ones are side by side. False when out of memory.
+static bool listedTwice(const PkgManifestT *m, const char **twice) {
+ int32_t count = (int32_t)arrlen(m->files);
+ const char **names = (const char **)malloc((size_t)(count ? count : 1) * sizeof(*names));
+
+ *twice = NULL;
+
+ if (!names) {
+ return false;
+ }
+
+ for (int32_t i = 0; i < count; i++) {
+ names[i] = m->files[i].name;
+ }
+
+ qsort(names, (size_t)count, sizeof(*names), compareNames);
+
+ for (int32_t i = 1; i < count && !*twice; i++) {
+ if (strcasecmp(names[i - 1], names[i]) == 0) {
+ *twice = names[i];
+ }
+ }
+
+ free(names);
+ return true;
+}
+
+
static bool parseFileLine(PkgManifestT *m, const PkgIniT *ini, char *err, int32_t errSize) {
- char hex[SHA_HEX_SIZE];
+ char hex[PKG_SHA256_TEXT];
uint8_t sha[PKG_SHA256_SIZE];
uint32_t size = 0;
char *end = NULL;
@@ -244,7 +281,7 @@ static bool parseFileLine(PkgManifestT *m, const PkgIniT *ini, char *err, int32_
snprintf(hex, sizeof(hex), "%s", end);
- if (strlen(end) != SHA_HEX_SIZE - 1 || !pkgHexDecode(hex, sha, PKG_SHA256_SIZE)) {
+ if (strlen(end) != PKG_SHA256_TEXT - 1 || !pkgHexDecode(hex, sha, PKG_SHA256_SIZE)) {
return fail(err, errSize, "Line %d: the SHA-256 of %s is not 64 hex digits.", (int)ini->line, ini->key);
}
@@ -591,12 +628,16 @@ bool pkgManifestCheck(const PkgManifestT *m, char *err, int32_t errSize) {
if (!pkgFileAllowed(m, name)) {
return fail(err, errSize, "This %s package may not put a file at %s.", pkgKindName(m->kind), name);
}
+ }
- for (int32_t j = 0; j < i; j++) {
- if (strcasecmp(m->files[j].name, name) == 0) {
- return fail(err, errSize, "%s is listed twice.", name);
- }
- }
+ const char *twice = NULL;
+
+ if (!listedTwice(m, &twice)) {
+ return fail(err, errSize, PKG_ERR_NO_MEMORY);
+ }
+
+ if (twice) {
+ return fail(err, errSize, "%s is listed twice.", twice);
}
for (int32_t i = 0; i < (int32_t)arrlen(m->depends); i++) {
@@ -621,6 +662,28 @@ bool pkgManifestCheck(const PkgManifestT *m, char *err, int32_t errSize) {
}
+bool pkgManifestCopy(PkgManifestT *dst, const PkgManifestT *src) {
+ *dst = *src;
+ dst->files = NULL;
+ dst->depends = NULL;
+
+ for (int32_t i = 0; i < (int32_t)arrlen(src->depends); i++) {
+ arrput(dst->depends, src->depends[i]);
+ }
+
+ for (int32_t i = 0; i < (int32_t)arrlen(src->files); i++) {
+ const PkgFileT *f = &src->files[i];
+
+ if (!pkgManifestAddFile(dst, f->name, f->size, f->sha256)) {
+ pkgManifestFree(dst);
+ return false;
+ }
+ }
+
+ return true;
+}
+
+
const PkgFileT *pkgManifestFindFile(const PkgManifestT *m, const char *name) {
for (int32_t i = 0; i < (int32_t)arrlen(m->files); i++) {
if (strcasecmp(m->files[i].name, name) == 0) {
@@ -688,7 +751,7 @@ char *pkgManifestFormat(const PkgManifestT *m, int32_t *outLen) {
}
for (int32_t i = 0; i < (int32_t)arrlen(m->files); i++) {
- char hex[SHA_HEX_SIZE];
+ char hex[PKG_SHA256_TEXT];
pkgHexEncode(m->files[i].sha256, PKG_SHA256_SIZE, hex);
textAdd(&t, "%s = %" PRIu32 " %s" EOL, m->files[i].name, m->files[i].size, hex);
diff --git a/src/libs/kpunch/pkg/pkgManifest.h b/src/libs/kpunch/pkg/pkgManifest.h
index c5e6c326..04e28216 100644
--- a/src/libs/kpunch/pkg/pkgManifest.h
+++ b/src/libs/kpunch/pkg/pkgManifest.h
@@ -78,6 +78,7 @@
#define PKG_TEXT_MAX 128 // name, summary, publisher, ...
#define PKG_VERSION_MAX 18 // "65535.65535.65535" and NUL
#define PKG_SHA256_SIZE 32
+#define PKG_SHA256_TEXT (PKG_SHA256_SIZE * PKG_HEX_PER_BYTE + 1) // a SHA-256 as hex, with the NUL
#define PKG_SYSTEM_ID "kpunch.dvx" // the one `system` package: DVX itself
#define PKG_BASE_FONTS_ID "kpunch.basefont" // DVX's base set of fonts (src/fonts/PACKAGE.INI)
#define PKG_DVX_NAME "DVX" // its manifest (pkgDvxManifest)
@@ -151,6 +152,10 @@ bool pkgManifestAddFile(PkgManifestT *m, const char *name, uint32_t size,
// False with the reason in err.
bool pkgManifestCheck(const PkgManifestT *m, char *err, int32_t errSize);
+// dst as a copy of src, with its own files and dependencies. False (dst
+// freed) when out of memory.
+bool pkgManifestCopy(PkgManifestT *dst, const PkgManifestT *src);
+
// The file named name (any case), or NULL.
const PkgFileT *pkgManifestFindFile(const PkgManifestT *m, const char *name);
diff --git a/src/libs/kpunch/pkg/pkgOp.c b/src/libs/kpunch/pkg/pkgOp.c
index ca7029b9..78c671a4 100644
--- a/src/libs/kpunch/pkg/pkgOp.c
+++ b/src/libs/kpunch/pkg/pkgOp.c
@@ -25,10 +25,11 @@
// (pkgOp.h)
#include "pkgOp.h"
+#include "pkgInt.h"
#include "pkgPlan.h"
#include "pkgZip.h"
#include "dvxPlat.h"
-#include "serial/security/secHash.h"
+#include "crypto/secHash.h"
#include "stb_ds_wrap.h"
#include
@@ -44,6 +45,7 @@
#define OP_FILES_MAX 99999 // what those names can number
#define OP_NAME_MAX 16 // "PKG00000.$$$" and NUL
#define OP_KB 1024
+#define OP_SWAP_STEP 8 // files swapped in, put back, or cleared a step
typedef enum {
OpInstallE,
@@ -53,6 +55,8 @@ typedef enum {
typedef enum {
StageUnpackE,
StageSwapE,
+ StageUndoE, // the swap failed (msg says why): every file as it was
+ StageAsideE, // swapped in: the files it replaced go
StageDropE, // install: the old version's leftovers; remove: the package's files
StageTidyE,
StageRecordE,
@@ -78,6 +82,9 @@ struct PkgOpT {
SecSha256T outSha;
uint32_t outSize;
bool *scratch; // per manifest file: its scratch file is there
+ bool *aside; // per manifest file: the file it replaces is set aside
+ bool *placed; // per manifest file: it is in place
+ int32_t swapAt; // the manifest file the swap (or its undoing or clearing) is on
PkgFileT *drop; // files to check and delete (stb_ds; names owned)
int32_t dropAt;
FILE *hashFile; // the one being checked
@@ -99,24 +106,27 @@ static void addDirs(PkgOpT *op, const char *name);
static int compareDirs(const void *a, const void *b);
static bool destPath(const PkgOpT *op, const char *name, char *out, int32_t outSize);
static bool dropAdd(PkgOpT *op, const PkgFileT *file);
-static int32_t fileRead(void *ctx, uint8_t *buf, int32_t len);
static PkgOpStateE opFail(PkgOpT *op, const char *fmt, ...) __attribute__((format(printf, 2, 3)));
static PkgOpT *opNew(PkgDbT *db, OpKindE kind);
void pkgOpFree(PkgOpT *op);
-PkgOpT *pkgOpInstall(PkgDbT *db, const char *path, const PkgKeyringT *ring, bool explicitly);
+PkgOpT *pkgOpInstall(PkgDbT *db, const PkgOfferT *offer, bool explicitly);
+PkgOpT *pkgOpInstallFile(PkgDbT *db, const char *path, const PkgKeyringT *ring, bool explicitly);
char *const *pkgOpKept(const PkgOpT *op);
const char *pkgOpMessage(const PkgOpT *op);
void pkgOpProgress(const PkgOpT *op, uint32_t *done, uint32_t *total);
PkgOpT *pkgOpRemove(PkgDbT *db, const char *id, bool removeChanged);
PkgOpStateE pkgOpStep(PkgOpT *op);
+void pkgOpStop(PkgOpT *op);
static bool scratchPath(const PkgOpT *op, int32_t index, const char *fmt, char *out, int32_t outSize);
static bool scratchWrite(void *ctx, const uint8_t *data, int32_t len);
+static PkgOpStateE stepAside(PkgOpT *op);
static PkgOpStateE stepDrop(PkgOpT *op);
static PkgOpStateE stepRecord(PkgOpT *op);
static PkgOpStateE stepSwap(PkgOpT *op);
+static PkgOpStateE stepUndo(PkgOpT *op);
static PkgOpStateE stepUnpack(PkgOpT *op);
static void tidy(PkgOpT *op);
-static const char *zipReason(const PkgZipT *zip);
+static PkgOpStateE undoBegin(PkgOpT *op, const char *fmt, ...) __attribute__((format(printf, 2, 3)));
// The folders of name from its own down to its vendor's folder (the first
@@ -196,14 +206,6 @@ static bool dropAdd(PkgOpT *op, const PkgFileT *file) {
}
-static int32_t fileRead(void *ctx, uint8_t *buf, int32_t len) {
- FILE *f = (FILE *)ctx;
- size_t n = fread(buf, 1, (size_t)len, f);
-
- return (n == 0 && ferror(f)) ? -1 : (int32_t)n;
-}
-
-
// Stop: nothing of a package that was not swapped in is left (its scratch
// files, and the folders they leave empty).
static PkgOpStateE opFail(PkgOpT *op, const char *fmt, ...) {
@@ -274,11 +276,7 @@ void pkgOpFree(PkgOpT *op) {
return;
}
- // Stopped while unpacking: nothing changed. Once swapped in, the
- // rest is done, so no package is left half installed.
- if (op->stage == StageUnpackE) {
- opFail(op, "Stopped.");
- }
+ pkgOpStop(op);
while (pkgOpStep(op) == PkgOpRunningE) {
}
@@ -300,14 +298,15 @@ void pkgOpFree(PkgOpT *op) {
arrfree(op->dirs);
pkgManifestFree(&op->manifest);
free(op->scratch);
+ free(op->aside);
+ free(op->placed);
free(op->buf);
free(op);
}
-PkgOpT *pkgOpInstall(PkgDbT *db, const char *path, const PkgKeyringT *ring, bool explicitly) {
+PkgOpT *pkgOpInstall(PkgDbT *db, const PkgOfferT *offer, bool explicitly) {
PkgOpT *op = opNew(db, OpInstallE);
- PkgOfferT offer;
const PkgInstalledT *old;
int32_t count;
uint64_t diskTotal = 0;
@@ -317,26 +316,28 @@ PkgOpT *pkgOpInstall(PkgDbT *db, const char *path, const PkgKeyringT *ring, bool
return NULL;
}
- if (!pkgOfferRead(path, ring, &offer, op->msg, sizeof(op->msg))) {
- op->stage = StageFailedE;
+ pkgManifestFree(&op->manifest);
+
+ if (!pkgManifestCopy(&op->manifest, &offer->manifest)) {
+ opFail(op, PKG_ERR_NO_MEMORY);
return op;
}
- pkgManifestFree(&op->manifest);
- op->manifest = offer.manifest;
op->explicitly = explicitly;
- op->hasSigner = offer.hasSigner;
- memcpy(op->signer, offer.signer, PKG_KEY_ID_SIZE);
+ op->hasSigner = offer->hasSigner;
+ memcpy(op->signer, offer->signer, PKG_KEY_ID_SIZE);
snprintf(op->id, sizeof(op->id), "%s", op->manifest.id);
count = (int32_t)arrlen(op->manifest.files);
op->scratch = (bool *)calloc((size_t)count + 1, sizeof(bool));
+ op->aside = (bool *)calloc((size_t)count + 1, sizeof(bool));
+ op->placed = (bool *)calloc((size_t)count + 1, sizeof(bool));
- if (!op->scratch) {
- opFail(op, "There is not enough memory.");
+ if (!op->scratch || !op->aside || !op->placed) {
+ opFail(op, PKG_ERR_NO_MEMORY);
return op;
}
- if (pkgTrustIsDvxId(op->id) && offer.trust != PkgTrustSystemE) {
+ if (pkgTrustIsDvxId(op->id) && offer->trust != PkgTrustSystemE) {
opFail(op, "%s says it is part of DVX, but it is not signed by DVX's key.", op->manifest.name);
return op;
}
@@ -358,7 +359,7 @@ PkgOpT *pkgOpInstall(PkgDbT *db, const char *path, const PkgKeyringT *ring, bool
for (int32_t i = 0; i < (int32_t)arrlen(old->manifest.files); i++) {
if (!pkgManifestFindFile(&op->manifest, old->manifest.files[i].name) && !dropAdd(op, &old->manifest.files[i])) {
- opFail(op, "There is not enough memory.");
+ opFail(op, PKG_ERR_NO_MEMORY);
return op;
}
}
@@ -370,11 +371,11 @@ PkgOpT *pkgOpInstall(PkgDbT *db, const char *path, const PkgKeyringT *ring, bool
return op;
}
- op->zipFile = fopen(path, "rb");
- op->zip = op->zipFile ? pkgZipOpen(fileRead, op->zipFile) : NULL;
+ op->zipFile = fopen(offer->where, "rb");
+ op->zip = op->zipFile ? pkgZipOpen(pkgZipFileRead, op->zipFile) : NULL;
if (!op->zip) {
- opFail(op, "Cannot read %s.", path);
+ opFail(op, "Cannot read %s.", offer->where);
return op;
}
@@ -383,6 +384,27 @@ PkgOpT *pkgOpInstall(PkgDbT *db, const char *path, const PkgKeyringT *ring, bool
}
+PkgOpT *pkgOpInstallFile(PkgDbT *db, const char *path, const PkgKeyringT *ring, bool explicitly) {
+ PkgOfferT offer;
+ char err[PKG_ERR_MAX];
+ PkgOpT *op;
+
+ if (pkgOfferRead(path, ring, &offer, err, sizeof(err))) {
+ op = pkgOpInstall(db, &offer, explicitly);
+ pkgOfferFree(&offer);
+ return op;
+ }
+
+ op = opNew(db, OpInstallE);
+
+ if (op) {
+ opFail(op, "%s", err);
+ }
+
+ return op;
+}
+
+
char *const *pkgOpKept(const PkgOpT *op) {
return op->kept;
}
@@ -422,7 +444,7 @@ PkgOpT *pkgOpRemove(PkgDbT *db, const char *id, bool removeChanged) {
for (int32_t i = 0; i < (int32_t)arrlen(rec->manifest.files); i++) {
if (!dropAdd(op, &rec->manifest.files[i])) {
- opFail(op, "There is not enough memory.");
+ opFail(op, PKG_ERR_NO_MEMORY);
return op;
}
}
@@ -440,6 +462,12 @@ PkgOpStateE pkgOpStep(PkgOpT *op) {
case StageSwapE:
return stepSwap(op);
+ case StageUndoE:
+ return stepUndo(op);
+
+ case StageAsideE:
+ return stepAside(op);
+
case StageDropE:
return stepDrop(op);
@@ -460,6 +488,15 @@ PkgOpStateE pkgOpStep(PkgOpT *op) {
}
+// Stopped while unpacking: nothing changed. Once swapped in, the rest is
+// done, so no package is left half installed.
+void pkgOpStop(PkgOpT *op) {
+ if (op->stage == StageUnpackE) {
+ opFail(op, "Stopped.");
+ }
+}
+
+
// The scratch (or set-aside) name of the manifest's file index, in the
// folder it goes to.
static bool scratchPath(const PkgOpT *op, int32_t index, const char *fmt, char *out, int32_t outSize) {
@@ -481,6 +518,28 @@ static bool scratchWrite(void *ctx, const uint8_t *data, int32_t len) {
}
+// Swapped in: the files it replaced, set aside until now, go,
+// OP_SWAP_STEP at a time.
+static PkgOpStateE stepAside(PkgOpT *op) {
+ int32_t count = (int32_t)arrlen(op->manifest.files);
+ int32_t end = op->swapAt + OP_SWAP_STEP;
+
+ for (; op->swapAt < count && op->swapAt < end; op->swapAt++) {
+ char bak[DVX_MAX_PATH];
+
+ if (op->aside[op->swapAt] && scratchPath(op, op->swapAt, OP_ASIDE_FMT, bak, sizeof(bak))) {
+ remove(bak);
+ }
+ }
+
+ if (op->swapAt == count) {
+ op->stage = StageDropE;
+ }
+
+ return PkgOpRunningE;
+}
+
+
// Check one file to delete a step at a time: unchanged (or all are to go)
// it is deleted; changed it is kept. A file another package has is left.
static PkgOpStateE stepDrop(PkgOpT *op) {
@@ -577,97 +636,93 @@ static PkgOpStateE stepRecord(PkgOpT *op) {
}
op->stage = StageDoneE;
+ platformInstalledChanged();
return PkgOpDoneE;
}
-// Every unpacked file in place at once: the files they replace go aside
-// first, and come back if a swap fails.
+// Swap the unpacked files in, OP_SWAP_STEP at a time: the file each
+// replaces set aside, then it is put in its place. Should one fail, every
+// file goes back as it was (stepUndo).
static PkgOpStateE stepSwap(PkgOpT *op) {
- int32_t count = (int32_t)arrlen(op->manifest.files);
- bool *aside = (bool *)calloc((size_t)count + 1, sizeof(bool));
- bool *placed = (bool *)calloc((size_t)count + 1, sizeof(bool));
- char failed[PKG_ERR_MAX];
- bool ok = aside && placed;
+ int32_t count = (int32_t)arrlen(op->manifest.files);
+ int32_t end = op->swapAt + OP_SWAP_STEP;
- snprintf(failed, sizeof(failed), "There is not enough memory.");
-
- for (int32_t i = 0; ok && i < count; i++) {
+ for (; op->swapAt < count && op->swapAt < end; op->swapAt++) {
+ int32_t i = op->swapAt;
+ const char *name = op->manifest.files[i].name;
char dest[DVX_MAX_PATH];
char scratch[DVX_MAX_PATH];
char bak[DVX_MAX_PATH];
struct stat st;
- if (!destPath(op, op->manifest.files[i].name, dest, sizeof(dest)) || !scratchPath(op, i, OP_SCRATCH_FMT, scratch, sizeof(scratch)) || !scratchPath(op, i, OP_ASIDE_FMT, bak, sizeof(bak))) {
- snprintf(failed, sizeof(failed), "The path of %s is too long.", op->manifest.files[i].name);
- ok = false;
- break;
+ if (!destPath(op, name, dest, sizeof(dest)) || !scratchPath(op, i, OP_SCRATCH_FMT, scratch, sizeof(scratch)) || !scratchPath(op, i, OP_ASIDE_FMT, bak, sizeof(bak))) {
+ return undoBegin(op, "The path of %s is too long.", name);
}
if (stat(dest, &st) == 0) {
if (!S_ISREG(st.st_mode)) {
- snprintf(failed, sizeof(failed), "%s is a folder.", dest);
- ok = false;
- break;
+ return undoBegin(op, "%s is a folder.", dest);
}
remove(bak);
if (rename(dest, bak) != 0) {
- snprintf(failed, sizeof(failed), "Cannot move %s aside.", dest);
- ok = false;
- break;
+ return undoBegin(op, "Cannot move %s aside.", dest);
}
- aside[i] = true;
+ op->aside[i] = true;
}
if (rename(scratch, dest) != 0) {
- snprintf(failed, sizeof(failed), "Cannot put %s in place.", dest);
- ok = false;
- break;
+ return undoBegin(op, "Cannot put %s in place.", dest);
}
- placed[i] = true;
+ op->placed[i] = true;
op->scratch[i] = false;
}
- // Undo: the new files out, the old ones back
- for (int32_t i = 0; !ok && aside && placed && i < count; i++) {
- char dest[DVX_MAX_PATH];
- char bak[DVX_MAX_PATH];
+ if (op->swapAt == count) {
+ op->swapAt = 0;
+ op->stage = StageAsideE;
+ }
+
+ return PkgOpRunningE;
+}
+
+
+// The swap failed (op->msg says why): the new files out and the old ones
+// back, OP_SWAP_STEP at a time; then the operation fails.
+static PkgOpStateE stepUndo(PkgOpT *op) {
+ int32_t count = (int32_t)arrlen(op->manifest.files);
+ int32_t end = op->swapAt + OP_SWAP_STEP;
+
+ for (; op->swapAt < count && op->swapAt < end; op->swapAt++) {
+ int32_t i = op->swapAt;
+ char dest[DVX_MAX_PATH];
+ char bak[DVX_MAX_PATH];
if (!destPath(op, op->manifest.files[i].name, dest, sizeof(dest)) || !scratchPath(op, i, OP_ASIDE_FMT, bak, sizeof(bak))) {
continue;
}
- if (placed[i]) {
+ if (op->placed[i]) {
remove(dest);
}
- if (aside[i]) {
+ if (op->aside[i]) {
rename(bak, dest);
}
}
- // Done: the old files are not needed now
- for (int32_t i = 0; ok && i < count; i++) {
- char bak[DVX_MAX_PATH];
-
- if (aside[i] && scratchPath(op, i, OP_ASIDE_FMT, bak, sizeof(bak))) {
- remove(bak);
- }
+ if (op->swapAt < count) {
+ return PkgOpRunningE;
}
- free(aside);
- free(placed);
+ char why[PKG_ERR_MAX];
- if (!ok) {
- return opFail(op, "%s", failed);
- }
-
- op->stage = StageDropE;
- return PkgOpRunningE;
+ snprintf(why, sizeof(why), "%s", op->msg);
+ return opFail(op, "%s", why);
}
@@ -691,7 +746,7 @@ static PkgOpStateE stepUnpack(PkgOpT *op) {
op->out = NULL;
if (st != PkgZipFileDoneE) {
- return opFail(op, "%s: %s", file->name, zipReason(op->zip));
+ return opFail(op, "%s: %s", file->name, pkgZipProblemText(op->zip));
}
secSha256Final(&op->outSha, sha);
@@ -727,7 +782,7 @@ static PkgOpStateE stepUnpack(PkgOpT *op) {
}
if (st != PkgZipEntryE) {
- return opFail(op, "The package: %s", zipReason(op->zip));
+ return opFail(op, "The package: %s", pkgZipProblemText(op->zip));
}
if (e.isDir) {
@@ -736,7 +791,7 @@ static PkgOpStateE stepUnpack(PkgOpT *op) {
// The manifest and signature: read past
if (strcasecmp(e.name, PKG_MANIFEST_NAME) == 0 || strcasecmp(e.name, PKG_SIG_NAME) == 0) {
- return pkgZipData(op->zip, NULL, NULL, INT32_MAX) == PkgZipFileDoneE ? PkgOpRunningE : opFail(op, "The package: %s", zipReason(op->zip));
+ return pkgZipData(op->zip, NULL, NULL, INT32_MAX) == PkgZipFileDoneE ? PkgOpRunningE : opFail(op, "The package: %s", pkgZipProblemText(op->zip));
}
const PkgFileT *file = pkgManifestFindFile(&op->manifest, e.name);
@@ -790,18 +845,15 @@ static void tidy(PkgOpT *op) {
}
-static const char *zipReason(const PkgZipT *zip) {
- switch (pkgZipProblem(zip)) {
- case PkgZipReadFailedE:
- return "it cannot be read.";
+// The swap failed, saying why: put everything back (stepUndo) from the
+// first file.
+static PkgOpStateE undoBegin(PkgOpT *op, const char *fmt, ...) {
+ va_list ap;
- case PkgZipWriteFailedE:
- return "it cannot be written (is the disk full?).";
-
- case PkgZipUnreadableE:
- return "it uses a form DVX cannot read.";
-
- default:
- return "it is damaged.";
- }
+ va_start(ap, fmt);
+ vsnprintf(op->msg, sizeof(op->msg), fmt, ap);
+ va_end(ap);
+ op->swapAt = 0;
+ op->stage = StageUndoE;
+ return PkgOpRunningE;
}
diff --git a/src/libs/kpunch/pkg/pkgOp.h b/src/libs/kpunch/pkg/pkgOp.h
index aa1e8f7a..579580de 100644
--- a/src/libs/kpunch/pkg/pkgOp.h
+++ b/src/libs/kpunch/pkg/pkgOp.h
@@ -43,12 +43,13 @@
#define PKG_OP_H
#include "pkgDb.h"
+#include "pkgPlan.h"
#include "pkgTrust.h"
#include
#include
-#define PKG_OP_STEP 32768 // bytes unpacked or checked a step
+#define PKG_OP_STEP 16384 // bytes unpacked or checked a step
typedef enum {
PkgOpRunningE,
@@ -62,10 +63,17 @@ typedef struct PkgOpT PkgOpT;
// after, the rest of it is done first.
void pkgOpFree(PkgOpT *op);
-// Install the package at path into db's DVX folder, trusted as ring says
-// (a kpunch.* package must be signed by a SYSTEM key). explicitly: asked
-// for (an upgrade keeps a package that was). NULL when out of memory.
-PkgOpT *pkgOpInstall(PkgDbT *db, const char *path, const PkgKeyringT *ring, bool explicitly);
+// Install offer (pkgOfferRead: its manifest, and the trust checked then;
+// a kpunch.* package must be signed by a SYSTEM key) into db's DVX
+// folder; each file unpacked from offer->where is checked against that
+// manifest. explicitly: asked for (an upgrade keeps a package that
+// was). NULL when out of memory.
+PkgOpT *pkgOpInstall(PkgDbT *db, const PkgOfferT *offer, bool explicitly);
+
+// The same from the package at path, read (pkgOfferRead) and trusted as
+// ring says now (for an offer read before ring was). The op fails at
+// once, saying why, when it cannot be read. NULL when out of memory.
+PkgOpT *pkgOpInstallFile(PkgDbT *db, const char *path, const PkgKeyringT *ring, bool explicitly);
// The files a removal or upgrade left because they changed since they
// were installed (relative names, an stb_ds array owned by the op).
@@ -84,4 +92,8 @@ PkgOpT *pkgOpRemove(PkgDbT *db, const char *id, bool removeChanged);
// One step: PkgOpRunningE until it is done or has failed.
PkgOpStateE pkgOpStep(PkgOpT *op);
+// Ask op to stop: before its files are swapped in it fails at once
+// (nothing changed); after, its steps run on to the end.
+void pkgOpStop(PkgOpT *op);
+
#endif // PKG_OP_H
diff --git a/src/libs/kpunch/pkg/pkgPlan.c b/src/libs/kpunch/pkg/pkgPlan.c
index 2d8aabdc..393c1c2c 100644
--- a/src/libs/kpunch/pkg/pkgPlan.c
+++ b/src/libs/kpunch/pkg/pkgPlan.c
@@ -24,6 +24,7 @@
// pkgPlan.c -- planning installs and removals (pkgPlan.h)
#include "pkgPlan.h"
+#include "pkgInt.h"
#include "dvxTypes.h"
#include "stb_ds_wrap.h"
@@ -33,6 +34,8 @@
#include
#include
+#define PERCENT_MAX 100
+
typedef enum {
VisitNoneE,
VisitActiveE, // its dependencies are being planned
@@ -66,6 +69,10 @@ static bool needs(const PkgManifestT *m, const char *id);
static bool offerHasFile(const PkgOfferT *offer, const char *name);
void pkgOfferFree(PkgOfferT *offer);
bool pkgOfferRead(const char *path, const PkgKeyringT *ring, PkgOfferT *offer, char *err, int32_t errSize);
+void pkgOfferScanFree(PkgOfferScanT *scan);
+int32_t pkgOfferScanPercent(const PkgOfferScanT *scan);
+void pkgOfferScanStart(PkgOfferScanT *scan, const char *dir, const char *first, const PkgKeyringT *ring);
+PkgScanStepE pkgOfferScanStep(PkgOfferScanT *scan, char *err, int32_t errSize);
PkgOfferT *pkgOffersFor(const char *path, const PkgKeyringT *ring, char *err, int32_t errSize);
void pkgOffersFree(PkgOfferT *offers);
PkgOfferT *pkgOffersInFolder(const char *dir, const PkgKeyringT *ring);
@@ -74,6 +81,7 @@ void pkgPlanFree(PkgPlanT *plan);
bool pkgPlanInstall(const PkgDbT *db, const PkgOfferT *offers, int32_t offerCount, const int32_t *wanted, int32_t wantedCount, PkgPlanT *plan);
const PkgInstalledT **pkgPlanOrphans(const PkgDbT *db, const char *const *ids, int32_t count);
static void planStale(PlanT *p);
+static PkgOfferT *scanAll(const char *dir, const char *first, const PkgKeyringT *ring, char *err, int32_t errSize);
static bool visitOffer(PlanT *p, int32_t i, bool explicitly);
@@ -307,35 +315,88 @@ bool pkgOfferRead(const char *path, const PkgKeyringT *ring, PkgOfferT *offer, c
}
-PkgOfferT *pkgOffersFor(const char *path, const PkgKeyringT *ring, char *err, int32_t errSize) {
- PkgOfferT mine;
- PkgOfferT *offers = NULL;
- PkgOfferT *others;
- char dir[DVX_MAX_PATH];
+void pkgOfferScanFree(PkgOfferScanT *scan) {
+ pkgOffersFree(scan->offers);
+ dvxReadDirFree(scan->names);
+ free(scan->dir);
+ free(scan->first);
+ free(scan->skip);
+ memset(scan, 0, sizeof(*scan));
+}
- if (!pkgOfferRead(path, ring, &mine, err, errSize)) {
- return NULL;
+
+// By the folder's names gone through (the first package counts as none).
+int32_t pkgOfferScanPercent(const PkgOfferScanT *scan) {
+ int32_t count = (int32_t)arrlen(scan->names);
+
+ if (count == 0) {
+ return scan->first ? 0 : PERCENT_MAX;
}
- arrput(offers, mine);
+ return scan->at * PERCENT_MAX / count;
+}
- if (!platformPathDirName(path, dir, sizeof(dir))) {
- return offers;
+
+void pkgOfferScanStart(PkgOfferScanT *scan, const char *dir, const char *first, const PkgKeyringT *ring) {
+ memset(scan, 0, sizeof(*scan));
+ scan->ring = ring;
+
+ if (dir && dir[0]) {
+ scan->dir = strdup(dir);
+ scan->names = scan->dir ? dvxReadDir(dir) : NULL;
}
- // The folder's others (path itself is there too: once is enough)
- others = pkgOffersInFolder(dir, ring);
+ if (first) {
+ scan->first = strdup(first);
+ scan->skip = strdup(platformPathBaseName(first));
+ }
+}
- for (int32_t i = 0; i < (int32_t)arrlen(others); i++) {
- if (strcasecmp(platformPathBaseName(others[i].where), platformPathBaseName(path)) == 0) {
- pkgOfferFree(&others[i]);
- } else {
- arrput(offers, others[i]);
+
+PkgScanStepE pkgOfferScanStep(PkgOfferScanT *scan, char *err, int32_t errSize) {
+ PkgOfferT offer;
+
+ if (scan->first) {
+ snprintf(scan->current, sizeof(scan->current), "%s", platformPathBaseName(scan->first));
+
+ if (!pkgOfferRead(scan->first, scan->ring, &offer, err, errSize)) {
+ return PkgScanFailedE;
}
+
+ arrput(scan->offers, offer);
+ free(scan->first);
+ scan->first = NULL;
+ return PkgScanMoreE;
}
- arrfree(others);
- return offers;
+ while (scan->at < (int32_t)arrlen(scan->names)) {
+ const char *name = scan->names[scan->at++];
+ char path[DVX_MAX_PATH];
+ char why[PKG_ERR_MAX];
+
+ if (!dvxHasExt(name, PKG_EXT) || (scan->skip && strcasecmp(name, scan->skip) == 0) || !platformPathJoin(path, sizeof(path), scan->dir, name)) {
+ continue;
+ }
+
+ snprintf(scan->current, sizeof(scan->current), "%s", name);
+
+ if (pkgOfferRead(path, scan->ring, &offer, why, sizeof(why))) {
+ arrput(scan->offers, offer);
+ } else {
+ dvxLog("Packages: %s skipped: %s", path, why);
+ }
+
+ return PkgScanMoreE;
+ }
+
+ return PkgScanDoneE;
+}
+
+
+PkgOfferT *pkgOffersFor(const char *path, const PkgKeyringT *ring, char *err, int32_t errSize) {
+ char dir[DVX_MAX_PATH];
+
+ return scanAll(platformPathDirName(path, dir, sizeof(dir)) ? dir : NULL, path, ring, err, errSize);
}
@@ -349,27 +410,9 @@ void pkgOffersFree(PkgOfferT *offers) {
PkgOfferT *pkgOffersInFolder(const char *dir, const PkgKeyringT *ring) {
- PkgOfferT *offers = NULL;
- char **names = dvxReadDir(dir);
+ char err[PKG_ERR_MAX];
- for (int32_t i = 0; i < (int32_t)arrlen(names); i++) {
- char path[DVX_MAX_PATH];
- char err[PKG_ERR_MAX];
- PkgOfferT offer;
-
- if (!dvxHasExt(names[i], PKG_EXT) || !platformPathJoin(path, sizeof(path), dir, names[i])) {
- continue;
- }
-
- if (pkgOfferRead(path, ring, &offer, err, sizeof(err))) {
- arrput(offers, offer);
- } else {
- dvxLog("Packages: %s skipped: %s", path, err);
- }
- }
-
- dvxReadDirFree(names);
- return offers;
+ return scanAll(dir, NULL, ring, err, sizeof(err));
}
@@ -438,7 +481,7 @@ bool pkgPlanInstall(const PkgDbT *db, const PkgOfferT *offers, int32_t offerCoun
p.dvx.patch = DVX_VERSION_PATCH;
if (!p.visit) {
- return fail(&p, "There is not enough memory.");
+ return fail(&p, PKG_ERR_NO_MEMORY);
}
if (dvx) {
@@ -509,8 +552,6 @@ const PkgInstalledT **pkgPlanOrphans(const PkgDbT *db, const char *const *ids, i
}
-// After a DVX update to another ABI: the installed code it does not
-// replace that will not load.
static void planStale(PlanT *p) {
for (int32_t i = 0; p->abi != DVX_ABI_VERSION && i < (int32_t)arrlen(p->db->pkgs); i++) {
const PkgInstalledT *pkg = &p->db->pkgs[i];
@@ -522,6 +563,30 @@ static void planStale(PlanT *p) {
}
+// After a DVX update to another ABI: the installed code it does not
+// replace that will not load.
+// A scan run to its end at once: its offers, or NULL with the reason in
+// err when first cannot be read.
+static PkgOfferT *scanAll(const char *dir, const char *first, const PkgKeyringT *ring, char *err, int32_t errSize) {
+ PkgOfferScanT scan;
+ PkgScanStepE step;
+ PkgOfferT *offers = NULL;
+
+ pkgOfferScanStart(&scan, dir, first, ring);
+
+ while ((step = pkgOfferScanStep(&scan, err, errSize)) == PkgScanMoreE) {
+ }
+
+ if (step == PkgScanDoneE) {
+ offers = scan.offers;
+ scan.offers = NULL;
+ }
+
+ pkgOfferScanFree(&scan);
+ return offers;
+}
+
+
// Plan offer i after everything it needs that is not installed.
static bool visitOffer(PlanT *p, int32_t i, bool explicitly) {
const PkgManifestT *m = &p->offers[i].manifest;
diff --git a/src/libs/kpunch/pkg/pkgPlan.h b/src/libs/kpunch/pkg/pkgPlan.h
index 6bfc1b4f..3a750f53 100644
--- a/src/libs/kpunch/pkg/pkgPlan.h
+++ b/src/libs/kpunch/pkg/pkgPlan.h
@@ -62,6 +62,26 @@ typedef enum {
PkgActReinstallE // the same version again (a repair)
} PkgActE;
+// Reading packages as offers one at a time (pkgOfferScanStart), so a
+// program can draw progress between them: checking a signature is slow
+// on a 486
+typedef struct {
+ PkgOfferT *offers; // stb_ds array: those read so far (first's first)
+ char **names; // the folder's files (dvxReadDir)
+ const PkgKeyringT *ring;
+ char *dir;
+ char *first; // read first (NULL once it is, or for none)
+ char *skip; // its name: not read again from the folder
+ int32_t at; // the next of names
+ char current[DVX_MAX_PATH]; // the name of the file read last
+} PkgOfferScanT;
+
+typedef enum {
+ PkgScanMoreE,
+ PkgScanDoneE,
+ PkgScanFailedE // the first package cannot be read
+} PkgScanStepE;
+
typedef struct {
PkgActE act;
int32_t offer; // index in the offers
@@ -83,15 +103,34 @@ void pkgOfferFree(PkgOfferT *offer);
bool pkgOfferRead(const char *path, const PkgKeyringT *ring, PkgOfferT *offer, char *err, int32_t errSize);
// The package at path as the first offer, then every other package in
-// its folder (what it may need): an stb_ds array (pkgOffersFree), or NULL
-// with the reason in err when path itself cannot be read.
+// its folder (what it may need), all at once (pkgOfferScanStart): an
+// stb_ds array (pkgOffersFree), or NULL with the reason in err when path
+// itself cannot be read.
PkgOfferT *pkgOffersFor(const char *path, const PkgKeyringT *ring, char *err, int32_t errSize);
+// Free what scan holds: its offers too, unless taken (scan->offers set
+// NULL).
+void pkgOfferScanFree(PkgOfferScanT *scan);
+
+// How far scan is, 0 to 100.
+int32_t pkgOfferScanPercent(const PkgOfferScanT *scan);
+
+// Start reading every package (*.DPK) in dir as offers (none when dir is
+// NULL or ""), the one at first before them when first is not NULL (offer
+// 0; not read again from dir), trusted as ring says (it must stay while
+// the scan runs). Free it with pkgOfferScanFree.
+void pkgOfferScanStart(PkgOfferScanT *scan, const char *dir, const char *first, const PkgKeyringT *ring);
+
+// Read the next package: PkgScanMoreE, PkgScanDoneE, or PkgScanFailedE
+// (first cannot be read: the reason in err). A package of the folder
+// that does not read is passed over (and logged).
+PkgScanStepE pkgOfferScanStep(PkgOfferScanT *scan, char *err, int32_t errSize);
+
// Free an stb_ds array of offers and each offer in it.
void pkgOffersFree(PkgOfferT *offers);
-// Every package (*.DPK) in dir that reads, as offers: an stb_ds array
-// (pkgOffersFree).
+// Every package (*.DPK) in dir that reads, as offers, all at once: an
+// stb_ds array (pkgOffersFree).
PkgOfferT *pkgOffersInFolder(const char *dir, const PkgKeyringT *ring);
// The installed packages that need any of ids (count of them), directly
diff --git a/src/libs/kpunch/pkg/pkgSign.c b/src/libs/kpunch/pkg/pkgSign.c
index 8f3ee20e..ccd151a4 100644
--- a/src/libs/kpunch/pkg/pkgSign.c
+++ b/src/libs/kpunch/pkg/pkgSign.c
@@ -28,10 +28,9 @@
#include "pkgInt.h"
#include "dvxTypes.h"
#include "dvxPlat.h"
-#include "monocypher.h"
-#include "optional/monocypher-ed25519.h"
-#include "serial/security/secHash.h"
-#include "serial/security/security.h"
+#include "crypto/secCurve.h"
+#include "crypto/secHash.h"
+#include "crypto/security.h"
#include
#include
@@ -48,7 +47,6 @@
#define KEY_SEED "seed"
#define KEY_KEY "key"
#define KEY_SIG "sig"
-#define EXPANDED_SIZE 64 // Monocypher's secret key: the seed and the public key
#define HEX_PER_BYTE 2
#define TEXT_SLACK 96 // the brackets, keys, ' = ', and line ends
#define TEXT_MAX (PKG_KEY_NAME_MAX + PKG_SIG_SIZE * HEX_PER_BYTE + TEXT_SLACK)
@@ -69,11 +67,10 @@ typedef struct {
// ============================================================
static bool fail(char *err, int32_t errSize, const char *fmt, ...) __attribute__((format(printf, 3, 4)));
-static void idOf(const uint8_t publicKey[PKG_PUBLIC_SIZE], uint8_t id[PKG_KEY_ID_SIZE]);
static bool parseFields(const char *text, int32_t len, const char *section, FieldT *fields, int32_t count, char *err, int32_t errSize);
char *pkgKeyFormat(const PkgKeyT *key, int32_t *outLen);
+void pkgKeyIdOf(const uint8_t publicKey[PKG_PUBLIC_SIZE], uint8_t id[PKG_KEY_ID_SIZE]);
void pkgKeyIdText(const uint8_t id[PKG_KEY_ID_SIZE], char *out);
-void pkgKeyMake(const uint8_t seed[PKG_SEED_SIZE], const char *name, PkgKeyT *key, PkgSecretT *secret);
bool pkgKeyParse(const char *text, int32_t len, PkgKeyT *key, char *err, int32_t errSize);
bool pkgKeyRead(const char *path, PkgKeyT *key, char *err, int32_t errSize);
bool pkgKeyWrite(const uint8_t seed[PKG_SEED_SIZE], const char *name, const char *base, PkgKeyT *key, char *err, int32_t errSize);
@@ -84,7 +81,6 @@ bool pkgSecretRead(const char *path, PkgSecretT *secret, char *err, int3
void pkgSecretWipe(PkgSecretT *secret);
bool pkgSigCheck(const PkgSigT *sig, const PkgKeyT *key, const uint8_t *msg, int32_t len);
char *pkgSigFormat(const PkgSigT *sig, int32_t *outLen);
-void pkgSign(const PkgSecretT *secret, const uint8_t *msg, int32_t len, PkgSigT *sig);
bool pkgSigParse(const char *text, int32_t len, PkgSigT *sig, char *err, int32_t errSize);
bool pkgSigRead(const char *path, PkgSigT *sig, char *err, int32_t errSize);
static char *textOf(int32_t *outLen, const char *fmt, ...) __attribute__((format(printf, 2, 3)));
@@ -100,14 +96,6 @@ static bool fail(char *err, int32_t errSize, const char *fmt, ...) {
}
-static void idOf(const uint8_t publicKey[PKG_PUBLIC_SIZE], uint8_t id[PKG_KEY_ID_SIZE]) {
- uint8_t digest[SEC_SHA256_SIZE];
-
- secSha256(publicKey, PKG_PUBLIC_SIZE, digest);
- memcpy(id, digest, PKG_KEY_ID_SIZE);
-}
-
-
// The keys of one [section] into fields; every field must come, once.
// Other keys are ignored (from a newer DVX).
static bool parseFields(const char *text, int32_t len, const char *section, FieldT *fields, int32_t count, char *err, int32_t errSize) {
@@ -165,24 +153,16 @@ char *pkgKeyFormat(const PkgKeyT *key, int32_t *outLen) {
}
-void pkgKeyIdText(const uint8_t id[PKG_KEY_ID_SIZE], char *out) {
- pkgHexEncode(id, PKG_KEY_ID_SIZE, out);
+void pkgKeyIdOf(const uint8_t publicKey[PKG_PUBLIC_SIZE], uint8_t id[PKG_KEY_ID_SIZE]) {
+ uint8_t digest[SEC_SHA256_SIZE];
+
+ secSha256(publicKey, PKG_PUBLIC_SIZE, digest);
+ memcpy(id, digest, PKG_KEY_ID_SIZE);
}
-void pkgKeyMake(const uint8_t seed[PKG_SEED_SIZE], const char *name, PkgKeyT *key, PkgSecretT *secret) {
- uint8_t expanded[EXPANDED_SIZE];
- uint8_t seedCopy[PKG_SEED_SIZE];
-
- // Monocypher wipes the seed it is given
- memcpy(seedCopy, seed, PKG_SEED_SIZE);
- crypto_ed25519_key_pair(expanded, key->publicKey, seedCopy);
- crypto_wipe(expanded, sizeof(expanded));
- idOf(key->publicKey, key->id);
- snprintf(key->name, sizeof(key->name), "%s", name);
- memcpy(secret->name, key->name, sizeof(secret->name));
- memcpy(secret->id, key->id, PKG_KEY_ID_SIZE);
- memcpy(secret->seed, seed, PKG_SEED_SIZE);
+void pkgKeyIdText(const uint8_t id[PKG_KEY_ID_SIZE], char *out) {
+ pkgHexEncode(id, PKG_KEY_ID_SIZE, out);
}
@@ -200,7 +180,7 @@ bool pkgKeyParse(const char *text, int32_t len, PkgKeyT *key, char *err, int32_t
return false;
}
- idOf(key->publicKey, id);
+ pkgKeyIdOf(key->publicKey, id);
if (memcmp(id, key->id, PKG_KEY_ID_SIZE) != 0) {
return fail(err, errSize, "The key's id is not its public key's.");
@@ -239,7 +219,7 @@ bool pkgKeyWrite(const uint8_t seed[PKG_SEED_SIZE], const char *name, const char
pkgSecretWipe(&secret);
if (text) {
- crypto_wipe(text, (size_t)len);
+ secSecureZero(text, len);
free(text);
}
@@ -273,7 +253,7 @@ bool pkgRandomBytes(uint8_t *buf, int32_t len) {
}
secRngAddEntropy(pool, n);
- crypto_wipe(pool, sizeof(pool));
+ secSecureZero(pool, sizeof(pool));
secRngBytes(buf, len);
return true;
}
@@ -287,7 +267,7 @@ char *pkgSecretFormat(const PkgSecretT *secret, int32_t *outLen) {
pkgHexEncode(secret->id, PKG_KEY_ID_SIZE, id);
pkgHexEncode(secret->seed, PKG_SEED_SIZE, seed);
text = textOf(outLen, "[" SECTION_SECRET "]" EOL KEY_NAME " = %s" EOL KEY_ID " = %s" EOL KEY_SEED " = %s" EOL, secret->name, id, seed);
- crypto_wipe(seed, sizeof(seed));
+ secSecureZero(seed, sizeof(seed));
return text;
}
@@ -310,7 +290,7 @@ bool pkgSecretRead(const char *path, PkgSecretT *secret, char *err, int32_t errS
bool ok = text ? pkgSecretParse(text, len, secret, err, errSize) : fail(err, errSize, "Cannot read %s.", path);
if (text) {
- crypto_wipe(text, (size_t)len);
+ secSecureZero(text, len);
free(text);
}
@@ -319,12 +299,12 @@ bool pkgSecretRead(const char *path, PkgSecretT *secret, char *err, int32_t errS
void pkgSecretWipe(PkgSecretT *secret) {
- crypto_wipe(secret, sizeof(*secret));
+ secSecureZero(secret, sizeof(*secret));
}
bool pkgSigCheck(const PkgSigT *sig, const PkgKeyT *key, const uint8_t *msg, int32_t len) {
- return memcmp(sig->keyId, key->id, PKG_KEY_ID_SIZE) == 0 && crypto_ed25519_check(sig->sig, key->publicKey, msg, (size_t)len) == 0;
+ return memcmp(sig->keyId, key->id, PKG_KEY_ID_SIZE) == 0 && secEd25519Check(sig->sig, key->publicKey, msg, len);
}
@@ -338,19 +318,6 @@ char *pkgSigFormat(const PkgSigT *sig, int32_t *outLen) {
}
-void pkgSign(const PkgSecretT *secret, const uint8_t *msg, int32_t len, PkgSigT *sig) {
- uint8_t expanded[EXPANDED_SIZE];
- uint8_t publicKey[PKG_PUBLIC_SIZE];
- uint8_t seedCopy[PKG_SEED_SIZE];
-
- memcpy(seedCopy, secret->seed, PKG_SEED_SIZE);
- crypto_ed25519_key_pair(expanded, publicKey, seedCopy);
- crypto_ed25519_sign(sig->sig, expanded, msg, (size_t)len);
- crypto_wipe(expanded, sizeof(expanded));
- idOf(publicKey, sig->keyId);
-}
-
-
bool pkgSigParse(const char *text, int32_t len, PkgSigT *sig, char *err, int32_t errSize) {
FieldT fields[] = {
{ KEY_KEY, NULL, 0, sig->keyId, PKG_KEY_ID_SIZE },
diff --git a/src/libs/kpunch/pkg/pkgSign.h b/src/libs/kpunch/pkg/pkgSign.h
index d45f6066..eae3614e 100644
--- a/src/libs/kpunch/pkg/pkgSign.h
+++ b/src/libs/kpunch/pkg/pkgSign.h
@@ -22,7 +22,7 @@
// pkgSign.h -- the keys and signatures that make a package trusted
//
-// A signature is Ed25519 (Monocypher) over the bytes of a package's
+// A signature is Ed25519 (the cryptography library, secCurve.h) over the bytes of a package's
// manifest (PACKAGE.INI), which lists every file's size and SHA-256, so
// it covers the whole package. It travels in the package (PACKAGE.SIG,
// the publisher's) or beside it (NAME.SIG, or the catalogue's: the DVX
@@ -48,6 +48,8 @@
#ifndef PKG_SIGN_H
#define PKG_SIGN_H
+#include "../crypto/secCurve.h"
+
#include
#include
@@ -56,9 +58,9 @@
#define PKG_SECRET_EXT ".SEC"
#define PKG_SIG_EXT ".SIG"
#define PKG_KEY_ID_SIZE 8
-#define PKG_PUBLIC_SIZE 32
-#define PKG_SEED_SIZE 32
-#define PKG_SIG_SIZE 64
+#define PKG_PUBLIC_SIZE SEC_ED25519_PUBLIC_SIZE
+#define PKG_SEED_SIZE SEC_ED25519_SEED_SIZE
+#define PKG_SIG_SIZE SEC_ED25519_SIG_SIZE
#define PKG_KEY_NAME_MAX 64
#define PKG_KEY_ID_TEXT (PKG_KEY_ID_SIZE * 2 + 1) // as hex, with the NUL
diff --git a/src/libs/kpunch/pkg/pkgSignMake.c b/src/libs/kpunch/pkg/pkgSignMake.c
new file mode 100644
index 00000000..164d0223
--- /dev/null
+++ b/src/libs/kpunch/pkg/pkgSignMake.c
@@ -0,0 +1,72 @@
+// 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.
+
+// pkgSignMake.c -- making keys and signatures (see pkgSign.h): the
+// Ed25519 key pair and signing, which only building and signing packages
+// need. In DVX they are the package writer module's (pkgWriter.h), so
+// the signing code is loaded only when a package is made; the tools that
+// link the package library in whole (DVXPKG.EXE, the release tools) have
+// them as they are.
+
+#include "pkgSign.h"
+#include "pkgInt.h"
+#include "crypto/secCurve.h"
+#include "crypto/security.h"
+
+#include
+#include
+
+// ============================================================
+// Prototypes
+// ============================================================
+
+void pkgKeyMake(const uint8_t seed[PKG_SEED_SIZE], const char *name, PkgKeyT *key, PkgSecretT *secret);
+void pkgSign(const PkgSecretT *secret, const uint8_t *msg, int32_t len, PkgSigT *sig);
+
+
+void pkgKeyMake(const uint8_t seed[PKG_SEED_SIZE], const char *name, PkgKeyT *key, PkgSecretT *secret) {
+ uint8_t expanded[SEC_ED25519_SECRET_SIZE];
+ uint8_t seedCopy[PKG_SEED_SIZE];
+
+ // The key pair wipes the seed it is given
+ memcpy(seedCopy, seed, PKG_SEED_SIZE);
+ secEd25519KeyPair(expanded, key->publicKey, seedCopy);
+ secSecureZero(expanded, sizeof(expanded));
+ pkgKeyIdOf(key->publicKey, key->id);
+ snprintf(key->name, sizeof(key->name), "%s", name);
+ memcpy(secret->name, key->name, sizeof(secret->name));
+ memcpy(secret->id, key->id, PKG_KEY_ID_SIZE);
+ memcpy(secret->seed, seed, PKG_SEED_SIZE);
+}
+
+
+void pkgSign(const PkgSecretT *secret, const uint8_t *msg, int32_t len, PkgSigT *sig) {
+ uint8_t expanded[SEC_ED25519_SECRET_SIZE];
+ uint8_t publicKey[PKG_PUBLIC_SIZE];
+ uint8_t seedCopy[PKG_SEED_SIZE];
+
+ memcpy(seedCopy, secret->seed, PKG_SEED_SIZE);
+ secEd25519KeyPair(expanded, publicKey, seedCopy);
+ secEd25519Sign(sig->sig, expanded, msg, len);
+ secSecureZero(expanded, sizeof(expanded));
+ pkgKeyIdOf(publicKey, sig->keyId);
+}
diff --git a/src/libs/kpunch/pkg/pkgTrust.c b/src/libs/kpunch/pkg/pkgTrust.c
index 89d675e1..6040cd33 100644
--- a/src/libs/kpunch/pkg/pkgTrust.c
+++ b/src/libs/kpunch/pkg/pkgTrust.c
@@ -44,6 +44,7 @@ static void loadFolder(PkgKeyringT *ring, const char *dir);
bool pkgKeyringAdd(PkgKeyringT *ring, const char *userDir, const char *path, char *err, int32_t errSize);
void pkgKeyringFree(PkgKeyringT *ring);
void pkgKeyringLoad(PkgKeyringT *ring, const char *root, const char *userDir);
+bool pkgKeyringRemove(PkgKeyringT *ring, const char *userDir, const char *idText, char *err, int32_t errSize);
bool pkgTrustIsDvxId(const char *id);
PkgTrustE pkgTrustOf(const PkgKeyringT *ring, const PkgDpkInfoT *info, const PkgSigT *detached, const PkgKeyT **signer);
@@ -129,6 +130,59 @@ void pkgKeyringLoad(PkgKeyringT *ring, const char *root, const char *userDir) {
}
+bool pkgKeyringRemove(PkgKeyringT *ring, const char *userDir, const char *idText, char *err, int32_t errSize) {
+ char dir[DVX_MAX_PATH];
+ char text[PKG_KEY_ID_TEXT];
+ int32_t found = -1;
+
+ for (int32_t i = 0; i < (int32_t)arrlen(ring->keys) && found < 0; i++) {
+ pkgKeyIdText(ring->keys[i].id, text);
+
+ if (idText && strcasecmp(text, idText) == 0) {
+ found = i;
+ }
+ }
+
+ if (found < 0) {
+ return fail(err, errSize, "No key with the id %s is trusted.", idText ? idText : "");
+ }
+
+ if (found < ring->systemCount) {
+ return fail(err, errSize, "The key %s is DVX's own, and stays trusted.", ring->keys[found].name);
+ }
+
+ if (!platformPathJoin(dir, sizeof(dir), userDir, PKG_KEYS_DIR)) {
+ return fail(err, errSize, "The path of the KEYS folder is too long.");
+ }
+
+ // Every file of the key: one copied in under another name too
+ char **names = dvxReadDir(dir);
+ bool ok = true;
+
+ for (int32_t i = 0; i < (int32_t)arrlen(names); i++) {
+ char path[DVX_MAX_PATH];
+ char why[PKG_ERR_MAX];
+ PkgKeyT key;
+
+ if (!dvxHasExt(names[i], PKG_KEY_EXT) || !platformPathJoin(path, sizeof(path), dir, names[i]) || !pkgKeyRead(path, &key, why, sizeof(why))) {
+ continue;
+ }
+
+ if (memcmp(key.id, ring->keys[found].id, PKG_KEY_ID_SIZE) == 0 && remove(path) != 0) {
+ ok = fail(err, errSize, "Cannot delete %s.", path);
+ }
+ }
+
+ dvxReadDirFree(names);
+
+ if (ok) {
+ arrdel(ring->keys, found);
+ }
+
+ return ok;
+}
+
+
bool pkgTrustIsDvxId(const char *id) {
char vendor[PKG_ID_PART_MAX + 1];
char name[PKG_ID_PART_MAX + 1];
diff --git a/src/libs/kpunch/pkg/pkgTrust.h b/src/libs/kpunch/pkg/pkgTrust.h
index 17d70c14..9fcb4280 100644
--- a/src/libs/kpunch/pkg/pkgTrust.h
+++ b/src/libs/kpunch/pkg/pkgTrust.h
@@ -63,6 +63,11 @@ bool pkgKeyringAdd(PkgKeyringT *ring, const char *userDir, const char *path
void pkgKeyringFree(PkgKeyringT *ring);
+// Stop trusting the key with id idText (hex, pkgKeyIdText): its files in
+// the user's KEYS folder (userDir) are deleted and it leaves ring. A
+// SYSTEM key is DVX's own and stays. False with the reason.
+bool pkgKeyringRemove(PkgKeyringT *ring, const char *userDir, const char *idText, char *err, int32_t errSize);
+
// Read the keys in root's SYSTEM\KEYS and userDir's KEYS (NULL: none). A
// file that is not a key is skipped and logged.
void pkgKeyringLoad(PkgKeyringT *ring, const char *root, const char *userDir);
diff --git a/src/libs/kpunch/pkg/pkgWriter.h b/src/libs/kpunch/pkg/pkgWriter.h
new file mode 100644
index 00000000..9403e317
--- /dev/null
+++ b/src/libs/kpunch/pkg/pkgWriter.h
@@ -0,0 +1,62 @@
+// 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.
+
+// pkgWriter.h -- the package writer module: writing package ZIPs (miniz's
+// deflate) and making keys and signatures (Ed25519 signing), which only
+// programs that make packages use (the Package Builder, the IDE's Make
+// Package, BASIC's PkgBuildStart and the rest). In DVX they are a module
+// of their own, writer.drv in PKG_WRITER_DIR (a driver module: dvxDrv.h),
+// that exports one PkgWriterModT; pkg.lib's calls of these functions are
+// through it (pkgWriterMod.c), and the first one loads it. The tools that
+// link the package library in whole (DVXPKG.EXE, the release tools, the
+// host tests) compile pkgZipWrite.c and pkgSignMake.c instead.
+
+#ifndef PKG_WRITER_H
+#define PKG_WRITER_H
+
+#include "pkgSign.h"
+#include "pkgZip.h"
+#include "dvxDrv.h"
+#include "dvxPlat.h"
+
+#define PKG_WRITER_DIR "libs" DVX_PATH_SEP "kpunch" DVX_PATH_SEP "pkg" DVX_PATH_SEP "writer"
+#define PKG_WRITER_SYMBOL "pkgWriterModule"
+
+// What the module exports as PKG_WRITER_SYMBOL: the package library's
+// functions of the same names
+typedef struct {
+ DvxDriverHeaderT hdr;
+ __typeof__(&pkgKeyMake) keyMake;
+ __typeof__(&pkgSign) sign;
+ __typeof__(&pkgZipAbort) zipAbort;
+ __typeof__(&pkgZipAdded) zipAdded;
+ __typeof__(&pkgZipAddMem) zipAddMem;
+ __typeof__(&pkgZipBeginCopy) zipBeginCopy;
+ __typeof__(&pkgZipBeginFile) zipBeginFile;
+ __typeof__(&pkgZipBeginMem) zipBeginMem;
+ __typeof__(&pkgZipCopyWrite) zipCopyWrite;
+ __typeof__(&pkgZipCreate) zipCreate;
+ __typeof__(&pkgZipFinish) zipFinish;
+ __typeof__(&pkgZipStep) zipStep;
+} PkgWriterModT;
+
+#endif // PKG_WRITER_H
diff --git a/src/libs/kpunch/pkg/pkgWriterDrv.c b/src/libs/kpunch/pkg/pkgWriterDrv.c
new file mode 100644
index 00000000..e0de4c71
--- /dev/null
+++ b/src/libs/kpunch/pkg/pkgWriterDrv.c
@@ -0,0 +1,44 @@
+// 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.
+
+// pkgWriterDrv.c -- the package writer module's descriptor (see
+// pkgWriter.h): writer.drv is this, pkgZipWrite.c, pkgSignMake.c, and what
+// they reach of miniz and Monocypher; everything else it calls is
+// pkg.lib's.
+
+#include "pkgWriter.h"
+
+const PkgWriterModT pkgWriterModule = {
+ DVX_DRIVER_HDR(PkgWriterModT, "writer", 0),
+ .keyMake = pkgKeyMake,
+ .sign = pkgSign,
+ .zipAbort = pkgZipAbort,
+ .zipAdded = pkgZipAdded,
+ .zipAddMem = pkgZipAddMem,
+ .zipBeginCopy = pkgZipBeginCopy,
+ .zipBeginFile = pkgZipBeginFile,
+ .zipBeginMem = pkgZipBeginMem,
+ .zipCopyWrite = pkgZipCopyWrite,
+ .zipCreate = pkgZipCreate,
+ .zipFinish = pkgZipFinish,
+ .zipStep = pkgZipStep
+};
diff --git a/src/libs/kpunch/pkg/pkgWriterMod.c b/src/libs/kpunch/pkg/pkgWriterMod.c
new file mode 100644
index 00000000..bd8a6b04
--- /dev/null
+++ b/src/libs/kpunch/pkg/pkgWriterMod.c
@@ -0,0 +1,180 @@
+// 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.
+
+// pkgWriterMod.c -- the package library's calls of the package writer
+// module (see pkgWriter.h): writing package ZIPs and making keys and
+// signatures. The first pkgZipCreate, pkgKeyMake, or pkgSign loads the
+// module, and it stays until the library is unloaded. A ZIP writer exists
+// only while the module is loaded (pkgZipCreate made it), so the calls on
+// one need no check. Without the module (a damaged installation) nothing
+// is written: pkgZipCreate gives NULL, and a key or signature made is all
+// zeros, which nothing accepts.
+
+#include "pkgWriter.h"
+#include "stb_ds_wrap.h"
+
+#include
+
+// ============================================================
+// Prototypes
+// ============================================================
+
+static bool driverValid(const DvxDriverHeaderT *desc, int32_t kind);
+void pkgKeyMake(const uint8_t seed[PKG_SEED_SIZE], const char *name, PkgKeyT *key, PkgSecretT *secret);
+void pkgSign(const PkgSecretT *secret, const uint8_t *msg, int32_t len, PkgSigT *sig);
+static const PkgWriterModT *pkgWriter(void);
+static void pkgWriterUnload(void) __attribute__((destructor));
+void pkgZipAbort(PkgZipWriterT *w);
+uint32_t pkgZipAdded(const PkgZipWriterT *w);
+bool pkgZipAddMem(PkgZipWriterT *w, const char *name, const void *data, uint32_t len, time_t when);
+bool pkgZipBeginCopy(PkgZipWriterT *w, const PkgZipEntryT *e);
+bool pkgZipBeginFile(PkgZipWriterT *w, const char *name, const char *path);
+bool pkgZipBeginMem(PkgZipWriterT *w, const char *name, const void *data, uint32_t len, time_t when);
+bool pkgZipCopyWrite(void *ctx, const uint8_t *data, int32_t len);
+PkgZipWriterT *pkgZipCreate(const char *path);
+bool pkgZipFinish(PkgZipWriterT *w);
+PkgZipStepE pkgZipStep(PkgZipWriterT *w, int32_t maxWork);
+
+static const char *const sDrvSymbols[] = { DVX_SYM(PKG_WRITER_SYMBOL) };
+static const uint32_t sDrvSizes[] = { sizeof(PkgWriterModT) };
+static const DvxDriverClassT sDrvClass = { sDrvSymbols, sDrvSizes, 1, driverValid };
+
+static DvxDriverModT sMod = { NULL, NULL, 0 }; // desc NULL: not loaded
+static const PkgWriterModT *sWriter = NULL; // sMod's descriptor
+
+
+static bool driverValid(const DvxDriverHeaderT *desc, int32_t kind) {
+ (void)kind;
+
+ const PkgWriterModT *m = (const PkgWriterModT *)desc;
+
+ return m->keyMake && m->sign && m->zipAbort && m->zipAdded && m->zipAddMem && m->zipBeginCopy && m->zipBeginFile && m->zipBeginMem && m->zipCopyWrite && m->zipCreate && m->zipFinish && m->zipStep;
+}
+
+
+void pkgKeyMake(const uint8_t seed[PKG_SEED_SIZE], const char *name, PkgKeyT *key, PkgSecretT *secret) {
+ const PkgWriterModT *writer = pkgWriter();
+
+ if (writer) {
+ writer->keyMake(seed, name, key, secret);
+ return;
+ }
+
+ memset(key, 0, sizeof(*key));
+ memset(secret, 0, sizeof(*secret));
+}
+
+
+void pkgSign(const PkgSecretT *secret, const uint8_t *msg, int32_t len, PkgSigT *sig) {
+ const PkgWriterModT *writer = pkgWriter();
+
+ if (writer) {
+ writer->sign(secret, msg, len, sig);
+ return;
+ }
+
+ memset(sig, 0, sizeof(*sig));
+}
+
+
+// The module, loaded if it is not (NULL: it is not there, logged).
+static const PkgWriterModT *pkgWriter(void) {
+ if (sWriter) {
+ return sWriter;
+ }
+
+ DvxDriverModT *mods = dvxDriversLoad(PKG_WRITER_DIR, &sDrvClass);
+
+ if (arrlen(mods) > 0) {
+ sMod = dvxDriverKeep(mods, 0);
+ }
+
+ dvxDriversFree(mods);
+ sWriter = (const PkgWriterModT *)sMod.desc;
+
+ if (!sWriter) {
+ dvxLog("Packages: the package writer is missing from %s", PKG_WRITER_DIR);
+ }
+
+ return sWriter;
+}
+
+
+// The library is being unloaded.
+static void pkgWriterUnload(void) {
+ dvxDriverUnload(&sMod);
+ sWriter = NULL;
+}
+
+
+void pkgZipAbort(PkgZipWriterT *w) {
+ if (w) {
+ sWriter->zipAbort(w);
+ }
+}
+
+
+uint32_t pkgZipAdded(const PkgZipWriterT *w) {
+ return sWriter->zipAdded(w);
+}
+
+
+bool pkgZipAddMem(PkgZipWriterT *w, const char *name, const void *data, uint32_t len, time_t when) {
+ return sWriter->zipAddMem(w, name, data, len, when);
+}
+
+
+bool pkgZipBeginCopy(PkgZipWriterT *w, const PkgZipEntryT *e) {
+ return sWriter->zipBeginCopy(w, e);
+}
+
+
+bool pkgZipBeginFile(PkgZipWriterT *w, const char *name, const char *path) {
+ return sWriter->zipBeginFile(w, name, path);
+}
+
+
+bool pkgZipBeginMem(PkgZipWriterT *w, const char *name, const void *data, uint32_t len, time_t when) {
+ return sWriter->zipBeginMem(w, name, data, len, when);
+}
+
+
+bool pkgZipCopyWrite(void *ctx, const uint8_t *data, int32_t len) {
+ return sWriter->zipCopyWrite(ctx, data, len);
+}
+
+
+PkgZipWriterT *pkgZipCreate(const char *path) {
+ const PkgWriterModT *writer = pkgWriter();
+
+ return writer ? writer->zipCreate(path) : NULL;
+}
+
+
+bool pkgZipFinish(PkgZipWriterT *w) {
+ return sWriter->zipFinish(w);
+}
+
+
+PkgZipStepE pkgZipStep(PkgZipWriterT *w, int32_t maxWork) {
+ return sWriter->zipStep(w, maxWork);
+}
diff --git a/src/libs/kpunch/pkg/pkgZip.c b/src/libs/kpunch/pkg/pkgZip.c
index d8f0457b..08f57a09 100644
--- a/src/libs/kpunch/pkg/pkgZip.c
+++ b/src/libs/kpunch/pkg/pkgZip.c
@@ -27,6 +27,7 @@
#include "pkgZip.h"
#include "miniz.h"
+#include
#include
#include
@@ -69,27 +70,122 @@ struct PkgZipT {
tinfl_decompressor inflator;
uint8_t dict[TINFL_LZ_DICT_SIZE];
uint32_t dictPos;
+ uint32_t outPos; // in dict: what the last inflate made, not yet handed out
+ uint32_t outLeft;
+ bool inflated; // the inflate is done (what it made may still be handed out)
};
// ============================================================
// Prototypes
// ============================================================
+static PkgZipStepE dataStep(PkgZipT *z, PkgZipWriteFnT write, void *ctx, int32_t maxWork, bool packed);
static bool fill(PkgZipT *z);
static bool finishEntry(PkgZipT *z);
void pkgZipClose(PkgZipT *z);
PkgZipStepE pkgZipData(PkgZipT *z, PkgZipWriteFnT write, void *ctx, int32_t maxWork);
+PkgZipStepE pkgZipDataPacked(PkgZipT *z, PkgZipWriteFnT write, void *ctx, int32_t maxWork);
uint32_t pkgZipDoneBytes(const PkgZipT *z);
+void pkgZipDosStamp(time_t when, uint16_t *date, uint16_t *time);
time_t pkgZipDosTime(uint16_t date, uint16_t time);
+int32_t pkgZipFileRead(void *ctx, uint8_t *buf, int32_t len);
PkgZipStepE pkgZipNext(PkgZipT *z, PkgZipEntryT *entry);
uint32_t pkgZipOffset(const PkgZipT *z);
PkgZipT *pkgZipOpen(PkgZipReadFnT read, void *ctx);
PkgZipProblemE pkgZipProblem(const PkgZipT *z);
+const char *pkgZipProblemText(const PkgZipT *z);
static bool readBytes(PkgZipT *z, uint8_t *buf, uint32_t len);
static bool takeOut(PkgZipT *z, PkgZipWriteFnT write, void *ctx, const uint8_t *data, uint32_t len);
static PkgZipStepE zipFail(PkgZipT *z, PkgZipProblemE problem);
+// Up to maxWork bytes of the entry's data read and written (at least one
+// piece): unpacked, or as they are packed when packed (still unpacked, to
+// check them). An inflate makes up to a dictionary's worth at once, so
+// what it made is handed out a piece at a time, and it is given no more
+// input than the work left.
+static PkgZipStepE dataStep(PkgZipT *z, PkgZipWriteFnT write, void *ctx, int32_t maxWork, bool packed) {
+ if (z->problem != PkgZipNoProblemE) {
+ return PkgZipFailE;
+ }
+
+ if (z->state != ZipDataE) {
+ return PkgZipFileDoneE;
+ }
+
+ for (int32_t work = 0; work == 0 || work < maxWork; ) {
+ uint32_t room = work < maxWork ? (uint32_t)(maxWork - work) : 1;
+ bool done;
+
+ if (z->outLeft > 0) {
+ uint32_t n = z->outLeft < room ? z->outLeft : room;
+
+ if (!takeOut(z, packed ? NULL : write, ctx, z->dict + z->outPos, n)) {
+ return PkgZipFailE;
+ }
+
+ z->outPos += n;
+ z->outLeft -= n;
+ work += (int32_t)n;
+ done = z->outLeft == 0 && z->inflated;
+ } else {
+ if (z->packedLeft > 0 && !fill(z)) {
+ return PkgZipFailE;
+ }
+
+ uint32_t avail = z->inLen - z->inPos;
+
+ if (avail > z->packedLeft) {
+ avail = z->packedLeft;
+ }
+
+ if (avail > room) {
+ avail = room;
+ }
+
+ if (z->entry.method == PKG_ZIP_STORED) {
+ if (!takeOut(z, write, ctx, z->in + z->inPos, avail)) {
+ return PkgZipFailE;
+ }
+
+ z->inPos += avail;
+ z->packedLeft -= avail;
+ work += (int32_t)avail;
+ done = z->packedLeft == 0;
+ } else {
+ size_t inBytes = avail;
+ size_t outBytes = TINFL_LZ_DICT_SIZE - z->dictPos;
+ mz_uint32 flags = z->packedLeft > avail ? TINFL_FLAG_HAS_MORE_INPUT : 0;
+ tinfl_status status = tinfl_decompress(&z->inflator, z->in + z->inPos, &inBytes, z->dict, z->dict + z->dictPos, &outBytes, flags);
+
+ if (packed && inBytes > 0 && write && !write(ctx, z->in + z->inPos, (int32_t)inBytes)) {
+ return zipFail(z, PkgZipWriteFailedE);
+ }
+
+ if (status < TINFL_STATUS_DONE || (status == TINFL_STATUS_NEEDS_MORE_INPUT && z->packedLeft == inBytes)) {
+ return zipFail(z, PkgZipDamagedE);
+ }
+
+ z->inPos += (uint32_t)inBytes;
+ z->packedLeft -= (uint32_t)inBytes;
+ z->outPos = z->dictPos;
+ z->outLeft = (uint32_t)outBytes;
+ z->dictPos = (z->dictPos + (uint32_t)outBytes) & (TINFL_LZ_DICT_SIZE - 1);
+ z->inflated = status == TINFL_STATUS_DONE;
+ work += (int32_t)inBytes + 1; // a call that only makes output still counts
+ done = z->outLeft == 0 && z->inflated;
+ }
+ }
+
+ if (done) {
+ return finishEntry(z) ? PkgZipFileDoneE : PkgZipFailE;
+ }
+ }
+
+ return PkgZipMoreE;
+}
+
+
// Make sure the input buffer has a byte. False (problem set) when the
// read callback stops or the bytes end.
static bool fill(PkgZipT *z) {
@@ -129,64 +225,12 @@ void pkgZipClose(PkgZipT *z) {
PkgZipStepE pkgZipData(PkgZipT *z, PkgZipWriteFnT write, void *ctx, int32_t maxWork) {
- if (z->problem != PkgZipNoProblemE) {
- return PkgZipFailE;
- }
+ return dataStep(z, write, ctx, maxWork, false);
+}
- if (z->state != ZipDataE) {
- return PkgZipFileDoneE;
- }
- for (int32_t work = 0; work < maxWork; ) {
- if (z->packedLeft > 0 && !fill(z)) {
- return PkgZipFailE;
- }
-
- uint32_t avail = z->inLen - z->inPos;
- bool done;
-
- if (avail > z->packedLeft) {
- avail = z->packedLeft;
- }
-
- if (z->entry.method == PKG_ZIP_STORED) {
- if (!takeOut(z, write, ctx, z->in + z->inPos, avail)) {
- return PkgZipFailE;
- }
-
- z->inPos += avail;
- z->packedLeft -= avail;
- work += (int32_t)avail;
- done = z->packedLeft == 0;
- } else {
- size_t inBytes = avail;
- size_t outBytes = TINFL_LZ_DICT_SIZE - z->dictPos;
- mz_uint32 flags = z->packedLeft > avail ? TINFL_FLAG_HAS_MORE_INPUT : 0;
- tinfl_status status = tinfl_decompress(&z->inflator, z->in + z->inPos, &inBytes, z->dict, z->dict + z->dictPos, &outBytes, flags);
-
- z->inPos += (uint32_t)inBytes;
- z->packedLeft -= (uint32_t)inBytes;
- work += (int32_t)(inBytes + outBytes);
-
- if (outBytes > 0 && !takeOut(z, write, ctx, z->dict + z->dictPos, (uint32_t)outBytes)) {
- return PkgZipFailE;
- }
-
- z->dictPos = (z->dictPos + (uint32_t)outBytes) & (TINFL_LZ_DICT_SIZE - 1);
-
- if (status < TINFL_STATUS_DONE || (status == TINFL_STATUS_NEEDS_MORE_INPUT && z->packedLeft == 0)) {
- return zipFail(z, PkgZipDamagedE);
- }
-
- done = status == TINFL_STATUS_DONE;
- }
-
- if (done) {
- return finishEntry(z) ? PkgZipFileDoneE : PkgZipFailE;
- }
- }
-
- return PkgZipMoreE;
+PkgZipStepE pkgZipDataPacked(PkgZipT *z, PkgZipWriteFnT write, void *ctx, int32_t maxWork) {
+ return dataStep(z, write, ctx, maxWork, true);
}
@@ -195,6 +239,26 @@ uint32_t pkgZipDoneBytes(const PkgZipT *z) {
}
+// A time as the ZIP's DOS date and time (1980 .. 2107, two-second steps).
+void pkgZipDosStamp(time_t when, uint16_t *date, uint16_t *time) {
+ struct tm *t = localtime(&when);
+ int32_t year = t ? t->tm_year + TM_YEAR_BASE - DOS_YEAR_BASE : 0;
+
+ if (!t || year < 0) {
+ *date = (uint16_t)((1 << DOS_MONTH_SHIFT) | 1);
+ *time = 0;
+ return;
+ }
+
+ if (year > DOS_YEAR_MASK) {
+ year = DOS_YEAR_MASK;
+ }
+
+ *date = (uint16_t)((year << DOS_YEAR_SHIFT) | ((t->tm_mon + 1) << DOS_MONTH_SHIFT) | t->tm_mday);
+ *time = (uint16_t)((t->tm_hour << DOS_HOUR_SHIFT) | (t->tm_min << DOS_MINUTE_SHIFT) | (t->tm_sec / DOS_SECOND_UNIT));
+}
+
+
time_t pkgZipDosTime(uint16_t date, uint16_t time) {
struct tm t;
@@ -210,6 +274,14 @@ time_t pkgZipDosTime(uint16_t date, uint16_t time) {
}
+int32_t pkgZipFileRead(void *ctx, uint8_t *buf, int32_t len) {
+ FILE *f = (FILE *)ctx;
+ size_t n = fread(buf, 1, (size_t)len, f);
+
+ return (n == 0 && ferror(f)) ? -1 : (int32_t)n;
+}
+
+
PkgZipStepE pkgZipNext(PkgZipT *z, PkgZipEntryT *entry) {
uint8_t hdr[PKG_ZIP_LOCAL_SIZE];
@@ -230,7 +302,7 @@ PkgZipStepE pkgZipNext(PkgZipT *z, PkgZipEntryT *entry) {
return PkgZipFailE;
}
- uint32_t sig = pkgGet32(hdr);
+ uint32_t sig = dvxGetLe32(hdr);
if (sig == PKG_ZIP_CENTRAL_SIG || sig == PKG_ZIP_EOCD_SIG) {
z->state = ZipEndE;
@@ -246,17 +318,17 @@ PkgZipStepE pkgZipNext(PkgZipT *z, PkgZipEntryT *entry) {
}
PkgZipEntryT e;
- uint16_t flags = pkgGet16(hdr + PKG_ZIP_LH_FLAGS);
- uint16_t nameLen = pkgGet16(hdr + PKG_ZIP_LH_NAME_LEN);
- uint16_t extraLen = pkgGet16(hdr + PKG_ZIP_LH_EXTRA_LEN);
+ uint16_t flags = dvxGetLe16(hdr + PKG_ZIP_LH_FLAGS);
+ uint16_t nameLen = dvxGetLe16(hdr + PKG_ZIP_LH_NAME_LEN);
+ uint16_t extraLen = dvxGetLe16(hdr + PKG_ZIP_LH_EXTRA_LEN);
memset(&e, 0, sizeof(e));
- e.method = pkgGet16(hdr + PKG_ZIP_LH_METHOD);
- e.dosTime = pkgGet16(hdr + PKG_ZIP_LH_TIME);
- e.dosDate = pkgGet16(hdr + PKG_ZIP_LH_DATE);
- e.crc = pkgGet32(hdr + PKG_ZIP_LH_CRC);
- e.packedSize = pkgGet32(hdr + PKG_ZIP_LH_PACKED);
- e.size = pkgGet32(hdr + PKG_ZIP_LH_SIZE);
+ e.method = dvxGetLe16(hdr + PKG_ZIP_LH_METHOD);
+ e.dosTime = dvxGetLe16(hdr + PKG_ZIP_LH_TIME);
+ e.dosDate = dvxGetLe16(hdr + PKG_ZIP_LH_DATE);
+ e.crc = dvxGetLe32(hdr + PKG_ZIP_LH_CRC);
+ e.packedSize = dvxGetLe32(hdr + PKG_ZIP_LH_PACKED);
+ e.size = dvxGetLe32(hdr + PKG_ZIP_LH_SIZE);
if ((flags & (PKG_ZIP_FLAG_DESC | PKG_ZIP_FLAG_CRYPT)) || (e.method != PKG_ZIP_STORED && e.method != PKG_ZIP_DEFLATED) || nameLen == 0 || nameLen >= PKG_NAME_MAX) {
return zipFail(z, PkgZipUnreadableE);
@@ -294,6 +366,9 @@ PkgZipStepE pkgZipNext(PkgZipT *z, PkgZipEntryT *entry) {
z->sizeDone = 0;
z->crc = (uint32_t)MZ_CRC32_INIT;
z->dictPos = 0;
+ z->outPos = 0;
+ z->outLeft = 0;
+ z->inflated = false;
z->state = e.isDir ? ZipHeaderE : ZipDataE;
tinfl_init(&z->inflator);
*entry = e;
@@ -326,6 +401,23 @@ PkgZipProblemE pkgZipProblem(const PkgZipT *z) {
}
+const char *pkgZipProblemText(const PkgZipT *z) {
+ switch (pkgZipProblem(z)) {
+ case PkgZipReadFailedE:
+ return "it cannot be read.";
+
+ case PkgZipWriteFailedE:
+ return "a part of it could not be kept (the disk is full, or memory is short).";
+
+ case PkgZipUnreadableE:
+ return "it uses a form DVX cannot read.";
+
+ default:
+ return "it is damaged.";
+ }
+}
+
+
// Exactly len bytes of the stream.
static bool readBytes(PkgZipT *z, uint8_t *buf, uint32_t len) {
while (len > 0) {
diff --git a/src/libs/kpunch/pkg/pkgZip.h b/src/libs/kpunch/pkg/pkgZip.h
index 3c0aa01d..ca229dde 100644
--- a/src/libs/kpunch/pkg/pkgZip.h
+++ b/src/libs/kpunch/pkg/pkgZip.h
@@ -34,7 +34,9 @@
// Writing makes a ZIP the reader takes: each local header carries its
// sizes, each file is deflated (or stored when deflating does not make it
// smaller), names are relative with '/', and the central directory comes
-// last. Files are added in the order given (a .DPK's manifest first).
+// last. Files are added in the order given (a .DPK's manifest first), a
+// step of a given amount of work at a time, as they are read; an entry of
+// another ZIP can be copied in as it is packed.
#ifndef PKG_ZIP_H
#define PKG_ZIP_H
@@ -56,8 +58,8 @@ typedef bool (*PkgZipWriteFnT)(void *ctx, const uint8_t *data, int32_t len);
typedef enum {
PkgZipEntryE, // pkgZipNext: an entry's header was read
PkgZipEndE, // pkgZipNext: the central directory: no more entries
- PkgZipMoreE, // pkgZipData: more of the entry to come
- PkgZipFileDoneE, // pkgZipData: the entry is complete and checked
+ PkgZipMoreE, // pkgZipData, pkgZipStep: more of the entry to come
+ PkgZipFileDoneE, // pkgZipData: the entry is complete and checked; pkgZipStep: it is in
PkgZipFailE // either: see pkgZipProblem
} PkgZipStepE;
@@ -95,12 +97,22 @@ void pkgZipClose(PkgZipT *z);
// done at once.
PkgZipStepE pkgZipData(PkgZipT *z, PkgZipWriteFnT write, void *ctx, int32_t maxWork);
+// As pkgZipData, but write takes the entry's bytes as they are packed
+// (to copy the entry into another ZIP: pkgZipBeginCopy); they are still
+// unpacked to check the entry's size and CRC-32.
+PkgZipStepE pkgZipDataPacked(PkgZipT *z, PkgZipWriteFnT write, void *ctx, int32_t maxWork);
+
// Bytes of the current entry's data written so far (for progress).
uint32_t pkgZipDoneBytes(const PkgZipT *z);
-// The time a ZIP date and time stand for (local time).
+// The time a ZIP date and time stand for (local time), and a time as a
+// ZIP date and time (1980 to 2107, two-second steps).
+void pkgZipDosStamp(time_t when, uint16_t *date, uint16_t *time);
time_t pkgZipDosTime(uint16_t date, uint16_t time);
+// A PkgZipReadFnT over a stdio file (ctx the FILE *).
+int32_t pkgZipFileRead(void *ctx, uint8_t *buf, int32_t len);
+
// The next entry's header (after the previous entry's data is done):
// PkgZipEntryE with *entry filled, PkgZipEndE at the central directory,
// or PkgZipFailE.
@@ -115,6 +127,10 @@ PkgZipT *pkgZipOpen(PkgZipReadFnT read, void *ctx);
// Why the last step failed.
PkgZipProblemE pkgZipProblem(const PkgZipT *z);
+// That reason in plain words, as the end of a sentence ("it is
+// damaged."): what the package library's messages say after a name.
+const char *pkgZipProblemText(const PkgZipT *z);
+
// ------------------------------------------------------------
// Writing
// ------------------------------------------------------------
@@ -122,19 +138,91 @@ PkgZipProblemE pkgZipProblem(const PkgZipT *z);
// Remove a half-made ZIP (the writer is freed).
void pkgZipAbort(PkgZipWriterT *w);
-// Add a file from disk under name. False on a read or write error, or
-// when the file is too big for the format (4 GB).
-bool pkgZipAddFile(PkgZipWriterT *w, const char *name, const char *path);
-
-// Add len bytes under name, dated when.
+// Add len bytes under name, dated when, all at once (pkgZipBeginMem and
+// pkgZipStep until it is in).
bool pkgZipAddMem(PkgZipWriterT *w, const char *name, const void *data, uint32_t len, time_t when);
+// Bytes of the source of the entry being added taken so far (for
+// progress).
+uint32_t pkgZipAdded(const PkgZipWriterT *w);
+
+// Start copying in e, the entry of another ZIP just read by pkgZipNext,
+// as it is packed: its packed bytes go through pkgZipCopyWrite (with the
+// writer as ctx), as pkgZipDataPacked hands them; the entry is in after
+// the last of them. False when it is a folder, its name will not do, or
+// the ZIP would be too big for the format (4 GB).
+bool pkgZipBeginCopy(PkgZipWriterT *w, const PkgZipEntryT *e);
+
+// Start adding the file at path under name, dated as it is; pkgZipStep
+// adds it. False when it cannot be read, the name will not do, or the
+// ZIP would be too big for the format (4 GB).
+bool pkgZipBeginFile(PkgZipWriterT *w, const char *name, const char *path);
+
+// Start adding len bytes under name, dated when; pkgZipStep adds them
+// (data must stay until it is done). False as pkgZipBeginFile.
+bool pkgZipBeginMem(PkgZipWriterT *w, const char *name, const void *data, uint32_t len, time_t when);
+
+// pkgZipBeginCopy's PkgZipWriteFnT: len more packed bytes of the entry
+// (ctx the writer). False on a write error or more than the entry has.
+bool pkgZipCopyWrite(void *ctx, const uint8_t *data, int32_t len);
+
// Start a ZIP at path (replaced if there is one). NULL when it cannot be
// made or out of memory.
PkgZipWriterT *pkgZipCreate(const char *path);
// Write the central directory and close the file (the writer is freed).
-// False on a write error (the file is removed).
+// False on a write error or an entry not all in (the file is removed).
bool pkgZipFinish(PkgZipWriterT *w);
+// Add up to maxWork bytes of the entry begun (at least one piece):
+// PkgZipMoreE, PkgZipFileDoneE once it is in, or PkgZipFailE on a read or
+// write error (then pkgZipAbort).
+PkgZipStepE pkgZipStep(PkgZipWriterT *w, int32_t maxWork);
+
+// ------------------------------------------------------------
+// Plain ZIP archives a step at a time (pkgZipJob.c)
+// ------------------------------------------------------------
+
+// A ZIP listed (every entry read and checked), unpacked into a folder, or
+// made from files, in steps of a given amount of work, so a program can
+// draw progress and stay responsive between them (pkg.bas's PkgZip*).
+typedef struct PkgZipJobT PkgZipJobT;
+
+typedef enum {
+ PkgZipJobMoreE, // more to do
+ PkgZipJobDoneE,
+ PkgZipJobFailedE // pkgZipJobMessage says why (a ZIP being made is removed)
+} PkgZipJobStepE;
+
+// Make a ZIP at path from count files: names[i] in the ZIP (relative, '/'
+// between parts) from the file sources[i] (both copied). NULL with the
+// reason in err.
+PkgZipJobT *pkgZipJobCreate(const char *path, const char *const *names, const char *const *sources, int32_t count, char *err, int32_t errSize);
+
+// The name of the entry the job is on ("" when none).
+const char *pkgZipJobCurrent(const PkgZipJobT *job);
+
+// The entries read or written so far (*count of them).
+const PkgZipEntryT *pkgZipJobEntries(const PkgZipJobT *job, int32_t *count);
+
+// Stop the job if it is not done (a ZIP being made is removed; a file
+// being unpacked is removed), and free it. NULL is ignored.
+void pkgZipJobFree(PkgZipJobT *job);
+
+// Why the job failed ("" when it did not).
+const char *pkgZipJobMessage(const PkgZipJobT *job);
+
+// How far the job is, 0 to 100.
+int32_t pkgZipJobPercent(const PkgZipJobT *job);
+
+// Read the ZIP at path to its end, each entry's size and CRC-32 checked;
+// with dir (not NULL) each entry is also written under it (folders made,
+// files dated as in the ZIP; an existing file is replaced). A name that
+// would leave dir (a drive, a leading '/', "..") fails it. NULL with the
+// reason in err.
+PkgZipJobT *pkgZipJobRead(const char *path, const char *dir, char *err, int32_t errSize);
+
+// Run the job for about maxWork bytes of work (at least one piece).
+PkgZipJobStepE pkgZipJobStep(PkgZipJobT *job, int32_t maxWork);
+
#endif // PKG_ZIP_H
diff --git a/src/libs/kpunch/pkg/pkgZipJob.c b/src/libs/kpunch/pkg/pkgZipJob.c
new file mode 100644
index 00000000..f1afd3e9
--- /dev/null
+++ b/src/libs/kpunch/pkg/pkgZipJob.c
@@ -0,0 +1,445 @@
+// 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.
+
+// pkgZipJob.c -- plain ZIP archives a step at a time: listed (every
+// entry read and checked), unpacked into a folder, or made from files,
+// with the package library's ZIP reader and writer (pkgZip.c,
+// pkgZipWrite.c). A step does at most a given amount of work, so a
+// program draws progress and stays responsive between steps.
+
+#include "pkgZip.h"
+#include "pkgInt.h"
+#include "pkgManifest.h"
+#include "dvxPlat.h"
+#include "stb_ds_wrap.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+#define PERCENT_MAX 100
+#define NAME_SEP '/'
+#define NAME_UP ".."
+#define DRIVE_SEP ':'
+#define BACKSLASH '\\'
+
+struct PkgZipJobT {
+ PkgZipT *zip; // reading
+ FILE *src; // reading: the ZIP
+ PkgZipWriterT *w; // writing
+ char *dir; // reading: the folder unpacked into (NULL: listing)
+ char **names; // writing: stb_ds of the names in the ZIP (strdup'd)
+ char **sources; // writing: the files they come from (strdup'd)
+ PkgZipEntryT *entries; // stb_ds: read or written so far
+ FILE *out; // unpacking: the file being written
+ uint64_t total; // bytes of the ZIP (reading) or of the files (writing)
+ uint64_t done; // writing: bytes of the files already in
+ int32_t at; // writing: the file being added
+ bool entryOpen; // an entry begun: being read or added
+ PkgZipJobStepE state;
+ PkgZipEntryT entry; // the one begun
+ char outPath[DVX_MAX_PATH];
+ char current[PKG_NAME_MAX];
+ char err[PKG_ERR_MAX];
+};
+
+// ============================================================
+// Prototypes
+// ============================================================
+
+static bool fail(char *err, int32_t errSize, const char *fmt, ...) __attribute__((format(printf, 3, 4)));
+static void jobFail(PkgZipJobT *job, const char *fmt, ...) __attribute__((format(printf, 2, 3)));
+static bool nameSafe(const char *name);
+static bool outBegin(PkgZipJobT *job);
+static void outEnd(PkgZipJobT *job, bool keep);
+static bool outWrite(void *ctx, const uint8_t *data, int32_t len);
+PkgZipJobT *pkgZipJobCreate(const char *path, const char *const *names, const char *const *sources, int32_t count, char *err, int32_t errSize);
+const char *pkgZipJobCurrent(const PkgZipJobT *job);
+const PkgZipEntryT *pkgZipJobEntries(const PkgZipJobT *job, int32_t *count);
+void pkgZipJobFree(PkgZipJobT *job);
+const char *pkgZipJobMessage(const PkgZipJobT *job);
+int32_t pkgZipJobPercent(const PkgZipJobT *job);
+PkgZipJobT *pkgZipJobRead(const char *path, const char *dir, char *err, int32_t errSize);
+PkgZipJobStepE pkgZipJobStep(PkgZipJobT *job, int32_t maxWork);
+static void readStep(PkgZipJobT *job, int32_t maxWork);
+static void writeStep(PkgZipJobT *job, int32_t maxWork);
+
+
+static bool fail(char *err, int32_t errSize, const char *fmt, ...) {
+ va_list ap;
+
+ va_start(ap, fmt);
+ vsnprintf(err, (size_t)errSize, fmt, ap);
+ va_end(ap);
+ return false;
+}
+
+
+static void jobFail(PkgZipJobT *job, const char *fmt, ...) {
+ va_list ap;
+
+ va_start(ap, fmt);
+ vsnprintf(job->err, sizeof(job->err), fmt, ap);
+ va_end(ap);
+ job->state = PkgZipJobFailedE;
+}
+
+
+// A name that stays inside the folder it is unpacked into: relative, no
+// drive, no backslash, and no ".." part.
+static bool nameSafe(const char *name) {
+ if (!name[0] || name[0] == NAME_SEP || strchr(name, DRIVE_SEP) || strchr(name, BACKSLASH)) {
+ return false;
+ }
+
+ for (const char *p = name; *p; ) {
+ const char *end = strchr(p, NAME_SEP);
+ size_t len = end ? (size_t)(end - p) : strlen(p);
+
+ if (len == strlen(NAME_UP) && strncmp(p, NAME_UP, len) == 0) {
+ return false;
+ }
+
+ p = end ? end + 1 : p + len;
+ }
+
+ return true;
+}
+
+
+// Unpacking: the entry begun's folder made (its parents too), or its file
+// opened for writing. False with the reason said.
+static bool outBegin(PkgZipJobT *job) {
+ char parent[DVX_MAX_PATH];
+
+ if (!nameSafe(job->entry.name)) {
+ jobFail(job, "%s would be unpacked outside %s.", job->entry.name, job->dir);
+ return false;
+ }
+
+ if (!platformPathJoin(job->outPath, sizeof(job->outPath), job->dir, job->entry.name)) {
+ jobFail(job, "The path of %s in %s is too long.", job->entry.name, job->dir);
+ return false;
+ }
+
+ platformPathNative(job->outPath);
+
+ if (job->entry.isDir) {
+ if (platformMkdirRecursive(job->outPath) != 0) {
+ jobFail(job, "Cannot make the folder %s.", job->outPath);
+ return false;
+ }
+
+ return true;
+ }
+
+ platformPathDirName(job->outPath, parent, sizeof(parent));
+ job->out = (!parent[0] || platformMkdirRecursive(parent) == 0) ? fopen(job->outPath, "wb") : NULL;
+
+ if (!job->out) {
+ jobFail(job, "Cannot write %s.", job->outPath);
+ return false;
+ }
+
+ return true;
+}
+
+
+// Unpacking: the file written closed, dated as in the ZIP when kept, else
+// removed (a half-unpacked file is not left behind).
+static void outEnd(PkgZipJobT *job, bool keep) {
+ if (!job->out) {
+ return;
+ }
+
+ bool ok = fclose(job->out) == 0 && keep;
+
+ job->out = NULL;
+
+ if (!ok) {
+ remove(job->outPath);
+ return;
+ }
+
+ struct utimbuf when;
+
+ when.actime = pkgZipDosTime(job->entry.dosDate, job->entry.dosTime);
+ when.modtime = when.actime;
+ utime(job->outPath, &when);
+}
+
+
+// A PkgZipWriteFnT: unpacked bytes into the open file (none for a list).
+static bool outWrite(void *ctx, const uint8_t *data, int32_t len) {
+ PkgZipJobT *job = (PkgZipJobT *)ctx;
+
+ return !job->out || fwrite(data, 1, (size_t)len, job->out) == (size_t)len;
+}
+
+
+PkgZipJobT *pkgZipJobCreate(const char *path, const char *const *names, const char *const *sources, int32_t count, char *err, int32_t errSize) {
+ PkgZipJobT *job = (PkgZipJobT *)calloc(1, sizeof(PkgZipJobT));
+
+ if (!job) {
+ fail(err, errSize, PKG_ERR_NO_MEMORY);
+ return NULL;
+ }
+
+ for (int32_t i = 0; i < count; i++) {
+ char *name = strdup(names[i]);
+ char *source = strdup(sources[i]);
+ struct stat st;
+
+ arrput(job->names, name);
+ arrput(job->sources, source);
+
+ if (!name || !source) {
+ fail(err, errSize, PKG_ERR_NO_MEMORY);
+ pkgZipJobFree(job);
+ return NULL;
+ }
+
+ if (stat(source, &st) != 0 || !S_ISREG(st.st_mode)) {
+ fail(err, errSize, "Cannot read %s.", source);
+ pkgZipJobFree(job);
+ return NULL;
+ }
+
+ job->total += (uint64_t)st.st_size;
+ }
+
+ job->w = pkgZipCreate(path);
+
+ if (!job->w) {
+ fail(err, errSize, "Cannot create %s.", path);
+ pkgZipJobFree(job);
+ return NULL;
+ }
+
+ return job;
+}
+
+
+const char *pkgZipJobCurrent(const PkgZipJobT *job) {
+ return job->current;
+}
+
+
+const PkgZipEntryT *pkgZipJobEntries(const PkgZipJobT *job, int32_t *count) {
+ *count = (int32_t)arrlen(job->entries);
+ return job->entries;
+}
+
+
+void pkgZipJobFree(PkgZipJobT *job) {
+ if (!job) {
+ return;
+ }
+
+ outEnd(job, false);
+ pkgZipAbort(job->w);
+
+ if (job->zip) {
+ pkgZipClose(job->zip);
+ }
+
+ if (job->src) {
+ fclose(job->src);
+ }
+
+ for (int32_t i = 0; i < (int32_t)arrlen(job->names); i++) {
+ free(job->names[i]);
+ free(job->sources[i]);
+ }
+
+ arrfree(job->names);
+ arrfree(job->sources);
+ arrfree(job->entries);
+ free(job->dir);
+ free(job);
+}
+
+
+const char *pkgZipJobMessage(const PkgZipJobT *job) {
+ return job->err;
+}
+
+
+// Reading: by bytes of the ZIP read; writing: by bytes of the files added.
+int32_t pkgZipJobPercent(const PkgZipJobT *job) {
+ if (job->state == PkgZipJobDoneE) {
+ return PERCENT_MAX;
+ }
+
+ if (!job->total) {
+ return 0;
+ }
+
+ uint64_t done = job->zip ? pkgZipOffset(job->zip) : job->done + (job->entryOpen ? pkgZipAdded(job->w) : 0);
+
+ return (int32_t)(done * PERCENT_MAX / job->total);
+}
+
+
+PkgZipJobT *pkgZipJobRead(const char *path, const char *dir, char *err, int32_t errSize) {
+ PkgZipJobT *job = (PkgZipJobT *)calloc(1, sizeof(PkgZipJobT));
+ struct stat st;
+
+ if (job && dir) {
+ job->dir = strdup(dir);
+ }
+
+ if (!job || (dir && !job->dir)) {
+ free(job);
+ fail(err, errSize, PKG_ERR_NO_MEMORY);
+ return NULL;
+ }
+
+ job->total = stat(path, &st) == 0 ? (uint64_t)st.st_size : 0;
+ job->src = fopen(path, "rb");
+ job->zip = job->src ? pkgZipOpen(pkgZipFileRead, job->src) : NULL;
+
+ if (!job->zip) {
+ fail(err, errSize, "Cannot read %s.", path);
+ pkgZipJobFree(job);
+ return NULL;
+ }
+
+ return job;
+}
+
+
+PkgZipJobStepE pkgZipJobStep(PkgZipJobT *job, int32_t maxWork) {
+ if (job->state == PkgZipJobMoreE) {
+ if (job->zip) {
+ readStep(job, maxWork);
+ } else {
+ writeStep(job, maxWork);
+ }
+ }
+
+ return job->state;
+}
+
+
+// Reading: the next entry's header (and, unpacking, its folder or file
+// begun), or a piece of its data; at its end it is checked (and its file
+// closed) and listed.
+static void readStep(PkgZipJobT *job, int32_t maxWork) {
+ if (!job->entryOpen) {
+ PkgZipStepE step = pkgZipNext(job->zip, &job->entry);
+
+ if (step == PkgZipEndE) {
+ job->current[0] = '\0';
+ job->state = PkgZipJobDoneE;
+ return;
+ }
+
+ if (step != PkgZipEntryE) {
+ jobFail(job, "%s", pkgZipProblemText(job->zip));
+ return;
+ }
+
+ snprintf(job->current, sizeof(job->current), "%s", job->entry.name);
+ job->entryOpen = !job->dir || outBegin(job);
+ return;
+ }
+
+ PkgZipStepE data = pkgZipData(job->zip, outWrite, job, maxWork);
+
+ if (data == PkgZipMoreE) {
+ return;
+ }
+
+ job->entryOpen = false;
+
+ if (data == PkgZipFailE && pkgZipProblem(job->zip) == PkgZipWriteFailedE) {
+ jobFail(job, "Cannot write %s. The disk may be full.", job->outPath);
+ } else if (data == PkgZipFailE) {
+ jobFail(job, "%s: %s", job->current, pkgZipProblemText(job->zip));
+ }
+
+ outEnd(job, data == PkgZipFileDoneE);
+
+ if (data == PkgZipFileDoneE) {
+ arrput(job->entries, job->entry);
+ }
+}
+
+
+// Writing: the next file begun, or a piece of it added; after the last,
+// the ZIP finished.
+static void writeStep(PkgZipJobT *job, int32_t maxWork) {
+ if (!job->entryOpen) {
+ if (job->at >= (int32_t)arrlen(job->names)) {
+ PkgZipWriterT *w = job->w;
+
+ job->w = NULL;
+ job->current[0] = '\0';
+
+ if (pkgZipFinish(w)) {
+ job->state = PkgZipJobDoneE;
+ } else {
+ jobFail(job, "Cannot finish the ZIP. The disk may be full.");
+ }
+
+ return;
+ }
+
+ snprintf(job->current, sizeof(job->current), "%s", job->names[job->at]);
+
+ if (!pkgZipBeginFile(job->w, job->names[job->at], job->sources[job->at])) {
+ jobFail(job, "Cannot add %s as %s.", job->sources[job->at], job->names[job->at]);
+ return;
+ }
+
+ job->entryOpen = true;
+ return;
+ }
+
+ PkgZipStepE step = pkgZipStep(job->w, maxWork);
+
+ if (step == PkgZipMoreE) {
+ return;
+ }
+
+ job->entryOpen = false;
+
+ if (step != PkgZipFileDoneE) {
+ jobFail(job, "Cannot add %s.", job->sources[job->at]);
+ return;
+ }
+
+ struct stat st;
+ PkgZipEntryT e;
+
+ memset(&e, 0, sizeof(e));
+ snprintf(e.name, sizeof(e.name), "%s", job->names[job->at]);
+ e.size = stat(job->sources[job->at], &st) == 0 ? (uint32_t)st.st_size : 0;
+ pkgZipDosStamp(st.st_mtime, &e.dosDate, &e.dosTime);
+ arrput(job->entries, e);
+ job->done += e.size;
+ job->at++;
+}
diff --git a/src/libs/kpunch/pkg/pkgZipWrite.c b/src/libs/kpunch/pkg/pkgZipWrite.c
index 4373f664..bc0c4dd8 100644
--- a/src/libs/kpunch/pkg/pkgZipWrite.c
+++ b/src/libs/kpunch/pkg/pkgZipWrite.c
@@ -19,11 +19,11 @@
// 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.
-
-// pkgZipWrite.c -- writing a package ZIP (see pkgZip.h): each file whole
-// in memory, deflated with miniz's tdefl at its best setting (stored when
-// that is no smaller), its local header written with the sizes, the
-// central directory at the end.
+// pkgZipWrite.c -- writing a package ZIP (see pkgZip.h): each file read
+// a chunk at a time and deflated as it goes by miniz's tdefl at its best
+// setting, its local header written first and completed when its data is
+// in (the data written again, stored, when deflating does not make it
+// smaller); the central directory at the end.
#include "pkgZip.h"
#include "miniz.h"
@@ -40,15 +40,15 @@
// The central directory's first capacity, doubled as it fills
#define ZIP_DIR_FIRST 16
-// DOS date and time fields
-#define DOS_YEAR_SHIFT 9
-#define DOS_YEAR_BASE 1980
-#define DOS_YEAR_MAX 127
-#define DOS_MONTH_SHIFT 5
-#define DOS_HOUR_SHIFT 11
-#define DOS_MINUTE_SHIFT 5
-#define DOS_SECOND_UNIT 2
-#define TM_YEAR_BASE 1900
+// Bytes of an entry's source read (and deflated) at a time
+#define ZIP_CHUNK 4096
+
+typedef enum {
+ ZipIdleE, // no entry being added
+ ZipDeflateE, // the entry's source read and deflated
+ ZipStoreE, // the entry's source read and written as it is
+ ZipCopyE // an entry of another ZIP written as it is packed
+} ZipAddE;
typedef struct {
char *name;
@@ -62,46 +62,121 @@ typedef struct {
} ZipDirEntryT;
struct PkgZipWriterT {
- FILE *f;
- char *path;
- ZipDirEntryT *entries; // in the order added
- int32_t count;
- int32_t cap;
- uint32_t offset; // bytes written
- bool failed;
+ FILE *f;
+ char *path;
+ ZipDirEntryT *entries; // in the order added (the one being added last)
+ FILE *src; // the source of the entry being added (NULL: mem)
+ const uint8_t *mem; // its bytes, when it is added from memory
+ tdefl_compressor *deflator; // made for the first deflated entry
+ int32_t count;
+ int32_t cap;
+ uint32_t offset; // where the next byte goes
+ uint32_t srcLen;
+ uint32_t srcDone; // bytes of the source taken
+ uint32_t left; // ZipCopyE: packed bytes still to come
+ ZipAddE adding;
+ bool failed;
+ bool storedAfter; // deflating came to the source's size: stored instead
+ uint8_t buf[ZIP_CHUNK];
};
// ============================================================
// Prototypes
// ============================================================
-static void dosStamp(time_t when, uint16_t *date, uint16_t *time);
-static void freeWriter(PkgZipWriterT *w);
-void pkgZipAbort(PkgZipWriterT *w);
-bool pkgZipAddFile(PkgZipWriterT *w, const char *name, const char *path);
-bool pkgZipAddMem(PkgZipWriterT *w, const char *name, const void *data, uint32_t len, time_t when);
-PkgZipWriterT *pkgZipCreate(const char *path);
-bool pkgZipFinish(PkgZipWriterT *w);
-static bool writeBytes(PkgZipWriterT *w, const void *data, uint32_t len);
+static PkgZipStepE entryEnd(PkgZipWriterT *w);
+static ZipDirEntryT *entryNew(PkgZipWriterT *w, const char *name, uint32_t dataMax);
+static void freeWriter(PkgZipWriterT *w);
+static void headerFill(const ZipDirEntryT *e, uint8_t hdr[PKG_ZIP_LOCAL_SIZE]);
+static bool headerWrite(PkgZipWriterT *w, const ZipDirEntryT *e);
+void pkgZipAbort(PkgZipWriterT *w);
+uint32_t pkgZipAdded(const PkgZipWriterT *w);
+bool pkgZipAddMem(PkgZipWriterT *w, const char *name, const void *data, uint32_t len, time_t when);
+bool pkgZipBeginCopy(PkgZipWriterT *w, const PkgZipEntryT *e);
+bool pkgZipBeginFile(PkgZipWriterT *w, const char *name, const char *path);
+bool pkgZipBeginMem(PkgZipWriterT *w, const char *name, const void *data, uint32_t len, time_t when);
+bool pkgZipCopyWrite(void *ctx, const uint8_t *data, int32_t len);
+PkgZipWriterT *pkgZipCreate(const char *path);
+bool pkgZipFinish(PkgZipWriterT *w);
+PkgZipStepE pkgZipStep(PkgZipWriterT *w, int32_t maxWork);
+static mz_bool putDeflated(const void *buf, int len, void *user);
+static bool sourceBegin(PkgZipWriterT *w, const char *name, time_t when, uint32_t len);
+static const uint8_t *sourceRead(PkgZipWriterT *w, uint32_t len);
+static bool storeInstead(PkgZipWriterT *w);
+static bool writeBytes(PkgZipWriterT *w, const void *data, uint32_t len);
-// A time as the ZIP's DOS date and time (1980 .. 2107, two-second steps).
-static void dosStamp(time_t when, uint16_t *date, uint16_t *time) {
- struct tm *t = localtime(&when);
- int32_t year = t ? t->tm_year + TM_YEAR_BASE - DOS_YEAR_BASE : 0;
+// The entry being added from a source is in: its source closed and its
+// local header completed (a copied entry's was complete from the start).
+static PkgZipStepE entryEnd(PkgZipWriterT *w) {
+ ZipDirEntryT *e = &w->entries[w->count - 1];
+ bool copied = w->adding == ZipCopyE;
+ uint8_t hdr[PKG_ZIP_LOCAL_SIZE];
- if (!t || year < 0) {
- *date = (uint16_t)((1 << DOS_MONTH_SHIFT) | 1);
- *time = 0;
- return;
+ if (w->src) {
+ fclose(w->src);
}
- if (year > DOS_YEAR_MAX) {
- year = DOS_YEAR_MAX;
+ w->src = NULL;
+ w->mem = NULL;
+ w->adding = ZipIdleE;
+
+ if (copied) {
+ return PkgZipFileDoneE;
}
- *date = (uint16_t)((year << DOS_YEAR_SHIFT) | ((t->tm_mon + 1) << DOS_MONTH_SHIFT) | t->tm_mday);
- *time = (uint16_t)((t->tm_hour << DOS_HOUR_SHIFT) | (t->tm_min << DOS_MINUTE_SHIFT) | (t->tm_sec / DOS_SECOND_UNIT));
+ e->size = w->srcLen;
+ e->packed = e->method == PKG_ZIP_STORED ? w->srcLen : e->packed;
+ headerFill(e, hdr);
+
+ if (fseek(w->f, (long)e->offset, SEEK_SET) != 0 || fwrite(hdr, 1, sizeof(hdr), w->f) != sizeof(hdr) || fseek(w->f, (long)w->offset, SEEK_SET) != 0) {
+ w->failed = true;
+ return PkgZipFailE;
+ }
+
+ return PkgZipFileDoneE;
+}
+
+
+// A new entry named name, at most dataMax bytes of data, onto the central
+// directory (its offset set; the caller fills the rest). NULL, leaving
+// the writer as it was, when the name will not do, the ZIP would be too
+// big, or out of memory.
+static ZipDirEntryT *entryNew(PkgZipWriterT *w, const char *name, uint32_t dataMax) {
+ size_t nameLen = strlen(name);
+
+ if (w->failed || w->adding != ZipIdleE || nameLen == 0 || nameLen >= PKG_NAME_MAX || w->count >= (int32_t)ZIP_MAX_ENTRIES) {
+ return NULL;
+ }
+
+ if ((uint64_t)w->offset + PKG_ZIP_LOCAL_SIZE + nameLen + dataMax > ZIP_MAX_OFFSET) {
+ return NULL;
+ }
+
+ if (w->count == w->cap) {
+ int32_t cap = w->cap ? w->cap * 2 : ZIP_DIR_FIRST;
+ ZipDirEntryT *grow = (ZipDirEntryT *)realloc(w->entries, (size_t)cap * sizeof(ZipDirEntryT));
+
+ if (!grow) {
+ return NULL;
+ }
+
+ w->entries = grow;
+ w->cap = cap;
+ }
+
+ ZipDirEntryT *e = &w->entries[w->count];
+
+ memset(e, 0, sizeof(*e));
+ e->name = strdup(name);
+ e->offset = w->offset;
+
+ if (!e->name) {
+ return NULL;
+ }
+
+ w->count++;
+ return e;
}
@@ -110,12 +185,41 @@ static void freeWriter(PkgZipWriterT *w) {
free(w->entries[i].name);
}
+ if (w->src) {
+ fclose(w->src);
+ }
+
+ free(w->deflator);
free(w->entries);
free(w->path);
free(w);
}
+// e's local header (its name follows it).
+static void headerFill(const ZipDirEntryT *e, uint8_t hdr[PKG_ZIP_LOCAL_SIZE]) {
+ memset(hdr, 0, PKG_ZIP_LOCAL_SIZE);
+ dvxPutLe32(hdr, PKG_ZIP_LOCAL_SIG);
+ dvxPutLe16(hdr + PKG_ZIP_LH_NEEDED, PKG_ZIP_VERSION);
+ dvxPutLe16(hdr + PKG_ZIP_LH_METHOD, e->method);
+ dvxPutLe16(hdr + PKG_ZIP_LH_TIME, e->dosTime);
+ dvxPutLe16(hdr + PKG_ZIP_LH_DATE, e->dosDate);
+ dvxPutLe32(hdr + PKG_ZIP_LH_CRC, e->crc);
+ dvxPutLe32(hdr + PKG_ZIP_LH_PACKED, e->packed);
+ dvxPutLe32(hdr + PKG_ZIP_LH_SIZE, e->size);
+ dvxPutLe16(hdr + PKG_ZIP_LH_NAME_LEN, (uint16_t)strlen(e->name));
+}
+
+
+// e's local header and name, at the end.
+static bool headerWrite(PkgZipWriterT *w, const ZipDirEntryT *e) {
+ uint8_t hdr[PKG_ZIP_LOCAL_SIZE];
+
+ headerFill(e, hdr);
+ return writeBytes(w, hdr, sizeof(hdr)) && writeBytes(w, e->name, (uint32_t)strlen(e->name));
+}
+
+
void pkgZipAbort(PkgZipWriterT *w) {
if (!w) {
return;
@@ -130,95 +234,102 @@ void pkgZipAbort(PkgZipWriterT *w) {
}
-bool pkgZipAddFile(PkgZipWriterT *w, const char *name, const char *path) {
+// A file stored after deflating it did no good has been read through
+// once already: it counts as done.
+uint32_t pkgZipAdded(const PkgZipWriterT *w) {
+ if (w->adding == ZipIdleE || w->adding == ZipCopyE) {
+ return 0;
+ }
+
+ return w->storedAfter ? w->srcLen : w->srcDone;
+}
+
+
+bool pkgZipAddMem(PkgZipWriterT *w, const char *name, const void *data, uint32_t len, time_t when) {
+ PkgZipStepE step;
+
+ if (!pkgZipBeginMem(w, name, data, len, when)) {
+ return false;
+ }
+
+ while ((step = pkgZipStep(w, INT32_MAX)) == PkgZipMoreE) {
+ }
+
+ return step == PkgZipFileDoneE;
+}
+
+
+bool pkgZipBeginCopy(PkgZipWriterT *w, const PkgZipEntryT *e) {
+ ZipDirEntryT *d = e->isDir ? NULL : entryNew(w, e->name, e->packedSize);
+
+ if (!d) {
+ return false;
+ }
+
+ d->crc = e->crc;
+ d->packed = e->packedSize;
+ d->size = e->size;
+ d->method = e->method;
+ d->dosDate = e->dosDate;
+ d->dosTime = e->dosTime;
+ w->left = e->packedSize;
+ w->adding = w->left ? ZipCopyE : ZipIdleE;
+ return headerWrite(w, d);
+}
+
+
+bool pkgZipBeginFile(PkgZipWriterT *w, const char *name, const char *path) {
struct stat st;
if (stat(path, &st) != 0 || (uint64_t)st.st_size > ZIP_MAX_OFFSET) {
return false;
}
- uint32_t len = (uint32_t)st.st_size;
- uint8_t *data = (uint8_t *)malloc(len ? len : 1);
- FILE *f = data ? fopen(path, "rb") : NULL;
- bool ok = f && fread(data, 1, len, f) == len;
+ FILE *src = fopen(path, "rb");
- if (f) {
- fclose(f);
+ if (src == NULL) {
+ return false;
}
- ok = ok && pkgZipAddMem(w, name, data, len, st.st_mtime);
- free(data);
- return ok;
+ if (!sourceBegin(w, name, st.st_mtime, (uint32_t)st.st_size)) {
+ fclose(src);
+ return false;
+ }
+
+ w->src = src;
+ return true;
}
-bool pkgZipAddMem(PkgZipWriterT *w, const char *name, const void *data, uint32_t len, time_t when) {
- size_t nameLen = strlen(name);
- size_t packed = 0;
- int flags = (int)tdefl_create_comp_flags_from_zip_params(MZ_UBER_COMPRESSION, -MZ_DEFAULT_WINDOW_BITS, MZ_DEFAULT_STRATEGY);
- void *deflated = len ? tdefl_compress_mem_to_heap(data, len, &packed, flags) : NULL;
- uint8_t hdr[PKG_ZIP_LOCAL_SIZE];
-
- if (w->failed || nameLen == 0 || nameLen >= PKG_NAME_MAX || w->count >= (int32_t)ZIP_MAX_ENTRIES) {
- free(deflated);
+bool pkgZipBeginMem(PkgZipWriterT *w, const char *name, const void *data, uint32_t len, time_t when) {
+ if (!sourceBegin(w, name, when, len)) {
return false;
}
- // Stored when deflating does not make it smaller
- bool stored = !deflated || packed >= len;
- const void *body = stored ? data : deflated;
- uint32_t bodyLen = stored ? len : (uint32_t)packed;
+ w->mem = (const uint8_t *)data;
+ return true;
+}
- if ((uint64_t)w->offset + PKG_ZIP_LOCAL_SIZE + nameLen + bodyLen > ZIP_MAX_OFFSET) {
- free(deflated);
+
+bool pkgZipCopyWrite(void *ctx, const uint8_t *data, int32_t len) {
+ PkgZipWriterT *w = (PkgZipWriterT *)ctx;
+
+ if (w->adding != ZipCopyE || len < 0 || (uint32_t)len > w->left) {
+ w->failed = true;
return false;
}
- if (w->count == w->cap) {
- int32_t cap = w->cap ? w->cap * 2 : ZIP_DIR_FIRST;
- ZipDirEntryT *grow = (ZipDirEntryT *)realloc(w->entries, (size_t)cap * sizeof(ZipDirEntryT));
-
- if (!grow) {
- free(deflated);
- return false;
- }
-
- w->entries = grow;
- w->cap = cap;
- }
-
- ZipDirEntryT *e = &w->entries[w->count];
-
- memset(e, 0, sizeof(*e));
- e->name = strdup(name);
- e->crc = (uint32_t)mz_crc32(MZ_CRC32_INIT, (const uint8_t *)data, len);
- e->packed = bodyLen;
- e->size = len;
- e->offset = w->offset;
- e->method = stored ? PKG_ZIP_STORED : PKG_ZIP_DEFLATED;
- dosStamp(when, &e->dosDate, &e->dosTime);
-
- if (!e->name) {
- free(deflated);
+ if (!writeBytes(w, data, (uint32_t)len)) {
return false;
}
- w->count++;
- memset(hdr, 0, sizeof(hdr));
- pkgPut32(hdr, PKG_ZIP_LOCAL_SIG);
- pkgPut16(hdr + PKG_ZIP_LH_NEEDED, PKG_ZIP_VERSION);
- pkgPut16(hdr + PKG_ZIP_LH_METHOD, e->method);
- pkgPut16(hdr + PKG_ZIP_LH_TIME, e->dosTime);
- pkgPut16(hdr + PKG_ZIP_LH_DATE, e->dosDate);
- pkgPut32(hdr + PKG_ZIP_LH_CRC, e->crc);
- pkgPut32(hdr + PKG_ZIP_LH_PACKED, e->packed);
- pkgPut32(hdr + PKG_ZIP_LH_SIZE, e->size);
- pkgPut16(hdr + PKG_ZIP_LH_NAME_LEN, (uint16_t)nameLen);
+ w->left -= (uint32_t)len;
- bool ok = writeBytes(w, hdr, sizeof(hdr)) && writeBytes(w, name, (uint32_t)nameLen) && writeBytes(w, body, bodyLen);
+ if (w->left == 0) {
+ entryEnd(w);
+ }
- free(deflated);
- return ok;
+ return true;
}
@@ -244,23 +355,28 @@ PkgZipWriterT *pkgZipCreate(const char *path) {
bool pkgZipFinish(PkgZipWriterT *w) {
uint32_t dirStart = w->offset;
+ // An entry not all in cannot be listed
+ if (w->adding != ZipIdleE) {
+ w->failed = true;
+ }
+
for (int32_t i = 0; i < w->count && !w->failed; i++) {
const ZipDirEntryT *e = &w->entries[i];
uint8_t rec[PKG_ZIP_CENTRAL_SIZE];
size_t nameLen = strlen(e->name);
memset(rec, 0, sizeof(rec));
- pkgPut32(rec, PKG_ZIP_CENTRAL_SIG);
- pkgPut16(rec + PKG_ZIP_CD_MADE_BY, PKG_ZIP_VERSION);
- pkgPut16(rec + PKG_ZIP_CD_NEEDED, PKG_ZIP_VERSION);
- pkgPut16(rec + PKG_ZIP_CD_METHOD, e->method);
- pkgPut16(rec + PKG_ZIP_CD_TIME, e->dosTime);
- pkgPut16(rec + PKG_ZIP_CD_DATE, e->dosDate);
- pkgPut32(rec + PKG_ZIP_CD_CRC, e->crc);
- pkgPut32(rec + PKG_ZIP_CD_PACKED, e->packed);
- pkgPut32(rec + PKG_ZIP_CD_SIZE, e->size);
- pkgPut16(rec + PKG_ZIP_CD_NAME_LEN, (uint16_t)nameLen);
- pkgPut32(rec + PKG_ZIP_CD_OFFSET, e->offset);
+ dvxPutLe32(rec, PKG_ZIP_CENTRAL_SIG);
+ dvxPutLe16(rec + PKG_ZIP_CD_MADE_BY, PKG_ZIP_VERSION);
+ dvxPutLe16(rec + PKG_ZIP_CD_NEEDED, PKG_ZIP_VERSION);
+ dvxPutLe16(rec + PKG_ZIP_CD_METHOD, e->method);
+ dvxPutLe16(rec + PKG_ZIP_CD_TIME, e->dosTime);
+ dvxPutLe16(rec + PKG_ZIP_CD_DATE, e->dosDate);
+ dvxPutLe32(rec + PKG_ZIP_CD_CRC, e->crc);
+ dvxPutLe32(rec + PKG_ZIP_CD_PACKED, e->packed);
+ dvxPutLe32(rec + PKG_ZIP_CD_SIZE, e->size);
+ dvxPutLe16(rec + PKG_ZIP_CD_NAME_LEN, (uint16_t)nameLen);
+ dvxPutLe32(rec + PKG_ZIP_CD_OFFSET, e->offset);
writeBytes(w, rec, sizeof(rec));
writeBytes(w, e->name, (uint32_t)nameLen);
}
@@ -268,11 +384,11 @@ bool pkgZipFinish(PkgZipWriterT *w) {
uint8_t eocd[PKG_ZIP_EOCD_SIZE];
memset(eocd, 0, sizeof(eocd));
- pkgPut32(eocd, PKG_ZIP_EOCD_SIG);
- pkgPut16(eocd + PKG_ZIP_EOCD_DISK_ENTRIES, (uint16_t)w->count);
- pkgPut16(eocd + PKG_ZIP_EOCD_ENTRIES, (uint16_t)w->count);
- pkgPut32(eocd + PKG_ZIP_EOCD_DIR_SIZE, w->offset - dirStart);
- pkgPut32(eocd + PKG_ZIP_EOCD_DIR_OFFSET, dirStart);
+ dvxPutLe32(eocd, PKG_ZIP_EOCD_SIG);
+ dvxPutLe16(eocd + PKG_ZIP_EOCD_DISK_ENTRIES, (uint16_t)w->count);
+ dvxPutLe16(eocd + PKG_ZIP_EOCD_ENTRIES, (uint16_t)w->count);
+ dvxPutLe32(eocd + PKG_ZIP_EOCD_DIR_SIZE, w->offset - dirStart);
+ dvxPutLe32(eocd + PKG_ZIP_EOCD_DIR_OFFSET, dirStart);
writeBytes(w, eocd, sizeof(eocd));
FILE *f = w->f;
@@ -290,6 +406,153 @@ bool pkgZipFinish(PkgZipWriterT *w) {
}
+// At least one chunk of the source each call (none for a copied entry:
+// its bytes come through pkgZipCopyWrite).
+PkgZipStepE pkgZipStep(PkgZipWriterT *w, int32_t maxWork) {
+ if (w->failed) {
+ return PkgZipFailE;
+ }
+
+ if (w->adding == ZipIdleE) {
+ return PkgZipFileDoneE;
+ }
+
+ if (w->adding == ZipCopyE) {
+ return PkgZipMoreE;
+ }
+
+ for (int32_t work = 0; ; ) {
+ ZipDirEntryT *e = &w->entries[w->count - 1];
+
+ if (w->adding == ZipStoreE && w->srcDone == w->srcLen) {
+ return entryEnd(w);
+ }
+
+ if (work > 0 && work >= maxWork) {
+ return PkgZipMoreE;
+ }
+
+ uint32_t n = w->srcLen - w->srcDone;
+ const uint8_t *in;
+
+ if (n > ZIP_CHUNK) {
+ n = ZIP_CHUNK;
+ }
+
+ in = sourceRead(w, n);
+
+ if (!in) {
+ w->failed = true;
+ return PkgZipFailE;
+ }
+
+ e->crc = (uint32_t)mz_crc32(e->crc, in, n);
+ w->srcDone += n;
+ work += (int32_t)n;
+
+ if (w->adding == ZipStoreE) {
+ if (!writeBytes(w, in, n)) {
+ return PkgZipFailE;
+ }
+
+ continue;
+ }
+
+ tdefl_status status = tdefl_compress_buffer(w->deflator, in, n, w->srcDone == w->srcLen ? TDEFL_FINISH : TDEFL_NO_FLUSH);
+
+ if (w->storedAfter) {
+ if (!storeInstead(w)) {
+ return PkgZipFailE;
+ }
+ } else if (status == TDEFL_STATUS_DONE) {
+ return entryEnd(w);
+ } else if (status != TDEFL_STATUS_OKAY) {
+ w->failed = true;
+ return PkgZipFailE;
+ }
+ }
+}
+
+
+// tdefl's output: written, unless it comes to the source's size (stored
+// instead: deflating stops). So what was written is always shorter than
+// the stored data written over it.
+static mz_bool putDeflated(const void *buf, int len, void *user) {
+ PkgZipWriterT *w = (PkgZipWriterT *)user;
+ ZipDirEntryT *e = &w->entries[w->count - 1];
+
+ if ((uint64_t)e->packed + (uint32_t)len >= w->srcLen) {
+ w->storedAfter = true;
+ return MZ_FALSE;
+ }
+
+ e->packed += (uint32_t)len;
+ return writeBytes(w, buf, (uint32_t)len) ? MZ_TRUE : MZ_FALSE;
+}
+
+
+// Start an entry of len bytes from a source (set by the caller): its
+// local header written with what is known, completed by entryEnd.
+static bool sourceBegin(PkgZipWriterT *w, const char *name, time_t when, uint32_t len) {
+ int flags = (int)tdefl_create_comp_flags_from_zip_params(MZ_UBER_COMPRESSION, -MZ_DEFAULT_WINDOW_BITS, MZ_DEFAULT_STRATEGY);
+ ZipDirEntryT *e = entryNew(w, name, len);
+
+ if (!e) {
+ return false;
+ }
+
+ // Nothing to deflate in an empty one
+ e->crc = (uint32_t)MZ_CRC32_INIT;
+ e->method = len ? PKG_ZIP_DEFLATED : PKG_ZIP_STORED;
+ w->adding = len ? ZipDeflateE : ZipStoreE;
+ w->srcLen = len;
+ w->srcDone = 0;
+ w->storedAfter = false;
+ pkgZipDosStamp(when, &e->dosDate, &e->dosTime);
+
+ if (len && !w->deflator) {
+ w->deflator = (tdefl_compressor *)malloc(sizeof(tdefl_compressor));
+ }
+
+ if (len && (!w->deflator || tdefl_init(w->deflator, putDeflated, w, flags) != TDEFL_STATUS_OKAY)) {
+ w->failed = true;
+ return false;
+ }
+
+ return headerWrite(w, e);
+}
+
+
+// The next len bytes of the source; NULL when it cannot be read.
+static const uint8_t *sourceRead(PkgZipWriterT *w, uint32_t len) {
+ if (!w->src) {
+ return w->mem + w->srcDone;
+ }
+
+ return fread(w->buf, 1, len, w->src) == len ? w->buf : NULL;
+}
+
+
+// Deflating came to the source's size: write it again from its start,
+// stored, over what was deflated.
+static bool storeInstead(PkgZipWriterT *w) {
+ ZipDirEntryT *e = &w->entries[w->count - 1];
+ uint32_t data = e->offset + PKG_ZIP_LOCAL_SIZE + (uint32_t)strlen(e->name);
+
+ if ((w->src && fseek(w->src, 0, SEEK_SET) != 0) || fseek(w->f, (long)data, SEEK_SET) != 0) {
+ w->failed = true;
+ return false;
+ }
+
+ e->crc = (uint32_t)MZ_CRC32_INIT;
+ e->method = PKG_ZIP_STORED;
+ w->offset = data;
+ w->srcDone = 0;
+ w->adding = ZipStoreE;
+ return true;
+}
+
+
static bool writeBytes(PkgZipWriterT *w, const void *data, uint32_t len) {
if (w->failed || (len > 0 && fwrite(data, 1, len, w->f) != len)) {
w->failed = true;
diff --git a/src/libs/kpunch/pkg/thirdparty/monocypher/DVX-VERSION.md b/src/libs/kpunch/pkg/thirdparty/monocypher/DVX-VERSION.md
deleted file mode 100644
index 2bda8a3a..00000000
--- a/src/libs/kpunch/pkg/thirdparty/monocypher/DVX-VERSION.md
+++ /dev/null
@@ -1,31 +0,0 @@
-# Monocypher for DVX
-
-| Item | Value |
-|---|---|
-| Project | Monocypher, a small, auditable crypto library, https://monocypher.org |
-| Version | 4.0.3 (2026-06-15; fixes a timing leak in EdDSA/Ed25519 signing) |
-| Download | https://monocypher.org/download/monocypher-4.0.3.tar.gz |
-| Tarball SHA-512 | 40904ada5c7ee4f7741733e38b69a30a4b0561cbffba5ffe7c2dce16136d540251ec0d9056ff606510d3b5b708fb8a40db7e0870d4a0b2dc17ba2bfb880f8965 (matches the .sha512 published beside the download) |
-| Files kept | src/monocypher.c, src/monocypher.h, src/optional/monocypher-ed25519.c, src/optional/monocypher-ed25519.h, LICENCE.md, AUTHORS.md -- byte for byte as in the tarball, in the same folders. Not kept: doc/, tests/, the makefile, README, CHANGELOG. |
-| Licence | Dual: 2-clause BSD or CC-0 (LICENCE.md); DVX takes the BSD licence. Keep the notice with the source. |
-| Patches | None |
-
-## Where it is used
-
-- `pkg.lib` (src/libs/kpunch/pkg): Ed25519 (the SHA-512 flavour in
- optional/) checks the signatures of add-on packages (pkgSign.c);
- crypto_wipe clears key material.
-- The package tool (`bin/host/dvxpkg`, and DVXPKG.EXE in DVX): makes keys
- and signs packages.
-- Host tests (src/test/pkg): an RFC 8032 vector checks the wiring.
-
-It compiles with the normal DVX warning flags for DJGPP and the host; no
-flags are relaxed for it.
-
-## Updating
-
-1. Download the new tarball, check its SHA-512 against the .sha512 file
- on the download page, record the new version and hash here.
-2. Replace the six files above, nothing else.
-3. Rebuild (`make`), run `make -C src/test/pkg` (the RFC 8032 vector must
- still match) and `make -C src/test/setup`.
diff --git a/src/libs/kpunch/pkgnet/pkgNet.c b/src/libs/kpunch/pkgnet/pkgNet.c
index fe5b22ab..ddfa5387 100644
--- a/src/libs/kpunch/pkgnet/pkgNet.c
+++ b/src/libs/kpunch/pkgnet/pkgNet.c
@@ -44,7 +44,8 @@
#include "dvxPrefs.h"
#include "dvxPlat.h"
#include "dvxTypes.h"
-#include "serial/security/secHash.h"
+#include "crypto/secHash.h"
+#include "crypto/security.h"
#include "stb_ds_wrap.h"
#include
@@ -146,6 +147,9 @@ struct PkgNetS {
PkgNetMineT *mine; // stb_ds: what the account has sent
char current[PKG_TEXT_MAX];
char err[PKGNET_ERR_MAX];
+ int32_t polling; // pkgNetPoll calls under way
+ bool cancelAsked; // pkgNetCancel asked meanwhile: done when they return
+ bool freeAsked; // pkgNetFree likewise
};
DVX_ABI_STAMP(pkgnet);
@@ -169,7 +173,6 @@ static bool entryRead(const JsonDocT *doc, int32_t node, PkgNetEn
static bool failed(PkgNetT *n, const char *fmt, ...);
static bool fetchPlan(PkgNetT *n);
static uint32_t fileSize(const char *path);
-static bool hexRead(const char *text, uint8_t *out, int32_t outLen);
static void itemAdd(PkgNetT *n, const PkgNetEntryT *e);
static void itemApiPath(const PkgNetT *n, const char *api, char *out, int32_t outSize);
static const PkgNetEntryT *itemEntry(const PkgNetT *n);
@@ -203,14 +206,15 @@ PkgNetStateE pkgNetPoll(PkgNetT *n, uint32_t nowMs);
void pkgNetProgress(const PkgNetT *n, uint32_t *done, uint32_t *total);
bool pkgNetUpdatesStart(PkgNetT *n, uint32_t nowMs);
bool pkgNetUploadStart(PkgNetT *n, const char *path, uint32_t nowMs);
+static PkgNetStateE pollStep(PkgNetT *n, uint32_t nowMs);
static bool requestBody(PkgNetT *n, const void *data, int32_t len, const char *type);
+static bool requestBodyFile(PkgNetT *n, const char *path, const char *type);
static void requestGo(PkgNetT *n, uint32_t nowMs);
static bool requestMake(PkgNetT *n, const char *method, const char *path, StageE stage);
static void signatureAnswer(PkgNetT *n, uint32_t nowMs);
static PkgNetStateE stateOf(const PkgNetT *n);
static void textCopy(char *out, int32_t outSize, const char *text);
static void updatesBody(PkgNetT *n, char **body);
-static void wipe(void *p, size_t len);
// What the server said to an upload or a list of the account's versions:
@@ -247,20 +251,12 @@ static void accountNext(PkgNetT *n, uint32_t nowMs) {
snprintf(auth, sizeof(auth), "%s%s", BEARER, n->token);
+ // The package is sent from its file as the server takes it
if (n->job == JobUploadE) {
- int32_t len = 0;
- char *data = platformReadFile(n->upload, &len);
-
- if (!data) {
- failed(n, "%s cannot be read.", n->upload);
- return;
- }
-
- if (requestMake(n, METHOD_POST, API_UPLOAD, StageUploadE) && httpHeaderAdd(n->req, HEADER_AUTH, auth) && requestBody(n, data, len, TYPE_PACKAGE)) {
+ if (requestMake(n, METHOD_POST, API_UPLOAD, StageUploadE) && httpHeaderAdd(n->req, HEADER_AUTH, auth) && requestBodyFile(n, n->upload, TYPE_PACKAGE)) {
requestGo(n, nowMs);
}
- free(data);
return;
}
@@ -486,7 +482,7 @@ static bool entryRead(const JsonDocT *doc, int32_t node, PkgNetEntryT *e) {
return false;
}
- if (!hexRead(jsonText(doc, jsonFind(doc, node, "sha256")), e->sha256, PKG_SHA256_SIZE) || size < 0 || size > SIZE_MAX_JSON) {
+ if (!pkgHexDecode(jsonText(doc, jsonFind(doc, node, "sha256")), e->sha256, PKG_SHA256_SIZE) || size < 0 || size > SIZE_MAX_JSON) {
return false;
}
@@ -581,27 +577,6 @@ static uint32_t fileSize(const char *path) {
}
-// outLen bytes from text, two hex digits each. False when text is not
-// exactly that.
-static bool hexRead(const char *text, uint8_t *out, int32_t outLen) {
- if ((int32_t)strlen(text) != outLen * 2) {
- return false;
- }
-
- for (int32_t i = 0; i < outLen; i++) {
- unsigned int byte;
-
- if (sscanf(text + i * 2, "%2x", &byte) != 1) {
- return false;
- }
-
- out[i] = (uint8_t)byte;
- }
-
- return true;
-}
-
-
// Add e to what the fetch downloads (once), its files named from its id
// and version.
static void itemAdd(PkgNetT *n, const PkgNetEntryT *e) {
@@ -761,13 +736,13 @@ static void loginStart(PkgNetT *n, uint32_t nowMs) {
jsonQuote(n->user, user, sizeof(user));
jsonQuote(n->password, password, sizeof(password));
len = snprintf(body, sizeof(body), "{\"email\":%s,\"password\":%s}", user, password);
- wipe(password, sizeof(password));
+ secSecureZero(password, (int)sizeof(password));
if (requestMake(n, METHOD_POST, API_LOGIN, StageLoginE) && requestBody(n, body, len, TYPE_JSON)) {
requestGo(n, nowMs);
}
- wipe(body, sizeof(body));
+ secSecureZero(body, (int)sizeof(body));
}
@@ -826,12 +801,17 @@ void pkgNetAccountSet(PkgNetT *n, const char *user, const char *password) {
if (n) {
textCopy(n->user, sizeof(n->user), user ? user : "");
textCopy(n->password, sizeof(n->password), password ? password : "");
- wipe(n->token, sizeof(n->token));
+ secSecureZero(n->token, (int)sizeof(n->token));
}
}
void pkgNetCancel(PkgNetT *n) {
+ if (n && n->polling > 0) {
+ n->cancelAsked = true;
+ return;
+ }
+
if (n && busy(n)) {
jobForget(n);
failed(n, "Stopped before it finished.");
@@ -1001,13 +981,18 @@ void pkgNetFree(PkgNetT *n) {
return;
}
+ if (n->polling > 0) {
+ n->freeAsked = true;
+ return;
+ }
+
jobForget(n);
pkgDbFree(&n->db);
entriesFree(&n->catalogue);
entriesFree(&n->updates);
arrfree(n->mine);
- wipe(n->password, sizeof(n->password));
- wipe(n->token, sizeof(n->token));
+ secSecureZero(n->password, (int)sizeof(n->password));
+ secSecureZero(n->token, (int)sizeof(n->token));
free(n);
}
@@ -1033,13 +1018,98 @@ bool pkgNetMineStart(PkgNetT *n, uint32_t nowMs) {
}
+// A cancel or free asked for during a poll (a wait for the network inside
+// it lets other code run) is carried out when the poll returns: the
+// request and the items it works on outlive the code still using them.
+// After a free the poll reports failure.
PkgNetStateE pkgNetPoll(PkgNetT *n, uint32_t nowMs) {
- HttpStateE st;
-
if (!n) {
return PkgNetFailedE;
}
+ n->polling++;
+
+ PkgNetStateE st = pollStep(n, nowMs);
+
+ n->polling--;
+
+ if (n->polling == 0 && n->freeAsked) {
+ n->freeAsked = false;
+ pkgNetFree(n);
+ return PkgNetFailedE;
+ }
+
+ if (n->polling == 0 && n->cancelAsked) {
+ n->cancelAsked = false;
+ pkgNetCancel(n);
+ st = stateOf(n);
+ }
+
+ return st;
+}
+
+
+void pkgNetProgress(const PkgNetT *n, uint32_t *done, uint32_t *total) {
+ uint32_t got = 0;
+ uint32_t size = 0;
+
+ *done = 0;
+ *total = 0;
+
+ if (!n || n->job != JobFetchE) {
+ return;
+ }
+
+ *done = n->doneBytes;
+ *total = n->totalBytes;
+
+ if (n->stage == StagePackageE) {
+ httpProgress(n->req, &got, &size);
+ *done += got;
+ } else if (n->stage == StageCheckE || n->stage == StageSignatureE) {
+ *done += itemEntry(n)->size;
+ }
+
+ if (*done > *total) {
+ *done = *total;
+ }
+}
+
+
+bool pkgNetUpdatesStart(PkgNetT *n, uint32_t nowMs) {
+ char *body = NULL;
+
+ if (!jobStart(n, JobUpdatesE)) {
+ return false;
+ }
+
+ updatesBody(n, &body);
+
+ if (requestMake(n, METHOD_POST, API_UPDATES, StageUpdatesE) && requestBody(n, body, (int32_t)arrlen(body), TYPE_JSON)) {
+ requestGo(n, nowMs);
+ }
+
+ arrfree(body);
+ return true;
+}
+
+
+bool pkgNetUploadStart(PkgNetT *n, const char *path, uint32_t nowMs) {
+ if (!jobStart(n, JobUploadE)) {
+ return false;
+ }
+
+ textCopy(n->upload, sizeof(n->upload), path ? path : "");
+ textCopy(n->current, sizeof(n->current), platformPathBaseName(n->upload));
+ accountNext(n, nowMs);
+ return true;
+}
+
+
+// pkgNetPoll's work: one step of the job.
+static PkgNetStateE pollStep(PkgNetT *n, uint32_t nowMs) {
+ HttpStateE st;
+
if (n->stage == StageCheckE) {
checkStep(n, nowMs);
return stateOf(n);
@@ -1104,63 +1174,6 @@ PkgNetStateE pkgNetPoll(PkgNetT *n, uint32_t nowMs) {
}
-void pkgNetProgress(const PkgNetT *n, uint32_t *done, uint32_t *total) {
- uint32_t got = 0;
- uint32_t size = 0;
-
- *done = 0;
- *total = 0;
-
- if (!n || n->job != JobFetchE) {
- return;
- }
-
- *done = n->doneBytes;
- *total = n->totalBytes;
-
- if (n->stage == StagePackageE) {
- httpProgress(n->req, &got, &size);
- *done += got;
- } else if (n->stage == StageCheckE || n->stage == StageSignatureE) {
- *done += itemEntry(n)->size;
- }
-
- if (*done > *total) {
- *done = *total;
- }
-}
-
-
-bool pkgNetUpdatesStart(PkgNetT *n, uint32_t nowMs) {
- char *body = NULL;
-
- if (!jobStart(n, JobUpdatesE)) {
- return false;
- }
-
- updatesBody(n, &body);
-
- if (requestMake(n, METHOD_POST, API_UPDATES, StageUpdatesE) && requestBody(n, body, (int32_t)arrlen(body), TYPE_JSON)) {
- requestGo(n, nowMs);
- }
-
- arrfree(body);
- return true;
-}
-
-
-bool pkgNetUploadStart(PkgNetT *n, const char *path, uint32_t nowMs) {
- if (!jobStart(n, JobUploadE)) {
- return false;
- }
-
- textCopy(n->upload, sizeof(n->upload), path ? path : "");
- textCopy(n->current, sizeof(n->current), platformPathBaseName(n->upload));
- accountNext(n, nowMs);
- return true;
-}
-
-
// A body for the request made (data copied), of type. False (the job
// failed) when out of memory.
static bool requestBody(PkgNetT *n, const void *data, int32_t len, const char *type) {
@@ -1168,6 +1181,12 @@ static bool requestBody(PkgNetT *n, const void *data, int32_t len, const char *t
}
+// The request's body from the file at path, sent as the server takes it.
+static bool requestBodyFile(PkgNetT *n, const char *path, const char *type) {
+ return httpBodyFile(n->req, path, type) || failed(n, "%s cannot be read.", path);
+}
+
+
// Start the request made (with its headers and body).
static void requestGo(PkgNetT *n, uint32_t nowMs) {
if (!httpStart(n->req, nowMs)) {
@@ -1261,13 +1280,3 @@ static void updatesBody(PkgNetT *n, char **body) {
arrput(*body, ']');
arrput(*body, '}');
}
-
-
-// Overwrite len bytes at p (a password, a token) before the memory goes.
-static void wipe(void *p, size_t len) {
- volatile uint8_t *b = (volatile uint8_t *)p;
-
- for (size_t i = 0; i < len; i++) {
- b[i] = 0;
- }
-}
diff --git a/src/libs/kpunch/pkgnet/pkgNetBas.c b/src/libs/kpunch/pkgnet/pkgNetBas.c
index 53f1e7c7..14aea3e5 100644
--- a/src/libs/kpunch/pkgnet/pkgNetBas.c
+++ b/src/libs/kpunch/pkgnet/pkgNetBas.c
@@ -71,6 +71,7 @@ const char *PkgNetField(int32_t h, const char *id, int32_t field)
int32_t PkgNetMine(int32_t h);
const char *PkgNetMineRows(int32_t h);
int32_t PkgNetOpen(const char *server);
+int32_t PkgNetOpenIn(const char *server, const char *root, const char *downloadDir);
const char *PkgNetPassword(void);
int32_t PkgNetPercent(int32_t h);
int32_t PkgNetPort(void);
@@ -290,6 +291,16 @@ const char *PkgNetField(int32_t h, const char *id, int32_t field) {
snprintf(text, sizeof(text), "%lu", (unsigned long)e->size);
break;
+ case PkgFieldSha256E: {
+ char hex[PKG_SHA256_TEXT];
+
+ pkgHexEncode(e->sha256, PKG_SHA256_SIZE, hex);
+ return basTextSet(&sText, hex);
+ }
+
+ case PkgFieldRepoSignedE:
+ return basTextSet(&sText, e->repoSigned ? PKG_BAS_YES : PKG_BAS_NO);
+
default:
break;
}
@@ -328,22 +339,36 @@ const char *PkgNetMineRows(int32_t h) {
// names, with its account), fetching into the user folder's DOWNLOAD: 0
// when out of memory.
int32_t PkgNetOpen(const char *server) {
+ return PkgNetOpenIn(server, "", "");
+}
+
+
+// PkgNetOpen for the DVX folder root ("" = this DVX's: what is installed
+// there decides the updates), fetching into downloadDir ("" = the user
+// folder's DOWNLOAD).
+int32_t PkgNetOpenIn(const char *server, const char *root, const char *downloadDir) {
PkgNetConfigT c;
char url[PKGNET_SERVER_MAX];
char dir[DVX_MAX_PATH];
PkgNetT *n;
int32_t h;
- if (!platformUserPath(dir, sizeof(dir), PKGNET_DIR)) {
+ if (downloadDir && downloadDir[0]) {
+ snprintf(dir, sizeof(dir), "%s", downloadDir);
+ } else if (!platformUserPath(dir, sizeof(dir), PKGNET_DIR)) {
return BAS_NO_HANDLE;
}
+ if (!root) {
+ root = "";
+ }
+
if (server && server[0]) {
- n = pkgNetCreate(server, "", dir);
+ n = pkgNetCreate(server, root, dir);
} else {
pkgNetConfigRead(&c);
pkgNetConfigUrl(&c, url, sizeof(url));
- n = pkgNetCreate(url, "", dir);
+ n = pkgNetCreate(url, root, dir);
pkgNetAccountSet(n, c.user, c.password);
}
diff --git a/src/libs/kpunch/pkgnet/pkgNetBas.h b/src/libs/kpunch/pkgnet/pkgNetBas.h
index 2167d8b9..6894106f 100644
--- a/src/libs/kpunch/pkgnet/pkgNetBas.h
+++ b/src/libs/kpunch/pkgnet/pkgNetBas.h
@@ -44,6 +44,7 @@ const char *PkgNetField(int32_t h, const char *id, int32_t field);
int32_t PkgNetMine(int32_t h);
const char *PkgNetMineRows(int32_t h);
int32_t PkgNetOpen(const char *server);
+int32_t PkgNetOpenIn(const char *server, const char *root, const char *downloadDir);
const char *PkgNetPassword(void);
int32_t PkgNetPercent(int32_t h);
int32_t PkgNetPort(void);
diff --git a/src/libs/kpunch/pkgnet/pkgnet.dep b/src/libs/kpunch/pkgnet/pkgnet.dep
index 447da455..cd605ebe 100644
--- a/src/libs/kpunch/pkgnet/pkgnet.dep
+++ b/src/libs/kpunch/pkgnet/pkgnet.dep
@@ -1,5 +1,5 @@
libdvx
-serial
+crypto
pkg
http
json
diff --git a/src/libs/kpunch/pkgnet/pkgnet.dhs b/src/libs/kpunch/pkgnet/pkgnet.dhs
index e9e6dde7..9d925e76 100644
--- a/src/libs/kpunch/pkgnet/pkgnet.dhs
+++ b/src/libs/kpunch/pkgnet/pkgnet.dhs
@@ -38,7 +38,7 @@ Header: pkgnet/pkgNet.h
Loaded as: bin/libs/kpunch/pkgnet/pkgnet.lib (it needs the package, HTTP, and JSON libraries)
-BASIC: pkgnet.bas (PkgNetOpen, PkgNetCatalogue, PkgNetUpdates, PkgNetFetch, PkgNetRunFor, PkgNetRows$, ...), with pkg.bas's PkgOfferFolder to plan what was fetched.
+BASIC: pkgnet.bas (PkgNetOpen, PkgNetOpenIn for another DVX folder or download folder, PkgNetCatalogue, PkgNetUpdates, PkgNetFetch, PkgNetRunFor, PkgNetRows$, PkgNetField$ -- PKG_FIELD_SHA256 and PKG_FIELD_REPO_SIGNED give a catalogue entry's sha256 and repoSigned --, ...), with pkg.bas's PkgOfferFolderStart to read what was fetched for planning.
.h2 A Fetch
diff --git a/src/libs/kpunch/print/Makefile b/src/libs/kpunch/print/Makefile
index 0a3d74f0..de03e5a3 100644
--- a/src/libs/kpunch/print/Makefile
+++ b/src/libs/kpunch/print/Makefile
@@ -23,8 +23,11 @@
# Printing Library Makefile for DJGPP cross-compilation
#
# Printing library (print.lib): printer ports and jobs, text pages,
-# picture drivers (ESC/P, PCL), the Print and Page Setup dialogs and the
-# BASIC functions. See print.h.
+# picture drivers (ESC/P, PCL, ImageWriter), the Print and Page Setup
+# dialogs and the BASIC functions. See print.h. Every drivers//
+# directory is a driver module built into drivers/.drv beside the
+# library (the PostScript printers', drivers/ps: printPsMod.c loads it
+# when one is first used).
include ../../../common.mk
@@ -38,9 +41,13 @@ OBJS = $(patsubst %.c,$(OBJDIR)/%.o,$(SRCS))
TARGETDIR = $(LIBSDIR)/kpunch/print
TARGET = $(TARGETDIR)/print.lib
+DRVDIR = $(TARGETDIR)/drivers
+DRV_OBJS = $(patsubst %.c,$(OBJDIR)/%.o,$(wildcard drivers/*/*.c))
+DRV_TARGETS = $(patsubst %,$(DRVDIR)/%.drv,$(DVX_DRIVERS))
+
.PHONY: all clean
-all: $(TARGET) $(TARGETDIR)/print.dep
+all: $(TARGET) $(TARGETDIR)/print.dep $(DRV_TARGETS)
$(TARGETDIR)/print.dep: print.dep | $(TARGETDIR)
sed 's/$$/\r/' $< > $@
@@ -49,7 +56,10 @@ $(TARGET): $(OBJS) | $(TARGETDIR)
$(DXE3GEN) -o $(TARGETDIR)/print.dxe -U $(OBJS)
mv $(TARGETDIR)/print.dxe $@
+$(foreach d,$(DVX_DRIVERS),$(eval $(call DVX_DRIVER_RULE,$d)))
+
$(OBJDIR)/%.o: %.c $(DVX_MAKEFILES) | $(OBJDIR)
+ @mkdir -p $(dir $@)
$(CC) $(CFLAGS) -c -o $@ $<
$(OBJDIR):
@@ -58,7 +68,10 @@ $(OBJDIR):
$(TARGETDIR):
mkdir -p $(TARGETDIR)
--include $(OBJS:.o=.d)
+$(DRVDIR):
+ mkdir -p $(DRVDIR)
+
+-include $(OBJS:.o=.d) $(DRV_OBJS:.o=.d)
clean:
- rm -rf $(OBJS) $(OBJS:.o=.d) $(TARGET) $(TARGETDIR)/print.dep $(TARGETDIR)
+ rm -rf $(OBJDIR) $(TARGETDIR)
diff --git a/src/libs/kpunch/print/README.md b/src/libs/kpunch/print/README.md
new file mode 100644
index 00000000..be044927
--- /dev/null
+++ b/src/libs/kpunch/print/README.md
@@ -0,0 +1,37 @@
+# print -- Printing
+
+Printing for every DVX program: jobs to a printer port or a file,
+written a piece at a time so the other programs keep running, with Retry
+/ Cancel when the printer is busy, out of paper, or offline; plain text
+laid out on pages; and pictures and drawings (BASIC's Printer object) on
+dot matrix (ESC/P, ImageWriter), LaserJet (PCL), and PostScript
+printers. BASIC reaches it through LPRINT, the Printer object, and
+src/include/basic/print.bas.
+
+## Documentation
+
+Full reference is in `print.dhs` (this directory), compiled into the DVX
+System Reference. Open via the Help Viewer app, or read
+`docs/dvx_system_reference.html` after `make`.
+
+## Source Files
+
+- `print.h`, `printInt.h` -- public and internal API
+- `printJob.c` -- jobs, ports, the busy and error questions
+- `printText.c` -- plain text on pages
+- `printFont.c` -- text on a page at printer sizes
+- `printRaster.c` -- documents of picture pages and the page pipeline
+ every printer driver is fed by
+- `printEscp.c`, `printIw.c`, `printPcl.c` -- dot matrix (ESC/P),
+ ImageWriter, and LaserJet (PCL) output
+- `drivers/ps/printPs.c`, `drivers/ps/printPsAfm.c` -- PostScript output
+ and its font metrics: the module `drivers/ps.drv`, loaded when a
+ PostScript printer is first used
+- `printPsMod.c` -- loads that module; the calls into it
+- `printUi.c` -- dialogs and ready-made printing (windows, pictures)
+- `printSys.c` -- the printer ports (the BIOS printer service on DOS)
+- `printBas.c` -- BASIC entry points (print.bas)
+
+## Build
+
+`make -C src/libs/kpunch/print` (invoked by the top-level `make`).
diff --git a/src/libs/kpunch/print/printPs.c b/src/libs/kpunch/print/drivers/ps/printPs.c
similarity index 90%
rename from src/libs/kpunch/print/printPs.c
rename to src/libs/kpunch/print/drivers/ps/printPs.c
index 0d801f69..ca25a086 100644
--- a/src/libs/kpunch/print/printPs.c
+++ b/src/libs/kpunch/print/drivers/ps/printPs.c
@@ -22,6 +22,10 @@
// printPs.c -- Printing library: the PostScript drivers (see printInt.h)
//
+// With printPsAfm.c the module drivers/ps.drv, which print.lib loads the
+// first time a PostScript printer is used (printPsMod.c): what the
+// library calls is in the PrintPsModT it exports.
+//
// PostScript Level 2, pictures at 300 dpi: each band of a page an
// imagemask of its black dots.
// PostScript color each band a colorimage of 4 bits a dot (cyan,
@@ -45,6 +49,7 @@
// (colour, width, dashes) and font are only sent when they change.
#include "printInt.h"
+#include "dvxEndian.h"
#include "dvxFontFmt.h"
#include "dvxText.h"
@@ -200,22 +205,12 @@ static void colorRow(const PrintPlanesT *planes, int32_t r, uint8_t *out);
static bool dashOn(uint16_t dash, int32_t i);
static int32_t hatchFirst(int32_t lo);
static bool nameHas(const char *name, const char *const *words, int32_t count);
-double printPsAscent(const PrintFontT *font);
-int32_t printPsFaceOf(const char *family, uint32_t style);
int32_t printPsLatin1(uint8_t ch);
-double printPsWidth(const PrintFontT *font, const char *text, int32_t len);
-bool printVecBegin(PrintDocT *doc);
-void printVecDot(PrintDocT *doc, int32_t x, int32_t y, uint32_t color, int32_t width);
-void printVecEllipse(PrintDocT *doc, int32_t cx, int32_t cy, int32_t rx, int32_t ry, uint32_t color, int32_t width, uint16_t dash, bool arc, double start, double end);
-void printVecEllipseFill(PrintDocT *doc, int32_t cx, int32_t cy, int32_t rx, int32_t ry, uint32_t color, PrintFillE fill);
-bool printVecEnd(PrintDocT *doc);
-void printVecImage(PrintDocT *doc, const uint8_t *rgb, int32_t w, int32_t h, int32_t sx, int32_t sy, int32_t sw, int32_t sh, int32_t dx, int32_t dy, int32_t dw, int32_t dh);
-void printVecLine(PrintDocT *doc, double x0, double y0, double x1, double y1, uint32_t color, int32_t width, uint16_t dash);
-void printVecRect(PrintDocT *doc, int32_t x0, int32_t y0, int32_t x1, int32_t y1, uint32_t color, PrintFillE fill);
-void printVecText(PrintDocT *doc, const PrintFontT *font, int32_t x, int32_t y, const char *text, int32_t len, uint32_t color);
+static double psAscent(const PrintFontT *font);
static bool psBand(const PrintDriverT *drv, PrintJobT *job, const PrintPlanesT *planes);
static bool psDocBegin(const PrintDriverT *drv, PrintJobT *job);
static bool psDocEnd(const PrintDriverT *drv, PrintJobT *job);
+static int32_t psFaceOf(const char *family, uint32_t style);
static bool psPageBegin(const PrintDriverT *drv, PrintJobT *job, int32_t widthDots, int32_t heightDots);
static bool psPageEnd(const PrintDriverT *drv, PrintJobT *job);
static bool psString(PrintJobT *job, const char *text, int32_t len);
@@ -224,6 +219,16 @@ static bool psTextEnd(PrintJobT *job);
static bool psTextLine(PrintJobT *job, const char *text, int32_t len);
static bool psTextPageBegin(PrintJobT *job);
static bool psTextPageEnd(PrintJobT *job);
+static bool psVecBegin(PrintDocT *doc);
+static void psVecDot(PrintDocT *doc, int32_t x, int32_t y, uint32_t color, int32_t width);
+static void psVecEllipse(PrintDocT *doc, int32_t cx, int32_t cy, int32_t rx, int32_t ry, uint32_t color, int32_t width, uint16_t dash, bool arc, double start, double end);
+static void psVecEllipseFill(PrintDocT *doc, int32_t cx, int32_t cy, int32_t rx, int32_t ry, uint32_t color, PrintFillE fill);
+static bool psVecEnd(PrintDocT *doc);
+static void psVecImage(PrintDocT *doc, const uint8_t *rgb, int32_t w, int32_t h, int32_t sx, int32_t sy, int32_t sw, int32_t sh, int32_t dx, int32_t dy, int32_t dw, int32_t dh);
+static void psVecLine(PrintDocT *doc, double x0, double y0, double x1, double y1, uint32_t color, int32_t width, uint16_t dash);
+static void psVecRect(PrintDocT *doc, int32_t x0, int32_t y0, int32_t x1, int32_t y1, uint32_t color, PrintFillE fill);
+static void psVecText(PrintDocT *doc, const PrintFontT *font, int32_t x, int32_t y, const char *text, int32_t len, uint32_t color);
+static double psWidth(const PrintFontT *font, const char *text, int32_t len);
static void vecColor(PrintDocT *doc, uint32_t color);
static void vecDashes(PrintDocT *doc, uint16_t dash, double unit, double phase);
static void vecFill(PrintDocT *doc, const char *shape, PrintFillE fill, int32_t x0, int32_t y0, int32_t x1, int32_t y1);
@@ -234,18 +239,36 @@ static void vecPrintf(PrintDocT *doc, const char *fmt, ...) __attribute__((fo
static const PrintTextOpsT sPsText = { psTextBegin, psTextPageBegin, psTextLine, psTextPageEnd, psTextEnd };
-const PrintDriverT printDrvPostScript = {
+static const PrintDriverT sDrvMono = {
PS_DPI, PS_DPI, PS_DPI * PRINT_PAGE_WIDTH_IN, PS_DPI * PRINT_PAGE_HEIGHT_IN, PS_BAND_ROWS,
PRINT_DITHER_LARGE, 1, printInksBlack, "", "", PRINT_LINES_LASER, &sPsText, &sPsMono,
psDocBegin, psPageBegin, psBand, psPageEnd, psDocEnd, true
};
-const PrintDriverT printDrvPostScriptColor = {
+static const PrintDriverT sDrvColor = {
PS_DPI, PS_DPI, PS_DPI * PRINT_PAGE_WIDTH_IN, PS_DPI * PRINT_PAGE_HEIGHT_IN, PS_BAND_ROWS,
PRINT_DITHER_LARGE, PRINT_PLANES_MAX, printInksKcmy, "", "", PRINT_LINES_LASER, &sPsText, &sPsColor,
psDocBegin, psPageBegin, psBand, psPageEnd, psDocEnd, true
};
+const PrintPsModT printPsModule = {
+ DVX_DRIVER_HDR(PrintPsModT, "ps", 0),
+ .mono = &sDrvMono,
+ .color = &sDrvColor,
+ .ascent = psAscent,
+ .faceOf = psFaceOf,
+ .width = psWidth,
+ .vecBegin = psVecBegin,
+ .vecDot = psVecDot,
+ .vecEllipse = psVecEllipse,
+ .vecEllipseFill = psVecEllipseFill,
+ .vecEnd = psVecEnd,
+ .vecImage = psVecImage,
+ .vecLine = psVecLine,
+ .vecRect = psVecRect,
+ .vecText = psVecText
+};
+
static void a85Byte(A85T *a, uint8_t b) {
a->group[a->n++] = b;
@@ -275,7 +298,7 @@ static bool a85End(A85T *a) {
// The group as five characters (z for four zero bytes); a short group of
// count bytes as count + 1 characters.
static void a85Group(A85T *a, int32_t count) {
- uint32_t v = ((uint32_t)a->group[0] << 24) | ((uint32_t)a->group[1] << 16) | ((uint32_t)a->group[2] << 8) | a->group[3];
+ uint32_t v = dvxGetBe32(a->group);
char c[A85_CHARS];
a->n = 0;
@@ -375,32 +398,6 @@ static bool nameHas(const char *name, const char *const *words, int32_t count) {
}
-// The baseline below the top of a line: the ascender, and half the
-// leading beyond the ascender and descender.
-double printPsAscent(const PrintFontT *font) {
- const PrintPsFaceT *face = &printPsFaces[font->psFace];
- int32_t extra = PRINT_PS_LEADING - (face->ascender - face->descender);
-
- return (face->ascender + extra / 2.0) * font->psSizeY / PRINT_PS_UNITS;
-}
-
-
-int32_t printPsFaceOf(const char *family, uint32_t style) {
- const char *name = family && family[0] ? family : FONT_SYSTEM_FAMILY;
- int32_t face = PRINT_PS_SANS;
-
- if (dvxFontFamilyFixed(name) || nameHas(name, sMonoWords, DVX_ARRAY_LEN(sMonoWords))) {
- face = PRINT_PS_MONO;
- } else if (nameHas(name, sSerifWords, DVX_ARRAY_LEN(sSerifWords)) && !nameHas(name, sSansWords, DVX_ARRAY_LEN(sSansWords))) {
- face = PRINT_PS_SERIF;
- }
-
- face += (style & FONT_STYLE_BOLD) ? PRINT_PS_BOLD : 0;
- face += (style & FONT_STYLE_ITALIC) ? PRINT_PS_SLANT : 0;
- return face;
-}
-
-
int32_t printPsLatin1(uint8_t ch) {
uint32_t u = dfnCp437[ch];
@@ -428,243 +425,13 @@ int32_t printPsLatin1(uint8_t ch) {
}
-// Dots across len characters of text in a printer's face (not rounded).
-double printPsWidth(const PrintFontT *font, const char *text, int32_t len) {
- const PrintPsFaceT *face = &printPsFaces[font->psFace];
- int32_t sum = 0;
-
- for (int32_t i = 0; i < len; i++) {
- sum += face->widths[printPsLatin1((uint8_t)text[i]) - PRINT_PS_FIRST_CHAR];
- }
-
- return sum * font->psSizeX / PRINT_PS_UNITS;
-}
-
-
-// The page started, clipped to the printable area, with round line ends
-// and corners; the pen and font as a page starts.
-bool printVecBegin(PrintDocT *doc) {
- if (!doc || !doc->ok || !doc->drv->vector || doc->vecPage) {
- return false;
- }
-
- const PrintDriverT *drv = doc->drv;
-
- doc->ok = drv->pageBegin(drv, doc->job, drv->pageWidthDots, drv->pageHeightDots);
- vecPrintf(doc, PS_VEC_PAGE_FMT, drv->pageWidthDots, drv->pageHeightDots);
- doc->vecPage = doc->ok;
- doc->vecColor = PS_BLACK;
- doc->vecWidth = 1;
- doc->vecDashed = false;
- doc->vecFace = PRINT_PS_NONE;
- return doc->vecPage;
-}
-
-
-void printVecDot(PrintDocT *doc, int32_t x, int32_t y, uint32_t color, int32_t width) {
- if (!vecOn(doc)) {
- return;
- }
-
- vecColor(doc, color);
- vecPrintf(doc, "%.6g %.6g %.6g dvxD\n", x + DOT_CENTRE, y + DOT_CENTRE, (width < 1 ? 1 : width) / 2.0);
-}
-
-
-// The outline through the dots' centres, dashed from angle 0 (an arc's
-// path starts its start along the pattern); an ellipse with no width or
-// height is the line it has become.
-void printVecEllipse(PrintDocT *doc, int32_t cx, int32_t cy, int32_t rx, int32_t ry, uint32_t color, int32_t width, uint16_t dash, bool arc, double start, double end) {
- if (!vecOn(doc) || dash == DASH_NONE) {
- return;
- }
-
- if (rx <= 0 || ry <= 0) {
- printVecLine(doc, cx - rx, cy - ry, cx + rx, cy + ry, color, width, dash);
- return;
- }
-
- vecColor(doc, color);
- vecPen(doc, width, dash != DASH_SOLID);
-
- if (dash != DASH_SOLID) {
- vecDashes(doc, dash, 1.0, arc ? start * (rx + ry) / 2.0 : 0.0);
- }
-
- if (arc) {
- vecPrintf(doc, "%.6g %.6g %" PRId32 " %" PRId32 " %.6g %.6g dvxEA stroke\n", cx + DOT_CENTRE, cy + DOT_CENTRE, rx, ry, start * DEGREES_PER_RADIAN, end * DEGREES_PER_RADIAN);
- } else {
- vecPrintf(doc, "%.6g %.6g %" PRId32 " %" PRId32 " dvxE stroke\n", cx + DOT_CENTRE, cy + DOT_CENTRE, rx, ry);
- }
-}
-
-
-// Out to the edges of the dots rx and ry from the centre.
-void printVecEllipseFill(PrintDocT *doc, int32_t cx, int32_t cy, int32_t rx, int32_t ry, uint32_t color, PrintFillE fill) {
- char shape[PS_VEC_CMD_MAX];
-
- if (!vecOn(doc)) {
- return;
- }
-
- rx = rx < 0 ? -rx : rx;
- ry = ry < 0 ? -ry : ry;
- vecColor(doc, color);
- snprintf(shape, sizeof(shape), "%.6g %.6g %.6g %.6g dvxE", cx + DOT_CENTRE, cy + DOT_CENTRE, rx + DOT_CENTRE, ry + DOT_CENTRE);
- vecFill(doc, shape, fill, cx - rx, cy - ry, cx + rx, cy + ry);
-}
-
-
-bool printVecEnd(PrintDocT *doc) {
- if (!doc || !doc->vecPage) {
- return false;
- }
-
- doc->vecPage = false;
- doc->ok = doc->ok && doc->drv->pageEnd(doc->drv, doc->job) && !printJobCancelled(doc->job);
- return doc->ok;
-}
-
-
-// The part of the picture as an image of its own dots, grey on a
-// black-and-white printer, the rows packed and in ASCII85.
-void printVecImage(PrintDocT *doc, const uint8_t *rgb, int32_t w, int32_t h, int32_t sx, int32_t sy, int32_t sw, int32_t sh, int32_t dx, int32_t dy, int32_t dw, int32_t dh) {
- if (!vecOn(doc) || !rgb || sw <= 0 || sh <= 0 || dw <= 0 || dh <= 0 || sx < 0 || sy < 0 || sx + sw > w || sy + sh > h) {
- return;
- }
-
- const PsParamsT *pp = (const PsParamsT *)doc->drv->params;
- int32_t comps = pp->color ? RGB_BYTES : GRAY_BYTES;
- int32_t rowLen = sw * comps;
- uint8_t *row = (uint8_t *)malloc((size_t)rowLen);
- uint8_t *packed = (uint8_t *)malloc((size_t)PRINT_PACK_MAX(rowLen));
- uint8_t eod = RLE_EOD;
- A85T a;
-
- if (!row || !packed) {
- free(row);
- free(packed);
- doc->ok = false;
- return;
- }
-
- memset(&a, 0, sizeof(a));
- a.job = doc->job;
- a.ok = true;
- vecPrintf(doc, "%" PRId32 " %" PRId32 " %" PRId32 " %" PRId32 " %" PRId32 " %" PRId32 " %" PRId32 " dvxImg\n", dx, dy, dw, dh, sw, sh, comps);
-
- for (int32_t r = 0; a.ok && doc->ok && r < sh; r++) {
- const uint8_t *src = rgb + ((int64_t)(sy + r) * w + sx) * RGB_BYTES;
-
- if (pp->color) {
- memcpy(row, src, (size_t)rowLen);
- } else {
- for (int32_t i = 0; i < sw; i++) {
- row[i] = (uint8_t)printLuma(src + i * RGB_BYTES);
- }
- }
-
- a85Put(&a, packed, printPackBits(row, rowLen, packed));
- }
-
- a85Put(&a, &eod, 1);
- doc->ok = doc->ok && a.ok && a85End(&a);
- free(row);
- free(packed);
-}
-
-
-// A solid line through the dots' centres, round at the ends; a dashed
-// one in steps of its longer side in dots, a bit of dash each from the
-// top one, each step centred on its dot (the ends half a step out, as
-// dots), square at the ends.
-void printVecLine(PrintDocT *doc, double x0, double y0, double x1, double y1, uint32_t color, int32_t width, uint16_t dash) {
- if (!vecOn(doc) || dash == DASH_NONE || !(fabs(x0) <= DBL_MAX && fabs(y0) <= DBL_MAX && fabs(x1) <= DBL_MAX && fabs(y1) <= DBL_MAX)) {
- return;
- }
-
- x0 += DOT_CENTRE;
- y0 += DOT_CENTRE;
- x1 += DOT_CENTRE;
- y1 += DOT_CENTRE;
- vecColor(doc, color);
-
- if (dash == DASH_SOLID) {
- vecPen(doc, width, false);
- vecPrintf(doc, "%.6g %.6g %.6g %.6g dvxL\n", x0, y0, x1, y1);
- return;
- }
-
- double ax = fabs(floor(x1) - floor(x0));
- double ay = fabs(floor(y1) - floor(y0));
- double steps = ax > ay ? ax : ay;
-
- if (steps == 0.0) {
- if (dash & DASH_TOP) {
- printVecDot(doc, (int32_t)floor(x0), (int32_t)floor(y0), color, width);
- }
-
- return;
- }
-
- double len = sqrt((x1 - x0) * (x1 - x0) + (y1 - y0) * (y1 - y0));
- double unit = len / steps;
- double ex = (x1 - x0) / len * unit * DOT_CENTRE;
- double ey = (y1 - y0) / len * unit * DOT_CENTRE;
-
- vecPen(doc, width, true);
- vecDashes(doc, dash, unit, 0.0);
- vecPrintf(doc, "%.6g %.6g %.6g %.6g dvxL\n", x0 - ex, y0 - ey, x1 + ex, y1 + ey);
-}
-
-
-void printVecRect(PrintDocT *doc, int32_t x0, int32_t y0, int32_t x1, int32_t y1, uint32_t color, PrintFillE fill) {
- int32_t left = x0 < x1 ? x0 : x1;
- int32_t right = x0 < x1 ? x1 : x0;
- int32_t top = y0 < y1 ? y0 : y1;
- int32_t bottom = y0 < y1 ? y1 : y0;
- char shape[PS_VEC_CMD_MAX];
-
- if (!vecOn(doc)) {
- return;
- }
-
- vecColor(doc, color);
- snprintf(shape, sizeof(shape), "%" PRId32 " %" PRId32 " %" PRId32 " %" PRId32 " dvxBox", left, top, right - left + 1, bottom - top + 1);
- vecFill(doc, shape, fill, left, top, right, bottom);
-}
-
-
-// The text at its baseline, then its underline and strike-through as
-// the face has them (the strike-through half the x-height up).
-void printVecText(PrintDocT *doc, const PrintFontT *font, int32_t x, int32_t y, const char *text, int32_t len, uint32_t color) {
- if (!vecOn(doc) || !font || font->psFace == PRINT_PS_NONE || !text || len <= 0) {
- return;
- }
-
+// The baseline below the top of a line: the ascender, and half the
+// leading beyond the ascender and descender.
+static double psAscent(const PrintFontT *font) {
const PrintPsFaceT *face = &printPsFaces[font->psFace];
- double base = y + printPsAscent(font);
- double wide = printPsWidth(font, text, len);
- double thick = face->underlineThick * font->psSizeY / PRINT_PS_UNITS;
- double lines[TEXT_LINES_MAX];
- int32_t count = 0;
+ int32_t extra = PRINT_PS_LEADING - (face->ascender - face->descender);
- vecColor(doc, color);
- vecFont(doc, font);
- doc->ok = doc->ok && psString(doc->job, text, len);
- vecPrintf(doc, " %" PRId32 " %.6g dvxT\n", x, base);
-
- if (font->style & FONT_STYLE_UNDERLINE) {
- lines[count++] = base - face->underlinePos * font->psSizeY / PRINT_PS_UNITS;
- }
-
- if (font->style & FONT_STYLE_STRIKE) {
- lines[count++] = base - face->xHeight / 2.0 * font->psSizeY / PRINT_PS_UNITS;
- }
-
- for (int32_t i = 0; i < count; i++) {
- vecPrintf(doc, "%" PRId32 " %.6g %.6g %.6g rectfill\n", x, lines[i] - thick / 2.0, wide, thick);
- }
+ return (face->ascender + extra / 2.0) * font->psSizeY / PRINT_PS_UNITS;
}
@@ -726,6 +493,22 @@ static bool psDocEnd(const PrintDriverT *drv, PrintJobT *job) {
}
+static int32_t psFaceOf(const char *family, uint32_t style) {
+ const char *name = family && family[0] ? family : FONT_SYSTEM_FAMILY;
+ int32_t face = PRINT_PS_SANS;
+
+ if (dvxFontFamilyFixed(name) || nameHas(name, sMonoWords, DVX_ARRAY_LEN(sMonoWords))) {
+ face = PRINT_PS_MONO;
+ } else if (nameHas(name, sSerifWords, DVX_ARRAY_LEN(sSerifWords)) && !nameHas(name, sSansWords, DVX_ARRAY_LEN(sSansWords))) {
+ face = PRINT_PS_SERIF;
+ }
+
+ face += (style & FONT_STYLE_BOLD) ? PRINT_PS_BOLD : 0;
+ face += (style & FONT_STYLE_ITALIC) ? PRINT_PS_SLANT : 0;
+ return face;
+}
+
+
static bool psPageBegin(const PrintDriverT *drv, PrintJobT *job, int32_t widthDots, int32_t heightDots) {
char cmd[PS_CMD_MAX];
@@ -807,6 +590,246 @@ static bool psTextPageEnd(PrintJobT *job) {
}
+// The page started, clipped to the printable area, with round line ends
+// and corners; the pen and font as a page starts.
+static bool psVecBegin(PrintDocT *doc) {
+ if (!doc || !doc->ok || !doc->drv->vector || doc->vecPage) {
+ return false;
+ }
+
+ const PrintDriverT *drv = doc->drv;
+
+ doc->ok = drv->pageBegin(drv, doc->job, drv->pageWidthDots, drv->pageHeightDots);
+ vecPrintf(doc, PS_VEC_PAGE_FMT, drv->pageWidthDots, drv->pageHeightDots);
+ doc->vecPage = doc->ok;
+ doc->vecColor = PS_BLACK;
+ doc->vecWidth = 1;
+ doc->vecDashed = false;
+ doc->vecFace = PRINT_PS_NONE;
+ return doc->vecPage;
+}
+
+
+static void psVecDot(PrintDocT *doc, int32_t x, int32_t y, uint32_t color, int32_t width) {
+ if (!vecOn(doc)) {
+ return;
+ }
+
+ vecColor(doc, color);
+ vecPrintf(doc, "%.6g %.6g %.6g dvxD\n", x + DOT_CENTRE, y + DOT_CENTRE, (width < 1 ? 1 : width) / 2.0);
+}
+
+
+// The outline through the dots' centres, dashed from angle 0 (an arc's
+// path starts its start along the pattern); an ellipse with no width or
+// height is the line it has become.
+static void psVecEllipse(PrintDocT *doc, int32_t cx, int32_t cy, int32_t rx, int32_t ry, uint32_t color, int32_t width, uint16_t dash, bool arc, double start, double end) {
+ if (!vecOn(doc) || dash == DASH_NONE) {
+ return;
+ }
+
+ if (rx <= 0 || ry <= 0) {
+ psVecLine(doc, cx - rx, cy - ry, cx + rx, cy + ry, color, width, dash);
+ return;
+ }
+
+ vecColor(doc, color);
+ vecPen(doc, width, dash != DASH_SOLID);
+
+ if (dash != DASH_SOLID) {
+ vecDashes(doc, dash, 1.0, arc ? start * (rx + ry) / 2.0 : 0.0);
+ }
+
+ if (arc) {
+ vecPrintf(doc, "%.6g %.6g %" PRId32 " %" PRId32 " %.6g %.6g dvxEA stroke\n", cx + DOT_CENTRE, cy + DOT_CENTRE, rx, ry, start * DEGREES_PER_RADIAN, end * DEGREES_PER_RADIAN);
+ } else {
+ vecPrintf(doc, "%.6g %.6g %" PRId32 " %" PRId32 " dvxE stroke\n", cx + DOT_CENTRE, cy + DOT_CENTRE, rx, ry);
+ }
+}
+
+
+// Out to the edges of the dots rx and ry from the centre.
+static void psVecEllipseFill(PrintDocT *doc, int32_t cx, int32_t cy, int32_t rx, int32_t ry, uint32_t color, PrintFillE fill) {
+ char shape[PS_VEC_CMD_MAX];
+
+ if (!vecOn(doc)) {
+ return;
+ }
+
+ rx = rx < 0 ? -rx : rx;
+ ry = ry < 0 ? -ry : ry;
+ vecColor(doc, color);
+ snprintf(shape, sizeof(shape), "%.6g %.6g %.6g %.6g dvxE", cx + DOT_CENTRE, cy + DOT_CENTRE, rx + DOT_CENTRE, ry + DOT_CENTRE);
+ vecFill(doc, shape, fill, cx - rx, cy - ry, cx + rx, cy + ry);
+}
+
+
+static bool psVecEnd(PrintDocT *doc) {
+ if (!doc || !doc->vecPage) {
+ return false;
+ }
+
+ doc->vecPage = false;
+ doc->ok = doc->ok && doc->drv->pageEnd(doc->drv, doc->job) && !printJobCancelled(doc->job);
+ return doc->ok;
+}
+
+
+// The part of the picture as an image of its own dots, grey on a
+// black-and-white printer, the rows packed and in ASCII85.
+static void psVecImage(PrintDocT *doc, const uint8_t *rgb, int32_t w, int32_t h, int32_t sx, int32_t sy, int32_t sw, int32_t sh, int32_t dx, int32_t dy, int32_t dw, int32_t dh) {
+ if (!vecOn(doc) || !rgb || sw <= 0 || sh <= 0 || dw <= 0 || dh <= 0 || sx < 0 || sy < 0 || sx + sw > w || sy + sh > h) {
+ return;
+ }
+
+ const PsParamsT *pp = (const PsParamsT *)doc->drv->params;
+ int32_t comps = pp->color ? RGB_BYTES : GRAY_BYTES;
+ int32_t rowLen = sw * comps;
+ uint8_t *row = (uint8_t *)malloc((size_t)rowLen);
+ uint8_t *packed = (uint8_t *)malloc((size_t)PRINT_PACK_MAX(rowLen));
+ uint8_t eod = RLE_EOD;
+ A85T a;
+
+ if (!row || !packed) {
+ free(row);
+ free(packed);
+ doc->ok = false;
+ return;
+ }
+
+ memset(&a, 0, sizeof(a));
+ a.job = doc->job;
+ a.ok = true;
+ vecPrintf(doc, "%" PRId32 " %" PRId32 " %" PRId32 " %" PRId32 " %" PRId32 " %" PRId32 " %" PRId32 " dvxImg\n", dx, dy, dw, dh, sw, sh, comps);
+
+ for (int32_t r = 0; a.ok && doc->ok && r < sh; r++) {
+ const uint8_t *src = rgb + ((int64_t)(sy + r) * w + sx) * RGB_BYTES;
+
+ if (pp->color) {
+ memcpy(row, src, (size_t)rowLen);
+ } else {
+ for (int32_t i = 0; i < sw; i++) {
+ row[i] = (uint8_t)printLuma(src + i * RGB_BYTES);
+ }
+ }
+
+ a85Put(&a, packed, printPackBits(row, rowLen, packed));
+ }
+
+ a85Put(&a, &eod, 1);
+ doc->ok = doc->ok && a.ok && a85End(&a);
+ free(row);
+ free(packed);
+}
+
+
+// A solid line through the dots' centres, round at the ends; a dashed
+// one in steps of its longer side in dots, a bit of dash each from the
+// top one, each step centred on its dot (the ends half a step out, as
+// dots), square at the ends.
+static void psVecLine(PrintDocT *doc, double x0, double y0, double x1, double y1, uint32_t color, int32_t width, uint16_t dash) {
+ if (!vecOn(doc) || dash == DASH_NONE || !(fabs(x0) <= DBL_MAX && fabs(y0) <= DBL_MAX && fabs(x1) <= DBL_MAX && fabs(y1) <= DBL_MAX)) {
+ return;
+ }
+
+ x0 += DOT_CENTRE;
+ y0 += DOT_CENTRE;
+ x1 += DOT_CENTRE;
+ y1 += DOT_CENTRE;
+ vecColor(doc, color);
+
+ if (dash == DASH_SOLID) {
+ vecPen(doc, width, false);
+ vecPrintf(doc, "%.6g %.6g %.6g %.6g dvxL\n", x0, y0, x1, y1);
+ return;
+ }
+
+ double ax = fabs(floor(x1) - floor(x0));
+ double ay = fabs(floor(y1) - floor(y0));
+ double steps = ax > ay ? ax : ay;
+
+ if (steps == 0.0) {
+ if (dash & DASH_TOP) {
+ psVecDot(doc, (int32_t)floor(x0), (int32_t)floor(y0), color, width);
+ }
+
+ return;
+ }
+
+ double len = sqrt((x1 - x0) * (x1 - x0) + (y1 - y0) * (y1 - y0));
+ double unit = len / steps;
+ double ex = (x1 - x0) / len * unit * DOT_CENTRE;
+ double ey = (y1 - y0) / len * unit * DOT_CENTRE;
+
+ vecPen(doc, width, true);
+ vecDashes(doc, dash, unit, 0.0);
+ vecPrintf(doc, "%.6g %.6g %.6g %.6g dvxL\n", x0 - ex, y0 - ey, x1 + ex, y1 + ey);
+}
+
+
+static void psVecRect(PrintDocT *doc, int32_t x0, int32_t y0, int32_t x1, int32_t y1, uint32_t color, PrintFillE fill) {
+ int32_t left = x0 < x1 ? x0 : x1;
+ int32_t right = x0 < x1 ? x1 : x0;
+ int32_t top = y0 < y1 ? y0 : y1;
+ int32_t bottom = y0 < y1 ? y1 : y0;
+ char shape[PS_VEC_CMD_MAX];
+
+ if (!vecOn(doc)) {
+ return;
+ }
+
+ vecColor(doc, color);
+ snprintf(shape, sizeof(shape), "%" PRId32 " %" PRId32 " %" PRId32 " %" PRId32 " dvxBox", left, top, right - left + 1, bottom - top + 1);
+ vecFill(doc, shape, fill, left, top, right, bottom);
+}
+
+
+// The text at its baseline, then its underline and strike-through as
+// the face has them (the strike-through half the x-height up).
+static void psVecText(PrintDocT *doc, const PrintFontT *font, int32_t x, int32_t y, const char *text, int32_t len, uint32_t color) {
+ if (!vecOn(doc) || !font || font->psFace == PRINT_PS_NONE || !text || len <= 0) {
+ return;
+ }
+
+ const PrintPsFaceT *face = &printPsFaces[font->psFace];
+ double base = y + psAscent(font);
+ double wide = psWidth(font, text, len);
+ double thick = face->underlineThick * font->psSizeY / PRINT_PS_UNITS;
+ double lines[TEXT_LINES_MAX];
+ int32_t count = 0;
+
+ vecColor(doc, color);
+ vecFont(doc, font);
+ doc->ok = doc->ok && psString(doc->job, text, len);
+ vecPrintf(doc, " %" PRId32 " %.6g dvxT\n", x, base);
+
+ if (font->style & FONT_STYLE_UNDERLINE) {
+ lines[count++] = base - face->underlinePos * font->psSizeY / PRINT_PS_UNITS;
+ }
+
+ if (font->style & FONT_STYLE_STRIKE) {
+ lines[count++] = base - face->xHeight / 2.0 * font->psSizeY / PRINT_PS_UNITS;
+ }
+
+ for (int32_t i = 0; i < count; i++) {
+ vecPrintf(doc, "%" PRId32 " %.6g %.6g %.6g rectfill\n", x, lines[i] - thick / 2.0, wide, thick);
+ }
+}
+
+
+// Dots across len characters of text in a printer's face (not rounded).
+static double psWidth(const PrintFontT *font, const char *text, int32_t len) {
+ const PrintPsFaceT *face = &printPsFaces[font->psFace];
+ int32_t sum = 0;
+
+ for (int32_t i = 0; i < len; i++) {
+ sum += face->widths[printPsLatin1((uint8_t)text[i]) - PRINT_PS_FIRST_CHAR];
+ }
+
+ return sum * font->psSizeX / PRINT_PS_UNITS;
+}
+
+
// The colour, sent when it changed: grey (its brightness) on a
// black-and-white printer.
static void vecColor(PrintDocT *doc, uint32_t color) {
diff --git a/src/libs/kpunch/print/printPsAfm.c b/src/libs/kpunch/print/drivers/ps/printPsAfm.c
similarity index 94%
rename from src/libs/kpunch/print/printPsAfm.c
rename to src/libs/kpunch/print/drivers/ps/printPsAfm.c
index fd429326..7d3273f5 100644
--- a/src/libs/kpunch/print/printPsAfm.c
+++ b/src/libs/kpunch/print/drivers/ps/printPsAfm.c
@@ -1,3 +1,25 @@
+// 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.
+
// printPsAfm.c -- Printing library: the widths of the PostScript
// printers' own text faces (printInt.h), made by src/tools/afmWidths.py
// -- do not edit.
diff --git a/src/libs/kpunch/print/print.dhs b/src/libs/kpunch/print/print.dhs
index 7442bff7..f6f4a44a 100644
--- a/src/libs/kpunch/print/print.dhs
+++ b/src/libs/kpunch/print/print.dhs
@@ -51,7 +51,7 @@ Printer ports and print jobs, text laid out on pages, pictures for dot-matrix an
Header: print/print.h
-Loaded as: bin/libs/kpunch/print/print.lib
+Loaded as: bin/libs/kpunch/print/print.lib; the PostScript printers' driver, with their text faces and vector pages, is bin/libs/kpunch/print/drivers/ps.drv, which the library loads the first time a PostScript printer is used (until then printVecBegin is false for every document).
.h2 Settings
diff --git a/src/libs/kpunch/print/print.h b/src/libs/kpunch/print/print.h
index 96abefe9..4c04ae8d 100644
--- a/src/libs/kpunch/print/print.h
+++ b/src/libs/kpunch/print/print.h
@@ -98,6 +98,9 @@
// Bytes sent to a port between two calls of the idle hook
#define PRINT_YIELD_BYTES 256
+// The longest a job's work goes without the idle hook (printJobPoll)
+#define PRINT_POLL_MS 50
+
// Page setup limits and defaults. linesPerPage 0 means the printer
// model's usual page length (PRINT_LINES_STANDARD, or PRINT_LINES_LASER
// for a laser printer).
@@ -374,6 +377,12 @@ bool printJobPuts(PrintJobT *job, const char *text);
// True once the job was cancelled.
bool printJobCancelled(const PrintJobT *job);
+// The idle hook (other programs run, Cancel is seen) once PRINT_POLL_MS
+// has passed since it last ran: work that sends little -- a white band, a
+// page laid out, a picture converted -- calls this as it goes. False when
+// the job is cancelled.
+bool printJobPoll(PrintJobT *job);
+
// Bytes sent so far.
uint32_t printJobBytes(const PrintJobT *job);
diff --git a/src/libs/kpunch/print/printFont.c b/src/libs/kpunch/print/printFont.c
index adf7afbb..06060951 100644
--- a/src/libs/kpunch/print/printFont.c
+++ b/src/libs/kpunch/print/printFont.c
@@ -61,6 +61,8 @@
#define ROUND_HALF 0.5
#define FIX_ONE 65536 // glyphSmoothSpans: strike positions in 16.16 fixed point
#define FIX_HALF (FIX_ONE / 2)
+#define LEVELS_PAD 2 // glyphRowLevels: blank columns each side of a strike row
+#define LEVELS_MAX (UINT8_MAX + 2 * LEVELS_PAD)
// One glyph as a bitmap: the system font's cells and a loaded font's
// glyphs alike
@@ -92,6 +94,7 @@ static int64_t fixFloor(int64_t v);
static bool glyphInk(const GlyphT *g, const uint8_t *row, int32_t col);
static int32_t glyphLevel(const GlyphT *g, int32_t col, int32_t row);
static bool glyphOf(const BitmapFontT *font, uint8_t ch, GlyphT *g);
+static void glyphRowLevels(const GlyphT *g, int32_t row, uint8_t *out);
static void glyphSmoothSpans(const PrintFontT *pf, const GlyphT *g, int32_t x, int32_t y, int32_t pen, int32_t top, int32_t bottom, PrintSpanFnT fn, void *ctx);
static void glyphSpans(const PrintFontT *pf, const GlyphT *g, int32_t x, int32_t y, int32_t pen, int32_t top, int32_t bottom, PrintSpanFnT fn, void *ctx);
static void lineSpans(const PrintFontT *pf, int32_t x, int32_t y, int32_t row, int32_t thick, int32_t width, int32_t top, int32_t bottom, PrintSpanFnT fn, void *ctx);
@@ -137,13 +140,9 @@ static bool glyphInk(const GlyphT *g, const uint8_t *row, int32_t col) {
}
-// How much ink the glyph pixel at col, row holds, 0 .. FONT_GRAY_FULL (a
-// 1-bit glyph's ink is full); 0 outside the glyph.
+// How much ink the glyph pixel at col, row (inside the glyph) holds,
+// 0 .. FONT_GRAY_FULL (a 1-bit glyph's ink is full).
static int32_t glyphLevel(const GlyphT *g, int32_t col, int32_t row) {
- if (col < 0 || row < 0 || col >= g->width || row >= g->height) {
- return 0;
- }
-
const uint8_t *bits = g->bits + row * g->rowBytes;
if (g->gray) {
@@ -189,11 +188,26 @@ static bool glyphOf(const BitmapFontT *font, uint8_t ch, GlyphT *g) {
}
+// Strike row row's ink levels: out[c + LEVELS_PAD] is column c's, the
+// pad columns each side (and every column of a row outside the glyph)
+// none.
+static void glyphRowLevels(const GlyphT *g, int32_t row, uint8_t *out) {
+ memset(out, 0, (size_t)g->width + 2 * LEVELS_PAD);
+
+ for (int32_t c = 0; row >= 0 && row < g->height && c < g->width; c++) {
+ out[c + LEVELS_PAD] = (uint8_t)glyphLevel(g, c, row);
+ }
+}
+
+
// A glyph scaled up both ways, as glyphSpans: each dot's centre taken
// back to the strike, its ink the four pixels around it weighed by
// nearness (bilinear), a dot at least half covered drawn. The strike
-// position of a row is worked out once; across, it steps by the strike
-// pixels a dot covers.
+// position of a row is worked out once, with the two strike rows' levels
+// (glyphRowLevels: no bounds checks per dot); across, it steps by the
+// strike pixels a dot covers. The weighing is done a row at a time,
+// (1 - fy) * row r's blend + fy * row r + 1's: the same sum, exactly, in
+// fewer and narrower multiplies.
static void glyphSmoothSpans(const PrintFontT *pf, const GlyphT *g, int32_t x, int32_t y, int32_t pen, int32_t top, int32_t bottom, PrintSpanFnT fn, void *ctx) {
int32_t rowTop = pf->font->ascent - g->top;
int32_t colLeft = pen + g->left;
@@ -212,15 +226,23 @@ static void glyphSmoothSpans(const PrintFontT *pf, const GlyphT *g, int32_t x, i
int64_t v = (int64_t)(2 * (dy - y) + 1) * pf->size * FIX_ONE / (2 * pf->dotsY) - (int64_t)rowTop * FIX_ONE - FIX_HALF;
int64_t vBase = fixFloor(v);
int32_t r = (int32_t)(vBase / FIX_ONE);
- int64_t fy = v - vBase;
+ int32_t fy = (int32_t)(v - vBase);
int64_t u = startU;
int32_t run = -1;
+ uint8_t lv0[LEVELS_MAX];
+ uint8_t lv1[LEVELS_MAX];
+
+ glyphRowLevels(g, r, lv0);
+ glyphRowLevels(g, r + 1, lv1);
for (int32_t dx = x0; dx <= x1; dx++, u += stepX) {
int64_t uBase = fixFloor(u);
int32_t c = (int32_t)(uBase / FIX_ONE);
- int64_t fx = u - uBase;
- int64_t ink = (FIX_ONE - fx) * (FIX_ONE - fy) * glyphLevel(g, c, r) + fx * (FIX_ONE - fy) * glyphLevel(g, c + 1, r) + (FIX_ONE - fx) * fy * glyphLevel(g, c, r + 1) + fx * fy * glyphLevel(g, c + 1, r + 1);
+ int32_t fx = (int32_t)(u - uBase);
+ int32_t i = (c < -LEVELS_PAD ? -LEVELS_PAD : (c > g->width ? g->width : c)) + LEVELS_PAD;
+ int32_t inkR0 = (FIX_ONE - fx) * lv0[i] + fx * lv0[i + 1];
+ int32_t inkR1 = (FIX_ONE - fx) * lv1[i] + fx * lv1[i + 1];
+ int64_t ink = (int64_t)(FIX_ONE - fy) * inkR0 + (int64_t)fy * inkR1;
bool on = 2 * ink >= full;
if (on && run < 0) {
@@ -361,10 +383,10 @@ PrintFontT *printFontOpen(const PrintPageInfoT *page, const char *family, int32_
pf->dotsX = pf->dotsX < 1 ? 1 : pf->dotsX;
pf->dotsY = pf->dotsY < 1 ? 1 : pf->dotsY;
pf->style = style;
- pf->psFace = PRINT_PS_NONE;
+ pf->psFace = page->vector ? printPsFaceOf(family, style) : PRINT_PS_NONE;
- if (page->vector) {
- pf->psFace = printPsFaceOf(family, style);
+ // A printer's face (none without the PostScript module: a strike then)
+ if (pf->psFace != PRINT_PS_NONE) {
pf->psSizeX = (double)decipoints * page->dpiX / DECIPOINTS_PER_INCH;
pf->psSizeY = (double)decipoints * page->dpiY / DECIPOINTS_PER_INCH;
return pf;
diff --git a/src/libs/kpunch/print/printInt.h b/src/libs/kpunch/print/printInt.h
index 7293a829..608308f8 100644
--- a/src/libs/kpunch/print/printInt.h
+++ b/src/libs/kpunch/print/printInt.h
@@ -26,6 +26,8 @@
#define PRINT_INT_H
#include "print.h"
+#include "dvxDrv.h"
+#include "dvxPlat.h"
// Port 0 is called LPT1
#define PRINT_PORT_NUMBER_BASE 1
@@ -181,8 +183,8 @@ extern const PrintPsFaceT printPsFaces[PRINT_PS_FACES];
// The face of printPsFaces a family and style (FONT_STYLE_BOLD, _ITALIC)
// print in: Courier for a fixed-pitch family (or one named Mono or
-// Courier), Times for a serif one (its name says so), else Helvetica
-// (printPs.c).
+// Courier), Times for a serif one (its name says so), else Helvetica;
+// PRINT_PS_NONE without the PostScript module (printPsMod.c).
int32_t printPsFaceOf(const char *family, uint32_t style);
// A CP437 character as ISOLatin1Encoding has it: Latin-1 letters and
@@ -192,10 +194,41 @@ int32_t printPsLatin1(uint8_t ch);
// A printer's face (PrintFontT.psFace): dots from the top of a line to
// its baseline, and across len characters of text, not rounded
-// (printPs.c).
+// (printPsMod.c).
double printPsAscent(const PrintFontT *font);
double printPsWidth(const PrintFontT *font, const char *text, int32_t len);
+// The PostScript printers' module, drivers/ps.drv (printPs.c and
+// printPsAfm.c, a driver module: dvxDrv.h), loaded by printPsMod.c the
+// first time a PostScript printer is used and kept until the library is
+// unloaded. It exports a PrintPsModT as PRINT_PS_SYMBOL: its two
+// drivers and the functions behind printPsAscent, printPsFaceOf,
+// printPsWidth, and the printVec* calls.
+#define PRINT_DRV_DIR "libs" DVX_PATH_SEP "kpunch" DVX_PATH_SEP "print" DVX_PATH_SEP "drivers"
+#define PRINT_PS_SYMBOL "printPsModule"
+
+typedef struct {
+ DvxDriverHeaderT header;
+ const PrintDriverT *mono;
+ const PrintDriverT *color;
+ __typeof__(printPsAscent) *ascent;
+ __typeof__(printPsFaceOf) *faceOf;
+ __typeof__(printPsWidth) *width;
+ __typeof__(printVecBegin) *vecBegin;
+ __typeof__(printVecDot) *vecDot;
+ __typeof__(printVecEllipse) *vecEllipse;
+ __typeof__(printVecEllipseFill) *vecEllipseFill;
+ __typeof__(printVecEnd) *vecEnd;
+ __typeof__(printVecImage) *vecImage;
+ __typeof__(printVecLine) *vecLine;
+ __typeof__(printVecRect) *vecRect;
+ __typeof__(printVecText) *vecText;
+} PrintPsModT;
+
+// A PostScript model's driver (color: the colour one), the module loaded
+// if it is not; NULL without it (logged).
+const PrintDriverT *printDriverPs(bool color);
+
// The one ink of a black-and-white printer, and the four of a printer
// with black (printRaster.c)
extern const PrintInkE printInksBlack[1];
@@ -212,10 +245,9 @@ extern const PrintDriverT printDrvImageWriter;
extern const PrintDriverT printDrvImageWriterColor;
extern const PrintDriverT printDrvLaserJet;
extern const PrintDriverT printDrvLaserJet300;
-extern const PrintDriverT printDrvPostScript;
-extern const PrintDriverT printDrvPostScriptColor;
-// The picture driver of a model, NULL for PrintModelTextE.
+// The picture driver of a model, NULL for PrintModelTextE (and for a
+// PostScript one without its module, which then prints as text).
const PrintDriverT *printDriver(PrintModelE model);
// One page through drv, widthDots x heightDots: fn fills each band in RGB
diff --git a/src/libs/kpunch/print/printJob.c b/src/libs/kpunch/print/printJob.c
index d80d36fa..470ae79e 100644
--- a/src/libs/kpunch/print/printJob.c
+++ b/src/libs/kpunch/print/printJob.c
@@ -63,15 +63,16 @@ DVX_ABI_STAMP(print);
#define PRINT_FILE_YIELD_BYTES 4096
struct PrintJobS {
- PrintHooksT hooks;
- FILE *fp; // file destination, else NULL
- int32_t port; // port destination
- uint32_t bytes;
- int32_t sinceIdle; // bytes since the idle hook ran
- int32_t idleEvery;
- bool cancelled;
- bool idled; // the idle hook ran during this write
- char where[PRINT_WHERE_MAX];
+ PrintHooksT hooks;
+ FILE *fp; // file destination, else NULL
+ int32_t port; // port destination
+ uint32_t bytes;
+ int32_t sinceIdle; // bytes since the idle hook ran
+ int32_t idleEvery;
+ PlatformTicksT idleAt; // when the idle hook last ran (printJobPoll)
+ bool cancelled;
+ bool idled; // the idle hook ran during this write
+ char where[PRINT_WHERE_MAX];
};
const PrefsChoiceT printDestChoices[PrintDestCountE] = {
@@ -132,6 +133,7 @@ uint32_t printJobBytes(const PrintJobT *job);
bool printJobCancelled(const PrintJobT *job);
bool printJobClose(PrintJobT *job);
PrintJobT *printJobOpen(PrintDestE dest, const char *filePath, const PrintHooksT *hooks, char *err, int32_t errSize);
+bool printJobPoll(PrintJobT *job);
bool printJobPuts(PrintJobT *job, const char *text);
bool printJobWrite(PrintJobT *job, const void *data, int32_t len);
const char *printModelLabel(PrintModelE model);
@@ -181,6 +183,7 @@ static int32_t baudOffered(int32_t baud) {
static bool idle(PrintJobT *job) {
job->idled = true;
job->sinceIdle = 0;
+ job->idleAt = platformClock();
if (!job->hooks.idle) {
tsYield();
@@ -321,6 +324,7 @@ PrintJobT *printJobOpen(PrintDestE dest, const char *filePath, const PrintHooksT
job->hooks = *hooks;
}
+ job->idleAt = platformClock();
printDestName(dest, filePath, job->where, sizeof(job->where));
if (dest == PrintDestFileE) {
@@ -332,6 +336,10 @@ PrintJobT *printJobOpen(PrintDestE dest, const char *filePath, const PrintHooksT
free(job);
return NULL;
}
+
+ // Flushed in pieces of the idle cadence: the C library's 16 KB
+ // writes take most of a second on a floppy
+ setvbuf(job->fp, NULL, _IOFBF, PRINT_FILE_YIELD_BYTES);
} else {
job->port = (int32_t)dest;
job->idleEvery = PRINT_YIELD_BYTES;
@@ -357,6 +365,15 @@ PrintJobT *printJobOpen(PrintDestE dest, const char *filePath, const PrintHooksT
}
+bool printJobPoll(PrintJobT *job) {
+ if (!job || job->cancelled) {
+ return false;
+ }
+
+ return platformClock() - job->idleAt < PLATFORM_MS_TO_TICKS(PRINT_POLL_MS) || idle(job);
+}
+
+
bool printJobPuts(PrintJobT *job, const char *text) {
return printJobWrite(job, text, (int32_t)strlen(text));
}
@@ -382,6 +399,8 @@ bool printJobWrite(PrintJobT *job, const void *data, int32_t len) {
}
}
+ ok = ok && printJobPoll(job);
+
if (job->idled && job->hooks.finished) {
job->hooks.finished(job->hooks.ctx, false);
}
diff --git a/src/libs/kpunch/print/printPsMod.c b/src/libs/kpunch/print/printPsMod.c
new file mode 100644
index 00000000..de98e2dc
--- /dev/null
+++ b/src/libs/kpunch/print/printPsMod.c
@@ -0,0 +1,202 @@
+// 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.
+
+// printPsMod.c -- Printing library: the PostScript printers' module
+//
+// The PostScript drivers, their text faces, and vector pages (printPs.c,
+// printPsAfm.c) are a module of their own, drivers/ps.drv in
+// PRINT_DRV_DIR, that exports one PrintPsModT (printInt.h). The first
+// PostScript printer used loads it (printDriverPs, or printPsFaceOf for
+// a vector page's fonts), and it stays until the library is unloaded:
+// the drivers it hands out are its own. The library's and programs'
+// calls are here, through it. A printer face or a vector page exists
+// only on a PostScript document, which the module made, so printPsAscent
+// and printPsWidth need no check; printVec* may be called on any
+// document and do nothing (false) without the module. A build that
+// links printPs.c in (the host tests) has the descriptor already.
+
+#include "printInt.h"
+#include "dvxApp.h"
+#include "stb_ds_wrap.h"
+
+#include
+
+// ============================================================
+// Prototypes
+// ============================================================
+
+static bool driverValid(const DvxDriverHeaderT *desc, int32_t kind);
+const PrintDriverT *printDriverPs(bool color);
+double printPsAscent(const PrintFontT *font);
+int32_t printPsFaceOf(const char *family, uint32_t style);
+static const PrintPsModT *printPsMod(void);
+static void printPsModUnload(void) __attribute__((destructor));
+double printPsWidth(const PrintFontT *font, const char *text, int32_t len);
+bool printVecBegin(PrintDocT *doc);
+void printVecDot(PrintDocT *doc, int32_t x, int32_t y, uint32_t color, int32_t width);
+void printVecEllipse(PrintDocT *doc, int32_t cx, int32_t cy, int32_t rx, int32_t ry, uint32_t color, int32_t width, uint16_t dash, bool arc, double start, double end);
+void printVecEllipseFill(PrintDocT *doc, int32_t cx, int32_t cy, int32_t rx, int32_t ry, uint32_t color, PrintFillE fill);
+bool printVecEnd(PrintDocT *doc);
+void printVecImage(PrintDocT *doc, const uint8_t *rgb, int32_t w, int32_t h, int32_t sx, int32_t sy, int32_t sw, int32_t sh, int32_t dx, int32_t dy, int32_t dw, int32_t dh);
+void printVecLine(PrintDocT *doc, double x0, double y0, double x1, double y1, uint32_t color, int32_t width, uint16_t dash);
+void printVecRect(PrintDocT *doc, int32_t x0, int32_t y0, int32_t x1, int32_t y1, uint32_t color, PrintFillE fill);
+void printVecText(PrintDocT *doc, const PrintFontT *font, int32_t x, int32_t y, const char *text, int32_t len, uint32_t color);
+
+static const char *const sDrvSymbols[] = { DVX_SYM(PRINT_PS_SYMBOL) };
+static const uint32_t sDrvSizes[] = { sizeof(PrintPsModT) };
+static const DvxDriverClassT sDrvClass = { sDrvSymbols, sDrvSizes, 1, driverValid };
+
+static DvxDriverModT sMod = { NULL, NULL, 0 }; // desc NULL: not loaded
+static const PrintPsModT *sPs = NULL; // sMod's descriptor
+
+
+static bool driverValid(const DvxDriverHeaderT *desc, int32_t kind) {
+ (void)kind;
+
+ const PrintPsModT *m = (const PrintPsModT *)desc;
+
+ return m->mono && m->color && m->ascent && m->faceOf && m->width && m->vecBegin && m->vecDot && m->vecEllipse && m->vecEllipseFill && m->vecEnd && m->vecImage && m->vecLine && m->vecRect && m->vecText;
+}
+
+
+const PrintDriverT *printDriverPs(bool color) {
+ const PrintPsModT *ps = printPsMod();
+
+ if (!ps) {
+ return NULL;
+ }
+
+ return color ? ps->color : ps->mono;
+}
+
+
+double printPsAscent(const PrintFontT *font) {
+ return sPs->ascent(font);
+}
+
+
+int32_t printPsFaceOf(const char *family, uint32_t style) {
+ const PrintPsModT *ps = printPsMod();
+
+ return ps ? ps->faceOf(family, style) : PRINT_PS_NONE;
+}
+
+
+// The module, loaded if it is not (NULL: it is not there, logged).
+static const PrintPsModT *printPsMod(void) {
+ if (sPs) {
+ return sPs;
+ }
+
+ const DvxDriverHeaderT *linked = (const DvxDriverHeaderT *)dlsym(NULL, sDrvSymbols[0]);
+
+ if (linked) {
+ sMod.desc = linked;
+ } else {
+ DvxDriverModT *mods = dvxDriversLoad(PRINT_DRV_DIR, &sDrvClass);
+
+ if (arrlen(mods) > 0) {
+ sMod = dvxDriverKeep(mods, 0);
+ }
+
+ dvxDriversFree(mods);
+ }
+
+ sPs = (const PrintPsModT *)sMod.desc;
+
+ if (!sPs) {
+ dvxLog("Print: the PostScript driver is missing from %s", PRINT_DRV_DIR);
+ }
+
+ return sPs;
+}
+
+
+// The library is being unloaded (a linked-in module stays).
+static void printPsModUnload(void) {
+ dvxDriverUnload(&sMod);
+ sPs = NULL;
+}
+
+
+double printPsWidth(const PrintFontT *font, const char *text, int32_t len) {
+ return sPs->width(font, text, len);
+}
+
+
+bool printVecBegin(PrintDocT *doc) {
+ return sPs && sPs->vecBegin(doc);
+}
+
+
+void printVecDot(PrintDocT *doc, int32_t x, int32_t y, uint32_t color, int32_t width) {
+ if (sPs) {
+ sPs->vecDot(doc, x, y, color, width);
+ }
+}
+
+
+void printVecEllipse(PrintDocT *doc, int32_t cx, int32_t cy, int32_t rx, int32_t ry, uint32_t color, int32_t width, uint16_t dash, bool arc, double start, double end) {
+ if (sPs) {
+ sPs->vecEllipse(doc, cx, cy, rx, ry, color, width, dash, arc, start, end);
+ }
+}
+
+
+void printVecEllipseFill(PrintDocT *doc, int32_t cx, int32_t cy, int32_t rx, int32_t ry, uint32_t color, PrintFillE fill) {
+ if (sPs) {
+ sPs->vecEllipseFill(doc, cx, cy, rx, ry, color, fill);
+ }
+}
+
+
+bool printVecEnd(PrintDocT *doc) {
+ return sPs && sPs->vecEnd(doc);
+}
+
+
+void printVecImage(PrintDocT *doc, const uint8_t *rgb, int32_t w, int32_t h, int32_t sx, int32_t sy, int32_t sw, int32_t sh, int32_t dx, int32_t dy, int32_t dw, int32_t dh) {
+ if (sPs) {
+ sPs->vecImage(doc, rgb, w, h, sx, sy, sw, sh, dx, dy, dw, dh);
+ }
+}
+
+
+void printVecLine(PrintDocT *doc, double x0, double y0, double x1, double y1, uint32_t color, int32_t width, uint16_t dash) {
+ if (sPs) {
+ sPs->vecLine(doc, x0, y0, x1, y1, color, width, dash);
+ }
+}
+
+
+void printVecRect(PrintDocT *doc, int32_t x0, int32_t y0, int32_t x1, int32_t y1, uint32_t color, PrintFillE fill) {
+ if (sPs) {
+ sPs->vecRect(doc, x0, y0, x1, y1, color, fill);
+ }
+}
+
+
+void printVecText(PrintDocT *doc, const PrintFontT *font, int32_t x, int32_t y, const char *text, int32_t len, uint32_t color) {
+ if (sPs) {
+ sPs->vecText(doc, font, x, y, text, len, color);
+ }
+}
diff --git a/src/libs/kpunch/print/printRaster.c b/src/libs/kpunch/print/printRaster.c
index d5c00429..37fc659b 100644
--- a/src/libs/kpunch/print/printRaster.c
+++ b/src/libs/kpunch/print/printRaster.c
@@ -55,6 +55,7 @@
#define TOP_BIT 0x80
#define RGB_BYTES 3
#define WHITE 0xFF
+#define PRINT_SUB_ROWS 8 // rows filled and dithered between two polls
#define BRIGHTNESS_LEVELS 256
// Luma weights (ITU-R BT.601, per mille)
@@ -122,7 +123,7 @@ static const uint8_t sPlaneShift[PRINT_PLANES_MAX][2] = {
// Prototypes
// ============================================================
-static void bandToPlanes(const PrintDriverT *drv, InkModeE mode, const uint8_t *rgb, int32_t y0, PrintPlanesT *pl);
+static void bandToPlanes(const PrintDriverT *drv, InkModeE mode, const uint8_t *rgb, int32_t y0, PrintPlanesT *pl, int32_t rowFrom, int32_t rowTo);
static InkModeE inkModeOf(const PrintDriverT *drv);
static int32_t inkValue(InkModeE mode, PrintInkE ink, const uint8_t *px);
static bool pictureBand(void *ctx, uint8_t *rgb, int32_t width, int32_t y0, int32_t rows);
@@ -141,13 +142,13 @@ bool printPlaneDot(const PrintPlanesT *pl, int32_t plane, int32_t
static int32_t threshold(int32_t size, int32_t plane, int32_t x, int32_t y);
-// The band's colours as the driver's inks, dithered (the planes come
-// clear). Each row's thresholds are worked out once: the pattern repeats
-// every ditherSize dots across.
-static void bandToPlanes(const PrintDriverT *drv, InkModeE mode, const uint8_t *rgb, int32_t y0, PrintPlanesT *pl) {
+// The band's rows rowFrom..rowTo - 1 as the driver's inks, dithered (the
+// planes come clear). Each row's thresholds are worked out once: the
+// pattern repeats every ditherSize dots across.
+static void bandToPlanes(const PrintDriverT *drv, InkModeE mode, const uint8_t *rgb, int32_t y0, PrintPlanesT *pl, int32_t rowFrom, int32_t rowTo) {
int32_t mask = drv->ditherSize - 1;
- for (int32_t r = 0; r < pl->rows; r++) {
+ for (int32_t r = rowFrom; r < rowTo; r++) {
uint8_t th[PRINT_PLANES_MAX][PRINT_DITHER_LARGE];
const uint8_t *src = rgb + (int64_t)r * pl->widthDots * RGB_BYTES;
@@ -222,11 +223,8 @@ static bool pictureBand(void *ctx, uint8_t *rgb, int32_t width, int32_t y0, int3
for (int32_t r = 0; r < rows; r++) {
const uint8_t *src = pic->rgb + (int64_t)((int64_t)(y0 + r) * pic->h / pic->outH) * pic->pitch;
- uint8_t *dst = rgb + (int64_t)r * width * RGB_BYTES;
- for (int32_t x = 0; x < width; x++) {
- memcpy(dst + x * RGB_BYTES, src + (int64_t)x * pic->w / pic->outW * RGB_BYTES, RGB_BYTES);
- }
+ drawScaleRow(rgb + (int64_t)r * width * RGB_BYTES, src, RGB_BYTES, 0, width, pic->outW, pic->w, false, 0);
}
return true;
@@ -347,10 +345,10 @@ const PrintDriverT *printDriver(PrintModelE model) {
return &printDrvDeskJetColor;
case PrintModelPostScriptE:
- return &printDrvPostScript;
+ return printDriverPs(false);
case PrintModelPostScriptColorE:
- return &printDrvPostScriptColor;
+ return printDriverPs(true);
case PrintModelImageWriterE:
return &printDrvImageWriter;
@@ -450,16 +448,26 @@ bool printPageRun(PrintJobT *job, const PrintDriverT *drv, int32_t widthDots, in
pl.rows = heightDots - y0 < drv->bandRows ? heightDots - y0 : drv->bandRows;
pl.y0 = y0;
memset(rgb, WHITE, (size_t)widthDots * (size_t)pl.rows * RGB_BYTES);
- ok = fn(ctx, rgb, widthDots, y0, pl.rows);
- for (int32_t p = 0; ok && p < pl.count; p++) {
+ for (int32_t p = 0; p < pl.count; p++) {
memset(pl.bits[p], 0, planeBytes);
}
- if (ok) {
- bandToPlanes(drv, mode, rgb, y0, &pl);
- ok = drv->band(drv, job, &pl);
+ // Filled and dithered PRINT_SUB_ROWS rows at a time, the job polled
+ // between: a band of a 300 dpi page is a lot of work, and a white
+ // one sends almost nothing that would let the others run
+ for (int32_t s = 0; ok && s < pl.rows; s += PRINT_SUB_ROWS) {
+ int32_t n = pl.rows - s < PRINT_SUB_ROWS ? pl.rows - s : PRINT_SUB_ROWS;
+
+ ok = fn(ctx, rgb + (size_t)s * (size_t)widthDots * RGB_BYTES, widthDots, y0 + s, n) && printJobPoll(job);
+
+ if (ok) {
+ bandToPlanes(drv, mode, rgb, y0, &pl, s, s + n);
+ ok = printJobPoll(job);
+ }
}
+
+ ok = ok && drv->band(drv, job, &pl);
}
ok = ok && drv->pageEnd(drv, job);
diff --git a/src/libs/kpunch/print/printText.c b/src/libs/kpunch/print/printText.c
index 3e0aaad3..30398394 100644
--- a/src/libs/kpunch/print/printText.c
+++ b/src/libs/kpunch/print/printText.c
@@ -34,6 +34,7 @@
// lines instead of the plain text.
#include "printInt.h"
+#include "taskSwch.h"
#include
#include
@@ -55,6 +56,7 @@
#define STAMP_TEXT_MAX 20
#define DATE_FORMAT "%Y-%m-%d"
#define TIME_FORMAT "%H:%M"
+#define LAYOUT_POLL_BYTES 16384 // text laid out between two polls (layoutPoll)
// One layout pass
typedef struct {
@@ -94,6 +96,7 @@ static void bodyLine(TextOutT *o, const char *text, int32_t len);
static void endSource(TextOutT *o, LineT *ln);
static void flushLine(TextOutT *o, LineT *ln);
static void layout(TextOutT *o, const char *text, int32_t len);
+static bool layoutPoll(TextOutT *o);
static void lineReset(LineT *ln);
static void outBytes(TextOutT *o, const char *data, int32_t len);
static void outHeader(TextOutT *o, const char *fmt);
@@ -157,6 +160,10 @@ static void layout(TextOutT *o, const char *text, int32_t len) {
for (int32_t i = 0; i < len; i++) {
char ch = text[i];
+ if ((i + 1) % LAYOUT_POLL_BYTES == 0 && !layoutPoll(o)) {
+ return;
+ }
+
if (ch == '\r') {
continue;
}
@@ -212,6 +219,21 @@ static void layout(TextOutT *o, const char *text, int32_t len) {
}
+// A long text lays out its pages without printing a byte (the page count,
+// the pages before the range): the job is polled as it goes, and without
+// one the others get a turn. False (o->ok false too) when the job was
+// cancelled.
+static bool layoutPoll(TextOutT *o) {
+ if (!o->job) {
+ tsYield();
+ return true;
+ }
+
+ o->ok = o->ok && printJobPoll(o->job);
+ return o->ok;
+}
+
+
static void lineReset(LineT *ln) {
ln->n = 0;
ln->col = 0;
diff --git a/src/libs/kpunch/print/printUi.c b/src/libs/kpunch/print/printUi.c
index cc2ac227..b1a4e0df 100644
--- a/src/libs/kpunch/print/printUi.c
+++ b/src/libs/kpunch/print/printUi.c
@@ -62,7 +62,6 @@
#define UI_PAGES_ALL 0
#define UI_PAGES_RANGE 1
#define RGB_BYTES 3
-#define PIXEL_BYTE_BITS 8
// A job's user interface state (the hooks' context)
typedef struct {
@@ -134,7 +133,6 @@ static void progressHide(UiRunT *run);
static void progressShow(UiRunT *run);
static bool setupDlgOk(WindowT *dlg, void *userCtx);
static void textOnlyBox(AppContextT *ctx);
-static uint8_t *toRgb(const AppContextT *ctx, const uint8_t *data, int32_t w, int32_t h, int32_t pitch);
static PrintAnswerE uiAsk(void *ctx, const char *message);
static void uiFinished(void *ctx, bool closing);
static bool uiIdle(void *ctx);
@@ -486,7 +484,7 @@ bool printUiImageFile(AppContextT *ctx, const char *path, const char *title, boo
return false;
}
- uint8_t *rgb = toRgb(ctx, data, w, h, pitch);
+ uint8_t *rgb = dvxBufferToRgb(&ctx->display, data, w, h, pitch);
dvxFreeImage(data);
@@ -573,7 +571,7 @@ bool printUiWindow(AppContextT *ctx, WindowT *win, bool askFirst) {
return false;
}
- uint8_t *rgb = toRgb(ctx, win->contentBuf, win->contentW, win->contentH, win->contentPitch);
+ uint8_t *rgb = dvxBufferToRgb(&ctx->display, win->contentBuf, win->contentW, win->contentH, win->contentPitch);
if (!rgb) {
dvxErrorBox(ctx, UI_TITLE, "Not enough memory to print the window.");
@@ -682,37 +680,6 @@ static void textOnlyBox(AppContextT *ctx) {
}
-// The display-format pixels of a picture or window as R, G, B bytes
-// (NULL when out of memory).
-static uint8_t *toRgb(const AppContextT *ctx, const uint8_t *data, int32_t w, int32_t h, int32_t pitch) {
- const DisplayT *d = &ctx->display;
- int32_t bpp = d->format.bytesPerPixel;
- uint8_t *rgb = (uint8_t *)malloc((size_t)w * (size_t)h * RGB_BYTES);
-
- if (!rgb) {
- return NULL;
- }
-
- for (int32_t y = 0; y < h; y++) {
- const uint8_t *src = data + (size_t)y * (size_t)pitch;
- uint8_t *dst = rgb + (size_t)y * (size_t)w * RGB_BYTES;
-
- for (int32_t x = 0; x < w; x++) {
- uint32_t pixel = 0;
-
- for (int32_t b = 0; b < bpp; b++) {
- pixel |= (uint32_t)src[x * bpp + b] << (b * PIXEL_BYTE_BITS);
- }
-
- unpackColor(d, pixel, &dst[0], &dst[1], &dst[2]);
- dst += RGB_BYTES;
- }
- }
-
- return rgb;
-}
-
-
static PrintAnswerE uiAsk(void *ctx, const char *message) {
UiRunT *run = (UiRunT *)ctx;
diff --git a/src/libs/kpunch/seclink/Makefile b/src/libs/kpunch/seclink/Makefile
new file mode 100644
index 00000000..bf2f7e23
--- /dev/null
+++ b/src/libs/kpunch/seclink/Makefile
@@ -0,0 +1,65 @@
+# 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.
+
+# Secure Link Library Makefile for DJGPP cross-compilation
+#
+# seclink.lib: reliable packets over a serial port or any byte stream
+# (packet.c: HDLC framing, CRC-16, Go-Back-N) and the encrypted,
+# multi-channel link on top of them (secLink.c), over serial.lib's UART
+# driver and crypto.lib's ciphers. Part of Communications.
+
+include ../../../common.mk
+
+CFLAGS = $(DOS_CFLAGS) -I. -I../libdvx -I../libdvx/platform -I../libtasks
+
+OBJDIR = ../../../../obj/seclink
+LIBSDIR = ../../../../bin/libs
+
+SRCS = $(wildcard *.c)
+OBJS = $(patsubst %.c,$(OBJDIR)/%.o,$(SRCS))
+TARGETDIR = $(LIBSDIR)/kpunch/seclink
+TARGET = $(TARGETDIR)/seclink.lib
+
+.PHONY: all clean
+
+all: $(TARGET) $(TARGETDIR)/seclink.dep
+
+$(TARGETDIR)/seclink.dep: seclink.dep | $(TARGETDIR)
+ sed 's/$$/\r/' $< > $@
+
+$(TARGET): $(OBJS) | $(TARGETDIR)
+ $(DXE3GEN) -o $(TARGETDIR)/seclink.dxe -U $(OBJS)
+ mv $(TARGETDIR)/seclink.dxe $@
+
+$(OBJDIR)/%.o: %.c $(DVX_MAKEFILES) | $(OBJDIR)
+ $(CC) $(CFLAGS) -c -o $@ $<
+
+$(OBJDIR):
+ mkdir -p $(OBJDIR)
+
+$(TARGETDIR):
+ mkdir -p $(TARGETDIR)
+
+-include $(OBJS:.o=.d)
+
+clean:
+ rm -rf $(OBJDIR) $(TARGETDIR)
diff --git a/src/libs/kpunch/seclink/README.md b/src/libs/kpunch/seclink/README.md
new file mode 100644
index 00000000..29c35e55
--- /dev/null
+++ b/src/libs/kpunch/seclink/README.md
@@ -0,0 +1,40 @@
+# seclink -- Secure Link
+
+Reliable packets and the encrypted link on top of them, over a serial
+port (the serial library's rs232 driver) or any byte stream, such as a
+network connection. Part of Communications.
+
+- packet: a reliable, ordered transport. HDLC-style byte stuffing
+ (FLAG=0x7E, ESC=0x7D), CRC-16-CCITT integrity, and Go-Back-N
+ sliding-window retransmission.
+- secLink: the multi-channel encrypted link: optional shared-passphrase
+ authentication, forward-secret X25519 handshake with key
+ confirmation, XChaCha20-Poly1305 on every encrypted packet (the
+ cryptography library),
+ up to 128 channels multiplexed on one link, and bulk send with
+ retransmit handled underneath.
+
+The Linux proxy (src/tools/proxy) builds the same sources against a
+socket shim in place of the rs232 driver.
+
+## Documentation
+
+Full reference is in `seclink.dhs` (this directory), compiled into the
+DVX System Reference -- open via the Help Viewer app, or read
+`docs/dvx_system_reference.html` after `make`.
+
+Topics covered: protocol, open/close, passphrase, handshake, send/poll,
+plaintext channels, error codes, the packet transport (frame format,
+window size, retransmit timer, CRC), and an end-to-end example.
+
+## Source Files
+
+- `secLink.h` -- public API and error codes
+- `secLink.c` -- implementation (handshake, channel mux, bulk send)
+- `packet.h` -- packet transport API and error codes
+- `packet.c` -- implementation (framing, CRC, sliding window)
+
+## Build
+
+Built into `seclink.lib` by `make -C src/libs/kpunch/seclink` (invoked
+by the top-level `make`).
diff --git a/src/libs/kpunch/serial/packet/packet.c b/src/libs/kpunch/seclink/packet.c
similarity index 81%
rename from src/libs/kpunch/serial/packet/packet.c
rename to src/libs/kpunch/seclink/packet.c
index 64a8d2bc..5221deb1 100644
--- a/src/libs/kpunch/serial/packet/packet.c
+++ b/src/libs/kpunch/seclink/packet.c
@@ -44,7 +44,9 @@
#include
#include
#include "packet.h"
-#include "../rs232/rs232.h"
+#include "../serial/rs232/rs232.h"
+#include "../libdvx/dvxEndian.h"
+#include "../libdvx/dvxHash.h"
// ========================================================================
@@ -83,15 +85,22 @@
// Per-connection buffer for draining the serial port in pktPoll
#define RX_READ_CHUNK 128
-// A frame write stalled by flow control (XOFF, CTS low) is retried until
-// the port accepts bytes again or this long passes without progress.
-#define FRAME_WRITE_TIMEOUT_MS 2000
+// Frames stuffed for the wire wait in a queue the port or stream takes
+// from as it can (never waited for): room for a full window and the
+// control frames between them
+#define TX_QUEUE_FRAMES (PKT_MAX_WINDOW + 2)
+#define TX_QUEUE_SIZE (TX_QUEUE_FRAMES * MAX_STUFFED_SIZE)
// Retransmit timeout. 500ms is conservative for a local serial link (RTT
// is < 1ms) but accounts for the remote side being busy. On a real BBS
// connection through the proxy, the round-trip includes TCP latency.
#define RETRANSMIT_TIMEOUT_MS 500
+// A wire whose untaken bytes have not moved for this long is not counted
+// busy (flow control holding the line, a stuck port): the retransmit and
+// link timers run again, as when frames were written byte by byte
+#define WIRE_STALL_MS 2000
+
// CRC-16-CCITT initial register value (RFC 1662 / HDLC convention).
#define CRC_INIT_VALUE 0xFFFF
@@ -139,6 +148,14 @@ struct PktConnS {
TxSlotT txSlots[PKT_MAX_WINDOW];
int txCount; // number of slots in use
int txHead; // physical index of oldest unacked slot
+ clock_t lastHeard; // the peer's last good frame (or the window's first frame)
+ clock_t wireMoved; // when the bytes on their way to the wire last changed (wireBusy)
+ int32_t wireBytes; // how many there were then
+
+ // Stuffed frames not yet taken by the port or stream (circular)
+ uint8_t txQueue[TX_QUEUE_SIZE];
+ int txQHead; // the first byte not taken
+ int txQLen;
// Receive state (Go-Back-N receiver: only accepts in-order frames)
uint8_t rxExpectSeq; // next expected sequence number
@@ -173,56 +190,12 @@ struct PktConnS {
static PktConnT *sOpenConns[RS232_NUM_PORTS];
-// ========================================================================
-// CRC-16-CCITT lookup table
-// ========================================================================
-
-// Regenerated from the polynomial (previous table had 42 corrupted
-// entries, breaking the guaranteed error-detection properties). To
-// verify: table[i ^ j] == table[i] ^ table[j] must hold for all i, j.
-static const uint16_t sCrcTable[256] = {
- 0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50A5, 0x60C6, 0x70E7,
- 0x8108, 0x9129, 0xA14A, 0xB16B, 0xC18C, 0xD1AD, 0xE1CE, 0xF1EF,
- 0x1231, 0x0210, 0x3273, 0x2252, 0x52B5, 0x4294, 0x72F7, 0x62D6,
- 0x9339, 0x8318, 0xB37B, 0xA35A, 0xD3BD, 0xC39C, 0xF3FF, 0xE3DE,
- 0x2462, 0x3443, 0x0420, 0x1401, 0x64E6, 0x74C7, 0x44A4, 0x5485,
- 0xA56A, 0xB54B, 0x8528, 0x9509, 0xE5EE, 0xF5CF, 0xC5AC, 0xD58D,
- 0x3653, 0x2672, 0x1611, 0x0630, 0x76D7, 0x66F6, 0x5695, 0x46B4,
- 0xB75B, 0xA77A, 0x9719, 0x8738, 0xF7DF, 0xE7FE, 0xD79D, 0xC7BC,
- 0x48C4, 0x58E5, 0x6886, 0x78A7, 0x0840, 0x1861, 0x2802, 0x3823,
- 0xC9CC, 0xD9ED, 0xE98E, 0xF9AF, 0x8948, 0x9969, 0xA90A, 0xB92B,
- 0x5AF5, 0x4AD4, 0x7AB7, 0x6A96, 0x1A71, 0x0A50, 0x3A33, 0x2A12,
- 0xDBFD, 0xCBDC, 0xFBBF, 0xEB9E, 0x9B79, 0x8B58, 0xBB3B, 0xAB1A,
- 0x6CA6, 0x7C87, 0x4CE4, 0x5CC5, 0x2C22, 0x3C03, 0x0C60, 0x1C41,
- 0xEDAE, 0xFD8F, 0xCDEC, 0xDDCD, 0xAD2A, 0xBD0B, 0x8D68, 0x9D49,
- 0x7E97, 0x6EB6, 0x5ED5, 0x4EF4, 0x3E13, 0x2E32, 0x1E51, 0x0E70,
- 0xFF9F, 0xEFBE, 0xDFDD, 0xCFFC, 0xBF1B, 0xAF3A, 0x9F59, 0x8F78,
- 0x9188, 0x81A9, 0xB1CA, 0xA1EB, 0xD10C, 0xC12D, 0xF14E, 0xE16F,
- 0x1080, 0x00A1, 0x30C2, 0x20E3, 0x5004, 0x4025, 0x7046, 0x6067,
- 0x83B9, 0x9398, 0xA3FB, 0xB3DA, 0xC33D, 0xD31C, 0xE37F, 0xF35E,
- 0x02B1, 0x1290, 0x22F3, 0x32D2, 0x4235, 0x5214, 0x6277, 0x7256,
- 0xB5EA, 0xA5CB, 0x95A8, 0x8589, 0xF56E, 0xE54F, 0xD52C, 0xC50D,
- 0x34E2, 0x24C3, 0x14A0, 0x0481, 0x7466, 0x6447, 0x5424, 0x4405,
- 0xA7DB, 0xB7FA, 0x8799, 0x97B8, 0xE75F, 0xF77E, 0xC71D, 0xD73C,
- 0x26D3, 0x36F2, 0x0691, 0x16B0, 0x6657, 0x7676, 0x4615, 0x5634,
- 0xD94C, 0xC96D, 0xF90E, 0xE92F, 0x99C8, 0x89E9, 0xB98A, 0xA9AB,
- 0x5844, 0x4865, 0x7806, 0x6827, 0x18C0, 0x08E1, 0x3882, 0x28A3,
- 0xCB7D, 0xDB5C, 0xEB3F, 0xFB1E, 0x8BF9, 0x9BD8, 0xABBB, 0xBB9A,
- 0x4A75, 0x5A54, 0x6A37, 0x7A16, 0x0AF1, 0x1AD0, 0x2AB3, 0x3A92,
- 0xFD2E, 0xED0F, 0xDD6C, 0xCD4D, 0xBDAA, 0xAD8B, 0x9DE8, 0x8DC9,
- 0x7C26, 0x6C07, 0x5C64, 0x4C45, 0x3CA2, 0x2C83, 0x1CE0, 0x0CC1,
- 0xEF1F, 0xFF3E, 0xCF5D, 0xDF7C, 0xAF9B, 0xBFBA, 0x8FD9, 0x9FF8,
- 0x6E17, 0x7E36, 0x4E55, 0x5E74, 0x2E93, 0x3EB2, 0x0ED1, 0x1EF0
-};
-
-
// ========================================================================
// Prototypes (alphabetical)
// ========================================================================
static int32_t ackAdvance(PktConnT *conn, uint8_t seq);
static PktConnT *connCreate(const PktStreamT *stream, int com, int windowSize, PktRecvCallbackT callback, void *callbackCtx);
-static uint16_t crcCalc(const uint8_t *data, int len);
bool pktCanSend(PktConnT *conn);
void pktClose(PktConnT *conn);
int pktGetPending(PktConnT *conn);
@@ -233,18 +206,23 @@ int pktReset(PktConnT *conn);
int pktSend(PktConnT *conn, const uint8_t *data, int len, bool block);
void pktSetResetCallback(PktConnT *conn, PktResetCallbackT callback);
int pktWaitSpace(PktConnT *conn);
+static int32_t portPending(void *ctx);
static int32_t portRead(void *ctx, uint8_t *buf, int32_t len);
static int32_t portWrite(void *ctx, const uint8_t *data, int32_t len);
static bool processFrame(PktConnT *conn, const uint8_t *frame, int len);
static void resetSequenceState(PktConnT *conn);
-static void retransmitCheck(PktConnT *conn);
+static bool retransmitCheck(PktConnT *conn);
static bool rxProcessByte(PktConnT *conn, uint8_t byte);
static void sendAck(PktConnT *conn, uint8_t seq);
-static void sendFrame(PktConnT *conn, uint8_t seq, uint8_t type, const uint8_t *payload, int len);
+static bool sendFrame(PktConnT *conn, uint8_t seq, uint8_t type, const uint8_t *payload, int len);
static void sendNakOnce(PktConnT *conn);
static void sendRst(PktConnT *conn, uint8_t marker);
static int seqInWindow(uint8_t seq, uint8_t base, int size);
+static void txFlush(PktConnT *conn);
+static bool txPut(PktConnT *conn, const uint8_t *bytes, int len);
+static bool txRoom(const PktConnT *conn);
static int txSlotIndex(PktConnT *conn, uint8_t seq);
+static bool wireBusy(PktConnT *conn);
// Returns how many outstanding slots a cumulative ACK frees, or -1 if the
@@ -289,26 +267,12 @@ static PktConnT *connCreate(const PktStreamT *stream, int com, int windowSize, P
}
-// Table-driven CRC-16-CCITT. Processing one byte per iteration with a
-// 256-entry table is ~10x faster than bit-by-bit on a 486. The table
-// costs 512 bytes of .rodata -- a worthwhile trade for a function called
-// on every frame received and transmitted.
-static uint16_t crcCalc(const uint8_t *data, int len) {
- uint16_t crc = CRC_INIT_VALUE;
-
- for (int i = 0; i < len; i++) {
- crc = (crc << 8) ^ sCrcTable[(crc >> 8) ^ data[i]];
- }
-
- return crc;
-}
-
-
+// A free slot, and room in the queue for its frame.
bool pktCanSend(PktConnT *conn) {
if (!conn) {
return false;
}
- return conn->txCount < conn->windowSize;
+ return conn->txCount < conn->windowSize && txRoom(conn);
}
@@ -356,10 +320,11 @@ PktConnT *pktOpen(int com, int windowSize, PktRecvCallbackT callback, void *call
return NULL;
}
- port.read = portRead;
- port.write = portWrite;
- port.ctx = (void *)(intptr_t)com;
- conn = connCreate(&port, com, windowSize, callback, callbackCtx);
+ port.read = portRead;
+ port.write = portWrite;
+ port.pending = portPending;
+ port.ctx = (void *)(intptr_t)com;
+ conn = connCreate(&port, com, windowSize, callback, callbackCtx);
if (conn) {
sOpenConns[com] = conn;
@@ -395,6 +360,7 @@ int pktPoll(PktConnT *conn) {
}
conn->pollDepth++;
+ txFlush(conn);
while (!conn->closePending) {
uint8_t byte;
@@ -417,7 +383,8 @@ int pktPoll(PktConnT *conn) {
}
if (!conn->closePending && !disconnected) {
- retransmitCheck(conn);
+ disconnected = retransmitCheck(conn);
+ txFlush(conn);
}
conn->pollDepth--;
@@ -468,10 +435,16 @@ int pktSend(PktConnT *conn, const uint8_t *data, int len, bool block) {
if (rc != PKT_SUCCESS) {
return rc;
}
- } else if (conn->txCount >= conn->windowSize) {
+ } else if (!pktCanSend(conn)) {
return PKT_ERR_TX_FULL;
}
+ // The first frame of an empty window: the link's silence counts from
+ // here
+ if (conn->txCount == 0) {
+ conn->lastHeard = clock();
+ }
+
// Store in retransmit buffer
slot = &conn->txSlots[(conn->txHead + conn->txCount) % PKT_MAX_WINDOW];
memcpy(slot->data, data, len);
@@ -514,7 +487,7 @@ int pktWaitSpace(PktConnT *conn) {
limit = (clock_t)PKT_SEND_TIMEOUT_MS * CLOCKS_PER_SEC / 1000;
lastCount = conn->txCount;
- while (conn->txCount >= conn->windowSize) {
+ while (!pktCanSend(conn)) {
if (pktPoll(conn) == PKT_ERR_DISCONNECTED) {
return PKT_ERR_DISCONNECTED;
}
@@ -524,7 +497,7 @@ int pktWaitSpace(PktConnT *conn) {
}
lastCount = conn->txCount;
- if (conn->txCount >= conn->windowSize && clock() - start >= limit) {
+ if (!pktCanSend(conn) && clock() - start >= limit) {
return PKT_ERR_TIMEOUT;
}
}
@@ -547,14 +520,21 @@ int pktWaitSpace(PktConnT *conn) {
//
// For NAK frames: the receiver wants us to retransmit from a specific
// sequence. Go-Back-N retransmits that frame AND all subsequent ones.
-// A COM port as a stream (ctx is the port index).
+// A COM port as a stream (ctx is the port index): writes go into its
+// transmit ring, which its interrupt handler sends (flow control and
+// XON/XOFF included); what the ring still holds is pending.
+static int32_t portPending(void *ctx) {
+ return rs232GetTxBuffered((int32_t)(intptr_t)ctx);
+}
+
+
static int32_t portRead(void *ctx, uint8_t *buf, int32_t len) {
return rs232Read((int32_t)(intptr_t)ctx, (char *)buf, len);
}
static int32_t portWrite(void *ctx, const uint8_t *data, int32_t len) {
- return rs232Write((int32_t)(intptr_t)ctx, (const char *)data, len);
+ return rs232WriteBuf((int32_t)(intptr_t)ctx, (const char *)data, len);
}
@@ -571,14 +551,16 @@ static bool processFrame(PktConnT *conn, const uint8_t *frame, int len) {
}
// Verify CRC
- calcCrc = crcCalc(frame, len - CRC_SIZE);
- rxCrc = frame[len - 2] | ((uint16_t)frame[len - 1] << 8);
+ calcCrc = dvxCrc16(CRC_INIT_VALUE, frame, len - CRC_SIZE);
+ rxCrc = dvxGetLe16(frame + len - CRC_SIZE);
if (calcCrc != rxCrc) {
// CRC mismatch -- request retransmit of what we expect
sendNakOnce(conn);
return false;
}
+ conn->lastHeard = clock();
+
seq = frame[0];
type = frame[1];
payloadLen = frame[2];
@@ -644,8 +626,10 @@ static bool processFrame(PktConnT *conn, const uint8_t *frame, int len) {
// Retransmit this slot and all after it (go-back-N)
for (int i = idx; i < conn->txCount; i++) {
TxSlotT *slot = &conn->txSlots[(conn->txHead + i) % PKT_MAX_WINDOW];
- sendFrame(conn, slot->seq, FRAME_DATA, slot->data, slot->len);
- slot->timer = clock();
+
+ if (sendFrame(conn, slot->seq, FRAME_DATA, slot->data, slot->len)) {
+ slot->timer = clock();
+ }
}
}
break;
@@ -701,18 +685,40 @@ static void resetSequenceState(PktConnT *conn) {
// timeout. This handles the case where an ACK or NAK was lost -- without
// this, the connection would stall forever. Each slot is retransmitted
// independently and its timer is reset, creating exponential backoff
-// behavior naturally (each retransmit resets the timer).
-static void retransmitCheck(PktConnT *conn) {
+// behavior naturally (each retransmit resets the timer). The timers only
+// run once the wire is idle: until then the window is still going out (a
+// window takes a second at 9600 bps). True when the link is lost: the
+// peer said nothing for PKT_LINK_TIMEOUT_MS with frames waiting for it.
+static bool retransmitCheck(PktConnT *conn) {
clock_t now = clock();
clock_t timeout = (clock_t)RETRANSMIT_TIMEOUT_MS * CLOCKS_PER_SEC / 1000;
+ clock_t lost = (clock_t)PKT_LINK_TIMEOUT_MS * CLOCKS_PER_SEC / 1000;
+
+ if (conn->txCount == 0) {
+ return false;
+ }
+
+ if (wireBusy(conn)) {
+ for (int i = 0; i < conn->txCount; i++) {
+ conn->txSlots[(conn->txHead + i) % PKT_MAX_WINDOW].timer = now;
+ }
+
+ conn->lastHeard = now;
+ return false;
+ }
+
+ if (now - conn->lastHeard >= lost) {
+ return true;
+ }
for (int i = 0; i < conn->txCount; i++) {
TxSlotT *slot = &conn->txSlots[(conn->txHead + i) % PKT_MAX_WINDOW];
- if (now - slot->timer >= timeout) {
- sendFrame(conn, slot->seq, FRAME_DATA, slot->data, slot->len);
+ if (now - slot->timer >= timeout && sendFrame(conn, slot->seq, FRAME_DATA, slot->data, slot->len)) {
slot->timer = now;
}
}
+
+ return false;
}
@@ -779,19 +785,19 @@ static void sendAck(PktConnT *conn, uint8_t seq) {
}
-// Build a raw frame, compute CRC, byte-stuff it, and transmit.
-// The stuffed buffer can be up to 2x the raw size (every byte might need
-// escaping) plus the flags. This is stack-allocated because frames are small
-// and we're not in a deeply recursive call path.
-static void sendFrame(PktConnT *conn, uint8_t seq, uint8_t type, const uint8_t *payload, int len) {
+// Build a raw frame, compute CRC, byte-stuff it, and queue it whole for
+// the wire (txFlush sends what the port or stream takes now). The stuffed
+// buffer can be up to 2x the raw size (every byte might need escaping)
+// plus the flags. This is stack-allocated because frames are small and
+// we're not in a deeply recursive call path. False when the queue has no
+// room: the frame is dropped (a data frame's retransmit timer resends it;
+// a lost ACK or NAK is recovered by the peer's).
+static bool sendFrame(PktConnT *conn, uint8_t seq, uint8_t type, const uint8_t *payload, int len) {
uint8_t raw[MAX_FRAME_SIZE];
uint8_t stuffed[MAX_STUFFED_SIZE];
int rawLen;
uint16_t crc;
int out;
- int sent;
- clock_t start;
- clock_t limit;
// Build raw frame: SEQ + TYPE + LEN + PAYLOAD
raw[0] = seq;
@@ -803,9 +809,8 @@ static void sendFrame(PktConnT *conn, uint8_t seq, uint8_t type, const uint8_t *
rawLen = HEADER_SIZE + len;
// Append CRC
- crc = crcCalc(raw, rawLen);
- raw[rawLen] = (uint8_t)(crc & 0xFF);
- raw[rawLen + 1] = (uint8_t)((crc >> 8) & 0xFF);
+ crc = dvxCrc16(CRC_INIT_VALUE, raw, rawLen);
+ dvxPutLe16(raw + rawLen, crc);
rawLen += CRC_SIZE;
// Byte-stuff into output buffer with leading flag
@@ -826,27 +831,12 @@ static void sendFrame(PktConnT *conn, uint8_t seq, uint8_t type, const uint8_t *
// Trailing flag to close the frame immediately
stuffed[out++] = FLAG_BYTE;
- // Send via the port or stream. rs232Write stops early while flow
- // control holds the line (XOFF, CTS low), a socket while its buffer is
- // full; keep writing the rest so a frame is never cut short, but give
- // up if nothing is accepted for a while (the retransmit timer recovers
- // the frame).
- sent = 0;
- start = clock();
- limit = (clock_t)FRAME_WRITE_TIMEOUT_MS * CLOCKS_PER_SEC / 1000;
- while (sent < out) {
- int n = conn->stream.write(conn->stream.ctx, stuffed + sent, out - sent);
-
- if (n < 0) {
- break;
- }
- if (n > 0) {
- sent += n;
- start = clock();
- } else if (clock() - start >= limit) {
- break;
- }
+ if (!txPut(conn, stuffed, out)) {
+ return false;
}
+
+ txFlush(conn);
+ return true;
}
@@ -877,6 +867,51 @@ static int seqInWindow(uint8_t seq, uint8_t base, int size) {
}
+// Send what the port or stream takes of the queue now (never waiting: flow
+// control holding the line, or a full socket, leaves the rest for the next
+// poll).
+static void txFlush(PktConnT *conn) {
+ while (conn->txQLen > 0) {
+ int run = TX_QUEUE_SIZE - conn->txQHead;
+ int n;
+
+ if (run > conn->txQLen) {
+ run = conn->txQLen;
+ }
+
+ n = conn->stream.write(conn->stream.ctx, conn->txQueue + conn->txQHead, run);
+
+ if (n <= 0) {
+ break;
+ }
+
+ conn->txQHead = (conn->txQHead + n) % TX_QUEUE_SIZE;
+ conn->txQLen -= n;
+ }
+}
+
+
+// A whole stuffed frame onto the queue; false when it does not fit.
+static bool txPut(PktConnT *conn, const uint8_t *bytes, int len) {
+ if (TX_QUEUE_SIZE - conn->txQLen < len) {
+ return false;
+ }
+
+ for (int i = 0; i < len; i++) {
+ conn->txQueue[(conn->txQHead + conn->txQLen + i) % TX_QUEUE_SIZE] = bytes[i];
+ }
+
+ conn->txQLen += len;
+ return true;
+}
+
+
+// Room on the queue for the largest frame.
+static bool txRoom(const PktConnT *conn) {
+ return TX_QUEUE_SIZE - conn->txQLen >= MAX_STUFFED_SIZE;
+}
+
+
static int txSlotIndex(PktConnT *conn, uint8_t seq) {
uint8_t diff = seq - conn->txAckSeq;
if (diff >= (uint8_t)conn->txCount) {
@@ -884,3 +919,19 @@ static int txSlotIndex(PktConnT *conn, uint8_t seq) {
}
return diff;
}
+
+
+// Bytes still on their way to the wire, queued here or taken by a port
+// that has not sent them yet, and moving: a count that has not changed
+// for WIRE_STALL_MS is not busy.
+static bool wireBusy(PktConnT *conn) {
+ int32_t bytes = conn->txQLen + (conn->stream.pending ? conn->stream.pending(conn->stream.ctx) : 0);
+ clock_t now = clock();
+
+ if (bytes != conn->wireBytes) {
+ conn->wireBytes = bytes;
+ conn->wireMoved = now;
+ }
+
+ return bytes > 0 && now - conn->wireMoved < (clock_t)WIRE_STALL_MS * CLOCKS_PER_SEC / 1000;
+}
diff --git a/src/libs/kpunch/serial/packet/packet.h b/src/libs/kpunch/seclink/packet.h
similarity index 81%
rename from src/libs/kpunch/serial/packet/packet.h
rename to src/libs/kpunch/seclink/packet.h
index 4fdc1913..aef6592b 100644
--- a/src/libs/kpunch/serial/packet/packet.h
+++ b/src/libs/kpunch/seclink/packet.h
@@ -42,9 +42,9 @@
// net for electrical noise, not a routine error recovery mechanism.
//
// CRC-16-CCITT:
-// Standard polynomial used in HDLC/X.25. Table-driven for speed
-// (256-entry lookup table trades 512 bytes of ROM for ~10x faster CRC
-// than the bit-by-bit method, important on a 486).
+// Standard polynomial used in HDLC/X.25, register seeded with 0xFFFF.
+// Computed a byte at a time by dvxCrc16 (libdvx's dvxHash.h), the code
+// file transfers use too.
#ifndef PACKET_H
#define PACKET_H
@@ -75,6 +75,10 @@
// long (a silent or vanished peer must not hang the caller forever).
#define PKT_SEND_TIMEOUT_MS 15000
+// The link is lost (pktPoll says PKT_ERR_DISCONNECTED) when the peer sends
+// nothing this long while frames wait for it and the wire is idle.
+#define PKT_LINK_TIMEOUT_MS 30000
+
// Callback for received packets
// ctx: user context pointer
// data: payload (valid only during callback)
@@ -91,15 +95,18 @@ typedef void (*PktResetCallbackT)(void *ctx);
typedef struct PktConnS PktConnT;
// A byte stream to run the packet layer over instead of a COM port (a TCP
-// socket, for example). Both calls must return at once.
-// read: up to len bytes into buf; the count, 0 when none has arrived,
-// negative when the stream is gone
-// write: bytes taken from data (0 = none right now), negative when the
-// stream is gone
+// socket, for example). Every call must return at once.
+// read: up to len bytes into buf; the count, 0 when none has arrived,
+// negative when the stream is gone
+// write: bytes taken from data (0 = none right now), negative when the
+// stream is gone
+// pending: bytes taken but not yet on the wire (NULL: it does not say);
+// retransmit timers wait for them
typedef struct {
int32_t (*read)(void *ctx, uint8_t *buf, int32_t len);
int32_t (*write)(void *ctx, const uint8_t *data, int32_t len);
void *ctx;
+ int32_t (*pending)(void *ctx);
} PktStreamT;
@@ -121,16 +128,21 @@ PktConnT *pktOpenStream(const PktStreamT *stream, int windowSize, PktRecvCallbac
// PKT_ERR_DISCONNECTED).
void pktClose(PktConnT *conn);
-// Send a packet. Returns PKT_SUCCESS or error.
+// Send a packet. Returns PKT_SUCCESS or error. Its frame is queued and
+// goes out as the port or stream takes it (pktPoll keeps it going); the
+// window is full while the queue has no room for another frame.
// Blocks if the transmit window is full and block is true (see
// pktWaitSpace for the bound). Returns PKT_ERR_TX_FULL if window is full
// and block is false.
int pktSend(PktConnT *conn, const uint8_t *data, int len, bool block);
-// Poll for incoming data, process received frames, handle retransmits.
-// Must be called frequently (e.g. in your main loop). May be re-entered
-// from the receive callback; nested calls continue the same byte stream.
-// Returns the number of valid data packets delivered to the callback.
+// Poll for incoming data, process received frames, handle retransmits,
+// and send what the port or stream takes of the queued frames (never
+// waiting). Must be called frequently (e.g. in your main loop). May be
+// re-entered from the receive callback; nested calls continue the same
+// byte stream. Returns the number of valid data packets delivered to the
+// callback, or PKT_ERR_DISCONNECTED (the port or stream is gone, or the
+// link is lost: PKT_LINK_TIMEOUT_MS).
int pktPoll(PktConnT *conn);
// Reset the connection state (sequence numbers, buffers).
diff --git a/src/libs/kpunch/serial/seclink/secLink.c b/src/libs/kpunch/seclink/secLink.c
similarity index 69%
rename from src/libs/kpunch/serial/seclink/secLink.c
rename to src/libs/kpunch/seclink/secLink.c
index 102700bd..02358174 100644
--- a/src/libs/kpunch/serial/seclink/secLink.c
+++ b/src/libs/kpunch/seclink/secLink.c
@@ -23,21 +23,26 @@
// Secure serial link -- ties rs232, packet, and security into one API
//
-// Packet types (first byte of every packet):
-// HELLO [type][version][flags][DH public key, 128 bytes]
+// Packet types (first byte of every packet), protocol version 3:
+// HELLO [type][version][flags][X25519 public key, 32 bytes]
// CONFIRM [type][HMAC-SHA256, 32 bytes]
// PLAIN [type][channel][payload]
-// CIPHER [type][channel][XTEA-CTR ciphertext][truncated HMAC, 8 bytes]
+// CIPHER [type][channel][XChaCha20 ciphertext][Poly1305 tag, 16 bytes]
//
// Handshake:
-// 1. Each side sends HELLO with a fresh DH public key. A HELLO is stored
-// whenever it arrives, so a peer that starts first is never missed.
+// 1. Each side sends HELLO with a fresh X25519 public key. A HELLO is
+// stored whenever it arrives, so a peer that starts first is never
+// missed. An X25519 takes about a sixth of a second on a 486, so a
+// poll does a slice of one (curveStep, about 20 ms there): our public
+// key, then (once both HELLOs are here) the shared secret. Our HELLO
+// goes out once our key is ready.
// 2. With both keys present each side checks the version and that both
-// or neither use a passphrase, computes the DH secret and runs
+// or neither use a passphrase, computes the shared secret (a peer key
+// that gives an all-zero secret fails the exchange) and runs
// HKDF-SHA256(salt = SHA-256(passphrase) or zeros, ikm = secret,
-// info = label || lowerPub || higherPub). The output is split into
-// an XTEA key and an HMAC key per direction plus a confirmation key;
-// the side with the lower public key sends with the "low" keys.
+// info = label || lowerPub || higherPub). The output is split into a
+// sending key per direction plus a confirmation key; the side with
+// the lower public key sends with the "low" key.
// 3. Each side sends CONFIRM = HMAC(confirmKey, SHA-256(lowerHello ||
// higherHello) || role). A wrong passphrase or a tampered HELLO makes
// the peer's CONFIRM mismatch and the handshake fails with
@@ -49,7 +54,7 @@
// previous HELLO) means the peer is re-keying: we answer with a fresh
// HELLO and CONFIRM from secLinkPoll. A re-key HELLO whose version or
// passphrase flag cannot match is dropped without touching the running
-// session. The DH private key is destroyed as soon as the session keys
+// session. The X25519 secret key is wiped as soon as the session keys
// are derived (forward secrecy).
//
// Clear frames:
@@ -60,12 +65,13 @@
// on the line can cause, so neither can downgrade a protected channel.
//
// Encrypted frames:
-// Encrypt-then-MAC. Each direction numbers its encrypted frames with an
-// implicit 64-bit sequence (never sent); the MAC covers type, channel,
-// sequence and ciphertext, and the CTR counter for a frame starts at
-// seq * CTR_BLOCKS_PER_FRAME, so every frame has its own keystream range
-// and a dropped frame does not desynchronize later ones. A replayed or
-// injected frame is checked against the wrong sequence number and
+// XChaCha20-Poly1305 with the type and channel bytes as associated data.
+// Each direction numbers its encrypted frames with an implicit 64-bit
+// sequence (never sent); the nonce is that number, little-endian, then
+// zeros, so no two frames share one and a frame sealed for one sequence
+// number does not open under another. Every frame is sealed on its own,
+// so a dropped frame does not desynchronize later ones, and a replayed
+// or injected frame is checked against the wrong sequence number and
// dropped.
#include
@@ -74,10 +80,15 @@
#include
#include
#include "secLink.h"
-#include "../rs232/rs232.h"
-#include "../packet/packet.h"
-#include "../security/security.h"
-#include "../security/secHash.h"
+#include "../serial/rs232/rs232.h"
+#include "packet.h"
+#include "../crypto/secCurve.h"
+#include "../crypto/security.h"
+#include "../crypto/secHash.h"
+#include "../libdvx/dvxTypes.h"
+
+// ABI stamp checked by the loader (see DVX_ABI_VERSION).
+DVX_ABI_STAMP(seclink);
// ========================================================================
@@ -96,7 +107,7 @@
#define HELLO_VERSION_OFS 1
#define HELLO_FLAGS_OFS 2
#define HELLO_PUB_OFS 3
-#define HELLO_SIZE (HELLO_PUB_OFS + SEC_DH_KEY_SIZE)
+#define HELLO_SIZE (HELLO_PUB_OFS + SEC_X25519_KEY_SIZE)
#define HELLO_FLAG_PASS 0x01
// CONFIRM layout
@@ -111,32 +122,20 @@
#define ROLE_HIGH 0x02
// HKDF info label; the two public keys (lower first) follow it
-#define KDF_LABEL "DVX secLink v2"
+#define KDF_LABEL "DVX secLink v3"
#define KDF_LABEL_LEN ((int32_t)sizeof(KDF_LABEL) - 1)
-#define KDF_INFO_SIZE (KDF_LABEL_LEN + 2 * SEC_DH_KEY_SIZE)
+#define KDF_INFO_SIZE (KDF_LABEL_LEN + 2 * SEC_X25519_KEY_SIZE)
-// HKDF output layout
-#define MAC_KEY_SIZE SEC_SHA256_SIZE
-#define KEY_ENC_LOW_OFS 0
-#define KEY_ENC_HIGH_OFS (KEY_ENC_LOW_OFS + SEC_XTEA_KEY_SIZE)
-#define KEY_MAC_LOW_OFS (KEY_ENC_HIGH_OFS + SEC_XTEA_KEY_SIZE)
-#define KEY_MAC_HIGH_OFS (KEY_MAC_LOW_OFS + MAC_KEY_SIZE)
-#define KEY_CONFIRM_OFS (KEY_MAC_HIGH_OFS + MAC_KEY_SIZE)
+// HKDF output layout: each side's sending key, then the confirmation key
+#define KEY_LOW_OFS 0
+#define KEY_HIGH_OFS (KEY_LOW_OFS + SEC_AEAD_KEY_SIZE)
+#define KEY_CONFIRM_OFS (KEY_HIGH_OFS + SEC_AEAD_KEY_SIZE)
#define KEY_BLOCK_SIZE (KEY_CONFIRM_OFS + SEC_SHA256_SIZE)
-// MAC input: frame header, then the big-endian sequence number
-#define SEQ_BYTES 8
+// The nonce: the frame's sequence number in its first bytes
+#define NONCE_SEQ_BYTES 8
-// Each frame owns 2^CTR_FRAME_SHIFT keystream blocks (32 blocks = 256
-// bytes, more than any payload)
-#define CTR_FRAME_SHIFT 5
-#define XTEA_BLOCK_BYTES 8
-typedef char secLinkCtrRangeFits[((1 << CTR_FRAME_SHIFT) * XTEA_BLOCK_BYTES >= PKT_MAX_PAYLOAD) ? 1 : -1];
-
-// Handshake wait bound. The peer may need several seconds for its DH
-// keypair on slow hardware, but a silent peer (proxy or BBS not running)
-// must not hang the single-threaded caller forever.
-#define HANDSHAKE_TIMEOUT_MS 30000
+#define MS_PER_SEC 1000
// ========================================================================
@@ -155,29 +154,35 @@ struct SecLinkS {
uint64_t txSeq;
uint64_t rxSeq;
PktConnT *pkt;
- SecDhT *dh;
- SecCipherT *txCipher;
- SecCipherT *rxCipher;
SecLinkRecvT userCallback;
+ clock_t exchangeStart; // when our current exchange began
void *userCtx;
int32_t dropped;
int32_t depth; // nesting of public calls (deferred close)
int32_t com;
SecLinkStateE state;
int32_t failRc; // result reported for StateErrorE
- SecHmacT txMac; // HMAC state primed with the send MAC key
- SecHmacT rxMac; // HMAC state primed with the receive MAC key
+ SecX25519JobT job; // the X25519 under way (curveStep)
+ uint8_t secretKey[SEC_X25519_KEY_SIZE]; // ours, for the exchange under way
+ uint8_t txKey[SEC_AEAD_KEY_SIZE];
+ uint8_t rxKey[SEC_AEAD_KEY_SIZE];
uint8_t passSalt[SEC_SHA256_SIZE];
uint8_t peerConfirm[SEC_SHA256_SIZE];
+ uint8_t earlyConfirm[SEC_SHA256_SIZE]; // the peer's, come before our secret was computed
uint8_t myHello[HELLO_SIZE];
uint8_t peerHello[HELLO_SIZE];
uint8_t myConfirm[CONFIRM_SIZE];
bool plainChannel[SECLINK_NUM_CHANNELS];
bool hasPass;
+ bool haveKeys; // session keys (txKey, rxKey) derived
bool havePeerHello; // stored HELLO not yet used
+ bool keysPending; // our public key still to compute (our HELLO not ready)
+ bool secretPending; // the shared secret still to compute
+ bool jobRunning; // job holds the X25519 of keysPending or secretPending
bool helloPending; // our HELLO still to be queued
bool confirmPending; // our CONFIRM still to be queued
bool peerConfirmed;
+ bool haveEarlyConfirm;
bool closePending;
bool established; // a session was READY once; clear frames stay refused until close
};
@@ -188,8 +193,10 @@ struct SecLinkS {
// ========================================================================
static void beginExchange(SecLinkT *link);
-static void computeMac(const SecHmacT *primed, uint64_t seq, const uint8_t *hdr, const uint8_t *body, int32_t len, uint8_t *tag);
-static void deriveKeys(SecLinkT *link);
+static void confirmCheck(SecLinkT *link, const uint8_t *mac);
+static void curveStep(SecLinkT *link);
+static void deriveFinish(SecLinkT *link, const uint8_t *secret);
+static void deriveStart(SecLinkT *link);
static void destroyLink(SecLinkT *link);
static void dropSession(SecLinkT *link);
static void enterCall(SecLinkT *link);
@@ -202,15 +209,18 @@ static void internalReset(void *ctx);
static int leaveCall(SecLinkT *link, int32_t rc);
static SecLinkT *linkCreate(int32_t com, SecLinkRecvT callback, void *ctx);
static SecLinkT *linkFinish(SecLinkT *link);
+static void nonceOf(uint64_t seq, uint8_t *nonce);
static void onCipher(SecLinkT *link, const uint8_t *data, int len);
static void onConfirm(SecLinkT *link, const uint8_t *data, int len);
static void onHello(SecLinkT *link, const uint8_t *data, int len);
static void onPlain(SecLinkT *link, const uint8_t *data, int len);
+static bool peerSilent(const SecLinkT *link);
static int pollOnce(SecLinkT *link);
void secLinkClose(SecLinkT *link);
int32_t secLinkGetDropped(SecLinkT *link);
int secLinkGetPending(SecLinkT *link);
int secLinkHandshake(SecLinkT *link);
+int secLinkHandshakeStart(SecLinkT *link);
int secLinkHandshakeStep(SecLinkT *link);
bool secLinkIsReady(SecLinkT *link);
SecLinkT *secLinkOpen(int com, int32_t bps, int dataBits, char parity, int stopBits, int handshake, SecLinkRecvT callback, void *ctx);
@@ -226,140 +236,121 @@ int secLinkSetPassphrase(SecLinkT *link, const char *pass);
// Static functions (alphabetical)
// ========================================================================
-// Generate a fresh keypair and queue our HELLO. If the peer's HELLO is
-// already stored the keys are derived right away.
+// Start a fresh key pair: curveStep computes its public key and queues
+// our HELLO (and then, with the peer's HELLO already stored, the secret
+// is next).
static void beginExchange(SecLinkT *link) {
- int pubLen = SEC_DH_KEY_SIZE; // secDhGetPublicKey takes an int
-
- secDhDestroy(link->dh);
- link->dh = secDhCreate();
- if (!link->dh) {
- failHandshake(link, SECLINK_ERR_ALLOC);
- return;
- }
-
- // Cannot fail for a non-NULL context
- secDhGenerateKeys(link->dh);
- secDhGetPublicKey(link->dh, link->myHello + HELLO_PUB_OFS, &pubLen);
+ secRngBytes(link->secretKey, SEC_X25519_KEY_SIZE);
+ secX25519JobWipe(&link->job);
+ link->jobRunning = false;
+ link->exchangeStart = clock();
link->myHello[TYPE_OFS] = TYPE_HELLO;
link->myHello[HELLO_VERSION_OFS] = SECLINK_PROTOCOL_VERSION;
link->myHello[HELLO_FLAGS_OFS] = link->hasPass ? HELLO_FLAG_PASS : 0;
link->state = StateHandshakeE;
- link->helloPending = true;
+ link->keysPending = true;
+ link->secretPending = false;
+ link->helloPending = false;
link->confirmPending = false;
link->peerConfirmed = false;
+}
+
+
+// The peer's CONFIRM MAC against the one expected: confirmed, or the
+// exchange fails (a wrong passphrase, or tampering).
+static void confirmCheck(SecLinkT *link, const uint8_t *mac) {
+ if (!secConstTimeEqual(mac, link->peerConfirm, SEC_SHA256_SIZE)) {
+ failHandshake(link, SECLINK_ERR_AUTH);
+ return;
+ }
+
+ link->peerConfirmed = true;
flushHandshake(link);
-
- if (link->havePeerHello) {
- deriveKeys(link);
- }
}
-// tag = first SECLINK_MAC_SIZE bytes of HMAC(macKey, hdr || seq || body),
-// where hdr is the type and channel bytes and seq is big-endian. primed
-// already holds the state after the key blocks (set up once per session),
-// so a frame costs no key-pad compressions.
-static void computeMac(const SecHmacT *primed, uint64_t seq, const uint8_t *hdr, const uint8_t *body, int32_t len, uint8_t *tag) {
- SecHmacT h = *primed;
- uint8_t seqBytes[SEQ_BYTES];
- uint8_t full[SEC_SHA256_SIZE];
+// A slice of the X25519 under way, the slow part of an exchange: our
+// public key (then our HELLO is queued and, with the peer's HELLO here,
+// the secret is next), or the shared secret (then the session keys are
+// derived).
+static void curveStep(SecLinkT *link) {
+ uint8_t out[SEC_X25519_KEY_SIZE];
+ bool ok;
- for (int32_t i = 0; i < SEQ_BYTES; i++) {
- seqBytes[i] = (uint8_t)(seq >> ((SEQ_BYTES - 1 - i) * 8));
+ if (!link->keysPending && !link->secretPending) {
+ return;
}
- secHmacUpdate(&h, hdr, SECLINK_FRAME_HDR_SIZE);
- secHmacUpdate(&h, seqBytes, SEQ_BYTES);
- secHmacUpdate(&h, body, len);
- secHmacFinal(&h, full);
+ if (!link->jobRunning) {
+ secX25519JobStart(&link->job, link->secretKey, link->keysPending ? NULL : link->peerHello + HELLO_PUB_OFS);
+ link->jobRunning = true;
+ }
- memcpy(tag, full, SECLINK_MAC_SIZE);
- secSecureZero(full, sizeof(full));
+ if (!secX25519JobStep(&link->job)) {
+ return;
+ }
+
+ link->jobRunning = false;
+ ok = secX25519JobResult(&link->job, out);
+
+ if (link->keysPending) {
+ link->keysPending = false;
+ link->helloPending = true;
+ memcpy(link->myHello + HELLO_PUB_OFS, out, SEC_X25519_KEY_SIZE);
+ flushHandshake(link);
+
+ if (link->havePeerHello) {
+ deriveStart(link);
+ }
+
+ return;
+ }
+
+ link->secretPending = false;
+
+ if (!ok) {
+ failHandshake(link, SECLINK_ERR_HANDSHAKE);
+ } else {
+ deriveFinish(link, out);
+ }
+
+ secSecureZero(out, sizeof(out));
}
-// Both HELLOs are present (ours in myHello, the peer's in peerHello):
-// validate, derive the session keys and queue our CONFIRM. The new keys
-// replace any previous session's immediately; the peer's frames under the
-// old keys all arrived before its HELLO (the packet layer is in order).
-static void deriveKeys(SecLinkT *link) {
- uint8_t secret[SEC_DH_KEY_SIZE];
+// The secret is computed: derive the session keys and queue our CONFIRM.
+// The new keys replace any previous session's immediately; the peer's
+// frames under the old keys all arrived before its HELLO (the packet layer
+// is in order).
+static void deriveFinish(SecLinkT *link, const uint8_t *secret) {
uint8_t info[KDF_INFO_SIZE];
uint8_t keys[KEY_BLOCK_SIZE];
uint8_t transcript[SEC_SHA256_SIZE];
uint8_t roleMsg[SEC_SHA256_SIZE + 1];
SecSha256T sha;
- SecCipherT *tx;
- SecCipherT *rx;
const uint8_t *lowHello;
const uint8_t *highHello;
- int secretLen = SEC_DH_KEY_SIZE; // secDhGetSecret takes an int
- int32_t cmp;
bool weAreLow;
- bool peerHasPass;
- link->havePeerHello = false;
-
- if (link->peerHello[HELLO_VERSION_OFS] != SECLINK_PROTOCOL_VERSION) {
- failHandshake(link, SECLINK_ERR_HANDSHAKE);
- return;
- }
-
- // No downgrade: a passphrase on either side requires one on both. Our
- // own flag is read from the HELLO we sent, the same one the peer sees
- // and the transcript binds.
- peerHasPass = (link->peerHello[HELLO_FLAGS_OFS] & HELLO_FLAG_PASS) != 0;
- if (peerHasPass != ((link->myHello[HELLO_FLAGS_OFS] & HELLO_FLAG_PASS) != 0)) {
- failHandshake(link, SECLINK_ERR_AUTH);
- return;
- }
-
- // Identical keys mean our own HELLO was reflected back at us.
- cmp = memcmp(link->myHello + HELLO_PUB_OFS, link->peerHello + HELLO_PUB_OFS, SEC_DH_KEY_SIZE);
- if (cmp == 0) {
- failHandshake(link, SECLINK_ERR_HANDSHAKE);
- return;
- }
- weAreLow = cmp < 0;
+ weAreLow = memcmp(link->myHello + HELLO_PUB_OFS, link->peerHello + HELLO_PUB_OFS, SEC_X25519_KEY_SIZE) < 0;
lowHello = weAreLow ? link->myHello : link->peerHello;
highHello = weAreLow ? link->peerHello : link->myHello;
- if (secDhComputeSecret(link->dh, link->peerHello + HELLO_PUB_OFS, SEC_DH_KEY_SIZE) != SEC_SUCCESS || secDhGetSecret(link->dh, secret, &secretLen) != SEC_SUCCESS) {
- failHandshake(link, SECLINK_ERR_HANDSHAKE);
- return;
- }
-
- // Private key no longer needed (forward secrecy)
- secDhDestroy(link->dh);
- link->dh = 0;
+ // Secret key no longer needed (forward secrecy)
+ secSecureZero(link->secretKey, SEC_X25519_KEY_SIZE);
memcpy(info, KDF_LABEL, KDF_LABEL_LEN);
- memcpy(info + KDF_LABEL_LEN, lowHello + HELLO_PUB_OFS, SEC_DH_KEY_SIZE);
- memcpy(info + KDF_LABEL_LEN + SEC_DH_KEY_SIZE, highHello + HELLO_PUB_OFS, SEC_DH_KEY_SIZE);
- secHkdf(link->hasPass ? link->passSalt : 0, link->hasPass ? SEC_SHA256_SIZE : 0, secret, secretLen, info, KDF_INFO_SIZE, keys, KEY_BLOCK_SIZE);
- secSecureZero(secret, sizeof(secret));
+ memcpy(info + KDF_LABEL_LEN, lowHello + HELLO_PUB_OFS, SEC_X25519_KEY_SIZE);
+ memcpy(info + KDF_LABEL_LEN + SEC_X25519_KEY_SIZE, highHello + HELLO_PUB_OFS, SEC_X25519_KEY_SIZE);
+ secHkdf(link->hasPass ? link->passSalt : 0, link->hasPass ? SEC_SHA256_SIZE : 0, secret, SEC_X25519_KEY_SIZE, info, KDF_INFO_SIZE, keys, KEY_BLOCK_SIZE);
- tx = secCipherCreate(keys + (weAreLow ? KEY_ENC_LOW_OFS : KEY_ENC_HIGH_OFS));
- rx = secCipherCreate(keys + (weAreLow ? KEY_ENC_HIGH_OFS : KEY_ENC_LOW_OFS));
- if (!tx || !rx) {
- secCipherDestroy(tx);
- secCipherDestroy(rx);
- secSecureZero(keys, sizeof(keys));
- failHandshake(link, SECLINK_ERR_ALLOC);
- return;
- }
-
- secCipherDestroy(link->txCipher);
- secCipherDestroy(link->rxCipher);
- link->txCipher = tx;
- link->rxCipher = rx;
- secHmacInit(&link->txMac, keys + (weAreLow ? KEY_MAC_LOW_OFS : KEY_MAC_HIGH_OFS), MAC_KEY_SIZE);
- secHmacInit(&link->rxMac, keys + (weAreLow ? KEY_MAC_HIGH_OFS : KEY_MAC_LOW_OFS), MAC_KEY_SIZE);
- link->txSeq = 0;
- link->rxSeq = 0;
+ memcpy(link->txKey, keys + (weAreLow ? KEY_LOW_OFS : KEY_HIGH_OFS), SEC_AEAD_KEY_SIZE);
+ memcpy(link->rxKey, keys + (weAreLow ? KEY_HIGH_OFS : KEY_LOW_OFS), SEC_AEAD_KEY_SIZE);
+ link->haveKeys = true;
+ link->txSeq = 0;
+ link->rxSeq = 0;
// Transcript binds both complete HELLOs (versions, flags, keys)
secSha256Init(&sha);
@@ -381,6 +372,47 @@ static void deriveKeys(SecLinkT *link) {
link->peerConfirmed = false;
link->confirmPending = true;
flushHandshake(link);
+
+ // The peer finished first: its CONFIRM waited for ours
+ if (link->haveEarlyConfirm) {
+ link->haveEarlyConfirm = false;
+ confirmCheck(link, link->earlyConfirm);
+ }
+}
+
+
+// Both HELLOs are present (ours in myHello, the peer's in peerHello):
+// validate, then curveStep computes the secret (then deriveFinish). A
+// newer HELLO from the peer before then is the one used: a secret under
+// way with the older one starts over.
+static void deriveStart(SecLinkT *link) {
+ bool peerHasPass;
+
+ link->havePeerHello = false;
+
+ if (link->peerHello[HELLO_VERSION_OFS] != SECLINK_PROTOCOL_VERSION) {
+ failHandshake(link, SECLINK_ERR_HANDSHAKE);
+ return;
+ }
+
+ // No downgrade: a passphrase on either side requires one on both. Our
+ // own flag is read from the HELLO we sent, the same one the peer sees
+ // and the transcript binds.
+ peerHasPass = (link->peerHello[HELLO_FLAGS_OFS] & HELLO_FLAG_PASS) != 0;
+ if (peerHasPass != ((link->myHello[HELLO_FLAGS_OFS] & HELLO_FLAG_PASS) != 0)) {
+ failHandshake(link, SECLINK_ERR_AUTH);
+ return;
+ }
+
+ // Identical keys mean our own HELLO was reflected back at us.
+ if (memcmp(link->myHello + HELLO_PUB_OFS, link->peerHello + HELLO_PUB_OFS, SEC_X25519_KEY_SIZE) == 0) {
+ failHandshake(link, SECLINK_ERR_HANDSHAKE);
+ return;
+ }
+
+ secX25519JobWipe(&link->job);
+ link->jobRunning = false;
+ link->secretPending = true;
}
@@ -401,19 +433,21 @@ static void destroyLink(SecLinkT *link) {
// Forget all key material and any exchange in progress.
static void dropSession(SecLinkT *link) {
- secDhDestroy(link->dh);
- secCipherDestroy(link->txCipher);
- secCipherDestroy(link->rxCipher);
- link->dh = 0;
- link->txCipher = 0;
- link->rxCipher = 0;
- link->havePeerHello = false;
- link->helloPending = false;
- link->confirmPending = false;
- link->peerConfirmed = false;
- secSecureZero(&link->txMac, sizeof(link->txMac));
- secSecureZero(&link->rxMac, sizeof(link->rxMac));
+ secX25519JobWipe(&link->job);
+ link->jobRunning = false;
+ link->haveKeys = false;
+ link->keysPending = false;
+ link->secretPending = false;
+ link->haveEarlyConfirm = false;
+ link->havePeerHello = false;
+ link->helloPending = false;
+ link->confirmPending = false;
+ link->peerConfirmed = false;
+ secSecureZero(link->secretKey, SEC_X25519_KEY_SIZE);
+ secSecureZero(link->txKey, SEC_AEAD_KEY_SIZE);
+ secSecureZero(link->rxKey, SEC_AEAD_KEY_SIZE);
secSecureZero(link->peerConfirm, SEC_SHA256_SIZE);
+ secSecureZero(link->earlyConfirm, SEC_SHA256_SIZE);
secSecureZero(link->myConfirm, CONFIRM_SIZE);
}
@@ -546,9 +580,6 @@ static int leaveCall(SecLinkT *link, int32_t rc) {
}
-// Encrypted data: verify the MAC against the next receive sequence number,
-// then decrypt. The sequence advances for every encrypted frame so a
-// rejected (tampered or injected) frame does not shift the frames after it.
// A new link (no packet layer yet); com is its port, or -1 for a stream.
static SecLinkT *linkCreate(int32_t com, SecLinkRecvT callback, void *ctx) {
SecLinkT *link = (SecLinkT *)calloc(1, sizeof(SecLinkT));
@@ -580,31 +611,40 @@ static SecLinkT *linkFinish(SecLinkT *link) {
}
+// The nonce of the frame numbered seq: the number, little-endian, then zeros.
+static void nonceOf(uint64_t seq, uint8_t *nonce) {
+ memset(nonce, 0, SEC_AEAD_NONCE_SIZE);
+
+ for (int32_t i = 0; i < NONCE_SEQ_BYTES; i++) {
+ nonce[i] = (uint8_t)(seq >> (i * 8));
+ }
+}
+
+
+// Encrypted data: check the tag against the next receive sequence number,
+// decrypting only when it matches. The sequence advances for every
+// encrypted frame so a rejected (tampered or injected) frame does not
+// shift the frames after it.
static void onCipher(SecLinkT *link, const uint8_t *data, int len) {
- uint8_t plain[PKT_MAX_PAYLOAD];
- uint8_t tag[SECLINK_MAC_SIZE];
- uint64_t seq;
- int32_t bodyLen = len - SECLINK_FRAME_HDR_SIZE - SECLINK_MAC_SIZE;
- uint8_t channel;
+ uint8_t plain[PKT_MAX_PAYLOAD];
+ uint8_t nonce[SEC_AEAD_NONCE_SIZE];
+ int32_t bodyLen = len - SECLINK_FRAME_HDR_SIZE - SECLINK_MAC_SIZE;
+ uint8_t channel;
- if (!link->rxCipher || bodyLen < 0) {
+ if (!link->haveKeys || bodyLen < 0) {
link->dropped++;
return;
}
- seq = link->rxSeq++;
- computeMac(&link->rxMac, seq, data, data + SECLINK_FRAME_HDR_SIZE, bodyLen, tag);
+ nonceOf(link->rxSeq++, nonce);
channel = data[DATA_CHANNEL_OFS];
+ memcpy(plain, data + SECLINK_FRAME_HDR_SIZE, bodyLen);
- if (!secConstTimeEqual(tag, data + SECLINK_FRAME_HDR_SIZE + bodyLen, SECLINK_MAC_SIZE) || channel > SECLINK_MAX_CHANNEL) {
+ if (channel > SECLINK_MAX_CHANNEL || !secAeadUnlock(link->rxKey, nonce, data, SECLINK_FRAME_HDR_SIZE, plain, bodyLen, data + SECLINK_FRAME_HDR_SIZE + bodyLen)) {
link->dropped++;
return;
}
- memcpy(plain, data + SECLINK_FRAME_HDR_SIZE, bodyLen);
- secCipherSetCounter(link->rxCipher, seq << CTR_FRAME_SHIFT);
- secCipherCrypt(link->rxCipher, plain, bodyLen);
-
if (link->userCallback) {
link->userCallback(link->userCtx, plain, bodyLen, channel, true);
}
@@ -615,19 +655,21 @@ static void onCipher(SecLinkT *link, const uint8_t *data, int len) {
// The peer's CONFIRM is only meaningful while we wait for it; a mismatch
// means a different passphrase or a tampered HELLO.
+// A CONFIRM that comes before our secret is computed (the peer was
+// faster) is kept until deriveFinish can check it.
static void onConfirm(SecLinkT *link, const uint8_t *data, int len) {
+ if (len == CONFIRM_SIZE && link->state == StateHandshakeE && link->secretPending) {
+ memcpy(link->earlyConfirm, data + CONFIRM_MAC_OFS, SEC_SHA256_SIZE);
+ link->haveEarlyConfirm = true;
+ return;
+ }
+
if (link->state != StateConfirmE || len != CONFIRM_SIZE) {
link->dropped++;
return;
}
- if (!secConstTimeEqual(data + CONFIRM_MAC_OFS, link->peerConfirm, SEC_SHA256_SIZE)) {
- failHandshake(link, SECLINK_ERR_AUTH);
- return;
- }
-
- link->peerConfirmed = true;
- flushHandshake(link);
+ confirmCheck(link, data + CONFIRM_MAC_OFS);
}
@@ -646,8 +688,9 @@ static void onHello(SecLinkT *link, const uint8_t *data, int len) {
memcpy(link->peerHello, data, HELLO_SIZE);
link->havePeerHello = true;
- if (link->state == StateHandshakeE) {
- deriveKeys(link);
+ // Our public key still to compute: curveStep goes on to the secret
+ if (link->state == StateHandshakeE && !link->keysPending) {
+ deriveStart(link);
} else if (rekey) {
beginExchange(link);
}
@@ -668,7 +711,7 @@ static void onPlain(SecLinkT *link, const uint8_t *data, int len) {
}
channel = data[DATA_CHANNEL_OFS];
- protectedLink = link->established || link->rxCipher;
+ protectedLink = link->established || link->haveKeys;
if (channel > SECLINK_MAX_CHANNEL || (protectedLink && !link->plainChannel[channel])) {
link->dropped++;
return;
@@ -680,6 +723,17 @@ static void onPlain(SecLinkT *link, const uint8_t *data, int len) {
}
+// The line died while we still waited for the peer's HELLO, and as late
+// as the packet layer gives up on a peer that says nothing: the peer never
+// answered. A dead port, or a stream the other end closed, ends the
+// exchange sooner and is a failure, not silence.
+static bool peerSilent(const SecLinkT *link) {
+ clock_t waited = clock() - link->exchangeStart;
+
+ return link->state == StateHandshakeE && !link->havePeerHello && !link->secretPending && waited >= (clock_t)PKT_LINK_TIMEOUT_MS * CLOCKS_PER_SEC / MS_PER_SEC;
+}
+
+
// One receive pass plus pending handshake output. Packet-layer codes are
// mapped so callers only ever see SECLINK_* errors.
static int pollOnce(SecLinkT *link) {
@@ -690,6 +744,7 @@ static int pollOnce(SecLinkT *link) {
}
if (!link->closePending) {
+ curveStep(link);
flushHandshake(link);
}
@@ -745,19 +800,11 @@ int secLinkHandshake(SecLinkT *link) {
enterCall(link);
- // An exchange already in flight (a timed-out earlier call, or one the
- // peer started) is resumed rather than restarted, so the peer never
- // sees two competing HELLOs from us. A ready link is re-keyed.
- if (link->state != StateHandshakeE && link->state != StateConfirmE) {
- beginExchange(link);
- }
-
- // The wait is bounded: pktPoll only reports a disconnect for a closed
- // port, never for an open but silent line.
+ // The wait is bounded: the packet layer reports a silent line as lost
+ // only after PKT_LINK_TIMEOUT_MS.
start = clock();
- timeout = (clock_t)HANDSHAKE_TIMEOUT_MS * CLOCKS_PER_SEC / 1000;
-
- rc = handshakeStatus(link);
+ timeout = (clock_t)SECLINK_HANDSHAKE_TIMEOUT_MS * CLOCKS_PER_SEC / MS_PER_SEC;
+ rc = secLinkHandshakeStart(link);
while (rc == SECLINK_PENDING) {
if (clock() - start >= timeout) {
@@ -771,10 +818,31 @@ int secLinkHandshake(SecLinkT *link) {
}
+// An exchange already in flight (a timed-out earlier call, or one the peer
+// started) is resumed rather than restarted, so the peer never sees two
+// competing HELLOs from us. A ready link is re-keyed.
+int secLinkHandshakeStart(SecLinkT *link) {
+ if (!link) {
+ return SECLINK_ERR_PARAM;
+ }
+
+ enterCall(link);
+
+ if (link->state != StateHandshakeE && link->state != StateConfirmE) {
+ beginExchange(link);
+ }
+
+ return leaveCall(link, handshakeStatus(link));
+}
+
+
// Non-blocking form: starts an exchange on an idle link, then polls once.
-// A failed exchange stays failed (secLinkHandshake starts a new one). The same poll may deliver the peer's CONFIRM and then its
-// hang-up (a proxy exits as soon as it sees a passphrase mismatch): the
-// exchange's outcome wins over the disconnect.
+// A failed exchange stays failed (secLinkHandshake starts a new one). The
+// same poll may deliver the peer's CONFIRM and then its hang-up (a proxy
+// exits as soon as it sees a passphrase mismatch): the exchange's outcome
+// wins over the disconnect. A line that went dead with the peer silent
+// all along is SECLINK_ERR_TIMEOUT (peerSilent), any other dead line
+// SECLINK_ERR_HANDSHAKE.
int secLinkHandshakeStep(SecLinkT *link) {
if (!link) {
return SECLINK_ERR_PARAM;
@@ -787,9 +855,11 @@ int secLinkHandshakeStep(SecLinkT *link) {
}
if (pollOnce(link) == SECLINK_ERR_SERIAL && handshakeStatus(link) == SECLINK_PENDING) {
+ int32_t rc = peerSilent(link) ? SECLINK_ERR_TIMEOUT : SECLINK_ERR_HANDSHAKE;
+
dropSession(link);
link->state = StateInitE;
- return leaveCall(link, SECLINK_ERR_HANDSHAKE);
+ return leaveCall(link, rc);
}
if (link->closePending) {
@@ -897,11 +967,10 @@ int secLinkSend(SecLinkT *link, const uint8_t *data, int len, uint8_t channel, b
frameLen = SECLINK_FRAME_HDR_SIZE + len;
if (encrypt) {
- uint64_t seq = link->txSeq++;
+ uint8_t nonce[SEC_AEAD_NONCE_SIZE];
- secCipherSetCounter(link->txCipher, seq << CTR_FRAME_SHIFT);
- secCipherCrypt(link->txCipher, buf + SECLINK_FRAME_HDR_SIZE, len);
- computeMac(&link->txMac, seq, buf, buf + SECLINK_FRAME_HDR_SIZE, len, buf + frameLen);
+ nonceOf(link->txSeq++, nonce);
+ secAeadLock(link->txKey, nonce, buf, SECLINK_FRAME_HDR_SIZE, buf + SECLINK_FRAME_HDR_SIZE, len, buf + frameLen);
frameLen += SECLINK_MAC_SIZE;
}
diff --git a/src/libs/kpunch/serial/seclink/secLink.h b/src/libs/kpunch/seclink/secLink.h
similarity index 83%
rename from src/libs/kpunch/serial/seclink/secLink.h
rename to src/libs/kpunch/seclink/secLink.h
index 26e9c2dd..78842f4a 100644
--- a/src/libs/kpunch/serial/seclink/secLink.h
+++ b/src/libs/kpunch/seclink/secLink.h
@@ -26,30 +26,31 @@
// three layers into one:
// rs232 -- ISR-driven UART I/O with ring buffers
// packet -- HDLC framing + CRC-16 + Go-Back-N ARQ (reliable delivery)
-// security -- DH key exchange, XTEA-CTR, SHA-256/HMAC/HKDF, DRBG
+// security -- X25519, XChaCha20-Poly1305, SHA-256/HMAC/HKDF, DRBG
//
// Usage:
// 1. secLinkOpen() -- opens COM port, sets up packet framing
// 2. secLinkSetPassphrase() -- optional shared secret (both sides or none)
-// 3. secLinkHandshake() -- authenticated DH key exchange
+// 3. secLinkHandshake() -- authenticated X25519 key exchange
// 4. secLinkSend() -- send data (optionally encrypted) on a channel
// 5. secLinkPoll() -- receive, verify, decrypt, deliver to callback
// 6. secLinkClose() -- tear everything down
//
-// Protocol (version 2):
+// Protocol (version 3):
// Every packet starts with a type byte. Handshake messages (HELLO,
// CONFIRM) are never delivered to the application and application data
// is never taken as key material. HELLO carries the protocol version,
-// flags (passphrase in use) and the DH public key; CONFIRM carries an
-// HMAC over the handshake transcript that proves both sides derived the
-// same keys (and, with a passphrase, that both know it). Session keys
-// come from HKDF-SHA256 over the DH secret, salted with SHA-256 of the
-// passphrase, and are independent per direction.
+// flags (passphrase in use) and the X25519 public key; CONFIRM carries
+// an HMAC over the handshake transcript that proves both sides derived
+// the same keys (and, with a passphrase, that both know it). Session
+// keys come from HKDF-SHA256 over the X25519 secret, salted with
+// SHA-256 of the passphrase, and are independent per direction.
//
-// Encrypted data frames are XTEA-CTR encrypted and carry an 8-byte
-// truncated HMAC-SHA256 over the frame type, channel, an implicit 64-bit
-// per-direction sequence number and the ciphertext. Frames with a bad
-// MAC (tampered, replayed or injected) are dropped and counted.
+// Encrypted data frames are XChaCha20-Poly1305: the type and channel
+// bytes are authenticated, the payload encrypted, and the nonce is an
+// implicit 64-bit per-direction sequence number, so a 16-byte tag
+// covers all of it. Frames with a bad tag (tampered, replayed or
+// injected) are dropped and counted.
//
// Passphrase:
// Without a passphrase on either side the link is encrypted and
@@ -77,13 +78,13 @@
#include
#include
-#include "../packet/packet.h"
+#include "packet.h"
+#include "../crypto/secCurve.h"
// Error codes
#define SECLINK_SUCCESS 0
#define SECLINK_ERR_PARAM (-1)
#define SECLINK_ERR_SERIAL (-2)
-#define SECLINK_ERR_ALLOC (-3)
#define SECLINK_ERR_HANDSHAKE (-4)
#define SECLINK_ERR_NOT_READY (-5)
#define SECLINK_ERR_SEND (-6)
@@ -91,14 +92,19 @@
#define SECLINK_ERR_AUTH (-8) // passphrase mismatch or tampering
#define SECLINK_PENDING 1 // secLinkHandshakeStep: exchange still running
+// secLinkHandshake gives up after this long. The peer may be slow to
+// answer on slow hardware, but a silent peer (proxy or BBS not running)
+// must not hang the caller forever.
+#define SECLINK_HANDSHAKE_TIMEOUT_MS 30000
+
// Wire protocol version carried in HELLO
-#define SECLINK_PROTOCOL_VERSION 2
+#define SECLINK_PROTOCOL_VERSION 3
// Data frame header: type byte + channel byte
#define SECLINK_FRAME_HDR_SIZE 2
-// Truncated HMAC-SHA256 tag appended to every encrypted data frame
-#define SECLINK_MAC_SIZE 8
+// Poly1305 tag appended to every encrypted data frame
+#define SECLINK_MAC_SIZE SEC_AEAD_TAG_SIZE
// Max payload per send (packet max minus frame header and MAC). The same
// limit applies to clear and encrypted sends so callers need one constant.
@@ -158,12 +164,20 @@ int secLinkGetPending(SecLinkT *link);
// ready (re-key) or has its own handshake pending.
int secLinkHandshake(SecLinkT *link);
+// Start an exchange as secLinkHandshake does (one in flight is resumed; a
+// ready link is re-keyed; a failed one starts over), not waiting: run it
+// with secLinkHandshakeStep, giving up when the caller's limit
+// (SECLINK_HANDSHAKE_TIMEOUT_MS, say) passes. Returns as the step does.
+int secLinkHandshakeStart(SecLinkT *link);
+
// Non-blocking handshake for callers with their own event loop: starts an
// exchange on an idle link (never opened one, or dropped by a peer reset),
// polls once and returns SECLINK_SUCCESS once the link is ready,
// SECLINK_PENDING while the exchange is still running, or the error that
// ended it (as secLinkHandshake; it keeps being returned, and
-// secLinkHandshake starts over). Call it again after SECLINK_PENDING.
+// secLinkHandshake or secLinkHandshakeStart starts over). A line the
+// packet layer gave up on while the peer had not answered at all is
+// SECLINK_ERR_TIMEOUT. Call it again after SECLINK_PENDING.
int secLinkHandshakeStep(SecLinkT *link);
// Returns true if handshake is complete and link is ready for data.
diff --git a/src/libs/kpunch/seclink/seclink.dep b/src/libs/kpunch/seclink/seclink.dep
new file mode 100644
index 00000000..a31094b4
--- /dev/null
+++ b/src/libs/kpunch/seclink/seclink.dep
@@ -0,0 +1,3 @@
+crypto
+serial
+libdvx
diff --git a/src/libs/kpunch/seclink/seclink.dhs b/src/libs/kpunch/seclink/seclink.dhs
new file mode 100644
index 00000000..80dace5f
--- /dev/null
+++ b/src/libs/kpunch/seclink/seclink.dhs
@@ -0,0 +1,551 @@
+# 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.
+
+.section Technical Reference (for Developers)|Libraries
+.topic lib.seclink
+.title Secure Link Library
+.toc 0 Secure Link Library
+.index secLink
+.index secLinkOpen
+.index secLinkOpenStream
+.index secLinkClose
+.index secLinkHandshake
+.index secLinkHandshakeStep
+.index secLinkSend
+.index secLinkPoll
+.index secLinkSetPassphrase
+.index secLinkSetChannelPlain
+
+.h1 Secure Link Library
+
+Top-level API composing all three lower layers (rs232, packet, security) into a single interface. Provides an authenticated key exchange, channel multiplexing (0-127), and per-packet encryption with integrity protection over a reliable serial link.
+
+Loaded as: bin/libs/kpunch/seclink/seclink.lib (part of Communications; needs the serial and cryptography libraries). The packet transport below it is in the same library.
+
+Header: seclink/secLink.h
+
+.link lib.seclink.packet Packet Transport
+.link lib.seclink.example End-to-End Example
+.link lib.serial.rs232 RS-232 UART Driver (the serial library)
+.link lib.crypto Cryptography Library
+
+.h2 Protocol (version 3)
+
+Every packet starts with a type byte:
+
+.table
+ Type Layout Purpose
+ ---- ------ -------
+ HELLO type, version, flags, X25519 public key (32 bytes) Starts a key exchange. Flag bit 0: passphrase in use.
+ CONFIRM type, HMAC-SHA256 (32 bytes) Proves both sides derived the same keys.
+ PLAIN type, channel, payload Clear application data.
+ CIPHER type, channel, ciphertext, tag (16 bytes) Encrypted, authenticated application data.
+.endtable
+
+Handshake packets are never delivered to the application, and application data is never taken as key material. A HELLO is kept whenever it arrives, so the side that starts first is never missed.
+
+Key exchange: X25519 (RFC 7748). An X25519 takes about a sixth of a second on a 486, so each poll computes a ninth of one (about 20 ms there): first the side's public key, then the shared secret once both HELLOs are in, so the handshake never holds up other programs for long. A peer key that gives an all-zero secret fails the exchange (SECLINK_ERR_HANDSHAKE).
+
+Key derivation: HKDF-SHA256 with salt = SHA-256(passphrase) (32 zero bytes without one), input = the X25519 shared secret, and info = "DVX secLink v3" followed by the lower then the higher public key. The output supplies a 32-byte sending key for each direction plus a confirmation key; the side with the lower public key sends with the "low" key. Directions never share a key.
+
+Confirmation: each side sends HMAC(confirmation key, SHA-256(lower HELLO + higher HELLO) + role byte). A different passphrase, a passphrase on only one side, or a modified HELLO makes the check fail on both sides with SECLINK_ERR_AUTH. A side that has a passphrase never accepts a peer without one.
+
+Encrypted frames: XChaCha20-Poly1305. Each direction numbers its encrypted frames with an implicit 64-bit sequence number that is never sent; a frame's nonce is that number, little-endian, followed by 16 zero bytes. The type and channel bytes are authenticated (associated data), the payload is encrypted, and the 16-byte Poly1305 tag follows it. Every frame is sealed on its own, so tampered, replayed, or injected frames fail the tag, are dropped and counted (secLinkGetDropped), and later frames still verify.
+
+Without a passphrase the link is encrypted and integrity-protected but not authenticated: an active attacker in the middle could run two separate exchanges. Use the same passphrase on both sides to rule that out.
+
+Clear frames are accepted only until the first session is established. From then on, until secLinkClose, a clear frame is delivered only on a channel marked plaintext with secLinkSetChannelPlain -- even after the session was dropped by a packet-layer reset or a failed re-key, which someone on the line can cause; so neither can turn a protected channel into a clear one. A re-key HELLO on a running session whose protocol version or passphrase flag cannot match is dropped (and counted) without ending the session.
+
+.h2 Constants
+
+.table
+ Constant Value Description
+ -------- ----- -----------
+ SECLINK_PROTOCOL_VERSION 3 Version carried in HELLO; a mismatch fails the handshake.
+ SECLINK_FRAME_HDR_SIZE 2 Type byte plus channel byte.
+ SECLINK_MAC_SIZE 16 Poly1305 tag on encrypted frames (SEC_AEAD_TAG_SIZE).
+ SECLINK_MAX_PAYLOAD 237 Maximum payload per send (PKT_MAX_PAYLOAD minus header and tag), clear or encrypted.
+ SECLINK_MAX_CHANNEL 127 Highest valid channel number.
+ SECLINK_NUM_CHANNELS 128 Array size needed to hold one slot per channel (SECLINK_MAX_CHANNEL + 1).
+ SECLINK_CHAN_BUF_SIZE 4096 Recommended per-channel receive buffer size for callers that buffer inbound data between the receive callback and an application read.
+.endtable
+
+.h2 Error Codes
+
+.table
+ Constant Value Description
+ -------- ----- -----------
+ SECLINK_SUCCESS 0 Operation succeeded.
+ SECLINK_ERR_PARAM -1 Invalid parameter (NULL pointer, bad channel, bad length).
+ SECLINK_ERR_SERIAL -2 Serial port closed or disconnected (secLinkPoll).
+ SECLINK_ERR_HANDSHAKE -4 Handshake failed: invalid public key, protocol version mismatch, reflected HELLO, a dead port or a line that died once the peer answered, or the peer reset mid-exchange.
+ SECLINK_ERR_NOT_READY -5 No session keys yet (encrypted send attempted).
+ SECLINK_ERR_SEND -6 Packet send failed, the peer stopped acknowledging, or a non-blocking send found the window full.
+ SECLINK_ERR_TIMEOUT -7 The peer did not complete the handshake within 30 seconds (secLinkHandshake; the exchange stays pending), or the line went silent before it answered at all (secLinkHandshakeStep).
+ SECLINK_ERR_AUTH -8 Passphrase mismatch, passphrase on only one side, or tampering.
+ SECLINK_PENDING 1 secLinkHandshakeStep only: the exchange is still running.
+.endtable
+
+.h2 SecLinkRecvT
+
+Receive callback type. Delivers plaintext with the channel number. encrypted is true when the frame arrived encrypted and its tag verified, false for a clear frame.
+
+.code
+typedef void (*SecLinkRecvT)(void *ctx, const uint8_t *data, int len, uint8_t channel, bool encrypted);
+.endcode
+
+.h2 secLinkOpen
+
+Open a secure serial link. Opens the COM port via rs232Open and creates a packet layer (default window size) with an internal receive dispatcher. The handshake must be performed separately via secLinkHandshake before encrypted data can be sent.
+
+.code
+SecLinkT *secLinkOpen(int com, int32_t bps, int dataBits, char parity, int stopBits, int handshake, SecLinkRecvT callback, void *ctx);
+.endcode
+
+.table
+ Parameter Description
+ --------- -----------
+ com COM port index (RS232_COM1..RS232_COM4).
+ bps Baud rate.
+ dataBits Data bits per character (5-8).
+ parity Parity mode ('N', 'O', 'E', 'M', 'S', or lowercase).
+ stopBits Stop bits (1 or 2).
+ handshake Flow control mode (RS232_HANDSHAKE_*).
+ callback Called when a packet is received, after verification and decryption. The data pointer is valid only during the callback.
+ ctx User pointer passed to the callback.
+.endtable
+
+Returns an opaque SecLinkT handle, or NULL on failure (allocation failure, rs232Open error, or pktOpen error). On failure all partially initialized resources are cleaned up.
+
+.h2 secLinkOpenStream
+
+Open a secure link over a byte stream (see pktOpenStream) instead of a COM port, e.g. a TCP connection to the host proxy's listening port. Everything else -- handshake, passphrase, channels, sending, and polling -- is the same as for a COM port link. secLinkClose does not close the stream; the caller closes it afterwards.
+
+.code
+SecLinkT *secLinkOpenStream(const PktStreamT *stream, SecLinkRecvT callback, void *ctx);
+.endcode
+
+.h2 secLinkClose
+
+Close the link and free all associated resources: wipe the key material, close the packet layer and the COM port, and free the link. Safe to call with a NULL link (no-op). Safe to call from inside the receive callback: the close then happens when the outermost secLink call returns, and no further packets are delivered.
+
+.code
+void secLinkClose(SecLinkT *link);
+.endcode
+
+.h2 secLinkSetPassphrase
+
+Set the shared passphrase for the next handshake. NULL or "" clears it. Only SHA-256 of the text is kept. Set it before calling secLinkHandshake; both sides must use the same passphrase, or both none. An exchange already in progress (for example one left pending by a timed-out secLinkHandshake) was started with the old setting, so it is abandoned: the next secLinkHandshake starts a fresh one.
+
+.code
+int secLinkSetPassphrase(SecLinkT *link, const char *pass);
+.endcode
+
+Returns SECLINK_SUCCESS or SECLINK_ERR_PARAM for a NULL link.
+
+.h2 secLinkHandshake
+
+Run the authenticated X25519 key exchange. Blocks, polling the packet layer, until both CONFIRM messages have been checked, or until 30 seconds pass.
+
+.code
+int secLinkHandshake(SecLinkT *link);
+.endcode
+
+Behavior:
+
+.list
+.item A fresh X25519 key pair is made from the DRBG and our HELLO is sent. If the peer's HELLO already arrived (it started first), the shared secret follows on the next poll.
+.item The X25519 secret key is wiped as soon as the session keys are derived (forward secrecy).
+.item After SECLINK_ERR_TIMEOUT the exchange stays pending: it still completes during secLinkPoll if the peer shows up, and calling secLinkHandshake again resumes the wait without sending a new key.
+.item On a ready link it negotiates fresh session keys. A peer that is already ready (or has an exchange pending) answers automatically from secLinkPoll.
+.item Encrypted traffic under the previous keys that was sent before the new HELLO is still accepted; encrypted sends are refused (SECLINK_ERR_NOT_READY) until the new exchange completes.
+.endlist
+
+Returns SECLINK_SUCCESS, SECLINK_ERR_PARAM, SECLINK_ERR_HANDSHAKE, SECLINK_ERR_TIMEOUT, or SECLINK_ERR_AUTH. When the peer confirms (or rejects) and hangs up in the same poll -- a proxy exits as soon as it sees a passphrase mismatch -- the exchange's outcome is reported, not the disconnect.
+
+.h2 secLinkHandshakeStep
+
+Non-blocking handshake for callers that run their own event loop.
+
+.code
+int secLinkHandshakeStep(SecLinkT *link);
+.endcode
+
+Starts an exchange on an idle link (never handshaken, or dropped by a peer reset), polls once, and returns SECLINK_SUCCESS once the link is ready, SECLINK_PENDING (1) while the exchange is still running, or the error that ended it (the same codes as secLinkHandshake). SECLINK_ERR_TIMEOUT here means the packet layer gave up on the line (PKT_LINK_TIMEOUT_MS) while the peer had not answered at all; the caller's own limit is its to keep. A failed exchange keeps returning its error; secLinkHandshake starts a new one.
+
+.h2 secLinkSend
+
+Send data on a channel, optionally encrypted and authenticated.
+
+.code
+int secLinkSend(SecLinkT *link, const uint8_t *data, int len, uint8_t channel, bool encrypt, bool block);
+.endcode
+
+.table
+ Parameter Description
+ --------- -----------
+ link Connection handle from secLinkOpen.
+ data Plaintext payload.
+ len Payload length. Must be in 1..SECLINK_MAX_PAYLOAD.
+ channel Logical channel number (0..SECLINK_MAX_CHANNEL).
+ encrypt If true, seal (encrypt and tag) before sending (requires a completed handshake).
+ block If true, wait (pumping the receive callback) until the transmit window has space. If false, return SECLINK_ERR_SEND immediately when the window is full.
+.endtable
+
+Returns SECLINK_SUCCESS on success or a negative error code (SECLINK_ERR_PARAM, SECLINK_ERR_NOT_READY, SECLINK_ERR_SEND).
+
+A frame takes its sequence number only once window space is guaranteed, and is queued without pumping in between, so frames always reach the wire in sequence order. The receive callback may itself send (blocking or not), including encrypted.
+
+Once session keys exist the receiver drops clear frames unless it marked the channel plaintext with secLinkSetChannelPlain.
+
+.h2 secLinkSendBuf
+
+Send an arbitrarily large buffer by splitting into SECLINK_MAX_PAYLOAD chunks. Always blocks until all data is sent.
+
+.code
+int secLinkSendBuf(SecLinkT *link, const uint8_t *data, int len, uint8_t channel, bool encrypt);
+.endcode
+
+Returns SECLINK_SUCCESS or the first error encountered.
+
+.h2 secLinkSetChannelPlain
+
+Allow (plain = true) or refuse (plain = false, the default) clear frames on a channel once session keys exist. Before the first handshake clear frames are always accepted.
+
+.code
+int secLinkSetChannelPlain(SecLinkT *link, uint8_t channel, bool plain);
+.endcode
+
+Returns SECLINK_SUCCESS or SECLINK_ERR_PARAM for a bad link or channel.
+
+.h2 secLinkPoll
+
+Poll for incoming data. Drains the serial port, processes frames and retransmits, completes pending handshake steps, verifies and decrypts data frames, and forwards them to the callback.
+
+.code
+int secLinkPoll(SecLinkT *link);
+.endcode
+
+Returns the number of packets delivered by the packet layer this call, SECLINK_ERR_PARAM if link is NULL, or SECLINK_ERR_SERIAL when the port is closed or disconnected. Must be called frequently in the main loop.
+
+If the remote packet layer resets, the session keys are dropped (the link is no longer ready) and a new handshake is needed. Clear frames stay refused on protected channels (see Protocol).
+
+.h2 secLinkIsReady
+
+Check whether the handshake is complete and the link is ready for encrypted traffic. Cleartext sends do not require the link to be ready.
+
+.code
+bool secLinkIsReady(SecLinkT *link);
+.endcode
+
+.h2 secLinkGetDropped
+
+Number of received frames dropped since open: bad tag (tampered, replayed, or injected), clear frames on a protected channel, encrypted frames without session keys, and malformed frames.
+
+.code
+int32_t secLinkGetDropped(SecLinkT *link);
+.endcode
+
+Returns the count or SECLINK_ERR_PARAM when link is NULL.
+
+.h2 secLinkGetPending
+
+Get the number of unacknowledged packets in the transmit window. Delegates directly to pktGetPending on the underlying packet connection. Useful for non-blocking send loops to decide when to throttle.
+
+.code
+int secLinkGetPending(SecLinkT *link);
+.endcode
+
+Returns the pending count on success or SECLINK_ERR_PARAM when link is NULL.
+
+.topic lib.seclink.packet
+.title Packet Transport
+.toc 1 Packet Transport
+.index packet
+.index HDLC
+.index CRC-16
+.index Go-Back-N
+.index pktOpen
+.index pktOpenStream
+.index PktStreamT
+.index pktClose
+.index pktSend
+.index pktPoll
+
+.h1 Packet Transport
+
+Reliable, ordered packet delivery over an RS-232 serial link. Uses HDLC-style byte-stuffed framing for frame delimiting, CRC-16-CCITT for error detection, and Go-Back-N ARQ with a configurable sliding window for retransmission.
+
+Header: seclink/packet.h
+
+.h2 Constants
+
+.table
+ Constant Value Description
+ -------- ----- -----------
+ PKT_MAX_PAYLOAD 255 Maximum bytes per packet payload.
+ PKT_DEFAULT_WINDOW 4 Default sliding window size (unacknowledged frames in flight).
+ PKT_MAX_WINDOW 8 Maximum sliding window size.
+.endtable
+
+.h2 Error Codes
+
+.table
+ Constant Value Description
+ -------- ----- -----------
+ PKT_SUCCESS 0 Operation succeeded.
+ PKT_ERR_INVALID_PARAM -1 Invalid parameter (NULL handle, bad data pointer, length out of range).
+ PKT_ERR_TX_FULL -2 Transmit window is full (non-blocking send only).
+ PKT_ERR_DISCONNECTED -3 Serial port returned a negative read (disconnect/error), or the connection was closed from its own callback.
+ PKT_ERR_TIMEOUT -4 A blocking send got no acknowledgement for PKT_SEND_TIMEOUT_MS (15000 ms).
+.endtable
+
+.h2 PktRecvCallbackT
+
+Callback type for received packets.
+
+.code
+typedef void (*PktRecvCallbackT)(void *ctx,
+ const uint8_t *data, int len);
+.endcode
+
+.table
+ Parameter Description
+ --------- -----------
+ ctx User context pointer (passed to pktOpen).
+ data Payload bytes (valid only during the callback).
+ len Payload length.
+.endtable
+
+.h2 pktOpen
+
+Open a packetized connection over an already-open COM port. Only one packet connection may exist on a given COM port at a time; a second pktOpen call on the same port returns NULL to avoid silent framing corruption.
+
+.code
+PktConnT *pktOpen(int com, int windowSize,
+ PktRecvCallbackT callback, void *callbackCtx);
+.endcode
+
+.table
+ Parameter Description
+ --------- -----------
+ com COM port index (RS232_COM1..RS232_COM4). Must already be open via rs232Open.
+ windowSize Sliding window size (1..PKT_MAX_WINDOW). Pass 0 for PKT_DEFAULT_WINDOW. Values greater than PKT_MAX_WINDOW are clamped.
+ callback Called when a complete, CRC-verified, in-order packet is received. The data pointer is valid only during the callback.
+ callbackCtx User pointer passed to the callback.
+.endtable
+
+Returns an opaque PktConnT handle, or NULL on failure (invalid port, duplicate open, or allocation failure).
+
+.h2 pktOpenStream
+
+Open a packetized connection over any byte stream instead of a COM port -- a TCP socket, for example. The stream is described by non-blocking calls and a context pointer, copied into the connection. Any number of stream connections may be open.
+
+.code
+typedef struct {
+ int32_t (*read)(void *ctx, uint8_t *buf, int32_t len);
+ int32_t (*write)(void *ctx, const uint8_t *data, int32_t len);
+ void *ctx;
+ int32_t (*pending)(void *ctx);
+} PktStreamT;
+
+PktConnT *pktOpenStream(const PktStreamT *stream, int windowSize, PktRecvCallbackT callback, void *callbackCtx);
+.endcode
+
+.table
+ Member Description
+ ------ -----------
+ read Up to len bytes into buf: the count, 0 when nothing has arrived, negative when the stream is gone (pktPoll then reports PKT_ERR_DISCONNECTED).
+ write Bytes taken from data (0 = none right now), negative when the stream is gone. Frames wait in the connection's transmit queue, and each pktPoll hands the stream what it takes; a frame that finds no room in the queue is dropped (a data frame is sent again by its retransmit timer).
+ ctx Passed to the calls.
+ pending Bytes taken but not yet on the wire (NULL: the stream does not say). Retransmit timers wait while there are any and their count keeps moving: a count that stays the same for 2 seconds (flow control holding the line) no longer holds them.
+.endtable
+
+Returns the handle, or NULL for a NULL stream or call, or on allocation failure. pktClose never closes the stream.
+
+.h2 pktClose
+
+Close a packetized connection and release its registry slot. Does not close the underlying COM port. The caller is responsible for calling rs232Close separately when no longer needed. May be called from inside the receive callback: the connection is then freed when the outermost pktPoll returns (which reports PKT_ERR_DISCONNECTED).
+
+.code
+void pktClose(PktConnT *conn);
+.endcode
+
+.h2 pktSend
+
+Send a data packet. The payload is copied into an internal retransmit slot before transmission, so the caller may reuse the supplied buffer immediately.
+
+.code
+int pktSend(PktConnT *conn, const uint8_t *data,
+ int len, bool block);
+.endcode
+
+.table
+ Parameter Description
+ --------- -----------
+ conn Connection handle from pktOpen.
+ data Payload bytes (1..PKT_MAX_PAYLOAD).
+ len Payload length.
+ block If true, wait for transmit window space via pktWaitSpace. If false, return PKT_ERR_TX_FULL immediately when the window is full.
+.endtable
+
+Returns PKT_SUCCESS on success, PKT_ERR_INVALID_PARAM for NULL/out-of-range input, PKT_ERR_TX_FULL for a non-blocking call with a full window, PKT_ERR_DISCONNECTED if the serial port drops during a blocking wait, or PKT_ERR_TIMEOUT if the peer stops acknowledging. A frame the port accepts only partially (flow control) is completed before the call returns.
+
+.h2 pktPoll
+
+Poll for incoming data, process received frames, and handle retransmits. Must be called frequently in the main loop. The callback is invoked synchronously for each complete, CRC-verified, in-order data frame and may call pktSend or pktPoll again: received bytes are consumed from a per-connection buffer, so a nested call continues the same byte stream in order. After a gap in the sequence only one NAK is sent until the missing frame arrives (go-back-N resends the whole window on one NAK).
+
+.code
+int pktPoll(PktConnT *conn);
+.endcode
+
+Returns the number of valid data packets delivered to the callback this call, or PKT_ERR_INVALID_PARAM if conn is NULL, or PKT_ERR_DISCONNECTED if the serial layer returned a negative read.
+
+.h2 pktReset
+
+Reset the local connection state (sequence numbers, transmit slots, RX state machine) and send a RST frame to the remote side. The remote's RST handler clears its own state (discarding its unacknowledged frames), runs its reset callback, and answers with a RST reply. A RST reply that answers no reset of ours is ignored. Useful for recovering from a desynchronized connection.
+
+.code
+int pktReset(PktConnT *conn);
+.endcode
+
+Returns PKT_SUCCESS on success or PKT_ERR_INVALID_PARAM if conn is NULL.
+
+.h2 pktCanSend
+
+Check whether there is room in the transmit window for another packet. Returns false when conn is NULL or the transmit window is full.
+
+.code
+bool pktCanSend(PktConnT *conn);
+.endcode
+
+.h2 pktGetPending
+
+Get the number of unacknowledged packets currently in the transmit window. Ranges from 0 (all sent packets acknowledged) to windowSize (window full).
+
+.code
+int pktGetPending(PktConnT *conn);
+.endcode
+
+Returns the pending count on success, or PKT_ERR_INVALID_PARAM if conn is NULL.
+
+.h2 pktSetResetCallback
+
+Register a callback run when the remote side resets the connection (its unacknowledged frames are gone at that point, so an upper layer with per-frame state must resynchronize). The callback receives the ctx passed to pktOpen. NULL removes it.
+
+.code
+typedef void (*PktResetCallbackT)(void *ctx);
+void pktSetResetCallback(PktConnT *conn, PktResetCallbackT callback);
+.endcode
+
+.h2 pktWaitSpace
+
+Poll until the transmit window has room. The wait restarts whenever an acknowledgement frees a slot, so only a peer that stops acknowledging times out.
+
+.code
+int pktWaitSpace(PktConnT *conn);
+.endcode
+
+Returns PKT_SUCCESS, PKT_ERR_INVALID_PARAM, PKT_ERR_DISCONNECTED, or PKT_ERR_TIMEOUT after PKT_SEND_TIMEOUT_MS without progress.
+
+.topic lib.seclink.example
+.title End-to-End Example
+.toc 1 End-to-End Example
+.index Serial Example
+.index BBS Client
+
+.h1 End-to-End Example
+
+Below is a complete secure BBS client that opens COM1, runs the authenticated key exchange, and then shuttles terminal traffic over a single encrypted channel. It demonstrates the typical serial stack usage: load the RNG seed file, open the link, set the passphrase, run the handshake, then enter a poll+send loop.
+
+.code
+#include "rs232.h"
+#include "packet.h"
+#include "security.h"
+#include "secLink.h"
+#include
+#include
+
+#define CHAN_TERMINAL 0
+
+#define SEED_FILE "SECLINK.RND"
+
+static void onPacket(void *ctx, const uint8_t *data, int len, uint8_t channel, bool encrypted) {
+ (void)ctx;
+ if (channel == CHAN_TERMINAL && encrypted) {
+ fwrite(data, 1, len, stdout);
+ fflush(stdout);
+ }
+}
+
+
+int main(void) {
+ // Carry entropy over from the last run (hardware entropy is
+ // gathered automatically; feed key timings with secRngAddEntropy).
+ secRngLoadSeed(SEED_FILE);
+
+ // Open the link at 115200 8N1 with RTS/CTS flow control.
+ SecLinkT *link = secLinkOpen(RS232_COM1, 115200, 8, 'N', 1, RS232_HANDSHAKE_RTSCTS, onPacket, NULL);
+ if (!link) {
+ fprintf(stderr, "secLinkOpen failed\n");
+ return 1;
+ }
+
+ // Authenticated key exchange. The proxy must use the same passphrase.
+ secLinkSetPassphrase(link, "shared secret words");
+ if (secLinkHandshake(link) != SECLINK_SUCCESS) {
+ fprintf(stderr, "Handshake failed\n");
+ secLinkClose(link);
+ return 1;
+ }
+
+ // Send an encrypted banner and then relay keyboard input.
+ const char *hello = "Hello, secure BBS!\r\n";
+ secLinkSend(link, (const uint8_t *)hello, (int)strlen(hello), CHAN_TERMINAL, true, true);
+
+ while (!feof(stdin)) {
+ // Deliver any incoming packets to onPacket().
+ secLinkPoll(link);
+
+ // If the user typed something, forward it encrypted.
+ int ch = getchar();
+ if (ch == EOF) {
+ break;
+ }
+ uint8_t byte = (uint8_t)ch;
+ secLinkSend(link, &byte, 1, CHAN_TERMINAL, true, true);
+ }
+
+ secLinkClose(link);
+ secRngSaveSeed(SEED_FILE);
+ return 0;
+}
+.endcode
+
+Notes:
+
+.list
+.item secLinkOpen internally calls rs232Open and pktOpen. secLinkClose tears them down in reverse order.
+.item The DRBG gathers hardware entropy on first use. On a CPU without a time stamp counter that alone is guessable; the seed file and user-input timings (secRngAddEntropy) are what make the X25519 keys unpredictable.
+.item secLinkPoll must be called frequently to drain the RX ring buffer, process ACKs, and dispatch received packets to the callback.
+.item For bulk transfers larger than SECLINK_MAX_PAYLOAD (237 bytes), use secLinkSendBuf which splits the data into chunks automatically.
+.endlist
diff --git a/src/libs/kpunch/serial/Makefile b/src/libs/kpunch/serial/Makefile
index bb96528a..40a881bb 100644
--- a/src/libs/kpunch/serial/Makefile
+++ b/src/libs/kpunch/serial/Makefile
@@ -20,22 +20,21 @@
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
# IN THE SOFTWARE.
-# DVX Serial Stack Makefile for DJGPP cross-compilation
+# Serial Library Makefile for DJGPP cross-compilation
#
-# Builds serial.lib -- combines rs232, packet, security, and seclink
-# into a single DXE library loaded by the DVX system.
+# serial.lib: the interrupt-driven UART driver (rs232/). The packets and
+# secure link over it are seclink.lib (../seclink), the cryptography
+# crypto.lib (../crypto).
include ../../../common.mk
-CFLAGS = $(DOS_CFLAGS) -Irs232 -Ipacket -Isecurity -Iseclink -I../libdvx -I../libdvx/platform -I../libtasks
+CFLAGS = $(DOS_CFLAGS) -Irs232 -I../libdvx -I../libdvx/platform -I../libtasks
-OBJDIR = ../../../../obj/serial
-LIBSDIR = ../../../../bin/libs
+OBJDIR = ../../../../obj/serial
+LIBSDIR = ../../../../bin/libs
-# One object per source; the source's subdirectory is dropped (the names
-# are unique across rs232/, packet/, security/ and seclink/).
-SRCS = rs232/rs232.c packet/packet.c security/secHash.c security/security.c security/secBas.c seclink/secLink.c
-OBJS = $(patsubst %.c,$(OBJDIR)/%.o,$(notdir $(SRCS)))
+SRCS = rs232/rs232.c
+OBJS = $(patsubst %.c,$(OBJDIR)/%.o,$(notdir $(SRCS)))
TARGETDIR = $(LIBSDIR)/kpunch/serial
TARGET = $(TARGETDIR)/serial.lib
@@ -64,4 +63,4 @@ $(TARGETDIR):
-include $(OBJS:.o=.d)
clean:
- rm -rf $(OBJS) $(OBJS:.o=.d) $(TARGET) $(TARGETDIR)/serial.dep $(TARGETDIR)
+ rm -rf $(OBJDIR) $(TARGETDIR)
diff --git a/src/libs/kpunch/serial/packet/README.md b/src/libs/kpunch/serial/packet/README.md
deleted file mode 100644
index 80612027..00000000
--- a/src/libs/kpunch/serial/packet/README.md
+++ /dev/null
@@ -1,27 +0,0 @@
-# packet -- Reliable HDLC Transport
-
-A reliable, ordered serial-transport layer. HDLC-style byte stuffing
-(FLAG=0x7E, ESC=0x7D), CRC-16-CCITT integrity, and Go-Back-N sliding-
-window retransmission over any byte-oriented link. Sits on top of the
-rs232 driver (or the Linux socket shim used by the proxy).
-
-## Documentation
-
-Full reference is in the combined serial stack doc:
-`../serial.dhs` (the `packet` section). It compiles into the DVX System
-Reference -- open via the Help Viewer app, or read
-`docs/dvx_system_reference.html` after `make`.
-
-Topics covered: connection open/close, send/poll, frame format, window
-size, retransmit timer, CRC polynomial and initial value, error codes,
-callback contract.
-
-## Source Files
-
-- `packet.h` -- public API and error codes
-- `packet.c` -- implementation (framing, CRC, sliding window)
-
-## Build
-
-Built into `serial.lib` by `make -C src/libs/kpunch/serial` (invoked by
-the top-level `make`).
diff --git a/src/libs/kpunch/serial/rs232/rs232.c b/src/libs/kpunch/serial/rs232/rs232.c
index a041e767..e9c2bad4 100644
--- a/src/libs/kpunch/serial/rs232/rs232.c
+++ b/src/libs/kpunch/serial/rs232/rs232.c
@@ -32,6 +32,8 @@
#include "rs232.h"
#include "dvxPlat.h"
+#include
+
// ABI stamp checked by the loader (see DVX_ABI_VERSION).
DVX_ABI_STAMP(serial);
@@ -48,8 +50,8 @@ DVX_ABI_STAMP(serial);
#define OUTPW(a, w) (outportw((a), (w)), (w))
// Buffer sizes must be power of 2 so (index & MASK) wraps correctly.
-// 2048 bytes balances memory usage (8KB per port for both buffers) against
-// burst tolerance at 115200 bps.
+// 2048 bytes balances memory usage (4 KB a port for both buffers, taken
+// when it opens) against burst tolerance at 115200 bps.
#define RX_BUFFER_BITS 11 // 2048
#define TX_BUFFER_BITS 11 // 2048
#define RX_BUFFER_SIZE (1L << RX_BUFFER_BITS)
@@ -104,6 +106,11 @@ DVX_ABI_STAMP(serial);
#define IRQ_NONE 0xFF
#define IRQ_COUNT 16
+// findIrq: line-status reads to wait for the transmitter to empty before
+// the UART counts as stuck (each an ISA bus cycle, about a microsecond:
+// some 0.2 s, past one character at 110 bps)
+#define FIND_IRQ_TX_POLLS 200000
+
// Max valid UART port base (12-bit I/O address).
#define UART_MAX_BASE 0xFFF
@@ -124,7 +131,9 @@ DVX_ABI_STAMP(serial);
#define PIC_MASTER 0x20
#define PIC_SLAVE 0xA0
#define PIC_EOI 0x20
+#define PIC_SPECIFIC_EOI 0x60 // OCW2: end the in-service level in its low bits
#define PIC_RR 0x02
+#define PIC_RIS 0x01 // OCW3 with PIC_RR: read the in-service register
#define PIC_MASTER_IRQ_MAX 7 // highest IRQ on the master 8259
#define PIC_CASCADE_IRQ 2 // master input the slave is wired to
#define SLAVE_IRQ_BASE 8 // IRQ8 is slave bit 0 (IRQ8..15 -> slave IMR bit 0..7)
@@ -316,8 +325,8 @@ typedef struct {
uint8_t txCtrl; // XON/XOFF waiting for THR (TX_CTRL_NONE if none)
uint8_t txBurst; // bytes the THR/FIFO takes per THRE interrupt
uint8_t uartType; // RS232_UART_* probed at open
- uint8_t rxBuff[(uint16_t)RX_BUFFER_SIZE];
- uint8_t txBuff[(uint16_t)TX_BUFFER_SIZE];
+ uint8_t *rxBuff; // RX_BUFFER_SIZE bytes, allocated and locked while open
+ uint8_t *txBuff; // TX_BUFFER_SIZE bytes, the same
uint32_t base;
uint32_t dlatch;
uint32_t rxHead;
@@ -411,7 +420,11 @@ static void irqRestore(uint32_t flags);
static uint32_t irqSave(void);
static void mcrUpdate(Rs232StateT *port, uint8_t bits, bool set);
static int32_t oneBitIndex(uint8_t mask);
+static void picHealIrq(uint8_t irq);
static uint8_t picReadIrr(uint16_t port);
+static uint8_t picReadIsr(uint16_t port);
+static void portBuffersFree(Rs232StateT *port);
+static int32_t portBuffersGet(Rs232StateT *port);
static int32_t probeUartType(Rs232StateT *port);
static void queueTxCtrl(Rs232StateT *port, uint8_t ctrl);
static void removeIrqHandler(int32_t irq);
@@ -451,6 +464,7 @@ int32_t rs232SetRts(int32_t com, bool rts);
int32_t rs232SetStop(int32_t com, int32_t stopBits);
int32_t rs232Write(int32_t com, const char *data, int32_t len);
int32_t rs232WriteBuf(int32_t com, const char *data, int32_t len);
+static void uartRearm(Rs232StateT *port);
// ========================================================================
@@ -748,8 +762,12 @@ static int32_t findIrq(Rs232StateT *com) {
UART_WRITE_MCR(com, (UART_READ_MCR(com) & MCR_LINES) | MCR_OUT2);
UART_WRITE_FCR(com, 0);
- // Wait until TX hold register is empty
- while ((UART_READ_LSR(com) & LSR_TX_HOLD_EMPTY) == 0) {
+ // Wait until TX hold register is empty; a transmitter that never
+ // empties (auto-CTS held off, a broken device) is no IRQ found
+ for (int32_t polls = 0; (UART_READ_LSR(com) & LSR_TX_HOLD_EMPTY) == 0; polls++) {
+ if (polls >= FIND_IRQ_TX_POLLS) {
+ return RS232_ERR_IRQ_NOT_FOUND;
+ }
}
flags = irqSave();
@@ -881,12 +899,86 @@ static int32_t oneBitIndex(uint8_t mask) {
}
+// With interrupts off, outside the handler: irq shown in service at the
+// PIC had its end of interrupt never sent (seen in DOSBox under a serial
+// flood), and the PIC then holds that IRQ and every lower one back for
+// good. A specific EOI ends it (and the slave's cascade input on the
+// master when it is the slave's).
+static void picHealIrq(uint8_t irq) {
+ bool slave = irq > PIC_MASTER_IRQ_MAX;
+ uint16_t pic = slave ? PIC_SLAVE : PIC_MASTER;
+ uint8_t level = slave ? (uint8_t)(irq - SLAVE_IRQ_BASE) : irq;
+
+ if (!(picReadIsr(pic) & (1 << level))) {
+ return;
+ }
+
+ PIC_WRITE_OCW2(pic, PIC_SPECIFIC_EOI | level);
+
+ if (slave && (picReadIsr(PIC_MASTER) & (1 << PIC_CASCADE_IRQ))) {
+ PIC_WRITE_OCW2(PIC_MASTER, PIC_SPECIFIC_EOI | PIC_CASCADE_IRQ);
+ }
+}
+
+
static uint8_t picReadIrr(uint16_t port) {
PIC_WRITE_OCW3(port, PIC_RR);
return inportb(port);
}
+// The in-service register; the PIC is left reading IRR (picReadIrr's
+// mode, its default).
+static uint8_t picReadIsr(uint16_t port) {
+ PIC_WRITE_OCW3(port, PIC_RR | PIC_RIS);
+
+ uint8_t isr = inportb(port);
+
+ PIC_WRITE_OCW3(port, PIC_RR);
+ return isr;
+}
+
+
+// Unlock and free the buffers portBuffersGet gave a port (its interrupt
+// is off).
+static void portBuffersFree(Rs232StateT *port) {
+ platformUnlockRegion(port->rxBuff, RX_BUFFER_SIZE, false);
+ platformUnlockRegion(port->txBuff, TX_BUFFER_SIZE, false);
+ free(port->rxBuff);
+ free(port->txBuff);
+ port->rxBuff = NULL;
+ port->txBuff = NULL;
+}
+
+
+// A port's buffers, taken as it opens and locked for its interrupt
+// handler, so a port that is never opened costs no memory. The port has
+// them only when both are locked.
+static int32_t portBuffersGet(Rs232StateT *port) {
+ uint8_t *rx = (uint8_t *)calloc(1, RX_BUFFER_SIZE);
+ uint8_t *tx = (uint8_t *)calloc(1, TX_BUFFER_SIZE);
+ int32_t rc = RS232_ERR_NO_MEMORY;
+
+ if (rx && tx) {
+ rc = RS232_ERR_LOCK_MEM;
+
+ if (platformLockRegion(rx, RX_BUFFER_SIZE, false) == 0) {
+ if (platformLockRegion(tx, TX_BUFFER_SIZE, false) == 0) {
+ port->rxBuff = rx;
+ port->txBuff = tx;
+ return RS232_SUCCESS;
+ }
+
+ platformUnlockRegion(rx, RX_BUFFER_SIZE, false);
+ }
+ }
+
+ free(rx);
+ free(tx);
+ return rc;
+}
+
+
// Detect UART type by probing hardware features. The detection sequence:
// 1. An unpopulated address reads 0xFF everywhere: no UART
// 2. Write/read scratch register -- 8250 doesn't have one
@@ -1010,6 +1102,7 @@ int32_t rs232Close(int32_t com) {
irqRestore(flags);
detachIrq(port);
+ portBuffersFree(port);
return RS232_SUCCESS;
}
@@ -1240,6 +1333,7 @@ int32_t rs232GetTxBuffered(int32_t com) {
flags = irqSave();
count = TX_COUNT(port);
+ uartRearm(port);
irqRestore(flags);
return count;
@@ -1287,6 +1381,11 @@ int32_t rs232Open(int32_t com, int32_t bps, int32_t dataBits, char parity, int32
if (port->uartType == RS232_UART_UNKNOWN) {
return RS232_ERR_NO_UART;
}
+ rc = portBuffersGet(port);
+ if (rc != RS232_SUCCESS) {
+ return rc;
+ }
+
port->txBurst = port->uartType == RS232_UART_16550A ? FIFO_SIZE : NO_FIFO_BURST;
port->rxFlowOn = 1;
port->txFlowOn = 1;
@@ -1305,6 +1404,7 @@ int32_t rs232Open(int32_t com, int32_t bps, int32_t dataBits, char parity, int32
rc = attachIrq(port, irq);
if (rc != RS232_SUCCESS) {
+ portBuffersFree(port);
return rc;
}
port->isOpen = 1;
@@ -1362,6 +1462,7 @@ int32_t rs232Read(int32_t com, char *data, int32_t len) {
}
}
+ uartRearm(port);
irqRestore(flags);
return i;
@@ -1668,13 +1769,13 @@ int32_t rs232SetStop(int32_t com, int32_t stopBits) {
}
-// Blocking polled write: busy-waits on LSR_TX_HOLD_EMPTY for each byte.
-// This bypasses the TX ring buffer entirely and writes directly to the UART.
-// Used by the packet layer for frame transmission where we want guaranteed
-// delivery order and don't want ISR-driven TX reordering complications.
-// The empty check and the write happen with interrupts off so the ISR
-// cannot slip an XON/XOFF into THR between them. Stops early (returning
-// the count written) while flow control holds the line.
+// Blocking polled write: busy-waits on LSR_TX_HOLD_EMPTY for each byte, so
+// it takes as long as the bytes take on the wire (a caller that must not
+// wait uses rs232WriteBuf, as the packet layer does). This bypasses the
+// TX ring buffer entirely and writes directly to the UART. The empty
+// check and the write happen with interrupts off so the ISR cannot slip
+// an XON/XOFF into THR between them. Stops early (returning the count
+// written) while flow control holds the line.
int32_t rs232Write(int32_t com, const char *data, int32_t len) {
int32_t rc;
Rs232StateT *port = getOpenPort(com, &rc);
@@ -1726,12 +1827,37 @@ int32_t rs232WriteBuf(int32_t com, const char *data, int32_t len) {
TX_WRITE(port, data[i]);
}
- // If there's data to send, enable TX_HOLD_EMPTY interrupt
- if (!TX_EMPTY(port)) {
- UART_WRITE_IER(port, port->ier | IER_TX_HOLD_EMPTY);
- }
-
+ uartRearm(port);
irqRestore(flags);
return i;
}
+
+
+// With interrupts off, outside the handler: bytes in the ring get the
+// TX-hold-empty interrupt enabled. The port's IRQ left in service at the
+// PIC is ended (picHealIrq). A UART that holds an interrupt up had its
+// edge missed by the PIC: the interrupt enable is turned off and on,
+// which drops the UART's line and raises it again for a fresh edge. An
+// interrupt merely waiting for interrupts to come back on is unharmed
+// (reading IIR may clear a TX-hold-empty one, which the enable raises
+// again). Readers, writers, and the pending count all do this, so a
+// port polled at all never stays stuck (DOSBox stops delivering received
+// bytes until the guest reads them).
+static void uartRearm(Rs232StateT *port) {
+ if (!TX_EMPTY(port) && port->txFlowOn && !(port->ier & IER_TX_HOLD_EMPTY)) {
+ UART_WRITE_IER(port, port->ier | IER_TX_HOLD_EMPTY);
+ return;
+ }
+
+ if (port->irq != IRQ_NONE) {
+ picHealIrq(port->irq);
+ }
+
+ if (!(UART_READ_IIR(port) & IIR_NO_INT_PENDING)) {
+ uint8_t ier = port->ier;
+
+ UART_WRITE_IER(port, 0);
+ UART_WRITE_IER(port, ier);
+ }
+}
diff --git a/src/libs/kpunch/serial/rs232/rs232.h b/src/libs/kpunch/serial/rs232/rs232.h
index 2a57cf90..d815159c 100644
--- a/src/libs/kpunch/serial/rs232/rs232.h
+++ b/src/libs/kpunch/serial/rs232/rs232.h
@@ -93,6 +93,7 @@
#define RS232_ERR_NULL_PTR (-14)
#define RS232_ERR_IRQ_NOT_FOUND (-15)
#define RS232_ERR_LOCK_MEM (-16)
+#define RS232_ERR_NO_MEMORY (-17)
// Line status bits (rs232GetLsr): errors seen on received data
#define RS232_LSR_OVERRUN 0x02
diff --git a/src/libs/kpunch/serial/seclink/README.md b/src/libs/kpunch/serial/seclink/README.md
deleted file mode 100644
index a5e6d2d5..00000000
--- a/src/libs/kpunch/serial/seclink/README.md
+++ /dev/null
@@ -1,28 +0,0 @@
-# seclink -- Secure Serial Link
-
-The top-level API for the DVX serial stack. Wraps the packet transport
-and the security primitives to provide a multi-channel encrypted byte
-link: optional shared-passphrase authentication, forward-secret DH
-handshake with key confirmation, per-packet encryption with HMAC
-integrity, up to 128 channels multiplexed on a single physical port,
-and bulk send with retransmit handled underneath.
-
-## Documentation
-
-Full reference is in the combined serial stack doc:
-`../serial.dhs` (the `seclink` section). It compiles into the DVX
-System Reference -- open via the Help Viewer app, or read
-`docs/dvx_system_reference.html` after `make`.
-
-Topics covered: protocol, open/close, passphrase, handshake, send/poll,
-plaintext channels, error codes, typical end-to-end usage.
-
-## Source Files
-
-- `secLink.h` -- public API and error codes
-- `secLink.c` -- implementation (handshake, channel mux, bulk send)
-
-## Build
-
-Built into `serial.lib` by `make -C src/libs/kpunch/serial` (invoked by
-the top-level `make`).
diff --git a/src/libs/kpunch/serial/security/README.md b/src/libs/kpunch/serial/security/README.md
deleted file mode 100644
index 3cf55232..00000000
--- a/src/libs/kpunch/serial/security/README.md
+++ /dev/null
@@ -1,29 +0,0 @@
-# security -- Crypto Primitives
-
-Compact cryptographic primitives used by the secLink wrapper: 1024-bit
-Diffie-Hellman key exchange (RFC 2409 Group 2), XTEA block cipher in CTR
-mode for session encryption, SHA-256 / HMAC-SHA256 / HKDF-SHA256 for key
-derivation and message authentication, and a SHA-256 DRBG with an
-entropy pool and seed file. No external dependencies.
-
-## Documentation
-
-Full reference is in the combined serial stack doc:
-`../serial.dhs` (the `security` section). It compiles into the DVX
-System Reference -- open via the Help Viewer app, or read
-`docs/dvx_system_reference.html` after `make`.
-
-Topics covered: DH handshake API, XTEA-CTR encrypt/decrypt, hashing,
-DRBG entropy pool and seed file, remote-key validation range.
-
-## Source Files
-
-- `security.h` -- public API (DH, XTEA-CTR, DRBG)
-- `security.c` -- implementation (bignum, Montgomery ladder, XTEA, DRBG)
-- `secHash.h` -- SHA-256, HMAC-SHA256, HKDF-SHA256 API
-- `secHash.c` -- hash implementation
-
-## Build
-
-Built into `serial.lib` by `make -C src/libs/kpunch/serial` (invoked by
-the top-level `make`).
diff --git a/src/libs/kpunch/serial/serial.dhs b/src/libs/kpunch/serial/serial.dhs
index 352af9e8..60ca9dfe 100644
--- a/src/libs/kpunch/serial/serial.dhs
+++ b/src/libs/kpunch/serial/serial.dhs
@@ -27,22 +27,24 @@
.index Serial
.index RS-232
.index HDLC
-.index XTEA
-.index Diffie-Hellman
+.index X25519
+.index XChaCha20-Poly1305
.index secLink
.h1 Serial Stack
-The DVX serial/networking stack provides reliable, optionally encrypted communication over RS-232 serial ports. It is composed of four layers, each building on the one below:
+DVX's serial communications come in layers, in three libraries, each building on the one below:
.list
-.item rs232 -- ISR-driven UART driver with ring buffers and flow control
-.item packet -- HDLC framing, CRC-16, Go-Back-N ARQ (reliable delivery)
-.item security -- 1024-bit Diffie-Hellman key exchange, XTEA-CTR cipher, SHA-256/HMAC/HKDF, SHA-256 DRBG
-.item secLink -- Convenience wrapper: authenticated handshake, channel multiplexing, per-packet encryption and MAC
+.item rs232 (the serial library, serial.lib) -- ISR-driven UART driver with ring buffers and flow control
+.item packet (the secure link library, seclink.lib) -- HDLC framing, CRC-16, Go-Back-N ARQ (reliable delivery)
+.item security (the cryptography library, crypto.lib) -- X25519 key exchange, XChaCha20-Poly1305, SHA-256/HMAC/HKDF, SHA-256 DRBG (and Ed25519, Diffie-Hellman, and XTEA-CTR for other uses)
+.item secLink (the secure link library, seclink.lib) -- authenticated handshake, channel multiplexing, per-packet encryption and MAC
.endlist
-Loaded as: bin/libs/kpunch/serial/serial.lib
+The serial and cryptography libraries are part of every DVX; the secure link library comes with Communications.
+
+Loaded as: bin/libs/kpunch/serial/serial.lib (rs232)
.h2 Layered Architecture
@@ -53,11 +55,11 @@ Loaded as: bin/libs/kpunch/serial/serial.lib
| secLinkSend() -- send on channel, opt. encrypt |
| secLinkPoll() -- receive + dispatch |
+--------------------------------------------------+
-| secLink (handshake, channels, cipher + MAC) |
+| secLink (handshake, channels, sealed frames) |
+--------------------------------------------------+
| packet (HDLC framing, CRC-16, Go-Back-N ARQ) |
+--------------------------------------------------+
-| security -- DH, XTEA-CTR, SHA-256/HMAC, DRBG |
+| security -- X25519, XChaCha20-Poly1305, DRBG |
| (used by secLink during handshake) |
+--------------------------------------------------+
| rs232 (ISR-driven UART, 2048-byte rings) |
@@ -66,13 +68,13 @@ Loaded as: bin/libs/kpunch/serial/serial.lib
+--------------------------------------------------+
.endcode
-The security layer is invoked by secLink during the Diffie-Hellman handshake and for per-packet cipher operations. It has no runtime coupling to rs232 or packet, so it can be used standalone for cryptographic operations independent of the serial stack.
+The security layer is invoked by secLink during the X25519 handshake and to seal and open every encrypted packet. It has no runtime coupling to rs232 or packet, so it can be used standalone for cryptographic operations independent of the serial stack.
.link lib.serial.rs232 Layer 1: RS-232 UART Driver
-.link lib.serial.packet Layer 2: Packet Transport
-.link lib.serial.security Layer 3: Security (DH, XTEA, SHA-256)
-.link lib.serial.seclink Layer 4: Secure Link
-.link lib.serial.example End-to-End Example
+.link lib.seclink.packet Layer 2: Packet Transport
+.link lib.crypto Layer 3: Cryptography Library
+.link lib.seclink Layer 4: Secure Link Library
+.link lib.seclink.example End-to-End Example
.topic lib.serial.rs232
.title RS-232 UART Driver
@@ -89,9 +91,9 @@ The security layer is invoked by secLink during the Diffie-Hellman handshake and
.h1 RS-232 UART Driver
-ISR-driven serial port driver supporting up to 4 simultaneous COM ports. A shared ISR drains UART FIFOs into per-port ring buffers (2048 bytes, power-of-2 for fast index wrapping). Flow control (XON/XOFF, RTS/CTS, DTR/DSR) operates within the ISR using watermark thresholds. All ISR data structures are DPMI-locked to prevent page faults. XON/XOFF bytes are queued for the transmit interrupt instead of being written over a byte still in the transmit register, and every cached register (IER, MCR) is changed with interrupts off so the ISR and the application never lose each other's updates.
+ISR-driven serial port driver supporting up to 4 simultaneous COM ports. A shared ISR drains UART FIFOs into per-port ring buffers (2048 bytes, power-of-2 for fast index wrapping). An interrupt the interrupt controller misses (its edge lost, seen under a flood in DOSBox, which then stops delivering received bytes) cannot stall a port: rs232Read, rs232WriteBuf, and rs232GetTxBuffered find the UART still holding an interrupt up and turn its interrupt enable off and on, which raises a fresh one, so a port that is polled at all never stays stuck. Flow control (XON/XOFF, RTS/CTS, DTR/DSR) operates within the ISR using watermark thresholds. All ISR data structures are DPMI-locked to prevent page faults. XON/XOFF bytes are queued for the transmit interrupt instead of being written over a byte still in the transmit register, and every cached register (IER, MCR) is changed with interrupts off so the ISR and the application never lose each other's updates.
-Header: rs232/rs232.h
+Header: serial/rs232.h
.h2 Port Constants
@@ -152,6 +154,7 @@ Detected automatically by probing the scratch register and FIFO capability.
RS232_ERR_NULL_PTR -14 NULL pointer argument.
RS232_ERR_IRQ_NOT_FOUND -15 Could not detect IRQ for port.
RS232_ERR_LOCK_MEM -16 DPMI memory lock failed.
+ RS232_ERR_NO_MEMORY -17 No memory for the port's buffers.
.endtable
.h2 rs232Open
@@ -203,7 +206,7 @@ Returns the number of bytes actually read (may be less than len).
.h2 rs232Write
-Blocking polled write directly to UART transmit holding register. Bypasses the TX ring buffer. Polls LSR_TX_HOLD_EMPTY before writing each byte (the check and the write happen with interrupts off, so a flow-control byte from the ISR is never overwritten). Use for small, immediate writes where order matters (the packet layer uses this for frame transmission).
+Blocking polled write directly to UART transmit holding register. Bypasses the TX ring buffer. Polls LSR_TX_HOLD_EMPTY before writing each byte (the check and the write happen with interrupts off, so a flow-control byte from the ISR is never overwritten), so it takes as long as the bytes take on the wire. Use for small, immediate writes where order matters; the packet layer uses rs232WriteBuf instead, so it never waits.
.code
int32_t rs232Write(int32_t com, const char *data, int32_t len);
@@ -213,7 +216,7 @@ Returns the number of bytes actually written (may be less than len while flow co
.h2 rs232WriteBuf
-Non-blocking write to the transmit ring buffer. The ISR drains buffered data to the UART asynchronously.
+Non-blocking write to the transmit ring buffer. The ISR drains buffered data to the UART asynchronously, through the transmit-hold-empty interrupt.
.code
int32_t rs232WriteBuf(int32_t com, const char *data, int32_t len);
@@ -290,689 +293,3 @@ rs232GetParity returns a lowercase character on success. Callers comparing again
rs232ClearRxBuffer(com) Discard all data in the receive ring buffer.
rs232ClearTxBuffer(com) Discard all data in the transmit ring buffer.
.endtable
-
-.topic lib.serial.packet
-.title Packet Transport
-.toc 1 Packet Transport
-.index packet
-.index HDLC
-.index CRC-16
-.index Go-Back-N
-.index pktOpen
-.index pktOpenStream
-.index PktStreamT
-.index pktClose
-.index pktSend
-.index pktPoll
-
-.h1 Packet Transport
-
-Reliable, ordered packet delivery over an RS-232 serial link. Uses HDLC-style byte-stuffed framing for frame delimiting, CRC-16-CCITT for error detection, and Go-Back-N ARQ with a configurable sliding window for retransmission.
-
-Header: packet/packet.h
-
-.h2 Constants
-
-.table
- Constant Value Description
- -------- ----- -----------
- PKT_MAX_PAYLOAD 255 Maximum bytes per packet payload.
- PKT_DEFAULT_WINDOW 4 Default sliding window size (unacknowledged frames in flight).
- PKT_MAX_WINDOW 8 Maximum sliding window size.
-.endtable
-
-.h2 Error Codes
-
-.table
- Constant Value Description
- -------- ----- -----------
- PKT_SUCCESS 0 Operation succeeded.
- PKT_ERR_INVALID_PARAM -1 Invalid parameter (NULL handle, bad data pointer, length out of range).
- PKT_ERR_TX_FULL -2 Transmit window is full (non-blocking send only).
- PKT_ERR_DISCONNECTED -3 Serial port returned a negative read (disconnect/error), or the connection was closed from its own callback.
- PKT_ERR_TIMEOUT -4 A blocking send got no acknowledgement for PKT_SEND_TIMEOUT_MS (15000 ms).
-.endtable
-
-.h2 PktRecvCallbackT
-
-Callback type for received packets.
-
-.code
-typedef void (*PktRecvCallbackT)(void *ctx,
- const uint8_t *data, int len);
-.endcode
-
-.table
- Parameter Description
- --------- -----------
- ctx User context pointer (passed to pktOpen).
- data Payload bytes (valid only during the callback).
- len Payload length.
-.endtable
-
-.h2 pktOpen
-
-Open a packetized connection over an already-open COM port. Only one packet connection may exist on a given COM port at a time; a second pktOpen call on the same port returns NULL to avoid silent framing corruption.
-
-.code
-PktConnT *pktOpen(int com, int windowSize,
- PktRecvCallbackT callback, void *callbackCtx);
-.endcode
-
-.table
- Parameter Description
- --------- -----------
- com COM port index (RS232_COM1..RS232_COM4). Must already be open via rs232Open.
- windowSize Sliding window size (1..PKT_MAX_WINDOW). Pass 0 for PKT_DEFAULT_WINDOW. Values greater than PKT_MAX_WINDOW are clamped.
- callback Called when a complete, CRC-verified, in-order packet is received. The data pointer is valid only during the callback.
- callbackCtx User pointer passed to the callback.
-.endtable
-
-Returns an opaque PktConnT handle, or NULL on failure (invalid port, duplicate open, or allocation failure).
-
-.h2 pktOpenStream
-
-Open a packetized connection over any byte stream instead of a COM port -- a TCP socket, for example. The stream is described by two non-blocking calls and a context pointer, copied into the connection. Any number of stream connections may be open.
-
-.code
-typedef struct {
- int32_t (*read)(void *ctx, uint8_t *buf, int32_t len);
- int32_t (*write)(void *ctx, const uint8_t *data, int32_t len);
- void *ctx;
-} PktStreamT;
-
-PktConnT *pktOpenStream(const PktStreamT *stream, int windowSize, PktRecvCallbackT callback, void *callbackCtx);
-.endcode
-
-.table
- Member Description
- ------ -----------
- read Up to len bytes into buf: the count, 0 when nothing has arrived, negative when the stream is gone (pktPoll then reports PKT_ERR_DISCONNECTED).
- write Bytes taken from data (0 = none right now; a frame is retried until it is all out or nothing was taken for a while), negative when the stream is gone.
- ctx Passed to both.
-.endtable
-
-Returns the handle, or NULL for a NULL stream or call, or on allocation failure. pktClose never closes the stream.
-
-.h2 pktClose
-
-Close a packetized connection and release its registry slot. Does not close the underlying COM port. The caller is responsible for calling rs232Close separately when no longer needed. May be called from inside the receive callback: the connection is then freed when the outermost pktPoll returns (which reports PKT_ERR_DISCONNECTED).
-
-.code
-void pktClose(PktConnT *conn);
-.endcode
-
-.h2 pktSend
-
-Send a data packet. The payload is copied into an internal retransmit slot before transmission, so the caller may reuse the supplied buffer immediately.
-
-.code
-int pktSend(PktConnT *conn, const uint8_t *data,
- int len, bool block);
-.endcode
-
-.table
- Parameter Description
- --------- -----------
- conn Connection handle from pktOpen.
- data Payload bytes (1..PKT_MAX_PAYLOAD).
- len Payload length.
- block If true, wait for transmit window space via pktWaitSpace. If false, return PKT_ERR_TX_FULL immediately when the window is full.
-.endtable
-
-Returns PKT_SUCCESS on success, PKT_ERR_INVALID_PARAM for NULL/out-of-range input, PKT_ERR_TX_FULL for a non-blocking call with a full window, PKT_ERR_DISCONNECTED if the serial port drops during a blocking wait, or PKT_ERR_TIMEOUT if the peer stops acknowledging. A frame the port accepts only partially (flow control) is completed before the call returns.
-
-.h2 pktPoll
-
-Poll for incoming data, process received frames, and handle retransmits. Must be called frequently in the main loop. The callback is invoked synchronously for each complete, CRC-verified, in-order data frame and may call pktSend or pktPoll again: received bytes are consumed from a per-connection buffer, so a nested call continues the same byte stream in order. After a gap in the sequence only one NAK is sent until the missing frame arrives (go-back-N resends the whole window on one NAK).
-
-.code
-int pktPoll(PktConnT *conn);
-.endcode
-
-Returns the number of valid data packets delivered to the callback this call, or PKT_ERR_INVALID_PARAM if conn is NULL, or PKT_ERR_DISCONNECTED if the serial layer returned a negative read.
-
-.h2 pktReset
-
-Reset the local connection state (sequence numbers, transmit slots, RX state machine) and send a RST frame to the remote side. The remote's RST handler clears its own state (discarding its unacknowledged frames), runs its reset callback, and answers with a RST reply. A RST reply that answers no reset of ours is ignored. Useful for recovering from a desynchronized connection.
-
-.code
-int pktReset(PktConnT *conn);
-.endcode
-
-Returns PKT_SUCCESS on success or PKT_ERR_INVALID_PARAM if conn is NULL.
-
-.h2 pktCanSend
-
-Check whether there is room in the transmit window for another packet. Returns false when conn is NULL or the transmit window is full.
-
-.code
-bool pktCanSend(PktConnT *conn);
-.endcode
-
-.h2 pktGetPending
-
-Get the number of unacknowledged packets currently in the transmit window. Ranges from 0 (all sent packets acknowledged) to windowSize (window full).
-
-.code
-int pktGetPending(PktConnT *conn);
-.endcode
-
-Returns the pending count on success, or PKT_ERR_INVALID_PARAM if conn is NULL.
-
-.h2 pktSetResetCallback
-
-Register a callback run when the remote side resets the connection (its unacknowledged frames are gone at that point, so an upper layer with per-frame state must resynchronize). The callback receives the ctx passed to pktOpen. NULL removes it.
-
-.code
-typedef void (*PktResetCallbackT)(void *ctx);
-void pktSetResetCallback(PktConnT *conn, PktResetCallbackT callback);
-.endcode
-
-.h2 pktWaitSpace
-
-Poll until the transmit window has room. The wait restarts whenever an acknowledgement frees a slot, so only a peer that stops acknowledging times out.
-
-.code
-int pktWaitSpace(PktConnT *conn);
-.endcode
-
-Returns PKT_SUCCESS, PKT_ERR_INVALID_PARAM, PKT_ERR_DISCONNECTED, or PKT_ERR_TIMEOUT after PKT_SEND_TIMEOUT_MS without progress.
-
-.topic lib.serial.security
-.title Security (DH, XTEA, SHA-256)
-.toc 1 Security (DH, XTEA, SHA-256)
-.index Security
-.index Diffie-Hellman
-.index XTEA
-.index XTEA-CTR
-.index SHA-256
-.index HMAC
-.index HKDF
-.index DRBG
-.index secDhCreate
-.index secCipherCreate
-.index secRngBytes
-.index secRngAddEntropy
-.index secRngLoadSeed
-.index security.bas
-
-.h1 Security (DH, XTEA, SHA-256)
-
-Cryptographic primitives for the serial stack: 1024-bit Diffie-Hellman key exchange (RFC 2409 Group 2), XTEA block cipher in CTR mode, SHA-256, HMAC-SHA256, and HKDF-SHA256, and a SHA-256 based random number generator.
-
-XTEA requires no lookup tables and compiles to approximately 20 instructions per round, making it suitable for 486-class hardware where AES S-box tables would thrash the 8KB cache. SHA-256 needs only 32-bit arithmetic and a 256-byte constant table.
-
-Headers: security/security.h, security/secHash.h
-
-.h2 Constants
-
-.table
- Constant Value Description
- -------- ----- -----------
- SEC_DH_KEY_SIZE 128 Size of a DH public key or shared secret in bytes (1024 bits).
- SEC_XTEA_KEY_SIZE 16 Size of an XTEA key in bytes (128 bits).
- SEC_RNG_SEED_SIZE 32 Bytes stored in a seed file.
- SEC_SHA256_SIZE 32 SHA-256 digest size.
- SEC_SHA256_BLOCK_SIZE 64 SHA-256 block size.
- SEC_HKDF_MAX_OUT 8160 Largest HKDF output (255 digests).
-.endtable
-
-.h2 Error Codes
-
-.table
- Constant Value Description
- -------- ----- -----------
- SEC_SUCCESS 0 Operation succeeded.
- SEC_ERR_PARAM -1 Invalid parameter.
- SEC_ERR_NOT_READY -2 DH context not ready (keys not generated or secret not computed).
- SEC_ERR_IO -3 Seed file could not be read or written.
-.endtable
-
-.h2 RNG Functions
-
-A SHA-256 hash DRBG over a 32-byte entropy pool. secRngAddEntropy hashes new bytes into the pool and never discards what is already there, so it is safe to call at any time, before or after the first output. Each output block is a hash of the pool and a counter; after every request the pool is re-hashed so earlier output cannot be recomputed from a later state.
-
-The first secRngBytes call gathers hardware entropy automatically. On DOS that is 256 PIT samples, each taken after a busy wait sized by the previous sample, plus the full time stamp counter on CPUs that have one (Pentium and later) and the BIOS tick count. On a CPU without a time stamp counter the samples depend mostly on the PIT phase and time of day, which an observer can narrow down, so applications should feed user-input timings to secRngAddEntropy and keep a seed file: call secRngLoadSeed at startup and secRngSaveSeed at shutdown. Host builds read /dev/urandom.
-
-Under the DVX shell this is automatic when the serial library is installed: the shell loads the seed file SECLINK.RND in the user folder at startup, feeds the timing of every key press and mouse event to secRngAddEntropy, and saves a fresh seed at shutdown. Applications only need their own calls when they run without the shell.
-
-.code
-void secRngAddEntropy(const uint8_t *data, int32_t len);
-void secRngBytes(uint8_t *buf, int32_t len);
-int32_t secRngGatherEntropy(uint8_t *buf, int32_t len);
-int secRngLoadSeed(const char *path);
-int secRngSaveSeed(const char *path);
-void secSecureZero(void *ptr, int len);
-.endcode
-
-.table
- Function Description
- -------- -----------
- secRngAddEntropy Hash bytes into the pool (key timings, mouse positions, anything unpredictable).
- secRngBytes Generate pseudorandom bytes.
- secRngGatherEntropy Fill buf with condensed hardware entropy (one fresh gather per 32 bytes). Returns the byte count (0 if a host build cannot read its random device).
- secRngLoadSeed Mix a seed file into the pool, then immediately rewrite it with fresh output so a seed is never used twice. SEC_SUCCESS or SEC_ERR_IO.
- secRngSaveSeed Write SEC_RNG_SEED_SIZE bytes of fresh output for the next run. SEC_SUCCESS or SEC_ERR_IO.
- secSecureZero Zero memory in a way the compiler cannot optimize away (for key material).
-.endtable
-
-.h2 Hash Functions
-
-.code
-void secSha256(const uint8_t *data, int32_t len, uint8_t digest[32]);
-void secSha256Init(SecSha256T *ctx);
-void secSha256Update(SecSha256T *ctx, const uint8_t *data, int32_t len);
-void secSha256Final(SecSha256T *ctx, uint8_t digest[32]);
-void secHmacSha256(const uint8_t *key, int32_t keyLen, const uint8_t *data, int32_t len, uint8_t mac[32]);
-void secHmacInit(SecHmacT *h, const uint8_t *key, int32_t keyLen);
-void secHmacUpdate(SecHmacT *h, const uint8_t *data, int32_t len);
-void secHmacFinal(SecHmacT *h, uint8_t mac[32]);
-int secHkdf(const uint8_t *salt, int32_t saltLen, const uint8_t *ikm, int32_t ikmLen, const uint8_t *info, int32_t infoLen, uint8_t *okm, int32_t okmLen);
-bool secConstTimeEqual(const void *a, const void *b, int32_t len);
-.endcode
-
-SHA-256 follows FIPS 180-4, HMAC RFC 2104, and HKDF RFC 5869 (extract and expand; a NULL or empty salt means 32 zero bytes). secHkdf returns SEC_ERR_PARAM for okmLen outside 1..SEC_HKDF_MAX_OUT. secConstTimeEqual compares without revealing where the inputs differ; use it for every MAC check.
-
-.h2 Diffie-Hellman Key Exchange
-
-1024-bit DH with 256-bit private exponents using the RFC 2409 Group 2 safe prime. The modular exponentiation is a Montgomery ladder with branch-free operand swaps, so its timing does not depend on the private key.
-
-.code
-SecDhT *secDhCreate(void);
-int secDhGenerateKeys(SecDhT *dh);
-int secDhGetPublicKey(SecDhT *dh, uint8_t *buf, int *len);
-int secDhComputeSecret(SecDhT *dh, const uint8_t *remotePub, int len);
-int secDhGetSecret(SecDhT *dh, uint8_t *buf, int *len);
-void secDhDestroy(SecDhT *dh);
-.endcode
-
-.table
- Function Description
- -------- -----------
- secDhCreate Allocate a new DH context.
- secDhGenerateKeys Generate a private exponent and public key from the DRBG.
- secDhGetPublicKey Copy the local public key into buf. *len receives the key size.
- secDhComputeSecret Compute the shared secret from the remote public key. Keys outside [2, p-2] are rejected with SEC_ERR_PARAM (0, 1, and p-1 would force a trivially known secret).
- secDhGetSecret Copy the shared secret (SEC_DH_KEY_SIZE big-endian bytes) into buf. It is input for a key derivation function such as secHkdf, never a key by itself.
- secDhDestroy Securely zero and free the DH context.
-.endtable
-
-Typical usage order: secDhCreate, secDhGenerateKeys, exchange public keys, secDhComputeSecret, secDhGetSecret, secHkdf, secDhDestroy.
-
-.h2 XTEA-CTR Cipher
-
-XTEA in counter mode. Encrypt and decrypt are the same operation (XOR with keystream). The counter must never repeat with the same key.
-
-.code
-SecCipherT *secCipherCreate(const uint8_t *key);
-void secCipherCrypt(SecCipherT *c, uint8_t *data, int len);
-void secCipherSetCounter(SecCipherT *c, uint64_t counter);
-void secCipherDestroy(SecCipherT *c);
-.endcode
-
-.table
- Function Description
- -------- -----------
- secCipherCreate Allocate a cipher context with a 128-bit key. Returns NULL on allocation failure or NULL key. Initial counter is zero.
- secCipherCrypt Encrypt or decrypt data in place. The 64-bit counter increments by one every 8 bytes (one XTEA block).
- secCipherSetCounter Position the 64-bit block counter (secLink derives it from the frame sequence number).
- secCipherDestroy Securely zero the cipher context (key and counter state) and free its memory.
-.endtable
-
-secCipherCreate returns NULL if the key pointer is NULL or allocation fails. secCipherCrypt is a no-op when any of c/data is NULL or len <= 0.
-
-.h2 BASIC
-
-src/include/basic/security.bas (DECLARE LIBRARY "serial") declares the BASIC functions (security/secBas.c). Keys, data, and results are BINARY STRING (pointer and length in, a result buffer and length out), so every byte value passes; the result buffer is wiped before it is reused. A handle (1, 2, ...; 0 = none) is a key exchange, a cipher, or an incremental SHA-256 / HMAC; each records the program that made it and the shell's app-cleanup hook frees (and wipes) what a program leaves open.
-
-.table
- BASIC C it calls
- ----- ----------
- SecRandom$(count), SecAddEntropy bytes secRngBytes; secRngAddEntropy.
- SecDhCreate() secDhCreate + secDhGenerateKeys.
- SecDhPublicKey$(h), SecDhSecret$(h) secDhGetPublicKey; secDhGetSecret ("" before SecDhComputeSecret).
- SecDhComputeSecret(h, remotePublicKey) secDhComputeSecret: SEC_OK or a SEC_ERR_ code.
- SecDhFree h secDhDestroy.
- SecCipherCreate(key) secCipherCreate; 0 unless key is SEC_XTEA_KEY_SIZE bytes.
- SecCipherCrypt$(h, bytes) secCipherCrypt on a copy.
- SecCipherSetCounter h, counter secCipherSetCounter (a DOUBLE: exact up to 2^53).
- SecCipherFree h secCipherDestroy.
- SecSha256$(bytes); SecSha256Open, SecSha256Add, SecSha256End$ secSha256; secSha256Init / Update / Final (End frees the handle).
- SecHmac$(key, bytes); SecHmacOpen, SecHmacAdd, SecHmacEnd$ secHmacSha256; secHmacInit / Update / Final.
- SecHkdf$(salt, secret, info, length) secHkdf ("" for a length outside 1..SEC_HKDF_MAX_OUT).
- SecEqual(a, b) secConstTimeEqual (False for different lengths).
-.endtable
-
-The SEC_ CONSTs of security.bas are compared with security.h and secHash.h by a formrt test, which also calls every function for real.
-
-.topic lib.serial.seclink
-.title Secure Link
-.toc 1 Secure Link
-.index secLink
-.index secLinkOpen
-.index secLinkOpenStream
-.index secLinkClose
-.index secLinkHandshake
-.index secLinkHandshakeStep
-.index secLinkSend
-.index secLinkPoll
-.index secLinkSetPassphrase
-.index secLinkSetChannelPlain
-
-.h1 Secure Link
-
-Top-level API composing all three lower layers (rs232, packet, security) into a single interface. Provides an authenticated key exchange, channel multiplexing (0-127), and per-packet encryption with integrity protection over a reliable serial link.
-
-Header: seclink/secLink.h
-
-.h2 Protocol (version 2)
-
-Every packet starts with a type byte:
-
-.table
- Type Layout Purpose
- ---- ------ -------
- HELLO type, version, flags, DH public key (128 bytes) Starts a key exchange. Flag bit 0: passphrase in use.
- CONFIRM type, HMAC-SHA256 (32 bytes) Proves both sides derived the same keys.
- PLAIN type, channel, payload Clear application data.
- CIPHER type, channel, ciphertext, MAC (8 bytes) Encrypted, authenticated application data.
-.endtable
-
-Handshake packets are never delivered to the application, and application data is never taken as key material. A HELLO is kept whenever it arrives, so the side that starts first is never missed.
-
-Key derivation: HKDF-SHA256 with salt = SHA-256(passphrase) (32 zero bytes without one), input = the DH shared secret, and info = "DVX secLink v2" followed by the lower then the higher public key. The output supplies an XTEA key and an HMAC key for each direction plus a confirmation key; the side with the lower public key sends with the "low" keys. Directions never share a key.
-
-Confirmation: each side sends HMAC(confirmation key, SHA-256(lower HELLO + higher HELLO) + role byte). A different passphrase, a passphrase on only one side, or a modified HELLO makes the check fail on both sides with SECLINK_ERR_AUTH. A side that has a passphrase never accepts a peer without one.
-
-Encrypted frames: encrypt-then-MAC. Each direction numbers its encrypted frames with an implicit 64-bit sequence number that is never sent. The MAC is the first 8 bytes of HMAC-SHA256 over type, channel, big-endian sequence number, and ciphertext, and the CTR counter for a frame starts at sequence * 32, so every frame has its own keystream range. Tampered, replayed, or injected frames fail the MAC, are dropped and counted (secLinkGetDropped); later frames still verify.
-
-Without a passphrase the link is encrypted and integrity-protected but not authenticated: an active attacker in the middle could run two separate exchanges. Use the same passphrase on both sides to rule that out.
-
-Clear frames are accepted only until the first session is established. From then on, until secLinkClose, a clear frame is delivered only on a channel marked plaintext with secLinkSetChannelPlain -- even after the session was dropped by a packet-layer reset or a failed re-key, which someone on the line can cause; so neither can turn a protected channel into a clear one. A re-key HELLO on a running session whose protocol version or passphrase flag cannot match is dropped (and counted) without ending the session.
-
-.h2 Constants
-
-.table
- Constant Value Description
- -------- ----- -----------
- SECLINK_PROTOCOL_VERSION 2 Version carried in HELLO; a mismatch fails the handshake.
- SECLINK_FRAME_HDR_SIZE 2 Type byte plus channel byte.
- SECLINK_MAC_SIZE 8 Truncated MAC on encrypted frames.
- SECLINK_MAX_PAYLOAD 245 Maximum payload per send (PKT_MAX_PAYLOAD minus header and MAC), clear or encrypted.
- SECLINK_MAX_CHANNEL 127 Highest valid channel number.
- SECLINK_NUM_CHANNELS 128 Array size needed to hold one slot per channel (SECLINK_MAX_CHANNEL + 1).
- SECLINK_CHAN_BUF_SIZE 4096 Recommended per-channel receive buffer size for callers that buffer inbound data between the receive callback and an application read.
-.endtable
-
-.h2 Error Codes
-
-.table
- Constant Value Description
- -------- ----- -----------
- SECLINK_SUCCESS 0 Operation succeeded.
- SECLINK_ERR_PARAM -1 Invalid parameter (NULL pointer, bad channel, bad length).
- SECLINK_ERR_SERIAL -2 Serial port closed or disconnected (secLinkPoll).
- SECLINK_ERR_ALLOC -3 Memory allocation failed.
- SECLINK_ERR_HANDSHAKE -4 Handshake failed: invalid public key, protocol version mismatch, reflected HELLO, dead line, or the peer reset mid-exchange.
- SECLINK_ERR_NOT_READY -5 No session keys yet (encrypted send attempted).
- SECLINK_ERR_SEND -6 Packet send failed, the peer stopped acknowledging, or a non-blocking send found the window full.
- SECLINK_ERR_TIMEOUT -7 The peer did not complete the handshake within 30 seconds (the exchange stays pending).
- SECLINK_ERR_AUTH -8 Passphrase mismatch, passphrase on only one side, or tampering.
- SECLINK_PENDING 1 secLinkHandshakeStep only: the exchange is still running.
-.endtable
-
-.h2 SecLinkRecvT
-
-Receive callback type. Delivers plaintext with the channel number. encrypted is true when the frame arrived encrypted and its MAC verified, false for a clear frame.
-
-.code
-typedef void (*SecLinkRecvT)(void *ctx, const uint8_t *data, int len, uint8_t channel, bool encrypted);
-.endcode
-
-.h2 secLinkOpen
-
-Open a secure serial link. Opens the COM port via rs232Open and creates a packet layer (default window size) with an internal receive dispatcher. The handshake must be performed separately via secLinkHandshake before encrypted data can be sent.
-
-.code
-SecLinkT *secLinkOpen(int com, int32_t bps, int dataBits, char parity, int stopBits, int handshake, SecLinkRecvT callback, void *ctx);
-.endcode
-
-.table
- Parameter Description
- --------- -----------
- com COM port index (RS232_COM1..RS232_COM4).
- bps Baud rate.
- dataBits Data bits per character (5-8).
- parity Parity mode ('N', 'O', 'E', 'M', 'S', or lowercase).
- stopBits Stop bits (1 or 2).
- handshake Flow control mode (RS232_HANDSHAKE_*).
- callback Called when a packet is received, after verification and decryption. The data pointer is valid only during the callback.
- ctx User pointer passed to the callback.
-.endtable
-
-Returns an opaque SecLinkT handle, or NULL on failure (allocation failure, rs232Open error, or pktOpen error). On failure all partially initialized resources are cleaned up.
-
-.h2 secLinkOpenStream
-
-Open a secure link over a byte stream (see pktOpenStream) instead of a COM port, e.g. a TCP connection to the host proxy's listening port. Everything else -- handshake, passphrase, channels, sending, and polling -- is the same as for a COM port link. secLinkClose does not close the stream; the caller closes it afterwards.
-
-.code
-SecLinkT *secLinkOpenStream(const PktStreamT *stream, SecLinkRecvT callback, void *ctx);
-.endcode
-
-.h2 secLinkClose
-
-Close the link and free all associated resources: securely destroy the ciphers and key material, destroy any remaining DH context, close the packet layer and the COM port, and free the link. Safe to call with a NULL link (no-op). Safe to call from inside the receive callback: the close then happens when the outermost secLink call returns, and no further packets are delivered.
-
-.code
-void secLinkClose(SecLinkT *link);
-.endcode
-
-.h2 secLinkSetPassphrase
-
-Set the shared passphrase for the next handshake. NULL or "" clears it. Only SHA-256 of the text is kept. Set it before calling secLinkHandshake; both sides must use the same passphrase, or both none. An exchange already in progress (for example one left pending by a timed-out secLinkHandshake) was started with the old setting, so it is abandoned: the next secLinkHandshake starts a fresh one.
-
-.code
-int secLinkSetPassphrase(SecLinkT *link, const char *pass);
-.endcode
-
-Returns SECLINK_SUCCESS or SECLINK_ERR_PARAM for a NULL link.
-
-.h2 secLinkHandshake
-
-Run the authenticated DH key exchange. Blocks, polling the packet layer, until both CONFIRM messages have been checked, or until 30 seconds pass.
-
-.code
-int secLinkHandshake(SecLinkT *link);
-.endcode
-
-Behavior:
-
-.list
-.item A fresh DH key pair is generated from the DRBG and our HELLO is sent. If the peer's HELLO already arrived (it started first), the keys are derived at once.
-.item The DH private key is destroyed as soon as the session keys are derived (forward secrecy).
-.item After SECLINK_ERR_TIMEOUT the exchange stays pending: it still completes during secLinkPoll if the peer shows up, and calling secLinkHandshake again resumes the wait without sending a new key.
-.item On a ready link it negotiates fresh session keys. A peer that is already ready (or has an exchange pending) answers automatically from secLinkPoll.
-.item Encrypted traffic under the previous keys that was sent before the new HELLO is still accepted; encrypted sends are refused (SECLINK_ERR_NOT_READY) until the new exchange completes.
-.endlist
-
-Returns SECLINK_SUCCESS, SECLINK_ERR_PARAM, SECLINK_ERR_ALLOC, SECLINK_ERR_HANDSHAKE, SECLINK_ERR_TIMEOUT, or SECLINK_ERR_AUTH. When the peer confirms (or rejects) and hangs up in the same poll -- a proxy exits as soon as it sees a passphrase mismatch -- the exchange's outcome is reported, not the disconnect.
-
-.h2 secLinkHandshakeStep
-
-Non-blocking handshake for callers that run their own event loop.
-
-.code
-int secLinkHandshakeStep(SecLinkT *link);
-.endcode
-
-Starts an exchange on an idle link (never handshaken, or dropped by a peer reset), polls once, and returns SECLINK_SUCCESS once the link is ready, SECLINK_PENDING (1) while the exchange is still running, or the error that ended it (the same codes as secLinkHandshake, without SECLINK_ERR_TIMEOUT). A failed exchange keeps returning its error; secLinkHandshake starts a new one.
-
-.h2 secLinkSend
-
-Send data on a channel, optionally encrypted and MACed.
-
-.code
-int secLinkSend(SecLinkT *link, const uint8_t *data, int len, uint8_t channel, bool encrypt, bool block);
-.endcode
-
-.table
- Parameter Description
- --------- -----------
- link Connection handle from secLinkOpen.
- data Plaintext payload.
- len Payload length. Must be in 1..SECLINK_MAX_PAYLOAD.
- channel Logical channel number (0..SECLINK_MAX_CHANNEL).
- encrypt If true, encrypt and MAC before sending (requires a completed handshake).
- block If true, wait (pumping the receive callback) until the transmit window has space. If false, return SECLINK_ERR_SEND immediately when the window is full.
-.endtable
-
-Returns SECLINK_SUCCESS on success or a negative error code (SECLINK_ERR_PARAM, SECLINK_ERR_NOT_READY, SECLINK_ERR_SEND).
-
-A frame takes its sequence number only once window space is guaranteed, and is queued without pumping in between, so frames always reach the wire in sequence order. The receive callback may itself send (blocking or not), including encrypted.
-
-Once session keys exist the receiver drops clear frames unless it marked the channel plaintext with secLinkSetChannelPlain.
-
-.h2 secLinkSendBuf
-
-Send an arbitrarily large buffer by splitting into SECLINK_MAX_PAYLOAD chunks. Always blocks until all data is sent.
-
-.code
-int secLinkSendBuf(SecLinkT *link, const uint8_t *data, int len, uint8_t channel, bool encrypt);
-.endcode
-
-Returns SECLINK_SUCCESS or the first error encountered.
-
-.h2 secLinkSetChannelPlain
-
-Allow (plain = true) or refuse (plain = false, the default) clear frames on a channel once session keys exist. Before the first handshake clear frames are always accepted.
-
-.code
-int secLinkSetChannelPlain(SecLinkT *link, uint8_t channel, bool plain);
-.endcode
-
-Returns SECLINK_SUCCESS or SECLINK_ERR_PARAM for a bad link or channel.
-
-.h2 secLinkPoll
-
-Poll for incoming data. Drains the serial port, processes frames and retransmits, completes pending handshake steps, verifies and decrypts data frames, and forwards them to the callback.
-
-.code
-int secLinkPoll(SecLinkT *link);
-.endcode
-
-Returns the number of packets delivered by the packet layer this call, SECLINK_ERR_PARAM if link is NULL, or SECLINK_ERR_SERIAL when the port is closed or disconnected. Must be called frequently in the main loop.
-
-If the remote packet layer resets, the session keys are dropped (the link is no longer ready) and a new handshake is needed. Clear frames stay refused on protected channels (see Protocol).
-
-.h2 secLinkIsReady
-
-Check whether the handshake is complete and the link is ready for encrypted traffic. Cleartext sends do not require the link to be ready.
-
-.code
-bool secLinkIsReady(SecLinkT *link);
-.endcode
-
-.h2 secLinkGetDropped
-
-Number of received frames dropped since open: bad MAC (tampered, replayed, or injected), clear frames on a protected channel, encrypted frames without session keys, and malformed frames.
-
-.code
-int32_t secLinkGetDropped(SecLinkT *link);
-.endcode
-
-Returns the count or SECLINK_ERR_PARAM when link is NULL.
-
-.h2 secLinkGetPending
-
-Get the number of unacknowledged packets in the transmit window. Delegates directly to pktGetPending on the underlying packet connection. Useful for non-blocking send loops to decide when to throttle.
-
-.code
-int secLinkGetPending(SecLinkT *link);
-.endcode
-
-Returns the pending count on success or SECLINK_ERR_PARAM when link is NULL.
-
-.topic lib.serial.example
-.title End-to-End Example
-.toc 1 End-to-End Example
-.index Serial Example
-.index BBS Client
-
-.h1 End-to-End Example
-
-Below is a complete secure BBS client that opens COM1, runs the authenticated key exchange, and then shuttles terminal traffic over a single encrypted channel. It demonstrates the typical serial stack usage: load the RNG seed file, open the link, set the passphrase, run the handshake, then enter a poll+send loop.
-
-.code
-#include "rs232/rs232.h"
-#include "packet/packet.h"
-#include "security/security.h"
-#include "seclink/secLink.h"
-#include
-#include
-
-#define CHAN_TERMINAL 0
-
-#define SEED_FILE "SECLINK.RND"
-
-static void onPacket(void *ctx, const uint8_t *data, int len, uint8_t channel, bool encrypted) {
- (void)ctx;
- if (channel == CHAN_TERMINAL && encrypted) {
- fwrite(data, 1, len, stdout);
- fflush(stdout);
- }
-}
-
-
-int main(void) {
- // Carry entropy over from the last run (hardware entropy is
- // gathered automatically; feed key timings with secRngAddEntropy).
- secRngLoadSeed(SEED_FILE);
-
- // Open the link at 115200 8N1 with RTS/CTS flow control.
- SecLinkT *link = secLinkOpen(RS232_COM1, 115200, 8, 'N', 1, RS232_HANDSHAKE_RTSCTS, onPacket, NULL);
- if (!link) {
- fprintf(stderr, "secLinkOpen failed\n");
- return 1;
- }
-
- // Authenticated key exchange. The proxy must use the same passphrase.
- secLinkSetPassphrase(link, "shared secret words");
- if (secLinkHandshake(link) != SECLINK_SUCCESS) {
- fprintf(stderr, "Handshake failed\n");
- secLinkClose(link);
- return 1;
- }
-
- // Send an encrypted banner and then relay keyboard input.
- const char *hello = "Hello, secure BBS!\r\n";
- secLinkSend(link, (const uint8_t *)hello, (int)strlen(hello), CHAN_TERMINAL, true, true);
-
- while (!feof(stdin)) {
- // Deliver any incoming packets to onPacket().
- secLinkPoll(link);
-
- // If the user typed something, forward it encrypted.
- int ch = getchar();
- if (ch == EOF) {
- break;
- }
- uint8_t byte = (uint8_t)ch;
- secLinkSend(link, &byte, 1, CHAN_TERMINAL, true, true);
- }
-
- secLinkClose(link);
- secRngSaveSeed(SEED_FILE);
- return 0;
-}
-.endcode
-
-Notes:
-
-.list
-.item secLinkOpen internally calls rs232Open and pktOpen. secLinkClose tears them down in reverse order.
-.item The DRBG gathers hardware entropy on first use. On a CPU without a time stamp counter that alone is guessable; the seed file and user-input timings (secRngAddEntropy) are what make the DH keys unpredictable.
-.item secLinkPoll must be called frequently to drain the RX ring buffer, process ACKs, and dispatch received packets to the callback.
-.item For bulk transfers larger than SECLINK_MAX_PAYLOAD (245 bytes), use secLinkSendBuf which splits the data into chunks automatically.
-.endlist
diff --git a/src/libs/kpunch/sql/README.md b/src/libs/kpunch/sql/README.md
new file mode 100644
index 00000000..c5071df0
--- /dev/null
+++ b/src/libs/kpunch/sql/README.md
@@ -0,0 +1,25 @@
+# sql -- Databases (dvxsql)
+
+SQLite databases for DVX programs: connections and result sets by
+integer handle, statements that let the other programs run while they
+work, and a DOS file layer (8.3 names, DVX's random source). The Data
+control and DBGrid use it; BASIC reaches it through
+src/include/basic/sql.bas. Part of the optional Databases part.
+
+## Documentation
+
+Full reference is in `dvxsql.dhs` (this directory), compiled into the
+DVX System Reference. Open via the Help Viewer app, or read
+`docs/dvx_system_reference.html` after `make`.
+
+## Source Files
+
+- `dvxSql.h` -- public API
+- `dvxSql.c` -- connections, statements, result sets, the pump
+- `dvxSqlVfs.c` -- SQLite's file layer on DOS
+- `thirdparty/sqlite3` -- SQLite (public domain), built with the options
+ in sqliteOpts.mk
+
+## Build
+
+`make -C src/libs/kpunch/sql` (invoked by the top-level `make`).
diff --git a/src/libs/kpunch/sql/dvxSql.c b/src/libs/kpunch/sql/dvxSql.c
index 68af96af..d6b40ce6 100644
--- a/src/libs/kpunch/sql/dvxSql.c
+++ b/src/libs/kpunch/sql/dvxSql.c
@@ -30,6 +30,7 @@
#include "../libdvx/dvxTypes.h"
#include "thirdparty/sqlite3/sqlite3.h"
+#include
#include
#include