Repository navigation
Releases
BM-DOS 2026-11-11.82
BM-DOS 2026-11-11.82: the card's real free space, and copies that actually use the patches
DIR used to report a figure that was written into dosbox-core, not read from the
card: MOUNT's default geometry ("512,32,32765,16000") is 262 144 000 bytes free,
the same number on an empty card and on a full one. That is fixed here, and so is
a design mistake from the previous round: the patches that speed up copying only
took effect if "make fetch" had been run in that tree.
Free space
- src/platform/bare_freespace.cpp reads the card with f_getfree("SD:") and
bare_storage_dos_space() hands the four numbers INT 21h AH=36h wants to
patches/dosbox-core/0004, which is what localDrive::AllocationInfo() returns.
src/platform/bare_space.h does the arithmetic: it is integers only and
includes nothing, so the same code runs on the host (make check-storage drives
it) and on the board. - DOS takes those numbers in 16-bit registers, so a card with more than 65535
clusters is described in coarser ones - 2 GiB with 4 KB clusters shows as
58254 clusters of 36 KB - and the bytes free are right to within one of those
clusters. Above about 8.5 GB (65535 clusters of 255 sectors) DOS's own
ceiling is reached and the figure stops there. - -freesize and -size are untouched and they win. MOUNT reads them exactly as
before; the only thing added is that it remembers whether either was given
(localDrive::SetSpaceFromPort), so the card is asked only when the mount line
did not pin the number itself. Measured on the board: -freesize 100 still
reports 104 857 600. Floppy, overlay and CD/ISO drives are never asked, and
with no card to ask the old fixed figures stand. - Two separate files, not one, and it is not tidiness: addon/fatfs/ff.h has
"typedef struct DIR DIR" and newlib has its own DIR, so the file that calls
f_getfree() cannot include anything else.
The patches, applied by every build
- patches/dosbox-core/ holds the three patches that made copying from a mounted
ISO fast: 0001 asks for contiguous sectors on IMGMOUNT, 0002 raises COPY's
buffer, 0003 makes isoFile::Read() ask for the whole run of sectors at once
instead of one 2048-byte ReadSector() per sector (30 reads of 2048 bytes become
1 read of 61440 per buffer). - The mistake: they were applied by "make fetch" only. A tree that already had
third_party/dosbox-core compiled the core without them and nothing said so -
a quiet difference is worse than a loud error. Now core-patch is a prerequisite
of every build, it is idempotent, silent when there is nothing to do, and a core
that is not in the state these patches make is set up again from the pinned
revision. make deps says "core patches: 4 of 4 applied". - Unrelated to the card size: 0003 is measured, not assumed. The symptom that
started it was "the copies are still slow, especially with many small files",
and the reason is that a .iso mounted with -t iso is an isoDrive, whose reads go
through ReadSector() (singular); 0001 had only touched ReadSectors() (plural),
the path a MSCDEX CD device takes. The patch was in the wrong path, not too
small.
The watchdog, and a clock for copies
- The shell's own commands run inside the emulated INT 2Eh, so a four-second COPY
executes no emulated instruction and no frame happens while it runs. The
watchdog's two seconds therefore took the picture away in the middle of ordinary
copies and printed four lines about a stall that was not one. It is ten
seconds now, still short enough to be impossible to miss when the emulator
really has died, because that one never resumes. - Every core call over 250 ms now leaves "HOST: one core call took N ms" in the
log (PetSis), at most one a second. A shell command is one core call, so that
number is how long the whole command took - and it is the only clock a copy
has, because DOS's clock does not move while the emulator is stopped.
Docs and tests
- README (both languages) and sd/dosbox.conf: how the free figure is worked out
and why it can be coarser than the card, that -freesize and -size still win,
and what the watchdog and the HOST: line mean during a copy. - make check-storage: six geometries through the conversion - 250 MB unscaled,
2 GiB becoming 58254 clusters of 72 sectors (half free -> 1 073 737 728 B),
32 GB hitting the ceiling, a FSInfo that claims more free clusters than exist,
and four cases that must refuse to answer. - make check-host: 13 targets, 452 ok lines, 0 failures.
What is not verified here
- The AArch64 build and the board: this workspace has no toolchain and no card.
The patched core files are compiled with g++ -fsyntax-only on the host, and
bare_freespace.cpp is compiled against the shape of ff.h (csize, n_fatent,
f_getfree), not against the real header. - The speed of a copy after 0003: the patch removes the per-sector reads, but how
much that is worth on this card is a measurement, not a claim.
Co-authored-by: arena.ai
BM-DOS 2026-11-11.80
BM-DOS 2026-11-11.80: General MIDI music with a SoundFont
Until now the [midi] section was a dead end: no host MIDI, no MT-32, and a game
that asked for General MIDI got nothing. This release answers that with a
synthesiser built into the kernel that reads a .sf2 file from the card. Tested with GeneralUser-GS.sf2 (https://github.com/mrbumpy409/GeneralUser-GS).
MIDI
- src/dosbox-stub/midi_stub.cpp replaces src/gui/midi.cpp: it defines
MIDI_Init, MIDI_RawOutByte, MIDI_Available and MidiHandler::MidiHandler(),
which is all the core asks for. The emulated MPU-401 hands bytes to it and
TinySoundFont (src/dosbox-stub/tsf.h, header only) turns them into sound on a
mixer channel called TSF at 44100 Hz. - Configuration is two keys:
mpu401=intelligentandmidiconfig=<name or path>.
A bare name is looked up in /dosbox/system/soundfonts/ on the card, then
without the SD: prefix, then at the root; SD:/... and /... are taken as
written. No .sf2 ships in the image (size and licence are the author's). - All 16 channels are pre-initialised, so a game whose first message is a Note
On (no Program Change) is not silent; channel 10 starts on the drum bank. - CC is forwarded verbatim to TinySoundFont, which resolves sustain, volume,
pan, expression, bank select and RPN 0/1/2 (pitch range, fine and coarse
tune). Note On/Off, Program Change, 14-bit Pitch Wheel, System Reset, GM
System On and GS Reset are handled; other SysEx is dropped. - midi.handler is now assigned, so MIXER /LISTMIDI lists "
TSF <path> (<n> presets)". Subclassing MidiHandler emits the base vtable and that pulled in
GFX_ShowMsg (src/gui/sdlmain.cpp, not compiled here) through an inline
LOG_MSG; the macro is neutralised around the include and nm confirms zero
references.
Reading the SoundFont
- Measured on the board first, with a 31559 KB SoundFont: 9863 ms of fread
against 501 ms inside tsf. The parse was never the problem - one FatFs call
per kilobyte was, which is what newlib's stdio does with a large fread. - A bigger stdio buffer was tried and broke the load outright (tsf_load
returned NULL on the file that had just loaded), which says the trouble is
below stdio. So the bulk of the file no longer goes through stdio's buffer:
a 64 KB buffer of our own, ALIGNED to 64 bytes (large transfers to the SD
driver end in DMA, and malloc only guarantees 16), the stream in _IONBF so a
block is one call down to FatFs, and tsf reads through its own read/skip
pair, served from that block. Measured after: 3836 ms, 8.4 MB/s. - The file is never held in RAM: it is parsed with tsf_load() over that stream,
not tsf_load_memory. - A block that comes back short is retried in place - up to twice, in 4 KB
pieces - because on this card a 64 KB multi-sector read can fail with EIO
while smaller reads go through. Only if that fails does it fall back to the
plain streaming path, which says so and is 2.6 times slower. The log says
it once, not once per attempt: a failed fread leaves stdio's error flag set
and the next one fails at the same offset, which is where the two identical
lines came from. - Messages name the reason instead of saying "failed": the offset, and either
(errno N) or (file ends there) - decided with ferror()/feof(), because a
short read at end of file leaves errno holding an older call's number. - "... , N block retries" is appended to the timing line when a boot needed
any. Same file and same code: a count that grows boot after boot, or the
same offset every time, is the card; a count that disappears was a bad
contact. The port only calls fread(), FatFs already clips each disk_read at
the cluster boundary, and Circle retries the data command three times before
reporting anything - an error that reaches fread has already failed three
times below this code. - The SoundFont is loaded once per boot. MIDI_Init runs twice (libretro.cpp's
second check_variables() pass updates mt32.romdir), so the parsed
synthesiser is kept in RAM when the resolved path has not changed, and a
path that failed is not read again in the same boot.
Memory
- The size of the SoundFont is the user's choice, and TinySoundFont turns every
16-bit sample into a float, so the instrument costs about twice the file.
On this board an exhausted heap does not return NULL: Circle sets the low
heap up with a 256 KB reserve (lib/memory.cpp:100, heapallocator.h) and
panics. BareSysInfoHeapFree() returns GetHeapFreeSpace(HEAP_ANY) and
OpenStage() refuses a file that needs more than two and a half times its own
size, in one short line, before tsf allocates anything. If Circle answers 0
the check is skipped, and the game keeps its OPL music either way.
Docs and tests
- READMEs: a "MIDI with a SoundFont" section (how to enable it, path rules,
the four lines it prints, the memory limit, what an errno 5 means and what
is not implemented), and the feature table no longer says MIDI is missing. - sd/dosbox.conf documents the [midi] section instead of declaring it absent;
it stays off by default because no .sf2 ships with the image. - make check-sf2reader: the reader that ships, extracted verbatim from
midi_stub.cpp by scripts/hostcheck/gen-sf2reader.py, driven over files the
test writes itself - block boundaries, 1-byte reads, skips that cross the
window, one 500 KB read, a file shorter than it claims, the RAM guard, and a
synthetic EIO (fopencookie) that recovers once and one that never does.
61 checks, and it fails loudly if the markers it extracts disappear. - scripts/hostcheck/setup.sh now generates scripts/hostcheck/circle/memory.h.
It was written by hand in .79c, and that directory is generated and
git-ignored, so a clean clone of the published branch had no such header and
check-platform/check-kernel could not compile bare_sysinfo.cpp. Found by
diffing this tree against a clean clone of the branch, and verified by
deleting circle/ entirely and running check-host from scratch. - .gitignore lists the working scripts that live outside the tree, so
crear-mensaje-commit.sh cannot be committed again.
Co-authored-by: arena.ai
BM-DOS 2026-11-10.78
BM-DOS 2026-11-10.78: cmdline.txt owns the display mode, governor and boot fixes
Display mode
Circle creates its own console framebuffer before this port creates its own, and
its size comes from width= / height= in cmdline.txt, not from boot.conf. Two
different geometries meant a mode change at run time, and that is what left the
display scanning memory that still held the console's text.
- sd/cmdline.txt reduced to ONE line, under 2048 bytes. Circle's parser splits
on spaces only, and drops the whole file above 2048 bytes
(TPropertyTagCommandLine::String[2048]), silently: a commented cmdline.txt
was a cmdline.txt whose options were read as part of the last comment. - make-sd.sh and update-sd.sh write width= and height= from boot.conf,
collapse the file to one line, and refuse to write anything over the limit. - make-sd.sh now copies boot.conf (it never did, so a fresh card fell back to
the built-in 1280x720). - cmdline.txt has the LAST WORD on the display mode, and that is now what the
kernel does. It used to compare the console's physical mode against
boot.conf and refuse to boot when they disagreed, which turned a cosmetic
disagreement between two text files into a dead board. It now prints both
numbers, says which file it is using, adopts the console's geometry and
carries on. boot.conf keeps its display= because the scripts need a source
for width=/height= at card-build time; it is a claim, not an order. - A comment that had been lying for three rounds is fixed: CBcmFrameBuffer's
GetWidth() returns the number that went INTO the request, not what the
firmware granted. With width=/height= present the two coincide; without
them the constructor fills the width from the display dimensions, which
arrive with the overscan border already subtracted - that is where the
1824x984 came from. - READMEs: a new "Configuration: cmdline.txt" section - one line, spaces only,
2048 bytes, the exact line this project writes, and a table of the six
options that matter here with Circle's real ranges (loglevel 0..5 is a
severity ceiling and NOT a destination; keymap is the physical layout, not
the DOS one; socmaxtemp above 78 is ignored by Circle, so the 80 on the card
changes nothing). Everything else links to Circle's doc/cmdline.txt, with a
note on why fast= and sounddev= do nothing here. - sd/config.txt: disable_overscan=1. The firmware subtracts a 48 px border per
edge in CEA modes, which is the 1824x984 that appeared on the board.
Clock governor
- Idleness is decided by DURATION, not by counters. Three rules were tried on
the board and two of them were mine and wrong:- "identical frame is idle" walked a Pi 3 down to 600 MHz in the middle of
DOOM, because a game repeats its picture too; the audio broke up. - "identical and cheap is idle" fixed DOOM and pinned a DOS prompt at the
top level at 63 C, because most of a 42 ms text frame is the emulated
machine sleeping on its own timer, not working. - "advanced fewer times than the mode asks" was withdrawn before it shipped:
a healthy machine reads 30/60 or 35/70, so the test is meaningless.
What works is waiting ten seconds of a literally identical picture
(governor::IdleFramesForPrompt). A prompt does that; no game does. Being
wrong costs one level for three seconds.
- "identical frame is idle" walked a Pi 3 down to 600 MHz in the middle of
- IsStalledFrame() and IsWaitingFrame() removed, and the governor tick is back
to three arguments: the counters cannot separate idle from working. - BuildLevels() now guarantees a strictly increasing table. A ceiling below
1500 used to produce {600, ceiling, ceiling} with two levels, so the governor
believed it could step down while SetLevel() moved nothing.
Boot screen
- Banner() removed: it painted the version block on Circle's console and
Report() painted it again on ours. The nine key-help lines went the same way. - BareVideoStatus() erases the line before drawing it, so a replaced message
no longer overlaps the one before it. - boot_log = auto | quiet | off in boot.conf: how much of the boot is painted,
never what is logged.
Build and checks
- check-kernel.sh compiles to an object instead of -fsyntax-only: gcc only
reports -Wunused-function when it generates code. - setup.sh applies patches/*.patch automatically, so a clone works on a Pi 3
without a manual step. - Sources and script output in English.
- READMEs: display mode by board, the cycles table by emulated CPU, and a
"Lessons learned" section with the platform traps (cmdline.txt, the two
framebuffers, overscan, -fsyntax-only, idle detection, single core).
Co-authored-by: arena.ai