CHANGELOG 0.4.0.md — Notepad

Added

  • Per-song GENMIDI bank in libsxmidi. sx_midi_song_create_with_bank takes the lump with the MIDI, sx_midi_song_uses_genmidi reports the fallback, the port passes it always. Host checks plus doom-mus-test on the real D_E1M1 cover both paths (see docs/media/MIDI.md).

  • libsxmidi.so.0.4, a General MIDI synthesizer the tree builds and installs in /disk/lib. It parses a Standard MIDI File, plays it through a built-in 16-family instrument bank and a drum kit, and renders mono PCM for the mixer. No soundfont and no libm; the public surface is savanxp/midi.h and the design is in docs/media/MIDI.md.

  • The DoomGeneric port has music. Each MUS lump is converted to MIDI with the upstream mus2mid.c, libsxmidi synthesises it into the mixer's buffer as the game runs and a dedicated voice loops that buffer. The port is now PIE and declares a DT_NEEDED for the library, so building it requires ./build.sh build to have produced /disk/lib/libsxmidi.so.0.4 first.

  • The port's music is checked on the guest audio, and on what follows it. ./build.sh smoke doom-music-smoke opens the port from the desktop, starts the level, fails if the WAV QEMU captured comes out silent, then closes the window and fails if any audio outlives the last music.

  • Three SpicyGame pixel fonts join the baked set. RetroSans.ttf, PixelSans.ttf and ImpactfulBits.ttf (CC0) are baked by tools/font/genfont.py into committed tables; only the RetroSans caption is linked, the other two are spares. genfont.py now accepts any SX_* prefix.

  • Start menu (trimmed): Start button plus a flat Programs list. /bin/startmenu opens anchored above the button with the progman catalog grouped by category, launches on click and closes on toggle or outside click. Mouse only, no cascade.

  • The Start menu gains a footer, a banner strip, a logo button and a self-listed launcher. Shut Down (with its icon) and Restart with two-step confirm over a margin, a 24px gradient strip in title blue, the brand logo on Start, Program Manager under System.

  • libffmpeg.so.0.4, FFmpeg's five components as one shared library. The FFmpeg port installs it at /disk/lib, and ldso_load maps it: 6.8 MB on disk, 2334 exported symbols and 9228 relocations, which is the first workload the loader has seen that it was not built for.

  • The Media Player is a program that depends on a library. It went from 7 MB with FFmpeg linked in to 344 KB with one DT_NEEDED, and it decodes through /disk/lib/libffmpeg.so.0.4.

  • The Media Player is a program of the system tree, and the FFmpeg port builds no program at all. Its source moved to subsystems/posix/userland/mediaplayer/ and the port's only artifact is /disk/lib/libffmpeg.so.0.4. Opening .mp3, .avi and .flac from Files goes through /bin/mediaplayer, which says in its own window which library it could not load. The program is built when the port has been built — FFmpeg's headers are generated, not versioned — and CMake says so instead of skipping quietly; without the port the launcher drops the entry, as it does for Doom.

  • A program whose library is missing now says so and keeps running. It used to die with a page fault at the address of a PLT stub. ldso_missing() is part of the SDK, and the player checks it before its first call.

  • A missing dependency no longer leaves the program unrelocated, and no longer takes the rest of them with it. Relocations were skipped when a DT_NEEDED failed to load, which left every global pointer at its link-time value — stdout among them, so the program could not even print the reason it was broken. And the walk stopped at the first missing library, so a missing libffmpeg.so.0.4 also left libsxgfx, libgfx2d and libsxgui unloaded: the Media Player started and died on its first toolkit call, jumping through sx_rect_make@plt with no link between that address and its cause.

  • The loader applies R_X86_64_64. A data slot holding the address of an imported symbol needs a runtime resolution, and the loader aborted the load on it. libffmpeg.so.0.4 has 253 of them.

  • libgfx2d.so.0.4, the 2D painter and window chrome, as a shared library. No library asks the program for a single symbol now — every reference any of them makes is to the C runtime or a system call. A program no longer has to be PIE so that a toolkit can find its drawing code.

  • Up to 32 shared libraries per process, instead of 8. The longest chain the tree can build is seven — a program, its two interface libraries and FFmpeg's codec, utility, scale and resample libraries — so eight was one slot from a wall that FFmpeg integration would have discovered instead of us. Raising it alone would have cost 134 KiB of resident memory in every process, including programs that load no library at all, so the per-library slot was halved first.

  • The library loader no longer leaks a descriptor per library. It kept the file descriptor and a whole-file section open after the segments were mapped, two per library, forever. A session with the task bar and the keyboard popup ran out of descriptor headroom once a third library was added.

  • A rebuilt program now reaches the bootable image. The rootfs staging step depended on an aggregate build target that produces no file, so it was considered up to date while the programs were already rebuilt. A change to any program left the image booting the previous binary, with every check still passing.

  • The visual scenarios now assert the glyphs on screen, not just that something moved. A toolkit that painted text in the background colour passed: the scroll was checked with the scrollbar thumb, which moves regardless. The expected pixels are rendered from the font tables inside the built graphics library, so the check is about the characters rather than about ink.

  • A program can now ask the loader which dependency failed to load. The loader was never fatal — a program with an unresolvable library still starts — but there was no way to find that out, so the ability existed by accident rather than by design. Both properties are now covered by a self-test that removes a declared library from the volume.

  • The Media Player is built position-independent. Its objects were compiled -fno-pic, so they could not go into a shared library at all. libffmpeg.so.0.4 now links from them: 6.7 MB, 2334 exported symbols, no relocation errors.

  • Two Media Player smoke scenarios are removed. They tested a --availability mode that stopped existing when SxMedia was withdrawn, and they had been asking for output nothing produced since. The absence-detection they covered is going away for a better reason: the player becomes a system application and reports a missing library itself.

  • A missing shared library is now recorded as reported by the application itself. The loader does not abort when a dependency fails, so a program can still open a window and say what is missing; the two pieces the loader would need for that are written down, along with the fact that an installed library cannot be uninstalled and why that is correct.

  • Dead code in the binaries is now measured against what would recover it: 5.5 MB across the tree, held back by --export-dynamic, which has to stay while the libraries resolve the C runtime against the program.

  • A relocation type the loader does not implement now says so, instead of failing with a step number that leads nowhere. It cannot arise from this linker, which only emits the three supported types; it is there for a hand-linked object.

  • Three more loader failures now say what they are, and each has a self-test. A missing library and a full slot table were both just numbers, and they are different problems with different fixes. A dependency shared by two branches of a dependency graph was loaded once per branch; nothing exercised that case, because a two-library chain cannot produce it.

  • The library loader says which symbol it could not resolve. A load failure reported a step number, which says where the failure was and nothing about what it was. A dependency built with an incomplete DT_NEEDED now names the missing symbol and the library that wanted it, and a self-test asserts the name rather than just the failure.

  • The runtime for external applications is derived from the one for the system's own programs, instead of being listed again by hand. The two lists have to agree and nothing complained when they did not — the symptom is an undefined symbol much later than the change that caused it.

  • The build now fails if a program carries its own copy of a library it loads. Such a program links, runs and passes every test — the library is mapped and never called — so the sharing silently stops happening and nothing notices.

  • The kernel now tells a program where its own image starts. The library loader was guessing it by walking backwards a page at a time looking for the ELF header, which assumed every page on the way was mapped. A linker can leave a gap between two loadable segments, and reading one of those is a page fault — a program died before main. A self-test with text starting at 0xc000 and its first segment ending at 0xaff3 is enough to trigger it.

  • libsxgfx.so.0.4, the graphics layer, as a shared library. The toolkit now links against it instead of asking the program to export the graphics entry points, so a program no longer has to be PIE just to satisfy it. The task bar, the keyboard layout popup, the shell wallpaper, the window server and the desktop shell draw through it, and no executable carries its own copy of it.

  • sxgui-smoke, which drives the visual scenario as a smoke test. The SxGUI gate used to run only if someone remembered to ask for it.

  • libsxgui.so.0.4, and ten programs now draw through it. Every user of the toolkit — the calculator, the text editor, the control gallery, the file manager, the application wizard, the system properties window, Minesweeper, the self-test, Program Manager and Task Manager — declares it and carries a copy of neither. The toolkit resolves the graphics layer against the program that started it, so the layer underneath stays where it is for now. Verified at the pixel level: the editor's scroll screenshots are byte-identical with and without the library.

  • ldso_symbol_is_shared() reports whether a symbol came from a shared library or from the program itself, so a program can check that it really is using the library and not a copy of its own.

  • sx_start_dynamic reports a failing interpreter. A program that could not be relocated said nothing and failed later somewhere unrelated.

  • calc now links as a PIE, the first desktop program to do so. It joins libtest in proving the profile beyond the test programs.

  • crt0 now runs the library loader before main. A program that links the loader is ready to call into its libraries with no setup of its own; programs that do not link it are unaffected.

  • libtest, the first program that links against a shared library. It calls sqrt like any other function instead of resolving it by hand, after ldso_start() loads what the program declares and fills its GOT. It is built without math.c, so sqrt genuinely comes from libmath.so.0.4.

  • The loader relocates the executable, not just libraries. The kernel maps the main image but leaves its GOT empty, so a program that links a library has to call ldso_start() before using it. R_X86_64_RELATIVE is now applied: without it a relocated image keeps pointers to address zero.

  • The executable is now in the library loader's symbol scope. A library can resolve a symbol against the program that started it, so -fstack-protector- strong is back on for shared libraries. ldtest checks the stack canary resolves and holds a live value.

  • A library now loads the libraries it declares. DT_NEEDED is walked from /lib, each dependency is mapped once however many ask for it, and a symbol resolves through the whole chain. ldtest calls a function in a dependency and checks the result.

  • pietest, the first PIE executable in the tree. ET_DYN, relocated by the kernel onto a base, with .dynsym and .dynamic. It proves a relocated image loads and runs — and it proves the premise the whole feature rests on: a PIE executable exports symbols a library can resolve against, which lld denies to a non-PIE ET_EXEC.

  • The kernel loads ET_DYN images with a load bias. An ET_EXEC is unchanged — bias zero, exactly where the linker script put it. An ET_DYN is relocated onto a base, which starts at kUserBase. Per-process entropy for that base, and ASLR for the executable image, is the next step.

  • The kernel reads PT_INTERP and hands the path to crt0. A binary linked with --dynamic-linker now has its interpreter path delivered in rcx and readable through savanxp_interpreter_path(). interptest is linked with one and checks it arrives. The interpreter is not run yet: crt0 still goes straight to main.

  • libmath.so.0.4, a real shared library, and a minimal loader that maps it. /lib/libmath.so.0.4 is built as PIC and lands on the volume, not in /bin, so Program Manager never lists it. ldso reads its program headers, places each PT_LOAD at the address the ELF asks for, copies the writable segments into private anonymous sections, applies R_X86_64_JUMP_SLOT and R_X86_64_GLOB_DAT, and resolves symbols by name. /disk/bin/ldtest calls sqrt, fabs and floor through the loaded pointers.

  • section_open_range backs a section with part of a file. A PT_LOAD is a slice of the file, not the whole file, and mapping the whole thing puts the bytes at addresses that are not theirs.

  • section_open maps a file into a read-only section. Given an open file descriptor it returns a section handle holding the file's bytes, so user mode can map a library image. Write access cannot be requested: the backing is read-only and writable segments are separate anonymous sections. Two processes asking for the same file share one set of pages.

  • map_view_at maps a section view at an address the caller chooses. The base must be page-aligned and free. map_view is the same call with the base left to the kernel.

  • savanxp_system_info reports live section counts. sections_live and file_sections_live, so a shared mapping is observable instead of assumed.

  • SAVANXP_SECTION_EXEC maps a section view as executable. Execution is a permission of the view, granted by the section, instead of a hardcoded NX on every mapping. A view that asks for write and execute is still refused, with EINVAL. mmap keeps refusing PROT_EXEC: executable mappings go through section_create/map_view only.

  • fscheck reports SxFS consistency from inside the system. It reconciles the block bitmap against the inodes in both directions and walks the tree for unreachable inodes, aliased entries and duplicate names. Read-only: it repairs nothing and runs against the mounted /disk. Exits non-zero on findings.

  • Celeste Classic is now an official port. ./ports/ccleste/build.sh installs /disk/bin/ccleste plus the game assets under /disk/games/celeste, building the upstream engine unpatched because the overlay replaces its SDL frontend. ./build.sh smoke ccleste-selftest and ./tools/shoot.sh --scenario ccleste cover it. Music is deferred until the OS has a dynamic linker, so that a program can reach a codec it was not built with.

  • The SDK now owns Doom-style pixel presentation and timed PCM mixing. sx_scaled_presenter handles scaling/centering/row damage; opt-in savanxp/audio.h provides sx_audio_mixer, and Doom uses both.

  • Linux builds can now be driven by Bash and CMake. build.sh drives the freestanding CMake graph, stages the rootfs, validates sibling SxFS candidates, and produces EFI/ISO artifacts through one native workflow.

  • Official ports now have a versioned layout. Each port keeps its upstream pin, focused patches, SavanXP overlay, standalone build script, and persistence instructions; local experiments remain outside the official tree.

  • DoomGeneric now has a versioned official port. ports/doomgeneric pins upstream, separates reviewed patches from the SavanXP overlay, builds with Bash, and installs through the persistent SxFS candidate flow.

  • The optional Haxe subsystem now has a standalone Linux build. subsystems/native/build.sh pins Haxe/reflaxe, keeps the base CMake target independent, and installs native demos through the SxFS candidate flow.

  • Linux now has a headless smoke runner. ./build.sh smoke <scenario> injects the scenario into the initramfs, runs QEMU on a validated disposable image copy, records serial output, and supports QMP keyboard/taskbar input.

  • Linux smoke orchestration now covers audio, TCP, and external native apps. Audio backends/WAV files, the TCP echo server, and tools/build-user.sh keep these scenarios outside the base CMake target.

  • Linux build commands now match the one-word smoke aliases and SDK install destinations. Direct windowd-smoke/gpu-soak targets, build-user.sh --destination, and tools/run-user.sh cover the matching Bash workflows.

  • The host sxfs-cli check command validates SxFS metadata. Linux image synchronization checks before and after candidate applies and fails closed on corruption, dirty/pending journals, or no-space conditions.

  • SxFS images now compact safely when fragmentation blocks an apply. The Linux sync path preserves reachable external data, validates a same-size staging image, and atomically installs it without deleting the original first.

  • Stack canaries are randomized at kernel boot and for every new process. fork inherits the parent guard, while exec installs a fresh one before main; the runtime no longer embeds a reusable cookie in each image.

  • Kernel security boundaries are documented and enforced. NX/W^X, stack canaries, partial mapping ASLR and checked user/device/storage boundaries land together with the kernel audit.

  • Alt+F4 closes the active window, the same as its X button: a dialog is cancelled, a main window closes with its process, and it also works for an app in fullscreen.

  • System name and version at the bottom right of the desktop, above the taskbar, as classic desktop systems show them. Drawn with the wallpaper, so every wallpaper mode has it and no window is covered.

  • Dialogs are windows of their own. Under sxgui_app_run, sxgui_dialog_begin opens a WM window owned by the app — framed, kept above its owner, which takes no input meanwhile — so Minesweeper's About no longer gets cut off at Beginner. New gfx_window_open/gfx_window_close and SAVANXP_SYS_PIPE_SEND_HANDLE/ _RECEIVE_HANDLE (handle passing over a pipe), tested by handletest. How it works.

  • Media Player is now a system application in Accessories. /bin/mediaplayer is always built, delegates to /disk/bin/mediaplayer-ffmpeg when installed, and otherwise opens an in-OS warning explaining that ports/ffmpeg must be built; the installed player keeps the audio/video controls and associations. How it plays.

  • The FFmpeg port decodes the common formats. H.264, HEVC, VP8, VP9, MPEG-4, Theora and MPEG-1/2 video; AAC, MP3, Vorbis, Opus, FLAC and AC-3 audio; MP4, Matroska/WebM, AVI, Ogg and MPEG-PS/TS containers; plus swscale and swresample.

  • Gears (/bin/gears), in the Diagnostics group. A hand-written software rasterizer — matrices, viewport, z-buffer, backface culling and flat shading with one directional light — drawing the three meshed gears. It is the consumer that defines the subset of the future SxGL, not a port. New tools/shoot.sh --scenario gears. The plan it serves.

  • Fixed-size windows: window_flags=fixed_size in a .sxres. The edges stop grabbing and maximize is drawn disabled, so a layout that cannot stretch no longer gets an empty margin around it; Minesweeper and Calculator declare it. The program still sizes its own window — in a fixed one every size hint is honoured, so changing Minesweeper's level resizes the window. How it works.

  • Floating point in every app of the image. The in-tree userland compiles with -msse2 and links the libm of runtime/math.c, so double, %f, strtod and sqrt work with no switch to remember; --sse stays for external apps. The rule it replaces.

  • docs/desktop/SXGL_ROADMAP.md. OpenGL does not grow out of SxGFX: SxGL would be a sibling layer binding to the window surface and gfx_present_region, as a runtime module of the SDK. Nothing is implemented.

  • Calculator (/bin/calc), in the Accessories group. Four operations, %, sqrt, 1/x, memory keys, copy/paste and the numeric keypad, over a decimal engine of 16 significant digits written in integers, so 0.1 + 0.2 is 0.3. New ./build.sh smoke calc-smoke and tools/shoot.sh --scenario calc. Why its engine is decimal and integer anyway.

  • Minesweeper (/bin/mines), in the Games group. The first game that comes in the image instead of being installed like Doom: three levels, LED counters, the face button, ? marks, chord with both buttons, keyboard play and best times in /disk/mines.ini. New ./build.sh smoke mines-smoke, and apps that paint their own content now get the system 3D edges from sxgui_draw_raised_edge(). Why it is in C and not Haxe.

  • System master volume and mute. AUDIO_IOC_SET/GET_VOLUME (0..100, 100 is identity) and SET/GET_MUTED on /dev/audio0 scale every writer's PCM in the kernel; /bin/volume reads, sets and persists it to /disk/audio.cfg.

  • Taskbar volume button and popup. The speaker left of the layout indicator opens a frameless slider with mute, applied live through the master ioctls; a second click or a click outside closes it, like the Start menu.

  • sx_audio_mixer_set_voice_loop. A mixer voice can repeat its buffer instead of stopping at the end, for short music loops; starting a voice leaves it off, so looping stays opt-in per start.

  • Celeste Classic now plays music. The port links PIE against libffmpeg.so.0.4 and decodes the five OGG loops on first request into a dedicated looping mixer voice; without the FFmpeg port it runs silent.

  • Two programs can now sound at once. /bin/audiod owns /dev/audio0 and sums what clients send over UDP loopback; every writer falls back to direct turn-taking without it, so nothing changes where it does not run.

  • Per-application volume. Streams carry names now, the daemon levels each one, and volume list / volume set <app> <0-100> drive the census; the mixer, the player, Doom, Celeste and the test tone all introduce themselves.

  • Per-application sliders in the volume popup. Every application that is sounding gets a row with its own slider, refreshed from the daemon census once per second; levels are per session and are not persisted.

Changed

  • Doom music now plays the WAD's OPL2 instruments. With GENMIDI the song renders through a two-operator FM voice instead of the families, without it bit-identically; the smoke log names the path ((FM) vs (familias)).

  • The MIDI bank stopped being one waveform per family. A voice now has a second partial (interval and weight), a blend toward sine, an optional vibrato LFO and an attack pitch sweep, and the drum kit uses all of it.

  • The calculator's arithmetic now comes from libmath.so.0.4. Its own 16-digit decimal engine existed because the in-tree userland was built -mno-sse and double did not compile. sqrt is now resolved from the shared library, so calc carries no copy of it.

  • The calculator shows 15 significant digits instead of 16, and accepts 15 digits of input instead of 16. A double carries about 15.95 decimal digits, so the sixteenth was not always true. 1/3 × 3 now reads 1 instead of 0.9999999999999999, and 2/3 reads 0.666666666666667.

  • Section budgets raised: 64 section views per process, 256 sections system-wide. The old limits left no room for shared libraries, and 32 views per process was already tight for heap arenas alone. Exhaustion is still a clean ENOMEM.

  • Two PT_LOAD segments may now share a page. Required by any binary linked with an interpreter, where the text segment does not end on a page boundary. The page takes the union of both segments' permissions, and a union that is both writable and executable is still refused.

  • The ELF loader reports which program header it rejected. bad elf segment now carries the segment index, or 65535 when the rejection is not about any one segment. Without it, a PT_LOAD table with four entries fails as one opaque message.

  • The section cache keys on the file range, not just the inode. Asking for the whole file and asking for a slice of it are different sections, and confusing them mapped a segment with the whole image behind it.

  • savanxp_system_info reports live section counts and why the last view mapping failed, so a shared mapping is observable instead of assumed.

  • Window caption bars are two pixels taller. The title area now has a 20px caption, giving the compact title text and caption buttons more breathing room.

  • The test machine now boots with 1 GiB of RAM, and the persistent volume is 1 GiB while the LiveCD carries 256 MiB. The image stays sparse, so it costs ~30 MiB of real disk.

  • SxFS format v2: 4096 inodes and a 1 GiB ceiling. The inode table went from 64 to 1024 sectors and the block bitmap from 32 to 512, so a volume is no longer capped at 64 MiB. build/disk.img is reformatted once; a v1 image does not mount, and sxfs-cli says so instead of failing obscurely later.

  • ATA now splits large requests instead of rejecting them. A metadata commit asks for 1537 sectors and the PIO count register holds 8 bits, so writes over 255 sectors were being refused — harmless while the metadata fit in 97 sectors, fatal on the default IDE machine once it did not.

  • fscheck reports metadata commit cost. commits, bytes_written and bytes_per_commit per volume, so the journal's cost is a measurement instead of an inference from the wall clock.

  • The development path no longer carries the persistent volume as a boot module. build/disk.img reaches QEMU as an attached disk instead, which frees the 64 MiB it was occupying in RAM and drops the staged EFI tree from 542 MiB to 30 MiB. The ISO keeps its own tree and its livecd volume, since there is no disk to attach, staged at its exact filesystem size so a grown image does not pad the ISO.

  • build/disk.img may now be larger than the volume inside it. sxfs-cli accepts an oversized image and still rejects a truncated one, and a build preserves both the size and the sparseness of the file, so truncate -s is enough to make room for a later volume grow.

  • Window titles use RetroSans instead of Noto Sans. The caption face is RetroSans.ttf at 18px with no faux-bold; the desktop body stays on Noto Sans 13px.

  • Active captions follow the declared accent instead of tinting the blue base. A declared accent renders as its own shade-to-tint ramp; windows without one keep the Windows Standard gradient. Fixes the two-tone split on accents like Files.

  • init relaunches the audio daemon only when it dies serving. A deliberate startup exit (no network, no audio, another daemon) leaves it stopped and the system on direct turn-taking instead of respawning forever.

  • The test machine now boots with 512 MiB of RAM, up from 256 MiB, for run, debug, the smoke scenarios, gpu-soak and the ISO boot test.

  • Window title text is now one pixel smaller and bold. Caption text uses a dedicated 12px Noto Sans face while the rest of the UI keeps its regular font.

  • Normal builds no longer regenerate desktop source icons. Checked-in PNGs are treated as editable artwork; tools/gen_desktop_source_art.py remains available through the optional savanxp_desktop_source_art target.

  • Gears (/bin/gears) has a redesigned app icon. The launcher and taskbar now use fuller 16×16 and 32×32 gear artwork.

  • The About app now identifies the system as the Alpha edition.

  • The boot screen fades in from black, like a classic desktop splash. Logo, name, progress bar and status line brighten together over about a second, driven by the monotonic clock; without a calibrated clock the splash appears at once.

  • Visual desktop verification uses the native Bash launcher. tools/shoot.sh reuses the shared scenario driver over Unix QMP and a disposable SxFS copy.

  • Linux Program Manager and Add/Remove Programs smokes restore Doom when its external ELF exists. The optional image remains unchanged when no ELF is installed.

  • Doom persistence now has an isolated Linux regression command. tools/verify_doom_persistence.sh rebuilds a disposable image and compares the Doom binary, WAD, configuration, and savegame bytes.

  • ISO verification now covers El Torito plus BIOS and UEFI boot. tools/iso_boot_test.py boots the generated image through both firmware paths and waits for the guest handoff token.

  • WM↔client protocol v4: the session opens all 12 windows. Each costs windowd two descriptors instead of nine: one event pipe (SAVANXP_WM_FD_EVENTS), a wake event, one submit event per session, and hints and launches in the surface header (gfx_should_close is new). Apps no longer inherit windowd's descriptors. Breaks the client ABI: rebuild external apps such as doomgeneric. Descriptor budget.

  • The automated smokes honour --accel. ./build.sh smoke smoke --accel kvm runs the suite with KVM instead of TCG; the default remains tcg.

  • The system 3D edges moved to savanxp/sxchrome.h, out of the toolkit. sxchrome_draw_edge/_raised/_sunken/_inset/_etched, sxchrome_fill_raised and the disabled relief (_draw_text_disabled, _draw_glyph_disabled) are in every binary's base runtime, so windowd, calc and taskmgr dropped their private copies. Why not in SXGUI-C.

  • The userland links with --gc-sections. Each binary keeps only what it reaches from _start instead of whole runtime objects: the initramfs goes from ~14 MB to ~5.5 MB, libm included.

  • The managed app layer (Haxe on a VM) is deferred until after v1.0. Until then everything ships in C against the POSIX SDK; subsystems/native stays as a frozen, validated experiment that nothing in the image depends on. If it is ever needed, it arrives as a selective port, UWP-style, beside the native apps. The decision and the shape it would have to fit.

  • About is now System Properties. General shows the edition, the version, where it is installed and the processor (brand, speed and features from CPUID); Hardware lists the devices found. The live counters it used to show moved to the Task Manager.

  • The Shell window opens and repaints far faster. /bin/shellapp dropped an unused 8 MiB static backbuffer, repaints only the text rows that actually changed instead of the whole surface, blinks the cursor without a repaint, and keeps scrollback in a ring. Monospace text blits ~16x faster system-wide. New shellapp-stats: line over /dev/serial and tools/shoot.sh --scenario shell.

  • The compositor accumulates damage as an exact region. windowd stopped merging dirty rectangles by bounding box: dragging a window now repaints and presents the ring that changed, not the box around the old and new frames.

  • tools/shoot.sh drives other hardware and loads the display path. --virtio/--accel (tcg/kvm) pick devices and accelerator; the bench, saturate and spin scenarios feed windowd-stats.

  • Launcher icon captions wrap to two lines. A name that does not fit on one line breaks at a space and is centred over two, and only what still does not fit is cut with an ellipsis — before, a long caption was clipped at both ends and read as broken rather than as truncated. The cell now derives its height from the active font instead of a baked 76 pixels.

  • The boot screen is now a splash with the project logo. Black background, assets/brand/logo.png baked in by tools/gen_boot_logo.py, the system name below it, a sliding block bar, and the step names now in English.

  • The splash bar keeps moving through the long boot steps. It advances on monotonic_ns(), now calibrated at the start of boot by timer::calibrate_monotonic() instead of during the ACPI bring-up.

  • All repository documentation is in English, and the README leads with a quickstart. The Linux requirements move to docs/build/BUILD_LINUX.md, docs/README.md indexes the design docs, and CLAUDE.md points at AGENTS.md.

  • The filesystem is now called SxFS, and the 2 leaves the name. The constants and the namespace never carried the number (SVFS_*, svfs::), so the name said one version and the code another. The on-disk magic goes from SVFS2 to SXFS (the journal's from SVJNL2 to SXJNL) and SXFS_VERSION returns to 1. Every earlier image stops mounting: build/disk.img regenerates itself, but a persistent VirtualBox/QEMU disk must be reformatted.

  • assert() reports when it fails. It was ((void)(expression)), so a failing assert went unnoticed. It now prints and terminates the process; with NDEBUG it still disappears.

  • struct stat has the fields third-party code names. st_uid, st_gid, st_nlink, st_blksize, st_blocks, st_atime, st_mtime, st_ctime. The kernel only reports type and size, so the rest stay zero.

  • exec no longer copies the whole image into physically contiguous pages. elf::load_user_image takes an elf::ImageReader and reads the file directly onto the process's pages: half the memory during exec, and no contiguous reservation that fails on a fragmented heap.

  • The user stack goes from a fixed 128 KiB to 1 MiB on demand, with a guard page. Only 8 pages are mapped at startup and the rest appear when touched (process::grow_user_stack), so a shallow process uses LESS memory than before. An overflow now dies with a readable fault.

  • spawn/exec accept 128 arguments and 8 KiB total, instead of 15 arguments of 63 characters. The buffer now comes from the kernel heap instead of 4 pages of kernel stack, and overflow fails with E2BIG (new) instead of truncating silently.

  • Streams read in batches and malloc aligns to 16. fgets was issuing one read() per character; file streams now fill a 512-byte buffer. 16 is x86-64's max_align_t: with 8, a movaps in an --sse app could fault.

  • The libc stops renaming with #define: the standard names are now real symbols. The SDK headers defined #define read sx_read and ~140 more, so the preprocessor rewrote any identifier with those names — a .close struct field included — and malloc or stat were not symbols a third-party object could link. The raw syscall layer is prefixed savanxp_*; math.h and setjmp.h keep their macros on purpose.

  • One userland runtime and one printf. runtime/posix.c is linked into every program and runtime/libc.c is reduced to the raw syscall layer plus gfx. Which formatter ran used to depend on the headers each .c included, and libc.c's did not even understand %ld/%lu. Raw puts becomes puts_out and putchar(fd, c) becomes putchar_fd.

  • icon= in progman.ini points at a program, not at an art catalog. It stores a PATH and the icon is read from the binary it points at. The old names still work as aliases, and an explicit icon= beats the binary.

  • The sxgui chrome moves to the Win95 two-pixel bevel. A period 3D border takes four tones — SXGUI_COLOR_BEVEL (223,223,223) was missing — and sunken is now the exact reverse of raised. Also dotted focus rectangle, 50% scrollbar trough, redrawn checkbox tick, concentric radio button, engraved disabled text.

  • The sxgui apps share a single layout grid. The metrics live in savanxp/sxgui.h (SXGUI_MARGIN, SXGUI_GAP, SXGUI_BUTTON_WIDTH, ...) and are used by notepad, files, progman, aboutapp and widgetsdemo.

  • tools/shoot.sh gains the files scenario. The explorer is the window with the most distinct controls at once, so it is where a stray toolkit margin shows up.

  • The keyboard event carries the modifiers. savanxp_input_event gains modifiers (SAVANXP_KEY_MOD_*); the kernel already computed it and threw it away, so the WM tracked Ctrl by hand and got stuck if a KEY_UP was lost. The event grows from 12 to 16 bytes: external apps built against the previous SDK need a rebuild — Doom reads this struct.

Removed

  • The SxMedia multimedia layer is withdrawn, and the Media Player is the optional FFmpeg port again rather than a system program. A program can only use the codec libraries it was built with, so "one player that plays whatever is installed" cannot be arranged without a dynamic linker or a codec service. The design, the findings and the prerequisites to try again are in docs/media/SXMEDIA.md.

  • The alternate host compatibility surface is removed. Its entry points, bootstrap/toolchain cache, non-native accelerator selection, and TCP QMP fallback are gone; Bash/CMake is the canonical build and test path.

  • The legacy SDK trees for Doom and FFmpeg are removed. Doom and FFmpeg now live exclusively under ports/doomgeneric and ports/ffmpeg.

  • The wavinfo and player FFmpeg demos. Every binary that links libavcodec carries all its decoders, so they became modes of the player: mediaplayer --probe, --selftest and --gpu-hold.

  • The icon set baked into desktop_icons.h is down to one. Only DESKTOP_ICON_DESKTOP survives, as the safety net for a binary that cannot be read at all. The PNGs in assets/desktop/icons/ stay as the catalog each .sxres references with icon=<name>.

  • The start menu strip art is deleted. The menu was retired with the rest of the Win95 chrome, but the chain that drew it was still whole, down to a symbol nobody used.

  • The eight hand-written coreutils in subsystems/posix/userland are deleted. cat.c, echo.c, ls.c, mv.c, rm.c, sleep.c, true.c and false.c were built by nothing: /bin gets them from the busybox multicall.

Fixed

  • Celeste's effects no longer saturate at any volume. Its 16-bit WAVs were decoded taking the high byte unsigned, shifting everything half scale (even silence played full-negative); the mixer also compresses past 0.75 FS now.

  • A jittery audio stream is concealed instead of zeroed. While a stream has played a frame, the daemon repeats its last frame — scaled by its own volume — for up to 50 ms of jitter, so a brief packet gap does not click but a client that is gone does not keep the device busy either.

  • Changing the music no longer stalls the game for a second. The Doom port synthesised a whole track before playing it, which costs ~120 ms natively and about a second under TCG; it now keeps a one-second lead and fills a quarter-second chunk per frame while the looped voice plays the buffer.

  • A missing library is now named deterministically, and counted. It used to be whichever failed last, so the same image could name three different libraries for the same three absent files. It is now the first, plus a count of how many libraries could not be loaded — which is not the same as how many files are missing: two absent files can stop four libraries from loading.

  • The emulated disk transfers 32 bits per port access instead of 16, which makes a 390 KB write ~26% faster (measured: 365930 and 401218 ms down to 280703 and 278922). Most of that cost is not the port writes at all: dropping every cache flush changes nothing, and the per-sector cost on the device side is what remains. Bus-master DMA is the fix for that, and the kernel does not use it.

  • Four loader diagnostics can now fail. brokentest, missingtest, slottest and diamondtest asserted only through eprintf, so they had nothing a smoke scenario could look for and none of them had one: they were built, shipped in the image and never run. Each now prints a success token, each has a scenario, and each was checked against a build with the loader logic it tests disabled — where it stops passing.

  • The launcher's baked table and the manifests agree. The table said Main/Games/Diagnostics and every .sxres said Accessories/System/Games/Diagnostics, and had done for months without anything failing, because the real catalog is rebuilt by scanning the manifests. A plan B that disagrees with the plan A is a second answer to the same question; progman-smoke now fails if a group exists in one and not the other.

  • The visual smoke scenarios work on a virtio machine. They drove an absolute tablet with relative moves, so their clicks landed on the wrong window, and they compared screenshots pixel by pixel including the cursor glyph, so an editor that had not moved read as one that had. sxgui-smoke --virtio was failing on "the wheel did not scroll the editor".

  • Moving the mouse over a virtio machine no longer freezes the VM. Letting the cursor into the QEMU window was enough for the guest to take #14 page fault and halt. What the fix was about.

  • smoke_runner_layout_test.py runs again. It asserted two Media Player scenarios withdrawn with SxMedia, and a remove_paths entry from before libffmpeg.so.0.4 moved to /disk/lib, so it had been red and was guarding nothing.

  • progman-smoke passes again. It was failing on its 180 s timeout, not on anything it asserts. Copying the 390 KB stamped fixture into the volume costs 365930 ms on its own — five metadata commits, each copying the whole 1537-sector metadata region twice — while the rest of the smoke adds up to 3 s. The timeout is now 900 s and the smoke prints what the copy cost, because a test that goes red without saying why is what made this read as a broken launcher.

  • Relocated global pointers in a PIE no longer become a pointer to the image base. R_X86_64_RELATIVE took its value from memory, but lld leaves the slot at zero and stores the address in the relocation's addend. Every stdin/stdout/stderr and any other relocated global in a PIE program was affected.

  • crt0 no longer loses argc and argv. Running the library loader before main used the registers the kernel had put them in, so a program with arguments lost them. Every program that ran the loader with real work to do was affected.

  • The interpreter path no longer overwrites an argument string. It was written above the argv pointer array, where the argument strings live; a program could read a path instead of its own argument.

  • PIE executables no longer declare a glibc interpreter path. lld inserts /lib64/ld-linux-x86-64.so.2 by default for -pie, a path from the host that builds and not one SavanXP has.

  • A shared library no longer resolves its calls against the wrong image. All its PT_LOAD segments now share one load bias, and each segment's file bytes land at their p_vaddr instead of at the start of the mapping. A PLT in the text segment jumping through a GOT in the data segment was landing in unrelated code with no visible error.

  • A library that calls nothing from outside now loads. A missing DT_JMPREL/DT_PLTRELSZ is skipped instead of treated as a failure.

  • The mouse no longer drifts away from the host's. A virtio-tablet event now carries the true pointer position to userland, not only a delta, so an event dropped by a full /dev/mouse0 queue re-places the cursor instead of leaving it offset for good. savanxp_mouse_event grows absolute_x, absolute_y and flags (SAVANXP_MOUSE_FLAG_ABSOLUTE); windowd assigns that position and still sums deltas for a relative mouse (PS/2). The rule the event used to break.

  • The AC'97 driver measures its own feed. An ac97-stats: line every ~11 s of audio over /dev/serial: wall time against audio actually delivered, periods discarded because the ring was full, underruns, and the longest gap between two writes. How to read it.

  • ./build.sh smoke clock-smoke. Measures uptime_ms and monotonic_ns against the RTC while spinning and while idle, and fails only if a clock changes rate with idleness. What it measured.

  • Sockets that ported code can use. recv()/send() work on streams, SO_RCVTIMEO/SO_SNDTIMEO govern the wait through the new setsockopt() (0 = wait forever, replacing a hardcoded 5 s cap on reads), and a write() larger than one segment writes what fits instead of failing. A blocking socket read() now parks the process instead of spinning. The contract.

  • A network adapter with no driver is reported as such. system_info gained net_hardware and the PCI id of the adapter it found, so System Properties and the Task Manager stop saying "no adapter" on a machine that has one SavanXP cannot drive — VirtualBox's default Intel PRO/1000, for one. What to do about it.

  • Task Manager (/bin/taskmgr), in the System group. Live CPU, memory and handles per process with an End Process that asks first; meters, history graphs and totals in Performance; adapter throughput in Networking; File > New Task and Shut Down. proc_info/system_info gained the counters it reads (cpu_ticks, memory_bytes, cpu_ticks_total, memory_free_bytes). New ./build.sh smoke taskmgr-smoke. How it measures.

  • TCP survives a network that loses and reorders. Segments are kept and resent with backoff (SYN included), out-of-order data is reassembled, and a dead connection now reports ECONNRESET/ETIMEDOUT instead of a clean end of file. New ./build.sh smoke tcp-smoke, NET_IOC_GET_TCP_STATS and NET_IOC_SET_TCP_FAULT. What it guarantees.

  • The mouse wheel works. PS/2 negotiates IntelliMouse 4-byte packets (ps2: mouse wheel enabled), virtio reads REL_WHEEL, and the new wheel field of savanxp_gui_pointer_event scrolls lists, text views and scrollbars under the cursor. Why it accumulates.

  • The compositor measures itself. windowd reports compose, sync and present times — present split into ipc/svc/gpu/tl, and blk_us for the total time blocked on compositord — plus damage area, over the new write-only /dev/serial. New monotonic_ns(); compositor protocol v2. How to read it.

  • Gfx Demo has an auto-motion mode. S moves the box every frame without input, so the demo presents as fast as the compositor allows — the load that shows the display path's real ceiling.

  • Every core runs user processes, under one big kernel lock. Each core the bootloader reports gets its own TSS, idle process and LAPIC timer (smp: planificando en N de M cores); /bin/smptest in ./build.sh smoke proves the overlap. Task Manager usage is now of all cores together. Unmapped kernel pages are flushed from every core's TLB (smp: TLB perezosa ok). --smp <n> still defaults to 1. Phases 0-3 of the SMP roadmap.

  • Add or Remove Programs (/bin/appwiz), in the System group. It lists what was installed outside the system image — in /disk/bin and not in /bin — and deletes the binary, optionally with the data directory the program declares as data_dir= in its .sxres. New ./build.sh smoke appwiz-smoke. Why that is the rule.

  • Program Manager lists every installed program on its own. A binary whose .sxres declares category= shows up under that group with nothing to register: installing is copying it to /disk/bin, uninstalling is deleting it. Without the key it stays launchable, just unlisted. F5 (File > Actualizar) rebuilds the catalog, and launching an entry that is gone says so instead of nothing. How it works.

  • virtio-net driver for the NIC. With --virtio, ./build.sh builds virtio-net-pci instead of rtl8139, and nic:: prefers virtio_net::driver(). Polling-only, no IRQ of its own.

  • --accel kvm. run/debug accelerate against /dev/kvm with -cpu host on Linux. The smokes stay on TCG on purpose, to remain deterministic.

  • Audio capture (virtio-sound RX) and duplex /dev/audio0. read() has its own session owner, independent of write(), so recording and playback can come from different processes. New audiotest --record and virtio-record.

  • Keyboard over virtio-input. --virtio adds virtio-keyboard-pci; its EV_KEY events become scancode set 1 (ps2::inject_scancode) and reuse the existing layout logic. ps2:: disables its own keyboard while it is active.

  • virtio-blk driver for the persistent disk. With --virtio the machine gets virtio-blk-pci instead of IDE, appearing in block: as vblk0. It flushes after every write, or a "successful" write could stay in QEMU's cache.

  • The file list shows an icon per type. The catalog is standalone .sxicon blobs in /disk/icons, mapped by extension in diskfs/mimeicon.ini. This is extension to icon, not type detection: MIME_OPEN is still unresolved.

  • Video player on FFmpeg: MJPEG decoding, YUV->RGB through swscale, on screen. ./build.sh smoke ffmpeg-smoke runs wavinfo, the player in --selftest and the player in --hold, which presents over /dev/gpu0.

  • FFmpeg runs inside SavanXP. libavutil, libavcodec, libavformat and libswresample compile against this libc and really decode. Minimal codec set (WAV + PCM s16le), no asm and no threads; built separately with GNU make, see ports/ffmpeg/README.md.

  • static_assert in <assert.h>, the PRI*/SCN* in <inttypes.h>, and fpclassify. Plus the missing errnos (EDOM, EILSEQ, EPERM, ESPIPE), mkdir, F_SETFD/FD_CLOEXEC and imaxabs/strtoimax/strtoumax.

  • ./build.sh smoke runs imagetest. It inspects itself to verify what a streaming loader can break: .rodata byte by byte, .bss zeroed and .data with its initial values, from /bin and from /disk/bin.

  • ./build.sh smoke runs stacktest. Deep recursion in the process and in a child, an overflow that must die with a #PF on the guard page, and a 40-argument argv.

  • The libc gains what a port takes for granted. Strings, integers, aligned memory, stdio (fgetc, ungetc, fdopen, setvbuf, fseeko, perror), sscanf with scansets, and a full calendar. Everything is UTC.

  • Real %f, %e and %g, and strtod. %f was a stub that consumed the argument and wrote 0. Now with precision, width, rounding carry and the odd cases (nan, inf, signed zero). All behind __SSE2__, like libm.

  • libm: the missing ones. asin, acos, hypot, cbrt, rint, lrint, lround, frexp, modf, scalbn, nextafter, fdim, fma, log1p, expm1, the hyperbolics, remainder and their float variants.

  • ./build.sh smoke runs libctest. Exercises the libc surface a port consumes: standard names as struct fields, qsort/bsearch, streams, directories and the POSIX error convention (-1 plus errno).

  • ES/EN keyboard layout selector, in the taskbar. US QWERTY joins the baked ES map, chosen at runtime through /dev/input0 (INPUT_IOC_SET_LAYOUT/GET_LAYOUT), saved in /disk/keyboard.cfg and applied at boot, before windowd. UI in kbdlayoutpopup.c.

  • ./build.sh smoke kbd-smoke: the keyboard driver is tested on its own. It closes the real PS/2 -> IRQ -> kernel/ps2.cpp -> /dev/input0 path, which the harnesses that inject already-formed events do not cover.

  • ./build.sh smoke pointer-smoke --virtio: the pointer is tested on its own. The pointer twin of kbd-smoke: mousetest --selftest reads the real /dev/mouse0 while the host moves the emulated tablet over QMP, and asserts the centre, a click and the opposite corner as screen positions. Nothing covered the virtio-input queue: cursor-repro injects events by hand.

  • SXE stamping is no longer opt-in: everything built for the system comes out in that format. Coverage over the complete image is 68/68 installed binaries, in-tree and external (Doom, busybox) alike. Every program gets a .sxmeta with or without a .sxres — name, version, subsystem and the short commit as BUILD_ID; icon, accent, description and mimes are never invented and stay .sxres enrichment.

  • icon_file= in the manifest: the icon travels in the binary, not in assets/. It takes a PNG relative to the .sxres and derives the two sizes the runtime needs. Mutually exclusive with icon=.

  • Floating point in userland, with --sse and a libm of our own. Code using float/double compiled but did NOT link: under -mno-sse clang resolves each operation through compiler-rt soft-float helpers this system does not have. The switch is opt-in, and math.h declares the library under #if defined(__SSE2__) so the error appears at COMPILE time naming the function. New ./build.sh smoke float-smoke.

  • Tab control in sxgui (sxgui_tabs). The active tab is drawn taller and covers the page's top border, so both parts read as one sheet. Program Manager's group selector now uses it.

  • Taskbar at the bottom of the screen, listing open windows. It is a WM CLIENT (/bin/taskbar), not windowd chrome — the explorer.exe model. No start menu and no notification area. Buttons keep a STABLE order by slot, not by z-order, or the next click would land on something else. What a client cannot know arrives over the new channels in savanxp/wm_shell_protocol.h.

  • tools/shoot.sh: visual verification of the session, headless. It boots without a window, sends keys over QMP (holding modifiers, which the monitor's sendkey cannot) and captures PNGs. The taskbar scenario verifies pixels and is wired as ./build.sh smoke taskbar-smoke. Not part of the build.

  • Alt+Tab to switch windows. While Alt is held each Tab moves the selection and releasing it confirms; Alt+Shift+Tab goes backwards. The switcher shown is the Task List: no new UI.

  • Text selection in sxgui, with Cut, Copy and Paste. Shift with the arrows, Home, End and the page keys extends the selection; click anchors, drag stretches. Ctrl+C, Ctrl+X, Ctrl+V and Ctrl+A; other Ctrl+letter combinations are consumed instead of typed, except with AltGr. Also exposed as sxgui_textedit_copy, _cut, _paste, _select_all and _has_selection; Notepad gains its Edit menu.

  • System clipboard, in /dev/clipboard. The content is a value, not a stream: a write replaces everything and a read returns from the start. Over SAVANXP_CLIPBOARD_CAPACITY (8 KiB) it fails with ENOSPC. SDK wrappers clipboard_set_text, _get_text, _get_info and _clear.

  • Closing a window no longer freezes the desktop while the program is still loading. windowd reaped the client with a blocking waitpid inside its event loop, so killing a program stuck in its loader stalled input, compose and present until it died.

  • The native SXGUI smoke label is now distinct from the native GUI host. sxguihost no longer reports NATIVEGUI HOST when its runner exits.

  • CMake-generated UEFI images now include startup.nsh. The file selects fs0:\EFI\BOOT\BOOTX64.EFI for firmware and removable-media boot paths.

  • Linux gpu-soak now uses the legacy 96-iteration default. Explicit iteration counts derive a proportional timeout on both build frontends.

  • DoomGeneric verifies its vendored upstream tree before building. The deterministic digest in UPSTREAM rejects accidental source drift.

  • build.sh --jobs now applies to every CMake build target. Kernel, userland, ISO, smoke, run, debug, test and clean paths share the same limit.

  • Linux QEMU launches honor SAVANXP_QEMU and custom OVMF pairs. Normal runs and smoke scenarios use the same explicit toolchain overrides.

  • Native Haxe --force now clears stale generated artifacts. The normal path preserves reusable output directories instead of silently rebuilding from an identical clean state.

  • BusyBox is built with stack canaries enabled. The port boots and runs its applets with the same runtime protection as other external applications.

  • SxFS directory reconstruction now uses a bounded 32-level depth. The visited-cycle check remains, while recursive frames stay within the kernel stack budget used by the current 32 KiB stacks.

  • SxFS now rejects data extents claimed by more than one inode. Metadata validation tracks a per-volume claim bitmap across the complete inode table.

  • SMEP and kernel write protection are enabled on supported processors. Every application processor establishes the same execution boundary; SMAP remains a separate follow-up.

  • Initramfs archives that exceed the VFS node limit now fail closed. The parser no longer silently discards entries and mounts a partial root filesystem.

  • RTL8139 accepts valid maximum-size frames and recovers from bad records. RX validation now accounts for the CRC header and wrap tail, and resynchronizes with the hardware pointer instead of repeatedly parsing one malformed frame.

  • NX is now enabled explicitly on every application processor. APs no longer depend on the per-core EFER.NXE state inherited from the bootloader.

  • The keyboard-layout popup can change the active layout again. It opens /dev/input0 for the write access required by the setter and no longer persists a selection that the kernel rejected.

  • Kernel stack-canary failures report the actual return address. The panic diagnostic no longer assumes a compiler-specific stack-frame offset.

  • User memory is no longer uniformly executable. NX is now enforced for data, stacks and shared sections; ELF segments must be valid, non-RWX and remain in the user half. Stack canaries cover kernel, in-tree, BusyBox and supported external SDK builds.

  • Malformed storage and boot images are rejected before use. SxFS extents, inode metadata and directory graphs, CPIO names/hex/trailers, GPU geometry and device-reported packet lengths are now bounded before they reach kernel memory.

  • Mutating device ioctls require a writable descriptor. Init and idle tasks cannot be killed, and reparented descendants no longer leave permanent zombies.

  • Restoring a maximized window no longer leaves residue on the wallpaper. Only the restored frame was repainted, so the rest of the maximized area kept stale pixels until something passed over it. windowd-smoke now checks it.

  • fork hands the child the floating-point registers too. It used to start with the clean FPU/SSE state the kernel seeds, so a double live across the call read as 0 in the child. forktest now checks it.

  • A command typed in the desktop Shell keeps its arguments. The parser tokenizes the line in place, and the window then handed that same line to /bin/sh -c: only the first word survived, so echo hola printed nothing.

  • netinfo over virtio-net no longer kills the machine on VirtualBox. A 64-bit virtio MMIO field now goes in two 32-bit halves: a single 64-bit access is what VirtualBox answers with a guru meditation. The rule.

  • QEMU starts from a path with a space in it. ./build.sh run, smoke targets, and tools/shoot.sh now pass OVMF, disk, and log paths as quoted arguments, so QEMU cannot interpret half a path as a second drive.

  • The kernel/userland build handles host paths with spaces. CMake and Ninja receive quoted include and output paths, so a directory containing whitespace is passed as one argument instead of being split.

  • Closing a window that was playing sound no longer mutes the machine. windowd kills the client, so the close handler of /dev/audio0 ran in the killer's context and the device stayed owned by a dead pid: every later program got EBUSY. It is released by pid on exit, like the GPU session.

  • The wall clock follows the ACPI PM timer on machines that expose a usable one. The TSC measures the host's time, not the virtual machine's: with VirtualBox under load it counted 561% of real time, and everything paced on it ran ahead — Doom offered three times the audio the device could take and the driver dropped the rest, which is what a chopped-up sound is. Needs a 32-bit PM timer; QEMU's is 24-bit and stays on the TSC. Why.

  • The kernel's wall clock is a free-running counter, not a count of timer interrupts. uptime_ms and every deadline — sleep_ms, poll timeouts, the TCP RTO — used to advance only when an interrupt was delivered, and on VirtualBox that delivery turns bursty once the machine idles: measured between 151 Hz and 4000 Hz with the timer set to 1000. Anything paced on it stalled and then fast-forwarded. How it was found.

  • A process waiting in poll() wakes on the event, not only on the next tick. Blocking poll() left the compositor noticing a client's frame up to a tick late, and that latency landed on every frame drawn.

  • ./build.sh build no longer deletes build/disk.img when it cannot read it. A locked image — a VM running on it — raised the same error as a corrupt one and fell into the branch that recreates it. It now reports and stops.

  • poll() blocks instead of spinning, and an idle machine halts. The syscall parks the caller (WaitReason::poll) and the tick wakes it, so a process that only waits no longer books every tick of the wait: the idle desktop went from windowd 99% to idle 99%, CPU Usage: 0%, with average compose time ~4x lower. yield now halts for the idle process. What it found and how.

  • Notepad's editor ignored the mouse wheel and its own scrollbar. The wheel dispatcher and the embedded-scrollbar hit test only knew about SXGUI_LISTBOX/SXGUI_TEXTVIEW, not the editable SXGUI_TEXTEDIT; a click on the bar moved the caret instead of scrolling. tools/shoot.sh --scenario notepadwheel covers it.

  • tools/shoot.sh guarded the wrong path for a planted automation spec. It checked build/image/SMOKE while ./build.sh plants build/rootfs/SMOKE, so after any smoke target the guest silently ran that harness, not the desktop.

  • A blocking pipe read did not get the CPU when its data arrived. Only event waits asked for the preemptive wakeup, so a synchronous RPC over pipes waited for the round-robin. Present drops ~470x, and 1.4 to 5.8 fps.

  • The taskbar drew the generic icon for every window. The shell window list only carried a baked-in icon id, and that set is now just the generic one, so each button gets the window's own 16x16 .sxicon instead.

  • The ELF loader failed on two PT_LOADs sharing a page. map_segment_pages treated an already-mapped page as an error and the load died reporting "out of memory". The shared page now keeps the union of the permissions.

  • argc could exceed the arguments that existed. The kernel copied at most 15 arguments but handed the process the original argc, so a longer argv made the program read pointers that were never written.

  • time() returned the uptime, not the Unix epoch. It computed uptime_ms() / 1000 with the RTC right there, so any derived date came out as

  • strtoul and friends wrapped silently on overflow. A number larger than ULONG_MAX returned garbage; it now saturates and sets ERANGE.

  • waitpid() was infinite recursion. The variadic macro in <sys/wait.h> expanded back into itself. Nothing in the tree reached it, but any port calling POSIX waitpid ate the 128 KiB stack with no net.

  • A read-only SxFS volume became writable again once published. sxfs::attach() set the status to mounted without looking at how the mount had ended, so a volume whose journal could not be recovered accepted writes. New ./build.sh smoke sxfs-smoke mounts a broken volume from a host test.

  • Two windowd bugs uncovered by the keyboard selector popup. The click-down landed on the overlay window path, and the popup never entered the composed/retire signalling lists, so its second gfx_present() hung forever.

  • The SXE resource generator blurred an icon when enlarging it. collect_icons_from_file() only detected the integer multiple when shrinking, so a 16x16 original derived its 32x32 with LANCZOS instead of NEAREST.

  • The Type column in files was clipped when the scrollbar appeared. The width split subtracted a fixed 6 pixels instead of the two bevels plus the bar's 16. The bar is now always reserved, so columns no longer jump width.

  • Row text did not line up with its column label. The header cell's bevel eats two pixels the row was not subtracting.

  • In widgetsdemo the free-standing scrollbar overlapped the second column. The column origin was computed from the list width without counting the bar between the two.

  • Pressing Ctrl dropped the selection, so Ctrl+C copied nothing. A modifier key produces its own event with ascii at zero, and the text widgets treat "any other key" as a reason to drop the selection. Modifiers no longer reach the widgets.

Version 0.4.02026-10-08225 changes