Skip to content

LordOS 0.8.6-beta

Pre-release
Pre-release

Choose a tag to compare

@lordbasex lordbasex released this 30 Oct 12:32
· 436 commits to main since this release

Added

  • LordBench, first part (docs/BENCHMARK.md, "Built so far"): System Information has a Performance tab (on lord.sysinfo.bench) with a Quick test (less than a minute) and a Full test (adds storage and three minutes of sustained load). The work runs in lord-bench, a new sandboxed helper launched by lord-server like lord-doc and lord-media (its own folder only, no arguments, sandbox_apply_helper before any work, scope lordos-bench.scope, 2 GB cap), which gets only the GPU's render node and an unnamed scratch file opened by lord-server and answers only with fixed-size records lord-server checks.

    • Workloads, each the median of three runs with its spread, on one core and on all:

      • SHA-256 and a 256×256 matrix product;
      • the package engine's xz decoder on a fixed 2 MB text generated at build time;
      • QuickJS running a fixed script;
      • LordOS's own interface work (bench/lbui.c, the engine's real code linked into lord-bench): layout and paint of a fixed 1920×1080 scene built in C (529 nodes: a translucent top bar, a file manager with a 40-row list, a settings window with a wrapped paragraph and controls, a notification and the Dock with backdrop blur, shadows, a rotated badge, a half-transparent card), every node invalidated and the whole screen painted each frame, in frames per second (cpu.ui.1; cpu.ui.n is the same with the paint's row bands on up to 8 threads, as lord-server's paint threads); and shaping fixed paragraphs in Latin, Arabic with embedded left-to-right text, Devanagari, Thai and Chinese with kana (bench/lbtext.h) as the engine's text layout does (bidi, script runs, each character's font, GSUB and GPOS, the visual order), in runs per second, one core and all (cpu.shape.1/n). The helper opens no files: the scene is fixed data and the fonts are built into the binary (build/gen/lb_fonts.h): LordOS's Inter and Debian 13's Noto Sans Arabic, Devanagari, Thai and CJK SC subset to exactly those paragraphs with every layout feature kept (tools/gen-lbfonts.py, committed in server/src/bench/fonts with their OFL licence; Inter pinned by its SHA-256 in mk/lordbench.mk, so a new Inter fails the build until the references are measured again); build/shape-test gives the same glyph positions with each subset as with the full system font, though the subsets shape about 12 % faster than the full fonts (fewer lookups), so the workload is comparable only with itself. These four numbers measure LordOS's engine with the machine: a reference value of one carries the LordOS version it was measured with and gives a "×N" only to a result of that version (ENGINE_IDS in apps/sysinfo/perf.js). The paint threads of cpu.ui.n start each run from a clean pool, and the shaping threads only read the shared script table (set before they start; ThreadSanitizer had found a race there). lord-bench grows from 1.48 to 2.42 MB of code; its sandbox, seccomp list and fds are unchanged;
      • memory copy and triad bandwidth and a 64 MB pointer-chase latency;
      • OpenGL ES 2 offscreen at 1920×1080 on the render node: the 3D scene, fill rate, texture upload and 8 layers;
      • (Full only) sequential write and read and random 4 KiB reads on up to 1 GB;
      • (Full only) a sustained all-core load sampled every 10 s with the temperature.
    • Guards, read by lord-server:

      • refused on battery below 30 %;
      • waits for a quiet CPU, at most 30 s; otherwise the run is marked busy;
      • stops within 2 °C of the passive trip point (10 °C of the critical one without one);
      • the peak tests also stop when the x86 throttle counters grow or the Raspberry Pi firmware reports throttling;
      • in the sustained test, throttling is recorded with its time and temperature instead.
    • Helper-sandbox self-test (sec.helper-sandbox, repeated inside lord-bench): right after sandbox_apply_helper and before any work, lord-bench tries six things that must fail, the Sandbox Probe's helper tries plus two: a unix socket, an internet socket, exec, fork, a new file (O_TMPFILE in /tmp) and reading /etc/passwd. None could change anything if it worked (exec is called with an invalid environment pointer, so an allowed one fails with EFAULT before anything is replaced; a socket or unnamed file is closed at once; a forked child only exits). Each try is a fixed record (LB_MSG_SANDBOX, checked by lb_msg_valid), the result lists them as sandboxTries (id, refused, errno), the Stability verdict fails a run where one got through or is missing (naming each try in the user's language, "run a program", …) and otherwise says that all six were refused; on a kernel without Landlock the two file tries only warn, since the sandbox mask already records that limit. lordos-sysreport bench checks the same six by name ("N of 6 tries refused", a NOT REFUSED line for each one that worked or is missing) and exits with 3 when one got through. On VM 116 all six were refused (EPERM from seccomp for the sockets, exec and fork; EACCES from Landlock for the files), and a trace of a run shows the CPU workloads open no file.

    • Results: shown as "×N" of each reference machine per category, never one score, with three verdicts:

      • performance;
      • compatibility, from the Compatibility tab;
      • stability: stops, failed measurements, throttling, under-voltage, a sustained drop, a busy start and the helper-sandbox self-test.

      Each result also shows the change against the previous run and the conditions that differ, every recorded condition, the boot time per LordOS version and Save to Documents. Nothing is sent anywhere.

    • Recorded conditions: temperatures and trip points, power, governor, platform profile, GPU kind, disk kind, composition and display modes. No names or identities.

    • Video: a lord-media job (MK_VBENCH, media/helper/lm_vbench.c, launched by bench/lbvideo.c with no fds) generates its own test clips in memory from a fixed synthetic picture (H.264, HEVC, VP9 and AV1 at 1080p; H.264 and HEVC at 4K) and decodes each in software the way the player does, as frames per second; then the remote desktop's own encoders (lm_stream_sw_encoder: H.264 and VP8) encode 1080p frames. Quick measures H.264 1080p and the H.264 encoder. lord-media answers only MM_BENCH records, checked by lbv_record_valid. Hardware decoding is not measured (neither test machine has it).

    • Desktop impact (its own button and result file, lordbench-desktop.json): System Information opens a scene window and lord-server moves it along a figure of eight through the drag's own path (lord.wm.script, grab_script_begin in ui/grab.c, no JS per frame; wm.scriptMove/wm.stopScript in the SDK). lord-server records every frame it builds and every frame the display shows (bench/lbframes.c): 10 s alone, then 10 s while lord-bench loads every core at the lowest priority (LB_MODE_LOAD, nice 19, idle I/O). Each phase gives frames per second, the time to build a frame (median, 99th percentile) and the share of late frames (intervals over 1.5 refresh periods); the verdict compares the 99th percentile with the refresh period in use and warns above 5 % late (problem above 25 %). Frame times never come from a helper: lb_msg_valid refuses ids past LB_HELPER_LAST.

    • Boot time test (Performance → Boot time, administrators): 3, 5 or 10 boots in a row. The closed admin action boot-test <2..10> writes /var/lib/lordos/boot-test in a fixed format (bench/boottest.c) and boot-test-cancel (anyone) removes it; the session restarts as usual. At each boot's first frame lord-sessiond records the boot with its number in the test and, while boots are left, restarts 30 s later. The login screen shows the boot and the countdown with Stop the test, and a login stops it too. The count only goes down, a state older than 30 minutes is discarded, and the action is refused in the live session. The Boot time group shows each version's latest test (median and range). Checked by unit tests and by 3 boots in a row on the Pi 5 (2026-10-30; autologin, started from the state file, so the app's button and the login screen's countdown were not exercised).

    • References: VM 116 (8 vCPU Xeon Silver 4108, llvmpipe) and a Raspberry Pi 5 16 GB (SD card, V3D), each a real Full run (apps/sysinfo/references.js):

      • VM 116: SHA-256 166/984 MB/s (one core/all), matrix product 3.3/19.9 GFLOP/s, memory copy 20.0 GB/s; software decoding H.264 1080p 399 fps, AV1 1080p 201 fps, HEVC 4K 102 fps; H.264 encoder 106 fps. The interface workloads, from a later Full run (2026-10-28): layout and paint 37.0 fps on one core and 39.9 fps with the paint threads (a frame is about 28 ms, almost all of it paint: the layout of the 529 nodes takes 0.15 ms, and only big row-local fills, shadows and blits are split into bands); shaping 619 runs/s on one core and 3,595 on eight. A Quick run from a terminal now takes 46 s.
      • Pi 5: SHA-256 933/3,693 MB/s, matrix product 2.26/5.99 GFLOP/s, memory copy 7.6 GB/s, latency 115 ns; V3D scene 795 fps, fill 2,455 Mpixel/s; SD card 66 MB/s write, 91 MB/s read; 3 minutes sustained at 5.86 → 5.88 GFLOP/s with the fan, 55.6 °C at most; software decoding H.264 1080p 258 fps, AV1 1080p 159 fps, H.264 4K 55 fps, HEVC 4K 57 fps; H.264 encoder 75 fps. Quick takes 34 s, Full 339 s. The interface workloads, from a later Full run (2026-10-30, from a terminal over SSH without the monitor, the arm64 build of main with its helpers in a temporary folder, the LordOS kernel 6.18.57-lordos.3, ondemand, 180 s sustained, 37.0 → 49.6 °C and 59.0 °C at most, under-voltage alarm 0, all six sandbox tries refused, 358 s): layout and paint 39.22 fps on one core and 45.23 fps with the paint threads; shaping 628.3 runs/s on one core and 2,514.0 on four, with "lordos": "0.8.5-beta"; its notMeasured list is gone. A second Full run without the sustained part (178 s) gave 39.37, 45.28, 628.8 and 2,513.4 (within 0.4 %), and the two Quick runs of 2026-10-29 were within 0.6 %. The rest of that run was within 3.2 % of the 2026-10-27 reference except the matrix product on four cores and the sustained load (+12 %: 6.72 and 6.55 GFLOP/s, same workload code, cause not looked for), random 4K reads (−10 %), xz (−4 %) and the GPU upload (−4 to −5 %); the reference keeps its 2026-10-27 values for those (docs/BENCHMARK.md).
    • Desktop impact measured on the Pi 5 with a 1920×1080 60 Hz monitor and GPU composition: alone 59.4 fps, frame build 10.9 ms median and 13.0 ms p99, 1.0 % late; under the low-priority all-core load 24.0 fps, 20.1/29.3 ms, 96.5 % late. A stability problem found by LordBench; its cause and the fix are under Fixed ("The desktop under load").

    • Pointer to screen (desktop test): a synthetic pointer in lord-server (bench/lbinput.c) plays a 125 Hz mouse that moves the scene's window: each event is stamped when it is made and announced on an eventfd the main loop polls like an input device, the main loop applies the newest position once per frame as it does with a real mouse, and each event's latency ends when the first frame showing it is presented (bench/lbframes.c). Median and 99th percentile alone and under load (comp.in50/in99, desk.in50/in99); the verdict warns above three refresh periods. It measures lord-server's part, not the mouse's; the positions only move LordBench's own window. VM 116 without a monitor: 14.6/25.2 ms alone, 15.1/30.9 ms under load. Pi 5 without a monitor: about 16/27 ms alone, 22/52 ms under load.

    • lordos-sysreport bench [--quick | --full] [--json]: LordBench from a terminal, over SSH or in CI, through lord-server's own code (bench/lbrun.c): the same guards, helpers and result file, which System Information then shows. lordos-sysreport is now installed in /usr/lib/lordos/bin with /usr/bin/lordos-sysreport and lordos-fetch as links (tools/iso/mkdeb.sh), because the helpers are only started from next to their own binary. A quick run took 39 s on VM 116 (46 s with the interface workloads). On VM 116 (headless, with a remote viewer) 59.6 fps alone, p99 13.3 ms; p99 22.9 ms and 12.5 % late under load.

    • Boot measured: the Pi 5 reaches the login screen on its monitor 5.99 s after the kernel starts (systemd: 2.1 s kernel + 4.2 s userspace).

    • Pi limitation: the LordOS Pi kernel exposes neither the firmware's get_throttled nor a cpufreq cooling device, so the firmware's own flags can't be seen there (the under-voltage alarm is read); the sustained curve and the temperature are the evidence instead, and the firmware's documented 80 °C limit now counts as throttling (next item).

    • The Raspberry Pi firmware's throttling during a run (bench/lbcond.c, bench/lbrun.c; docs/BENCHMARK.md, "The Raspberry Pi firmware's throttling"; tested on fake sysfs trees and VM 116, and on the Pi 5 on 2026-10-29: piFirmware {"flags": false, "voltageAlarm": true, "underVoltage": false, "tempLimit": false, "tempLimitC": 80}, the alarm 0 before and after the run with the official supply). Checked in Raspberry Pi's rpi-6.18.y source, the kernel LordOS ships: drivers/firmware/raspberrypi.c has no get_throttled file any more, and the rpi_volt hwmon (drivers/hwmon/raspberrypi-hwmon.c, a module in bcm2712_defconfig) asks the firmware every 2 s with the sticky bits cleared and exposes only in0_lcrit_alarm (0444), "under-voltage in the last 2 s"; the capped, throttled and soft-limit bits are not exposed without the mailbox, which LordBench never opens. So lord-server now: reads the alarm strictly (0 or 1); still reads get_throttled where an older kernel has it, strictly (1-8 hex digits, optional 0x, rpifw_parse_throttled, masked to bits 0-3 "now" and 16-19 "has occurred"; anything else is unknown instead of being half-parsed); knows a Pi from that hwmon, that file or the device tree's raspberrypi, compatible; and on a Pi counts the hottest zone at or above 80 °C as the firmware's limit (Raspberry Pi documents progressive Arm throttling from 80 °C; the Pi 5's thermal zone has no passive trip, so before this the guard waited for 100 °C). Every guard sample of the run (once a second) is accumulated (lb_rpi_watch): "now" bits and the 80 °C limit always count, while a sticky bit or alarm already set at the start counts only after it was seen clear and set again, so an event from before the run is never blamed on it. Before, the alarm and the bits were only looked at in the start and end snapshots, so an under-voltage in the middle of a run was lost. In a peak test, capped, throttled, soft limit or the 80 °C limit stop the run as throttled (like the x86 counters); in the sustained test the first one is recorded as throttleAtS/throttleTempC; under-voltage is never a stop and is recorded on its own. A result on a Pi carries piFirmware (flags, voltageAlarm, underVoltage, capped, throttled, softLimit, tempLimit, tempLimitC; what could not be read is left out, not false), and before/after add rpiUnderVoltageAlarm. System Information → Performance: the Stability verdict uses piFirmware for "The board reports throttling", a new "It reached 80 °C, where the board's firmware starts slowing the processor down" and "The board reports low supply voltage" (older results keep the end's bits), and Conditions has a new Board firmware row (en, es, pt). Machines that are not a Pi are unchanged.

    • Tests: tests/lordbench_test.c (records, workloads, fake sysfs trees of an x86 laptop and a Pi 5, boot history, the frame recorder and its pointer latency, the synthetic pointer's thread and eventfd, the video records, the boot test state file, the interface scene with every style key known, each paragraph shaped with no missing glyph and the same twice, the sandbox self-test's records, the Pi firmware's flags and alarm on a Pi 5 with LordOS's kernel, a Pi known from its device tree and an older kernel with get_throttled, and the strict get_throttled parser; 1,198 checks) and tests/sysinfo_perf_test.mjs (62 checks, with the desktop verdict, the pointer latency notes, the video category, the self-test in the stability verdict with and without Landlock, the engine workloads compared only within one LordOS version, and piFirmware in the stability verdict).

    • Not built yet: hardware video decoding and the x86 PC reference.

  • Flatpak and Flathub design review, kept for the future (docs/FLATPAK.md, no code; the maintainer decided on 2026-10-27 not to implement Flatpak for now, to keep the system stable, and it is not planned for any release): a launcher-owned scope per Flatpak app so the firewall and restrictions see it as itself, flatpak run arguments built in C, per-user installs through a closed set of commands in their own network scope with Flathub's key pinned, and the exceptions to AGENTS.md the maintainer has to approve before any code.

  • A pixel-exact check for the dragged window's frames (the groundwork for making a drag frame cheaper on the Raspberry Pi; docs/WINDOWS.md, "Checking it"). Tests and debugging only: no frame changes.

    • test_drag_exact in build/engine-test (make -C server test): LordBench's desktop scene (a 760×480 rounded window with a shadow, a turning 72 px square and six cards with shadows and text, a rounded window below, a menu bar and a Dock with backdrop blur, on a 1024×640 display) dragged along LordBench's figure of eight (lbi_path) for 480 frames, painted as main.c paints a frame, plus four 60-frame runs for what makes the layer paint again (a text that changes a child's size, a child moving inside the window, a terminal's blinking cursor, a square turning across the window's edge). Each of the 725 frames is hashed (FNV-1a over the pixels) on the CPU path and on the GPU path's canvas (gpu_active() on), and compared with tests/drag_golden_cpu.txt and tests/drag_golden_gpu.txt, written from the current code (x86_64; make drag-golden writes another architecture's): any change to a frame fails. The test also prints how many pixels differ from a full repaint: when it was added, at most 2,265 per frame on the CPU path, with black pixels at the dragged window's rounded top corners, and 70,940 on the GPU path, one level darker in the window's shadow (248 and 70,864 after the two corner changes below). About 11 s on VM 116.
    • Three more runs (the test's cases a reviewer found missing): neg, the window starting at (−200, −120) and moving 6 px right and 4 px down a frame, so its left edge crosses x = 0 and its top y = 0 while the square turns; remove, a badge inside the window removed (node_detach) at frame 35, after frames that only copy the layer (the square turns every 10th frame), then the square turning again at frame 40; and k2, the whole drag on a 2048×1280 display at 200 %. These three add a column of boxes with odd widths, shadows and text centered at half pixels. Their 183 hashes per path were written by make drag-golden on 001633b's code (the four functions the test calls that 001633b lacks, node_restyled, paint_root_clear, paint_root_full and paint_layer_parts, added beside it unchanged or as a no-op count; the first five runs' 725 hashes came out identical to the committed ones, which checks that), and the current code passes all 908 on both paths. Each run catches a change the first five miss (checked by breaking the code on purpose): partial repaints across x = 0 (neg, 42 of 61 frames), a node's part trusted after outside damage mapped through a stale layer.at (remove, 20 frames from frame 40) and a part's box computed without the display scale (k2, 59 frames). The whole of engine-test takes about 15 s on VM 116 (11 s before).
    • LORD_PAINT_VERIFY=N (lord-server's environment): every N-th frame of each display is compared with a full repaint of the tree into a scratch canvas (paint_root_full: no drag layer, no copy of what is below it, no skipped node; the GPU path's list of app windows is kept), and the count of differing pixels goes to stderr. Off by default.
    • The drag line in session.log now says how many of the layer's renders were only a part of it (paint_layer_parts). lord.setStyle's decision of what to place or repaint moved to node_restyled (ui/node.c), unchanged, so the test drives the same C path.
  • LordBench: where a frame's time goes, measured per frame (docs/BENCHMARK.md, "Frame phases"). On the Pi 5 with its monitor a loaded frame is built in about 13 ms and still the monitor shows 30 frames a second, 98 % of them late: the time outside the "build" number (JS before the frame, the GPU's own work, the wait for the vblank) was not measured. lord-server now records, for every frame the main display shows, while LordBench records or a scripted move runs (bench/lbframes.c, filled only by main.c and gpu.c; nothing comes from a helper, and lb_msg_valid is unchanged since no new result id exists):

    • the flip event to lord-server running (wake), that wake to render() (input, LordBench's scripted step, JS), render() itself, its layout and paint, the UI texture's upload (time and pixels), GPU drag's texture uploads (time, pixels, whether one was whole), eglSwapBuffers, the framebuffer and page flip ioctl, the GPU's finish time after the flip was queued, the wait to the vblank, and wake to flip;
    • the GPU's finish time is a native fence taken after eglSwapBuffers (EGL_ANDROID_native_fence_sync) and read once at the frame's flip with SYNC_IOC_FILE_INFO (the sync file's signal time): one ioctl a frame, no wait, no polling, and nothing at all while not recording; without the extension it is "unknown"; a frame scanned out straight (a fullscreen app's buffer) has no upload, swap or GPU time of its own, so it records none and drops a fence left from before instead of reporting the last composed frame's;
    • each frame against the first vblank after it started: on time (shown within half a period of it), CPU-late (the flip was queued after that vblank), GPU-late (queued before it, the GPU finished after it) or other;
    • per frame whether the paint threads were on, whether GPU drag composed it or why not (each refusal and, for layer, its cause: invalid, rendering, holes, surfaces, premul, opacity, part and the rest, GDRAG_L_* in ui/ui.h), GPU drag's whole uploads and their pixels, and frames not shown (gpu_frame_ex's -2). LordBench's desktop result has them as frames.phases.alone and frames.phases.load (p50 and p99 in ms, and counts), and the session log gets lordbench: desktop alone|under load: phases, …; the drag (scripted): … line ends with the same text, and its GPU drag part now lists … catching up and the layer's causes. Without a monitor the frame's end is its presentation, on the pacing grid. The System Information page is unchanged (it does not show them). Checked on VM 116: lordbench-test (1,192 ok) feeds known frames and checks each class, percentile and count, the text and a short buffer; and a desktop run in an isolated headless session of the worktree's lord-server (a test theme moving one window with lord.wm.script, CPU composition, one viewer on its remote.sock) wrote both phase objects in three runs (alone 601 frames, all on time; under load 720–721, 715–721 on time) and the drag line. The GPU fields (uploads, swap, fence) were 0 there: that path was built, not run. The first phases on a GPU came from the Raspberry Pi 5 with its monitor (V3D, 1920×1080, 2026-10-29; p50/p99 in ms). With the frame pipeline and GPU drag off (the defaults now): alone renderMs 8.15/11.57 (paintMs 6.31/9.56, uploadMs 1.41/1.72, 0.971 Mpx a frame) and gpuMs 1.51/1.57; under load renderMs 13.25/18.92 (paint 8.55/13.11, upload 4.24/5.25) and gpuMs 14.49/19.00, and of 374 frames 39 on time, 14 CPU-late and 320 GPU-late. With the pipeline off and GPU drag on: under load render 13.20/18.25, GPU 14.19/18.25, 23 on time, 15 CPU-late, 327 GPU-late of
    1. With the build's defaults then (the pipeline while frames are heavy, GPU drag on): under load render 13.85/19.07, GPU 15.63/23.38, heldMs 18.27/31.60, 148 on time, 1 CPU-late, 285 GPU-late of 436. So under load the frame is late on the GPU's side: its time per frame goes from about 1.5 ms alone to 14–16 ms.
  • Automatic default apps (docs/SETTINGS.md, "Default Apps"): when you never chose an app for a file type, a file of that type still opens. Before, lord.apps.launchWith('', path) only read the [Default Applications] lines of the mimeapps.list files and, on a fresh account with none, nothing opened (Files opened an .html in the Text Editor, and anything that asks for "the default app" got nothing). Now apps_default_for() (server/src/apps.c) follows the freedesktop MIME Applications order: the user's own [Default Applications] (an explicit choice), then what the system marks as default (/etc/xdg/mimeapps.list, /usr/local/share/applications/mimeapps.list, /usr/share/applications/mimeapps.list, defaults.list), skipping entries that aren't installed, then [Added Associations], then the first installed app whose MimeType declares the type, in mimeinfo.cache order and then by desktop id (deterministic); [Removed Associations] are honoured, MIME subclasses and aliases are not followed. Only apps the launcher itself runs count: a .desktop in /usr/share/applications or ~/.local/share/applications that it lists (not Hidden, NoDisplay or Terminal, TryExec found, OnlyShowIn/NotShowIn) with a MimeType and an Exec that takes a file (%f %F %u %U) and names a program that exists. No path, argument or way to write .desktop files was added; the file still goes through launchWith's checks (a real path in the home or the user's mounts, one argument, no shell). lord.mime.resolve(mime) returns { id, explicit, source } and lord.mime.unset(mime) goes back to automatic (removes the user's line from [Default Applications] and [Added Associations] of ~/.config/mimeapps.list, every other line kept, as GLib's reset does). Settings → Default Apps shows a category you never chose as "Automatic ()" ("Automática (…)", "Automático (…)"): the system's default, else LordOS's own app for it, else the first installed app that declares the type; an explicit choice can go back with "Automatic". Files' default (defaultOpenerId) uses the same order, so on a fresh account an .html file opens in the installed browser. Tests: build/defaults-test (38 checks, in make -C server test) over a fake XDG tree: the user's choice wins (also lordos-<app>.desktop, left to the theme), the system's lists in order with uninstalled entries skipped, the first declaring app by id and by mimeinfo.cache (the user's first), Hidden/NoDisplay/Terminal/OnlyShowIn/a missing TryExec/an Exec without a file code or with a missing program skipped, ids outside the launcher's folders ignored, [Removed] and [Added Associations], going back to automatic, no app → nothing, and that launchWith('') really starts the chosen app with the file. Checked on VM 116 in an isolated headless session with a fresh home and no mimeapps.list (2026-10-29): Settings → Default Apps showed "Automatic (Chromium Web Browser)" for the web browser and "Automatic (Mail/Player/Photos/PDF Viewer/Text Editor)" for the rest; choosing Chromium wrote the four browser types, "Automatic" removed them again; double-clicking page.html in Files opened it in Chromium (the only installed app that declares text/html there).

Changed

  • The frame pipeline and GPU drag are off by default, after the final Raspberry Pi 5 run (2026-10-29; docs/BENCHMARK.md, "The drag frame's cost"; docs/GPU.md). The Pi 5 16 GB with its 1920×1080 60 Hz HDMI monitor, the LordOS kernel 6.18.57-lordos.3, an autologin session and the arm64 build of this branch; LordBench → Desktop impact, "under load" = all four cores busy at the lowest priority:

    Configuration Runs Alone fps Pointer to screen p50 alone Under load fps Late under load Pointer to screen p50 / p99 under load
    Pipeline while frames are heavy, GPU drag on (the build's defaults then) 2 60.0 42.1–42.5 ms 33.0–36.2 65.8–81.6 % 77.3–79.8 / 98.7–99.8 ms
    LORD_PIPELINE=0, GPU drag on 3 59.9 25.5–25.7 ms 30.8–31.6 87.3–94.3 % 48.8–49.0 / 66.1–79.4 ms
    Both off (the defaults now) 2 59.9 25.7–25.9 ms 30.5–31.1 89.5–96.2 % 48.5–49.0 / 66.3–80.6 ms
    • LORD_PIPELINE is now off unless set to 1 (while frames are heavy) or 2 (always): it gave 2–5 frames a second under load but 17 ms more pointer latency alone and 29 ms more under load, and its auto mode engaged even alone at 60 fps. The code stays; the session log says lord-server: frame pipeline off (LORD_PIPELINE: unset or 0 off, 1 while frames are heavy, 2 always).
    • GPU drag is now off unless LORD_GPU_DRAG=1, and its self-test runs only then: it is correct on V3D (its self-test passes, 11,373,321 pixels in 341 ms; LORD_GPU_VERIFY=1 compared 138 frames, all equal) but gave no measurable gain (30.5–31.1 fps under load off, 30.8–31.6 on; it composed about one frame in five). The log says gpu: GPU drag off (LORD_GPU_DRAG=1 turns it on): the dragged window is composed by the CPU.
    • With both off the frames are the ones measured in the last row; the goldens are unchanged. So on the Pi 5 an all-core load still takes the desktop from 60 to about 31 frames a second. What remains is the GPU's time per frame: 1.5 ms alone, 14–16 ms under load (frames.phases: 320 of 374 frames GPU-late), and the upload of about 1 Mpx a frame from 1.4 to about 4 ms, while vcgencmd every 2 s showed V3D at 0.86–1.15 GHz, the ARM cores at 1.8–2.4 GHz, 36.7–47.2 °C and get_throttled=0x0 throughout: no clock or thermal throttling. Most likely memory contention with the load; not proven (no memory-bandwidth counter was read). Next: fewer pixels uploaded and composed per frame (a buffer-age scissor, PBO uploads) and finding what slows V3D under CPU load (ROADMAP.md).
    • In the same run: the firmware's power alarm (System Information's and LordBench's reader, hw/rpifw.c) is present on the Pi 5's LordOS kernel and reads 0 with the official supply (piFirmware: voltageAlarm true, underVoltage false; in0_lcrit_alarm 0 before and after); the dragged window's rounded corners are clean on the monitor (no black squares, over a window and over the wallpaper; docs/WINDOWS.md); lordos-sysreport bench --quick (two runs): cpu.ui.1 39.3–39.4 fps, cpu.ui.n 45.4 fps, cpu.shape.1 630–632 runs/s, cpu.shape.n 2,522–2,527 runs/s (docs/BENCHMARK.md).
    • Checked on VM 116 with the new defaults: make -j8 with no warnings; make test: 23 passed, 0 failed, 13 skipped; engine-test: 6,907 checks, 0 failures, drag_golden_{cpu,gpu}.txt unchanged; fpipe-test: 61 checks (unset, 0, an empty or any other value off, 1 while frames are heavy, 2 always; the auto mode's tests now ask for 1); lordbench-test: 1,217 ok.
  • The desktop under load, third step: a cheaper frame while a window is dragged (docs/BENCHMARK.md, "The drag frame's cost"; docs/WINDOWS.md; docs/GPU.md). Steps 0–11 (the next two entries), GPU drag (the third) and the Fixed entries for the black corners, the rotated node's repaint and LordBench's turning square, measured together on the Raspberry Pi 5 with its 1920×1080 60 Hz monitor (GPU composition, the session with the weight and the real-time priority, LordBench → Desktop impact, 2026-10-28); the uploads in slices, the paint pool rule, the phase timing and the frame pipeline came after these runs:

    • before this work, with the square turning (three runs): alone 53.2–58.1 fps (build p99 15.7–16.5 ms, 3.1–12.8 % late), under load 23.6–25.1 fps (build p50 19.8–20.3 ms, 100 % late);
    • with it and LORD_GPU_DRAG=0 (two runs): alone 59.8–60.0 fps (build p99 11.3–11.6 ms, 0–0.3 % late), under load 30.1–30.3 fps (build p50 12.9 ms, 98.1–99.2 % late). Under load the build, the GPU's part and the flip still run one after another and miss the 16.7 ms period, so the monitor shows every other vblank;
    • with GPU drag on, the same build stalled (52 fps alone; its entry below);
    • the frame pipeline was built after these runs; the final run with everything in this list (2026-10-29) has its own entry at the top of Changed: with the frame pipeline and GPU drag off, the defaults now, alone 59.9 fps (build p50 8.03–8.13 ms, p99 11.42–11.57 ms, 0.17 % late, pointer to screen p50 25.7–25.9 ms) and under load 30.5–31.1 fps (build p50 13.15–13.25 ms, 89.5–96.2 % late, pointer to screen p50 48.5–49.0 ms, p99 66.3–80.6 ms).
    • Checked on VM 116 at the frame pipeline's commit: make -j8 and lord-server with GPU=0 (no EGL, GBM or GLES linked): no warnings; make test: 23 passed, 0 failed, 13 skipped; engine-test: 6,883 checks, 0 failures (the 2,417 golden frames per path, the gpu-drag path and the pipelined drag runs); fpipe-test: 39 checks; lordbench-test: 1,197 ok. The codename "G0" of the work became "GPU drag" everywhere (LORD_GPU_DRAG, gdrag_*, LordBench's gpuDrag keys).
  • Less work in a dragged window's frame, same pixels (the first steps of making a drag frame cheaper on the Raspberry Pi). Every frame of test_drag_exact keeps its golden hash on the CPU and GPU paths (VM 116, x86_64) through these steps. The golden hashes were written again twice, each time after a pixel-by-pixel comparison with the code before: for the approved corner change of step 9 (its own entry below) and for the black-corner fix (Fixed). A third time, on the GPU path only, when this work was rebased onto the partial-repaint fixes (Fixed, "Partial repaints that left stale areas"): their rule that a blurred surface is repainted with the margin its blur samples (blur_near) changed frame 0 of three runs (neg, mix, k2mix), where the first dragged frame's damage reaches the menu bar's or the blurred panel's margin, and nothing else. With that one rule reverted every one of the 2,417 hashes matched the old file; the CPU path's file is unchanged; those three frames differ from a full repaint by at most 2 levels (1,596, 8,778 and 40,180 pixels; 1,532, 8,400 and 38,099 before). What the steps save was measured on the Pi 5 only all together (the first entry of this section).

    • No clear under the copy of what is below the window: paint_root_clear (ui/paint.c) replaces canvas_clear + paint_root in main.c, app/appclient.c and engine-test, and skips the clear when the drag layer's copy of what is below (below_copy) is about to write every pixel of the same rectangle. That copy was itself cleared to the same black before it was painted, so the GPU path's app-window holes stay the same.
    • lord.stats() no longer walks every JavaScript object: jsKB comes from JS_GetMallocSize, a local QuickJS addition (THIRD_PARTY.md) that returns the same malloc_size JS_ComputeMemoryUsage reported (checked equal on a runtime with 100,000 objects, a 50,000-entry Map and after GC). About, Settings → General and System Information poll it while they are open.
    • Only the changed part of the dragged window's layer is painted again: a node inside the window that changes (a style, a text, a transform such as LordBench's turning square, a child moving inside) now repaints only its old and new boxes in the layer, with a 4 px margin (layer_tree_part in ui/paint.c), instead of the whole window and its shadow. The layer is still painted whole for the window's own style or size, a part outside the layer or bigger than half of it, and whenever the result can't be shown to be the same: the window moved by a fraction of a physical pixel, the layer's box would change, a fractional display scale, positions or sizes that aren't sixteenths of a pixel within ±4096 px, fractional font sizes, text drawn through textlayout.c or in a font whose em isn't a power of two of at most 2048 units, as Inter's is (text_drew_inexact), an unchanged transformed node, an app window or a video inside (layer_part_exact, layer_note_exact). A transformed node's image steps its bilinear weights from the first pixel of each row, so a part cutting it on the left would change them; canvas_affine_watch/canvas_affine_cut (canvas.c) report that and the layer is painted whole. In test_drag_exact's first five runs 366 of the 611 layer renders are now parts (before: 6, the terminal's), and 438 of 738 with the three runs added later. Every golden hash is unchanged on both paths. With neither the margin nor the watch, 160 frames per path fail; with the watch but no margin, they all pass.
    • GPU path: no app-hole walk for a window without app windows, and a faster sentinel fill (ui/paint.c). The layer's hole map now carries a flag, layer.has_holes, that is 0 only while the map stands for all zeros; then layer_holes only turns the sentinel back into 0 (it no longer writes zeros over zeros) and layer_paint skips the per-pixel walk that reopens app holes over the blit, which for a window with no app surface never wrote anything. The flag is recomputed from what a whole-map pass wrote, a part pass can only raise it, and a part that brings the first hole into a map that had none zeroes the rest of the map first, so the map is byte for byte what the old code would have read. layer_fill fills one row with the sentinel and copies it to the others. Checked: the golden hashes (GPU path), and a differential run of the frame against 001633b's paint.c with an app window inside the dragged window (half of it an alpha-0 hole), present from the start and removed at frame 60, or appended at frame 40, identical on both paths over 3,874 frame hashes; with the walk always skipped, 139 of them differ.
    • A setStyle that only turns or scales a node no longer lays that node out again (node_restyled in ui/node.c). When the only change is rotate, scaleX, scaleY or transformOriginX/Y (what runtime/style.js makes of a transform), the node already had its StyleExt, it isn't a text node and has no text node above it, and its subtree has no text or image (at most 256 nodes), the node is marked for repaint and placed again by its parent (node_moved) instead of node_invalidate: its own layout pass and the measure flags are kept. That gives the same layout by construction: layout never reads the transform, place() computes every node's matrix on each layout_root, the measure flag is read only by text nodes, and a subtree without text or images lays out from styles and sizes alone. Its ancestors are still laid out again, as before, so the saving is only that node's own pass (small: LordBench's square has one child). setStyle(n, 'transform', …) is still not a key lord.setStyle takes and still changes and marks nothing. Checked by test_transform_restyle (engine-test): two copies of a tree, one through node_restyled and one through node_invalidate, 120 frames of turns and scales on five nodes (a leaf, one with boxes inside, one with text inside, one under a text node, one sized by flex) mixed with a column width moving by 0.3 px, a font size and a root width moving by 0.2 px: every node's box, absolute position, matrices and text measure, and every pixel, are equal; the 'transform' key changes nothing. The golden hashes are unchanged (the drag's square now takes this path) and the frame against 001633b's f_set_style logic is identical in the differential run.
    • The JS timers no longer look at every pending timer on each pass of the main loop (ui/js_prelude.h, the polyfills moved out of ui/js.c unchanged otherwise). __lordTick keeps the earliest deadline (minAt, lowered by every setTimeout/setInterval/setImmediate) and returns at once when nothing is due; when something is, one pass collects the due timers without building an [id, timer] pair per timer and computes the next deadline. Due timers still run by deadline, then by id, each at most once per tick, a timer set during the tick waits for the next one, and one cleared before its turn doesn't run. Clearing the earliest timer marks minAt stale and it is computed again before it is used, so the wait the tick returns to the main loop is the same number, not only the same order. lord-server also makes the atoms of __lordTick, __lordWantsFrame and __lordFrame once per runtime (call_hot) instead of on every call; the function is still looked up each time, so the recovery screen's replacements are the ones run. Checked by build/timer-order-test (make -C server test): 400 seeded random scripts run on this prelude and on 001633b's, in QuickJS, with timeouts, intervals and immediates at equal, zero, negative, fractional, NaN and string delays, clears of pending, due, fired, unknown and string ids, timers set and cleared from inside callbacks, callbacks that throw, and ticks whose now is behind the clock lord.now() reads (a clock that moves on every read, so both must also read it the same number of times): 427,538 callbacks and 21.6 MB of trace (what runs, in which order, with which arguments, every value the tick returns as %.17g, and what is logged) are identical. Breaking the code on purpose fails it: firing by id alone, 400 of 400 scripts differ; not recomputing after the earliest timer is cleared, 397. A tick with 200 timers pending and none due: 158.75 µs before, 0.30 µs now (build/timer-order-test bench, VM 116, x86_64).
    • Fewer React renders and style conversions that change nothing (runtime/index.js, apps/sysinfo/performance.jsx). applyProps keeps, per node (a WeakMap, so the instance a ref sees is unchanged), a copy of the flattened style it last converted, as keys and values in their order, and only when every value is a primitive and no converted value is NaN. A render whose flattened style has the same keys in the same order with the same values (Object.is) skips normalize and applyStyle: normalize is pure (no direction, theme or state), so it would give an equal style and applyStyle would send no call. A style object changed in place is still seen (the copy is compared, not the object), a different key order is not skipped (later keys override earlier ones: margin then marginTop), and a NaN (a bad #zz color, a gradient without an angle) keeps the old behaviour of re-sending that property on every render. Object values (a transform list) are never skipped. LordBench's Performance view no longer re-renders on a 500 ms poll that brings the same state, mode, step and progress as the last one (a result or a start error always counts as new). Checked by tests/runtime_props_test.mjs (make -C server test; needs node and node_modules, so it is skipped on VM 116, which has no node): fuzzed trees of 24 nodes (views, texts, images, an Activity that hides and shows half of them, reorders and removals) over 8 seeds × 300 renders, with styles kept as the same object, copied, changed in place, reordered, partly undefined, as arrays, with NaN, −0, overrides and transforms. The bundle with the skip and the same bundle with it turned off send identical lord.* call sequences and end with identical styles (72,322 lines; 31,498 of 53,311 applyProps calls skipped; on the development machine, not VM 116). Breaking the skip on purpose fails it: without the NaN rule all 8 seeds differ; in scratch runs, comparing keys without their order 6 of 6 seeds and comparing the props' object instead of a copy 3 of 3. The Performance view rendered with a scripted run (waiting, three steps, a mode change, cancelled) gives the same native call log with 001633b's view and this one, with 146 instead of 356 applyProps calls. No C code changed; the golden hashes and engine-test pass as before.
    • What is below the dragged window is painted again only where it changed, and copied where the old rule painted everything (below_ready in ui/paint.c; step 11, after the corner change in the next entry). The copy of what is below the window was built whole on any change outside the window, and two changes within 8 frames stopped it for the rest of the drag, so a progress bar below or the menu bar's clock made every frame paint the whole damaged box again (LordBench's Performance view polls every 500 ms; each render commits twice). That rule is kept as a shadow, and on top of it:
      • damage_walk knows the paint order around the window (below_order: the children of each of its ancestors, stable by zIndex; anything unknown counts as before it), so a change painted after the window no longer touches the copy. A change painted before it, and any damage from outside the tree, mark their boxes stale (DamageList, built as up to four rectangles), and only those are painted again (below_update), each one grown first to the whole reach of a backdrop blur it touches (the blur reads twice its radius around its box and starts its grid at the clip) and to the whole image of a turned or scaled node (canvas_blit_affine steps its rows from the first painted pixel), with the blurs and images the last build met; after the build the same check runs with the ones it met now, and the copy is built whole if a part was not closed. The copy is built whole when the order around the window changes (a signature of those children), the display's view or size changes, or the stale part is more than half the screen.
      • A frame where the old rule painted everything uses the copy anyway (below_switch, paint_below_switch) when that gives the same pixels: the canvas was to be cleared to black; no blurred node before the window meets the frame's box; no turned or scaled node before it meets the box with its image starting left of it; no border strip or rounded-corner square of one of the window's ancestors meets it (those are painted after the window, so over the copy they would be painted twice); the last build reached the window; and the stale part costs less than the box.
      • The old rule decides when the copy is rebuilt, so the pixels are the same as before (step 9's corners aside): damage from outside the tree inside the window's boxes (a Linux app under the window's shadow) still keeps the old copy until the old rule rebuilds it, and is repainted then. A frame uses the copy only when the window is drawn and untransformed (no transform, every ancestor shown and visible), the copy's builds walk every node (LORD_DAMAGE=multi's cull is off for them), and a hidden window's copy is always built whole.
      • main.c's drag: … line ends with below: … built whole, … in part, … frames switched to it (not: … ancestor, … blur, … transform, … unreached, … cost), and LordBench's scripted move writes the layer's counters and the same text in a drag (scripted): … line when it ends, for the Pi's session log.
      • Checked on VM 116 (x86_64): test_drag_exact has nine more runs, 1,500 frames per path (a progress bar in the window below changing width every 3 frames and its title every 30, with an app window and a blinking terminal in it; the menu bar's text every 2 frames; a square turning on the desktop and one turned and still; a blurred panel whose clock changes every 40 frames; the window below raised above the dragged one and back; all of them at 100 % and at 200 %; both windows inside a rounded container with a border; a dot changing color on the still square), whose hashes were written from e24af69 plus the corner change, with the GPU path's list of app windows hashed with the pixels (the test's wl_paint now keeps that list as wl.c does and opens the holes); the earlier 908 hashes per path are unchanged. All 2,417 frames per path match. 591 frames per path use the copy where the old rule painted everything (refused: 30 for an ancestor, 358 for a blur, 153 for a transform), and each of them is painted the old way on a copy of the canvas too (paint_below_verify): 0 pixels differ. The copy is built 19 times whole and 206 times in part, where the old code built it whole 32 times; with the switch off (LORD_BELOW_SWITCH=0), 17 whole and 20 in part, and the same hashes. Breaking it on purpose fails the test (goldens and self-check): without the ancestor check, the transform check, the growth of a part, the paint order (the nodes before the window counted as after it) or the outside damage. Without the blur check nothing fails in these scenes (the blur would only read differently where the menu bar or the Dock lies in its reach outside the box), so it stays as the safe side. The differential harnesses against 001633b's six files (3,390, 3,874 and 5,084 frame hashes) are identical. make -C server test: 23 passed.
      • Not done: skipping the copy under the window's opaque middle (it would have to know, before the layer is painted this frame, that the layer stays opaque there and that no ancestor's rounded corner reads it).
  • The corners of rounded windows below a dragged window now match a full repaint (step 9, the one pixel change of the drag-frame work's steps, approved by the maintainer on 2026-10-28; ui/paint.c). A drag frame copies what is below the window and then walks the tree to the window, painting nothing before it; that walk still saved and restored the corners of every rounded overflow: hidden node on the way, and the restore blended each saved pixel over the same pixel, which leaves it one level darker where the corner's coverage is partial. Now paint_root stamps the ancestors of the node the frame starts from (skip_stamp, up to 64 levels; deeper, every node is walked as before), and a node before it that isn't one of them returns at once: no corner save or restore, no walk of its children. The ancestors keep theirs, since the window is clipped by them. Checked on VM 116 before the golden hashes were written again: every frame of test_drag_exact (1,816 on both paths) was compared pixel by pixel with the frame of the code before (whose hashes equal 001633b's): 178,634 pixels in 1,686 frames differ, all by exactly one level in one or more channels, 155,462 of them inside the corner squares of a rounded overflow: hidden node (n = ceil(r) + 1 px, the square canvas_corners_save keeps) and 23,172 inside the Dock's or the menu bar's backdrop blur, within the blur's reach of such a square. On the frames that are also compared with a full repaint, none of the 9,619 differing pixels moved away from it and 9,132 now equal it. The pixels that differ from a full repaint go down from 2,265 to 2,217 (CPU path) and from 70,940 to 70,882 (GPU path) in the long run. tests/drag_golden_cpu.txt and drag_golden_gpu.txt were then written again by make drag-golden, and the same hashes came out of 001633b's code with only this change (and the test's entry points) added. The differential harnesses against 001633b's paint.c (5,324 frame outputs in the largest) are still identical: their scenes have no rounded overflow: hidden node below the window.

  • The dragged window composited on the GPU, with the CPU's pixels ("GPU drag"; step 10, approved by the maintainer on 2026-10-28; ui/paint.c, gpu/gpu.c, main.c; docs/GPU.md, "The dragged window on the GPU"). On the GPU path a frame that uses the copy of what is below the window paints only the strips of its damaged box around the window's rectangle W; the GPU writes W itself, from two textures of its own (the layer and the copy of what is below), with one shader that does blit_row's integer arithmetic (k = 256, over_alpha) in 24-bit floats, where every value is an integer below 2^24 and every scaling a power of two, so the pixels are the CPU's bit for bit. The textures get only what changed since the GPU last had them (the layer's part, the copy's partial rebuilds), so the per-frame upload of the old and new window box (2.2–2.6 MB on the Pi) becomes the strips and the layer's part.

    • When: only after below_ready decided to use the copy (its decisions are unchanged), on the display that owns the layer, one damaged box, no rotation and no fullscreen app; the layer valid and painted as it is (the same opacity, no full render pending, no app window in it, no pixel with a colour above its alpha: layer.premul_bad, checked where layer_holes already walks it); W is the blit cut by the ancestors' clips and the box; no ancestor paints over W afterwards (below_ancestors_ok); no app window of layer 0 is drawn into W (wl_draws_meet); the paint order around the window is known and no node painted after it that paints anything meets the box (damage_walk notes them, grown by a backdrop blur's reach and a transform's edge). Anything that changes while the strips are painted (the layer rendered again elsewhere, an app window) paints W on the CPU in the same frame. Otherwise the frame is painted as before.
    • The GPU side is off unless it proves itself: a highp fragment shader with at least 24-bit floats (glGetShaderPrecisionFormat), then a self-test in gpu_init that draws every valid premultiplied (alpha, colour ≤ alpha) over every destination value, and every source alpha over every destination alpha, into a framebuffer (3,145,728 pixels in three 1024 × 1024 tiles), then a placement pass (below), and compares each pixel with canvas_blit on the CPU (NEON on the Pi). One differing pixel turns GPU drag off. Since the Pi 5 run of 2026-10-29 GPU drag is off by default and LORD_GPU_DRAG=1 turns it on (the first entry under Changed); the self-test runs only then. LORD_GPU_VERIFY=N paints W on the CPU too every N-th GPU drag frame and compares it with the framebuffer read back (gpu-verify: frame N W=wxh differ=K worst=D).
    • main.c keeps, per display, that the scene and the UI texture are stale in W (gdrag_on, gdragw): W joins the next frame's damage; a frame with no UI damage (the cursor, an app's new frame) draws the window again from the textures if they are still that display's, else paints W on the CPU; direct scanout, a failed frame or the GPU going away paint it again. A frame whose quad cannot be drawn (a failed upload, GPU drag turned off) is not shown at all (gpu_frame_ex returns -2): the display keeps the frame before and the next one paints W on the CPU. LORD_PAINT_VERIFY leaves W out on GPU drag frames (LORD_GPU_VERIFY checks it). The drag: … and drag (scripted): … lines end with GPU drag: N of M frames (Q quads), strips … Mpx, uploaded below … Mpx, layer … Mpx (not: … layer, … clip, … ancestor, … app, … after, … post).
    • Checked on VM 116 (x86_64): canvas-test compares GPU drag's shader written in C (canvas_gdrag_model) with blit_row_c and the SIMD row over all 8,421,376 valid (alpha, colour, destination) combinations, every alpha pair, and 200,000 random pixels: 0 differ (NEON not run: no ARM machine was used). engine-test has a third path, gpu-drag: the GPU path with GPU drag and a stub GPU that keeps the UI texture, the two textures (only their dirty rectangles) and main.c's bookkeeping, and writes W with canvas_gdrag_model; all 2,417 frames equal drag_golden_gpu.txt, 250 of the 1,856 frames that use the copy are composited this way (refused: 436 layer, 287 app, 883 after: the big scene's window is mostly near the menu bar or the Dock). Four runs without goldens are compared frame by frame with the GPU path: a 30-frame pause with no damage (54 of 61 GPU drag), two displays side by side (150 of 240), the window over an app window below (5 of 180), and the window against a rounded, bordered container's corner (15 of 90; 44 refused for the ancestor); the single-display ones as the GPU shows them (the app windows under the UI texture, W written over both). Taking out the after-node, app-window or ancestor check fails the test; the layer's own checks (app holes, invalid pixels) are not reached by these scenes. make test: 23 passed, 0 failed, 13 skipped; engine-test: 3,688 checks, 0 failures. With GPU=0 the build has no warnings.
    • On VM 116 at runtime (llvmpipe forced with LORD_GPU=1, an isolated session lent the card by tests/seat_cycle.c, a test scene moved by lord.wm.script): the self-test of that time (tiles only, in the first drag frame) passes (75–113 ms); with LORD_GPU_VERIFY=1, 1,481 of 1,483 frames were GPU drag and 0 pixels differed in any; across 4 GPU context re-creations during the move, 1,328 GPU drag frames, 0 differ; with LORD_PAINT_VERIFY=16, 0 pixels differ from a full repaint outside W in 92 checks. Layout and paint of a drag frame: 0.29–0.35 ms with GPU drag, 0.92 ms without (llvmpipe's own composition dominates the rest, so this says nothing yet about the Pi). tests/gpu_cycle.sh 10 cycles: 38 fds and 226.6–227.6 MB throughout. A real screenshot of GPU drag frames was taken (seat-cycle's snapshot). The Pi 5 is below.
    • With the real desktop GPU drag never composed a frame, fixed: the list of nodes painted after the window held 128 boxes and refused every frame once it was full, and LordOS's menu bar and Dock alone fill it (drag: … GPU drag: 0 of 250 frames … 248 after). The list now grows as needed (up to 65,536 boxes, then the frame is refused as before) and skips a box inside the one noted just before it (an icon inside its bar: it meets the damaged box only if the bar does), so the menu bar and the Dock are 2 boxes. The decision is the same for every frame that fit before. engine-test's pause run now has 300 more nodes painted after the window, each in its own box: with the old 128-box list it fails (GPU drag composed 0 of 61), with this one 54 of 61 frames, the same as the GPU path frame by frame.
    • Checked again on VM 116 with the default theme (an isolated 1280 × 800 session lent the card by seat-cycle, llvmpipe, driven over the remote desktop socket; real screenshots with QMP screendump): the self-test of that time (tiles only) passes (3,145,728 pixels, 127–146 ms). Calculator dragged sideways in the band the menu bar and the Dock leave free: 152 of 153 frames GPU drag, and along a figure of eight that reaches them, 7 of 153; LORD_GPU_VERIFY=1 compared 160 GPU drag frames with the CPU (W up to 462 × 592), 0 pixels differ in any. LordBench's Desktop impact gets 1 GPU drag frame of 2,024 at 1280 × 800: its window, with its shadow, always reaches the Dock's or the menu bar's blurred backdrop (1,377 refused for that) and its turning square makes the layer be painted whole (646); result with GPU drag on: 72.3 fps alone (p50 12.0 ms, p99 16.7 ms), 63.8 fps under load (p50 14.4, p99 27.8 ms, 10.8 % late); LORD_GPU_DRAG=0: 73.9 fps, 12.0 / 16.7 ms alone, 62.0 fps, 14.8 / 26.3 ms, 15.6 % late under load (the same within llvmpipe's noise). LORD_GPU=0 (CPU composition): the same drags paint as before (no GPU drag tried) and LordBench gives 75.0 fps alone and under load (p50 5.1 / 5.7 ms). After the runs the VM's normal session (lordos.service) was started again.
    • Fixed after an adversarial review of step 10 (on VM 116; the runtime runs above came before these fixes and were not repeated, the VM's own session was left alone):
      • Wrong pixels under a rounded clipping box painted after the window. A view with overflow: hidden and rounded corners that paints nothing itself still saves and restores its corners around its children, which darkens what is below them by about 1 level; node_paints did not count it, so GPU drag left it out inside W. It now counts (ui/paint.c). New engine-test run corner (such a 200 × 150 box, radius 60, over the window's path), compared frame by frame with the GPU path: without the fix 59 of its 91 frames differ; with it 0 differ (its box is in every frame's damage: of 90 frames that use the copy, 89 are refused as after, 1 for the layer).
      • The pixel position no longer comes from gl_FragCoord, which GLSL ES 1.00 makes mediump: with 16-bit floats it cannot tell x or y from the next pixel past 1024. GPU drag has its own vertex shader that passes a highp varying in pixels (v_px, the quad's corners on pixel edges) and the shader uses floor(v_px). The self-test gained a placement pass: a framebuffer as large as the largest output and at least 4096 × 2048 (else the largest the GPU allows, and the log says so), a copy of what is below that size and a layer at an odd offset (37, 21) reaching its far edges, every pixel a different random value, compared with the CPU. A layer or output past what it covered turns GPU drag off (gpu_gdrag_ready(out) refuses such an output up front). Checked headless on VM 116 (a test driver that includes gpu.c, a surfaceless GLES2 context on llvmpipe): the full self-test passes (11,373,321 pixels at 1920 × 1080, 17,678,025 for a 5120 × 2880 output); with the same shader rounding v_px as 16-bit floats would, the tiles pass and the placement pass fails from x = 1024 on (6,207,545 pixels differ), so the old test alone would have passed it.
      • A failed GPU drag upload no longer shows a frame with a stale W: when the quad cannot be drawn gpu_frame_ex returns -2 before presenting, and main.c joins W to the damage and paints it on the CPU in the next frame without trying GPU drag (gdragcpu), keeping the GPU on. Headless, with a layer larger than the self-test covered: -2, nothing presented, GPU drag off; again with GPU drag off: -2. A real failed upload was not provoked, and main.c's side has only been built, not run.
      • layer.premul_bad on a layer rendered on the CPU path (no hole map) was never computed: it is now set, so GPU drag never trusts such a layer after the GPU comes back.
      • LORD_GPU_VERIFY's CPU reference walk paints on a scratch canvas of the scene's size (an ancestor's rounded corner above or left of W read before its old W-sized buffer).
      • The self-test runs in gpu_init, not in the first drag frame, and its outcome is kept per GL renderer and version across contexts (a GPU restart does not run it again): 311–406 ms on llvmpipe the first time (it was 75–146 ms without the placement pass), about 1 ms after. Stale GL errors are drained before it, dithering is off for the quad, the error drain before an upload is bounded, and GPU drag's program and textures are freed when it does not turn on.
      • make -j8 (GPU=1) and lord-server with GPU=0: no warnings; make test: 23 passed, 0 failed, 13 skipped; engine-test: 3,690 checks, 0 failures, drag_golden_{cpu,gpu}.txt unchanged (250 of 1,856 frames that use the copy still GPU drag); canvas-test: 8,621,376 cases, 0 differ.
    • On the Raspberry Pi 5 with its monitor (V3D, 1920×1080 at 60 Hz, LordBench → Desktop impact, 2026-10-28, with the build before the uploads in slices and the phase timing; three runs): with GPU drag on, 52.2–52.9 fps alone (build p99 21.3–21.7 ms, 13.5–15.0 % late) and 25.8–27.5 fps under load, against 59.8–60.0 and 30.1–30.3 fps with LORD_GPU_DRAG=0 in the same build. It stalled; its cause was not measured (the phases did not exist yet). The next entry (the uploads in slices) and LordBench's phase timing (Added) were made for it.
    • On the Raspberry Pi 5 with the final build (2026-10-29, with the uploads in slices): the self-test passes on V3D (gpu: GPU drag on: … self-test: 11373321 pixels, every valid premultiplied combination and a 4096x2048 placement pass, the same as the CPU's, 341 ms), and with LORD_GPU_VERIFY=1 the 138 GPU drag frames compared were all differ=0 worst=0. It no longer stalls: with the frame pipeline off, 59.9 fps alone and 30.8–31.6 fps under load (three runs) with GPU drag on, against 59.9 and 30.5–31.1 fps with it off (two runs). It composed only part of the frames (138 of 601 alone, refused: layer 214, after 241, catching up 8; 80 of 436 under load). Correct, but no measurable gain, so it is now off by default and LORD_GPU_DRAG=1 turns it on (the first entry under Changed).
  • GPU drag's textures catch up between frames instead of inside one (docs/GPU.md, "Uploads without a stall"). A drag's start, or a copy of what is below built again whole, made the first GPU drag frame send the whole copy (a screen, 8.3 MB at 1920 × 1080) and the layer; on V3D glTexSubImage2D converts to the GPU's tiled layout on the CPU, inside the frame. Now a frame GPU drag would compose but whose textures lack more than 262,144 pixels (GDRAG_FEED_PX, 1 MB) is painted by the CPU (GDRAG_R_FEED, "catching up"; the same pixels), and after its flip is queued, while the CPU waits for the vblank, a slice of up to that many pixels goes up (paint_gdrag_feed, gpu_gdrag_feed: rectangles cut by rows; each texture is asked first whether it holds that display's pixels at that size, else it is allocated and all of it is sent in slices; GL errors left by earlier calls are cleared first, so they cannot turn GPU drag off there). Only frames refused for that reason send slices. A GPU drag frame can still send a texture whole when it finds one that is another display's or the wrong size (after the GPU context is made again, or the layer's first frame within the budget), as before. Each texture now knows whose pixels it holds; the dirty list keeps 16 rectangles and, when full, merges the two whose box adds the fewest pixels (the last one used to grow, which could make a few small changes most of the screen). Frames don't overlap (the next starts after the flip), so the textures need no second copy. Checked on VM 116: engine-test's stub GPU does the same (a texture made again is filled with a pattern no frame has); all 2,417 frames of drag_golden_gpu.txt match and the five extra runs match the GPU path frame by frame; no GPU drag frame sent a texture whole, and the most one GPU drag frame sent fell from 934,022 to 170,330 pixels in the long run (1024 × 640); 241 of 1,856 frames that use the copy are GPU drag (250 before: 9 catch up), 2,358,312 pixels went up between frames; pause 45 of 61 (54), two 148 of 240 (150), app 2 of 180 (5), ancedge 12 of 90 (15), corner 0 of 90. Not run on a GPU yet.

  • The paint threads turn on by the whole frame too: on when the frame (render_frame: layout, paint, upload, submit) takes more than 60 % of a period, or painting more than 70 % as before; off after 60 frames in a row with painting under 25 % and the frame under 45 %. On the Pi 5 under load painting took about 7.5 ms (under 70 %) while the frame took about 13 ms, so the threads, which run at real-time priority, stayed off. The pixels are the same (the pool's bands are exact). On VM 116 headless under load they were on for 0, 10 and 91 of 720–721 frames in three runs; the gain on the Pi is not measured yet.

  • The frame pipeline: the next frame is painted while the flip is pending (measured on the Pi 5 and off by default since then, LORD_PIPELINE=1 or 2 turns it on; docs/GPU.md, "The frame pipeline"; src/fpipe.c and main.c). A frame was started only after the previous flip completed, so input, JS, layout, paint, the upload, the GPU and the flip ran one after another; on the Pi 5 under an all-core load that chain (about 13 ms of CPU, 3–5 ms more) passes the 16.7 ms period, every frame waits for the second vblank and the monitor shows 30 frames a second, 98 % late.

    • Now, while the flip of frame N is pending, frame N+1's CPU part is done ahead: pointer input and LordBench's scripted step are dispatched, the JS frame callbacks and the layout run and its damage is painted into the display's scene (out_prepare), and at N's flip event only its upload, gpu_frame_ex (or the copy into the dumb buffer) and the page flip are left (out_submit, through the same out_present a frame built at once uses; render_frame is split into out_paint and out_present). GPU drag's textures catch up after the submission and before the next frame is painted. At most one frame is prepared; with nothing pending a frame is still built and flipped at once, so an idle desktop answers input as before.
    • Only with one display that has real page flips, on the GPU and the CPU path; never without a monitor, with a driver paced in software, or with several displays (each frame built and submitted at once there, as before).
    • LORD_PIPELINE, read once at startup and named in the session log: unset, 0 or anything else off (the previous behaviour exactly; the default since the Pi 5 run), 2 always, 1 on while frames are heavy: after 2 frames in a row whose CPU time passes 60 % of the period, off after 60 in a row under 40 % or at the first frame after more than half a second without one (FPIPE_IDLE_MS; an idle desktop gives no frames to count, and without this the pipeline stayed on through it, so the first input after it was shown a vblank later; that frame counts as the first of the two heavy ones that engage it again) (fpipe_note; -d logs each change), because when frames already fit a prepared frame shows input one vblank later.
    • The rules: what changes between a frame's paint and its submission goes into the next frame (the prepared one is shown as a frame painted at that moment would have been; the cursor and a Linux app's new buffer are still taken at submission). A prepare that paints nothing tries nothing more until the flip (no busy loop). A prepared frame that can't be submitted as painted (the scene reallocated, the GPU context or output changed, or for a GPU drag frame the drag layer began, ended, changed display or reallocated: the new paint_gdrag_epoch) is undone into the next frame's damage, its GPU drag rectangle included, and that frame is built at once; cpu_takeover (recording, protected windows for a remote viewer, a failed frame), gpu_start, outputs_apply (hotplug, Settings → Displays) and display_detach (a session switch, before the card's master goes) undo it first, and so does the screen going off (DPMS), so it is never shown stale when it comes back. Stopping the GPU for a recording or protected windows (cpu_switch, whose cpu_takeover sets the dumb buffer with a modeset) never runs in a render that only prepares, while the flip is pending; and nothing is prepared in a render whose built frame still has GPU drag's catch-up queued (it would run between that prepared frame's paint and its upload). Direct scanout, the remote desktop's read-back, the protected windows' black, LORD_GPU_VERIFY and the -2 path are decided at submission by the same code. The Linux apps' frame callbacks go with a submitted frame, never with one only prepared. The theme's JS frame callbacks run once per frame shown: a prepare that painted nothing (FPIPE_EMPTY) had run them, and when they asked for another frame (an animation callback that changes nothing while an app's frames or the cursor keep the display flipping) the frame built after that flip ran them again, twice per frame shown; that frame now skips them and they run with the next one (fpipe_hold_callbacks, Out.prep_js). A prepared frame that is undone, or GPU drag's rectangle painted again on the CPU, joins the next frame's damage as a second frame's, so paint_out applies the blur rule again (paint_damage_settle). Checked on VM 116 after these two fixes (and the rebase onto the partial-repaint fixes): make -j8 with no warnings; fpipe-test 59 checks (new: the idle gap, at and past FPIPE_IDLE_MS, re-engaging after it, 2 and 0 ignoring it, a heavy drag then 2 s idle then a click; and a display flipping for an app on the GPU path while an animation callback changes nothing: 600 callbacks for 599 frames shown with the pipeline off, 1,200 with it before the fix, 601 after); engine-test 6,907 checks, 0 failures; lordbench-test 1,217 ok; make test 23 passed, 0 failed, 13 skipped. The pipeline itself still has not run on a display.
    • LordBench records a prepared frame at its submission: its prepare's paint, its submission's upload, swap and flip ioctl, both as renderMs, and a new heldMs (prepared → submitted) with a prepared count, in frames.phases and the drag (scripted): line (… prepared ahead N (held for the flip p50/p99)); such a frame is on time when shown by the first vblank after the wake that submitted it, and the synthetic pointer's events end at its own presentation (lbf_painted, lbf_submitted; lbf_frame gets its prepare plus its submission).
    • Checked on VM 116: make -j8 and the changed files with GPU=0: no warnings; make test: 23 passed, 0 failed, 13 skipped; the new fpipe-test (39 checks: the switch, the auto mode's hysteresis, every decision, and a display on a vblank grid where 13 ms of CPU, 1 ms to submit and 3 ms of GPU give 299 flips in 600 periods without the pipeline and 598 with it, a 20 ms frame 299 and 475, and a light frame 599 either way with auto never engaging); engine-test (6,883 checks, 0 failures) runs every drag case again with each frame presented only after the next frame's changes, as a prepared frame is, and all 2,417 frames match drag_golden_gpu.txt, unchanged, with no change between paint and submission moving the GPU drag epoch; lordbench-test (1,197 ok) checks a prepared frame's phases, its class, a late one and the pointer's latency across the pipeline. LordBench's desktop test in an isolated headless session with LORD_PIPELINE=1 and =0: the same results (60 fps alone and under load, prepared 0: no flips without a monitor). VM 116 has no free DRM card (lord-sessiond holds its virtio-gpu card), so the pipeline itself had not run on a display then.
    • On the Raspberry Pi 5 with its monitor (2026-10-29, GPU drag on in both, two runs each): with the pipeline while frames are heavy, alone 60.0 fps (build p50 8.26–8.31, p99 11.90–11.98 ms, 0 % late), pointer to screen p50 42.1–42.5 ms; under load 33.0–36.2 fps (build p50 13.85–13.90, p99 19.07–19.36 ms, 65.8–81.6 % late), pointer to screen p50 77.3–79.8, p99 98.7–99.8 ms; every frame was prepared ahead, alone too (held for the flip p50/p99 8.40/15.72 ms alone, 18.27/31.60 under load). With LORD_PIPELINE=0: alone 59.9 fps, pointer to screen p50 25.5–25.7 ms; under load 30.8 fps (93.8–94.3 % late), pointer to screen p50 48.8–49.0, p99 66.1–66.3 ms. So it gave 2–5 frames a second under load for 17 ms more pointer latency alone and 29 ms more under load, and its auto mode engaged even alone at 60 fps (a frame of about 11.5 ms with the GPU's part passes 60 % of 16.7 ms): it is now off by default (the first entry under Changed).
  • js: unsupported style '<key>' is logged once per key (style_warn in ui/js.c), not on every setStyle call: a style an animation sends every frame wrote a line to the session log every frame (as transform did before LordBench's square and the fans moved to rotate). The first 64 different keys are remembered; the 65th is logged with (further unknown style keys are not logged) and later ones are not. Nothing else changes: the key is still ignored the same way (docs/THEMES-GUIDE.md).

  • Docs for the frame-cost work (2026-10-29, branch perf-frame): FANS.md, BENCHMARK.md, WINDOWS.md, GPU.md, ROADMAP.md, PAPER-VALIDATION.md, the paper (§3.2, §7.2, §7.4, §10.2; en, es, pt; docs/PAPER.md regenerated), the FAQ and "A desktop of its own" (en, es, pt) now say that dragged windows' corners no longer turn black and that LordBench's square and the Cooling fans turn (the docs audit's "still there, fixed in development" wording under Fixed is superseded), with the Pi 5 frame numbers measured so far and HTML comments marking where the final Pi 5 run's numbers go, each next to a sentence that is true without them (all filled after that run on 2026-10-29, the first entry under Changed). The site's Markdown renderer (web/lib/md.mjs) now drops HTML comments, so a marker left behind is never published. THIRD_PARTY.md lists the QuickJS JS_GetMallocSize patch as a table row.

  • Docs (2026-10-28): FANS.md no longer says the Cooling fans turn in this build (the transform key the engine lacks; fixed since), and RASPBERRY.md records that frame rates on the Pi 5's monitor were measured by LordBench (two monitors and 4K still not).

  • The 0.8.6 plan (2026-10-28): docs/ROADMAP.md has a "Release 0.8.6" section listing what is built and what is left for LordBench and for the desktop under load on the Pi 5.

  • The 0.8.7 plan (2026-10-29, decided by the maintainer): LordBench's x86 PC reference (a modest PC still to be chosen) and hardware video decoding (VA-API on PCs, the Pi 5's HEVC decoder) move from 0.8.6 to a new "Release 0.8.7 (planned)" section of docs/ROADMAP.md, next to the community hardware reports. BENCHMARK.md, PAPER-VALIDATION.md and the paper (§7.5; en, es, pt) say so. (2026-10-30, decided by the maintainer) Two items of the desktop under load follow them: step 12 of GPU drag (the dragged window composited on the GPU for the whole drag, also where the Dock or the menu bar overlap it) and the next work on the Pi 5 under load (fewer pixels uploaded and composed per frame with a buffer-age scissor, PBO uploads or a linear canvas; finding what slows V3D under an all-core CPU load, 14–16 ms of GPU a frame instead of 1.5; optionally a 1000 Hz kernel tick). ROADMAP.md keeps a one-line note in 0.8.6 and lists them in 0.8.7 "moved from 0.8.6 on 2026-10-30"; BENCHMARK.md, GPU.md ("Pending"), PAPER-VALIDATION.md, the paper (§7.4; en, es, pt; dated 2026-10-30), the FAQ and the own-desktop feature page (en, es, pt) say so.

  • 0.8.5-beta published (2026-10-27): the GitHub release carries the live ISO, the Raspberry Pi 5 image (lordos-0.8.5-rpi5-arm64.img.xz, the LordOS kernel 6.18.57-lordos.3 for the Pi 5; the Pi 4 still boots Raspberry Pi's archive kernel), the amd64 and arm64 packages with their SBOMs, install.sh and the three desktop downloads of LordOS Imager 0.8.5 (the universal build, signed and notarized; the 64-bit installer; the Debian package). The Imager 0.8.5 is the first desktop build that reads point releases (v0.8.4.1-beta) in its app update check and its release list. SHA256SUMS (11 files) is signed with the release key. The site's downloads (lordos.media) now point to 0.8.5.

  • Release files catch up with LordBench: the Noto subsets built into lord-bench are in the package's copyright file (license in usr/share/doc/lordos/OFL-Noto.txt) and the SBOM; tools/results/collect.sh writes lordbench.json (--no-lordbench skips it); a table inside a list item keeps its lines in the release notes; System Information and Settings move to version 0.8.6.

Fixed

  • lordos-sysreport bench --full ran no sustained load (2026-10-30; server/src/sysreport/cli.c): it passed no sustained time to lbrun_start, so the sustained part was skipped ("skipped": {"sustained.start": "mode"}) and a Full run from a terminal took 178 s on the Pi 5, although its help says 3 minutes of sustained load. It now passes CLI_SUSTAINED_S, 180 s, the same as System Information's Full (SUSTAINED_S in apps/sysinfo/performance.jsx); checked on the Pi 5 (18 samples over 180 s, 358 s in all). On VM 116: the build with no warnings, make -C server test 23 passed, 0 failed, 13 skipped (sysreport-test 790 ok); sysinfo-perf-test 62 ok with the Pi 5's new reference values.

  • System Information's Raspberry Pi power and throttling rows (2026-10-29; server/src/hw/rpifw.c, sysreport/sysreport.c, apps/sysinfo; tested on fake sysfs trees and VM 116; the shared reader ran on the Pi 5 in LordBench on 2026-10-29, the alarm present and reading 0, while System Information's rows were not looked at there): the collector looked only for the firmware's get_throttled (soc@107c000000:firmware on the Pi 5), which the rpi-6.18.y kernel LordOS ships doesn't have, so on a Pi 5 with LordOS the Power and Throttling rows were always empty (and the Markdown report said nothing). System Information now reads the firmware the same way LordBench's guards do, from one shared reader, hw/rpifw.c (rpifw_read, rpifw_parse_throttled; linked into lord-server, lordos-sysreport and both tests; bench/lbcond.c uses it instead of its own copy): the rpi_volt hwmon's in0_lcrit_alarm, exactly 0 or 1 (with an optional newline; anything else is unknown), and get_throttled where an older kernel has it (directly under a /sys/devices/platform node or its <node>:firmware child), strictly parsed (1-8 hex digits, optional 0x, one optional newline, masked to the eight known bits). World-readable sysfs only: no mailbox, no vcgencmd, no exec. What the kernel shows is reported and what it doesn't is left out:

    • The LordOS kernel (rpi_volt only): the Power row says "No under-voltage in the last few seconds" or "Under-voltage in the last few seconds: use the official power supply" (the hwmon asks the firmware every 2 s and clears the sticky bits, so it can't tell "since boot"); there is no Throttling row (the kernel doesn't expose the capped, throttled and soft-limit bits), never a false "never slowed down".
    • An older kernel with get_throttled and no rpi_volt: the rows as before, "now" and "since boot", and the soft temperature limit now counts as slowed down too.
    • Both: the sticky bits only cover the last 2 s window (rpi_volt clears them), so the rows say "in the last few seconds" instead of "since boot".
    • Neither, or a value that doesn't parse: model and revision only. The report's JSON raspberryPi gains underVoltageAlarm (true, false or null; throttled stays the raw bits or null); the Markdown report says "no under-voltage in the last 2 s (rpi_volt); throttling not reported by this kernel" on the LordOS kernel, and the now/since-boot/last-2-s wording with the raw bits on older ones. The rows' choice is piStatus in apps/sysinfo/model.js. Four new strings (en, es, pt). Tests: tests/sysreport_test.c (the Pi 5 fixture now has the LordOS kernel's rpi_volt and an empty soc@107c000000:firmware; thirteen small trees: that kernel with the alarm clear, set, yes, 01 and missing; an older Pi 4 and Pi 5 kernel with get_throttled set, clear and "now"; both sources; a malformed value and a second line; a Pi with neither; a machine that isn't a Pi with both files; 790 checks) and tests/sysinfo_model_test.mjs (piStatus, 68 checks). build/lordbench-test still passes (1,198 checks).
  • No black squares at the corners of a dragged window. While any window was dragged, the area just outside its rounded corners showed opaque black squares, on the CPU and the GPU path (seen on VM 116 with Calculator; still windows were fine). The drag layer paints the window on a transparent canvas, and a rounded overflow: hidden window saves its corner squares before its children paint and restores them afterwards (canvas_corners_restore/canvas_corners_restore4 in server/src/canvas.c). The restore mixed the saved pixel in with blend(), which takes its source as opaque: the alpha came out as over(a, dst), so the window's shadow saved over transparency (or the GPU path's sentinel) came back opaque, its premultiplied colour near black, and the layer was blitted over the desktop with those squares. The restore now mixes both premultiplied pixels in every channel, alpha included (mix_px); where both are opaque, as on the screen's canvas, it gives blend()'s pixel exactly, so still windows and everything else on an opaque canvas are unchanged. On the GPU path this also gives transparent corners where a rounded window sits over a Linux app's hole (alpha 0), which were black before. New engine-test check test_drag_corners: a rounded window with a shadow, a title bar and a status bar over a light box, dragged three frames (one renders the layer, two copy it), on both paths, with one radius and with per-corner radii; each corner square must equal a full repaint within 2 levels (before: 217 to 221 levels, all 12 corner pixels dark). The golden hashes of test_drag_exact were written again after a pixel-by-pixel comparison of every frame with the code before: on the CPU path 498,743 pixels in the dragged window's corner squares and 445,199 inside the Dock's or the menu bar's backdrop blur, with such a square within the blur's reach, differed in 2,386 of 2,417 frames (GPU path: 498,692 and 431,316 in 2,387), none anywhere else. The pixels that differ from a full repaint in the long run go down from 2,217 to 248 (CPU path) and from 70,882 to 70,864 (GPU path).

  • A rotated or scaled node now repaints when its own style changes (approved by the maintainer on 2026-10-28). A node with its own rotate/scaleX/scaleY is painted into a cached raster (tfl[] in server/src/ui/paint.c). damage_walk cleared the node's paint_dirty before its children, and tf_check, which runs after them, tested that flag, so a change to the node's own background, border, radius or gradient (lord.setStyle or a React render) kept the raster and the old look stayed on screen until something else re-rasterized it. tf_check now compares the node's own style with the raster's every time (the same change as the partial-repaint fix below, which found it independently with changes made while a subtree was hidden or unscaled; on this branch it first kept the node's own flag for tf_check, which the stricter check replaces). A change of the transform alone still reuses the raster (the compared style leaves the transform out). New engine-test check test_transform_self_restyle (a 40 × 40 box rotated 15°, its background changed from red to blue, the centre pixel must be blue): it fails without the fix (ff0000) and passes with it. The drag goldens (server/tests/drag_golden_{cpu,gpu}.txt) are unchanged: 2417 frames per path, the same per-frame hashes, 0 pixels differ in the 591 frames switched to the copy of what is below; make test on VM 116: 23 passed, 0 failed, 13 skipped; engine-test: 3662 checks, 0 failures.

  • GPU composition without GL_EXT_unpack_subimage (approved by the maintainer on 2026-10-28; server/src/gpu/gpu.c). The UI's damage was always uploaded with GL_UNPACK_ROW_LENGTH_EXT; a GLES2 driver without that extension ignores it (GL_INVALID_ENUM) and reads each damaged rectangle narrower than the screen with the wrong row length, so the uploaded pixels came out sheared. The log line also said "the whole canvas is uploaded", which was not true. Now the extension is read from the GL extension list; without it the damage is uploaded as whole rows in one call when the scene's rows are contiguous (they are: the scene is created with stride == width), or one row at a time otherwise, and the log says so. With the extension (every Mesa driver, so every machine LordOS has been tested on) nothing changes. Built with GPU=1 and GPU=0 on VM 116 with no warnings; the fallback itself has not run on a driver that lacks the extension.

  • LordBench's turning square and the Cooling page's fans now turn (approved by the maintainer on 2026-10-28). apps/sysinfo/scene.jsx and sdk/cooling.jsx called lord.setStyle(n, 'transform', 'rotate(…deg)'), and transform is not a key the engine takes (it only logged js: unsupported style 'transform'), so the desktop test's square never turned and the fans only jumped when React rendered them. They now send the engine's own rotate key in degrees (the fans keep toFixed(1), the same angle React sends). The desktop test's window is no longer static: its layer is repainted (in part, where exact) every frame, so desktop-test numbers from before this change measured a lighter scene and aren't comparable (on the Pi 5 with its monitor the turning square took the test from about 59 to 53–58 fps alone before the frame-cost work, Changed).

  • Partial repaints that left stale areas on screen (the search for the 0.8.2 "grey rectangle", which keeps several damaged rectangles off by default; docs/MONITOR.md, "0.8.6: the search for the grey rectangle"). Two new build/engine-test suites compare every painted frame with a full repaint that bypasses the raster cache of transformed subtrees (paint_tf_nocache, tests only): test_damage_pipeline runs the reported scene (Monitor's moving map, a translucent, blurred Terminal minimized into the blurred Dock, the blurred menu bar, menus, the cursor) through a copy of main.c's whole bookkeeping (damage gathered while a page flip is pending, paint_out's parts, present_cpu's per-buffer lists and copy, the cursor) with 0, 30 and 60 % of the flips pending, and checks every presented buffer against the scene; test_damage_fuzz does the same with random trees and random changes (clipping, rounded, shadows, translucent, blurred, scaled, rotated, faded, zIndex, hidden, moved, resized, re-parented, destroyed). The double-buffer and cursor bookkeeping was right in every frame. The fuzz found seven bugs in the paint engine, all with one box per frame and with several rectangles, each able to leave a wrong area until something repainted it (before the fixes 11 of 12 seeds failed; now 1 of 48 in each mode, a single 1-pixel row of 17 pixels, plus a few levels on antialiased edges in a second rare case, both described in the test; the 12 seeds make test runs pass):

    • a node's border is drawn over its children; when a partial repaint started from an opaque child (the opaque-cover shortcut), the parent's border was skipped, so a child overflowing its parent lost the parent's border line where a small rectangle fell (paint_node);
    • a transformed subtree's cached raster stayed valid after a change made while the subtree was hidden or had scale 1, and came back with the old content (each node now has a generation counter, Node.gen, bumped by node_invalidate, in the subtree's signature, and the node's own style is compared every time);
    • a transform inside another transform: changes inside the inner subtree never reached the outer raster's signature, so the outer raster stayed cached with old content;
    • the raster's pixel grid moved with the subtree's extent, so a child moving inside an unclipped scaled subtree resampled every edge of it outside the damage (the grid is now fixed to the node);
    • when a blur, a terminal or an app window came into or left a transformed subtree, its raster changed resolution or kind and every pixel of it changed, but only the node that came or went was damaged (the whole subtree is now);
    • a blurred surface repainted with only its own box read, outside the repainted area, the last frame's final pixels, including whatever is painted above it (a sibling's shadow, a menu, a window's border), while a full repaint blurs only what lies below: its edges kept the difference. The margin its blur samples (twice the radius) is now repainted with it (blur_near, in blur_walk and the several-rectangles rule); in test_multi_damage this raises the pixels repainted per frame from 24,078 to 27,363;
    • a display whose flip was pending gathers several frames' rectangles and dlist_add joins the ones that overlap; a join, or an older frame's rectangle after a blurred node moved, could cut through a blurred node, whose blur then depended on where the cut fell. paint_out now applies the blur rule again to damage gathered over more than one frame, on the tree as it is painted (paint_damage_settle). Which of these was the 0.8.2 report is not proven (the reported scene itself never failed), so several rectangles stay off by default (LORD_DAMAGE=multi). Built with make -j8 (no warnings), make test and build/engine-test on VM 116. Checked on a running desktop (VM 116, 2026-10-29; docs/MONITOR.md, "0.8.6: the search for the grey rectangle"): an isolated headless session of the real default theme at 1280×800 (CPU composition), driven over its remote desktop socket by a viewer that keeps its own copy of the screen from the tiles the session sends every frame, so the copy holds exactly what the partial repaints left. The 0.8.2 scene: Monitor → Network with ten slow downloads to mirrors in ten countries (the map's dots moving), a Terminal at 30 % transparency with blur over the map, minimized into the Dock and restored 22 times, the Terminal's six menus opened and closed, 54 checks per run. At each check a second connection makes the session repaint the whole screen and the two are compared (pixels that changed within 2.2 s before or after are left out as animation; the downloads are paused for the check so the dots are still). Six runs: this commit and the previous main, each with one box (the default) and with LORD_DAMAGE=multi, and both builds again in the default mode with the scene still for 7 s before each check. No stale area in any run, before or after: with the scene still, 0 differing pixels in 54 checks (this commit) and 3 single pixels on the Dock's corner (the previous build); with the scene moving, only Monitor's live numbers and charts that changed between the last partial frame and the full one, a few antialiased pixels of the Dock's corner, whose blur takes in Monitor's live "Load" line (both builds, 1 to 7 pixels), and, in 5 of 108 checks of this commit, a map arc that faded under the blurred Terminal in that gap (at most 3 levels of 255; gone with the scene still). The 0.8.2 grey rectangle did not appear in either build, so it is still not reproduced. Cost (the same sessions, one remote viewer, lord-server whole process, % of one core, 5 × 30 s, two batches): Monitor → Network 56.7 / 57.2 % before, 58.7 / 59.7 % after (one box) and 59.2 / 58.1 → 57.2 / 58.0 % (several rectangles); with the blurred Terminal over the map 35.8 / 36.4 → 38.0 / 37.7 % and 36.7 / 36.6 → 37.5 / 35.9 %; layout and paint per frame (LORD_FRAME_STATS) the same within 0.1 ms in every case (14.6 to 14.8 ms). In these sessions (--profile) every frame repainted one box of 64 to 79 % of the screen, also with LORD_DAMAGE=multi (the moving dots, Monitor's live numbers and charts on both sides and its footer next to the blurred Dock never stayed several rectangles), so the blur margin adds next to nothing there; the per-frame difference measured in test_multi_damage (24,078 → 27,363 px) was not visible at the process level. Why the 0.8.2 VM 119 scene gave 5 to 11 rectangles and this one gave one: a translucent, blurred Terminal hidden behind Monitor (next entry). LordBench's desktop mode in the headless harness (three runs each): 58.9-60.0 fps, build p50 1.25-1.26 ms and p99 5.9-7.6 ms before; 60.0 fps, p50 1.24-1.27 ms and p99 5.95-6.32 ms after.
  • A blurred window hidden behind another no longer makes every frame repaint it (paint.c, blur_hidden; both damage modes). The blur rule (a change that touches a blurred surface or the margin its blur samples repaints the whole surface and its margin) also held for a blurred surface covered whole by an opaque window above it, which shows nothing of its blur. A translucent Terminal (30 % transparency, blur) behind Monitor → Network: every frame of the moving dots repainted the hidden Terminal's body and margin, one box of 45 % of a 1280×800 screen (62 to 73 % when its margin met the Dock's, which then joined), even with LORD_DAMAGE=multi, where without it the dots are a few rectangles under 1 % of the screen. Now a blurred node that an opaque node painted after it covers whole (the same test as the opaque-cover skip: opaque background, opacity 1 down the tree, no transform, away from rounded corners, clipped by its ancestors; CPU path without a dragged window) is left out of the rule; when the window on top moves, turns translucent or closes, the change reaches the blurred one and the rule applies again. A blurred Terminal in front of the map is still repainted whole with its margin every frame (75 to 80 % of the screen when its margin meets the Dock's): that is the rule itself. Found on VM 116: the sessions of the repaint check above (and their --profile numbers) had such a Terminal behind Monitor, restored from a lord-term-holder left by an earlier run (the harness killed holders with a pattern that never matched the holder's process name). Measured (VM 116, isolated headless session, 1280×800, CPU composition, one remote viewer, --profile, last 150 frames): repainted per frame 46.4 → 14.5 % (one box) and 46.2 → 3.8 % in 5.6 rectangles (several), paint 5.6 → 2.0 ms and 5.2 → 1.7 ms per frame; lord-server (whole process, % of one core, 5 × 30 s, two batches): 24.3 / 22.3 → 13.2 / 15.3 % (one box) and 21.5 / 21.7 → 15.0 / 14.3 % (several). With no hidden blurred window the frames are the same as before (Monitor → Network alone, one box: 10.2 / 13.8 % per frame, the live charts). Checked: test_hidden_blur_damage in build/engine-test (a blurred window under an opaque one with moving dots, both modes: every frame matches a full repaint while the hidden window moves and the one on top moves away and back, turns translucent, closes and reopens; the hidden window no longer joins the damage; a visible one is still repainted whole; three of its checks fail without the change); make test and a full build without warnings on VM 116; and on a running desktop with the repaint check above, the original scenario (54 checks per mode) and a new one with the Terminal hidden behind Monitor's moving map, raised over it and sent back ten times (21 checks per mode): no stale area in either mode, only Monitor's live numbers and charts and the clocks changing between the two copies. Rebased onto the frame-cost work and checked again (2026-10-29, VM 116): blur_hidden stays off with a dragged window, on the GPU path (so with GPU drag) and inside a transform; the damage of a frame the pipeline undoes, or of GPU drag's rectangle repainted on the CPU, goes through paint_damage_settle, which applies the same rule to the tree being painted. A build without warnings; make test (23 suites passed, 13 skipped for missing tools); engine-test 7,010 checks, its drag runs printing exactly what main's build prints (the golden hashes of 2,417 drag frames per path unchanged); fpipe-test 61 checks; lordbench-test 1,217. The first check of the 0.8.2 scenario above never minimized the Terminal: with the leftover holder gone a fresh Terminal opens at another place, and the scenario's click, placed for the restored one, missed its minimize button. The click now hits it, and the rebased build was checked with the Terminal minimized into the Dock and restored 22 times: 0 differing pixels outside Monitor's live numbers and charts and the clocks in all 54 checks, one box and several rectangles, with the downloads paused 2.5 s and with the scene still 7 s before each check (four runs); the hidden-Terminal scenario, 1 pixel (a Dock corner next to Monitor's live Load line) and 0. The live areas left out now also hold the fresh Terminal's footer clock, Monitor's left-panel chart and the curl row's sparkline, which the restored Terminal used to cover. --profile on the rebased build, the Terminal hidden behind Monitor → Network: 11.2 % of the screen per frame in one box (paint 1.24 ms) and 3.3 % in 5.8 rectangles (0.82 ms); Monitor alone: 9.1 % and 3.2 %.

  • The desktop under load (found by LordBench's desktop test; docs/BENCHMARK.md, "The desktop under load"): the compositor's cgroup (server) had the same cpu.weight as every app scope, and nice values don't count between cgroups, so an app keeping every core busy took half of the compositor's CPU even at nice 19. On a Raspberry Pi 5 the compositor's main thread waited 299 ms per second for a CPU under load (17 ms alone). Now lord-sessiond gives the session's server leaf cpu.weight 1000 (apps and helpers keep 100), in the root-owned subtree it already manages (CG_SERVER_CPU_WEIGHT, sec/cgroup.c). LordBench's desktop test under load on the Pi 5 without its monitor, two runs each: 43–45 → 56–57 frames per second, late frames 31–33 % → 7–8 %, pointer to screen 32–33 → 22 ms median and 67–75 → 52–53 ms for the slowest 1 %; nothing changes without load. Checked on VM 116 that a new session gets the weight. On the Pi's monitor with GPU composition the weight was measured only together with the real-time priority (next entry). A 0.5 ms scheduler time slice for the compositor's threads was also tried (it was in the previous commit) and removed: measured alone, it made no difference; its first numbers had been inflated because the test tool also gave the slice to the load. What remains is described there.

  • The desktop under load, second step: a real-time priority for the compositor (approved by the maintainer on 2026-10-28; docs/BENCHMARK.md, "The desktop under load"). With the weight, the compositor's main thread still waited up to a scheduler tick each time it woke up under load. Now lord-sessiond starts the session's lord-server with RLIMIT_RTPRIO 2 and RLIMIT_RTTIME 5 s (remote/sess.c); lord-server, right after dropping root and before it starts any process or thread (par_init, par.c, moved there in main.c), puts its main thread and its paint threads at SCHED_RR priority 2, gives the main thread SCHED_RESET_ON_FORK so everything it starts later gets the normal policy, and lowers RLIMIT_RTPRIO to 0, soft and hard, so no app, helper or shell can ask for real time. The root admin helper lowers its own limit to 0 as well (its download child drops to nobody). A thread that runs 5 s on real time without blocking gets SIGXCPU and every thread goes back to the normal policy; nothing is killed. The session log says compositor at real-time priority 2 (SCHED_RR, N threads). Checked on VM 116 with the new lord-sessiond (main thread SCHED_RR|SCHED_RESET_ON_FORK, 7 paint threads SCHED_RR, every other thread and child SCHED_OTHER with a limit of 0). LordBench's desktop test under load on VM 116, two runs each: late frames 2.65–2.92 % → 0.28 %, frame build p99 16.3–16.8 → 11.0–12.7 ms, pointer to screen p99 31.0–32.3 → 24.7–25.5 ms (as without load); the load got the same CPU time. On the Pi 5 with its monitor (GPU composition), two runs: under load 24.0 → 28.9–30.5 fps, frame build p50 20.1 → 15.4–16.2 ms, the main thread's wait for a CPU down to about 0.07 ms per second; still 94–98 % late, because under the load a frame takes 15–16 ms of the main thread's own CPU time to build (7 ms alone) and misses the 16.7 ms refresh. That cause is not found yet; the frame-cost work (Changed, "The desktop under load, third step") cuts that build to 12.9 ms with the scene's square turning, and the frame pipeline is built for the rest.

  • LordBench on the Raspberry Pi 5 measured no GPU: inside lord-bench's sandbox Mesa's loader could not identify the V3D render node ("failed to retrieve device information") and every GPU measurement was skipped. The sandbox now also allows reading /sys/firmware/devicetree and /dev/dri (read-only; the GPU is still reached only through the render node fd lord-server opens), and the Pi reports a hardware GPU.

  • Raspberry Pi image on an x86-64 host with an aarch64 server/build: tools/iso/mkdeb.sh checks the signed system code by running lord-server --check-code, which can't run an aarch64 binary on x86-64 (it failed as "the staged system code does not match code-manifest.json"). CODE_CHECK_SERVER now names a lord-server built for the host from the same tree, and tools/rpi/build-img.sh asks for it instead of failing later. Found building the 0.8.5 image; the check itself is unchanged (docs/SIGNING.md).

  • Same build, unstripped binaries: mkdeb.sh stripped with the host's strip, which can't read aarch64 binaries and whose errors are ignored, so that package shipped every binary with its symbols (17 MB instead of 11 MB, the same files). It now uses aarch64-linux-gnu-strip when it packages arm64 on another architecture, and stops if it is missing.

  • Public docs corrected ahead of 0.8.6 (a read-only audit, each finding checked against the code and the measurements):

    • Paper (en, es, pt; docs/PAPER.md regenerated): §7.2 no longer says a dragged window's rounded corners stopped turning black on the monitor; it says what the 0.8.5 fix did (on the GPU path only pixels inside an app surface are taken for a Linux window's hole) and that small black squares can still show at the corners, on the CPU and GPU paths. §10.2 lists them as a known limitation (fixed later in this release, see "No black squares at the corners of a dragged window"), and the remote-desktop line no longer says the monitor shows them right. §7.4: the board results are "without a monitor unless stated", and the CPU weight alone is said to have been measured on the monitor only together with the real-time priority (it said the monitor measurement was pending, then gave it). The PDF/Markdown header writes the organisation as iPERFEX, as in meta.json (web/lib/papermd.mjs no longer upper-cases it).
    • FAQ (en, es, pt): "about thirty native apps" instead of "more than 35" (the paper says thirty-one); memory given as the paper measured it (lord-server about 50 MB in a user session) instead of an unsourced "120 MB idle session"; the Raspberry Pi 5 answers say the image shows the desktop on an HDMI monitor since 0.8.4.1 and that two monitors and 4K are what is untested; "KDBX files" instead of a brand name; the LordBench answer says the desktop under load is being fixed (as without load on VM 116; 24 → 29–30 fps on the Pi 5 with its monitor, the rest still in progress) instead of "already fixed", and a doubled word in Spanish is gone.
    • Features (en, es, pt): Linux apps no longer says a Flathub section is planned for 0.8.6 (Flatpak is not planned for any release; docs/FLATPAK.md); A desktop of its own says the Pi 5 has been tested with one HDMI monitor (not with two or 4K) instead of never with a monitor; Private by design adds lord-fetch, lord-tls and lord-bench to the sandboxed helpers; the Portuguese page puts its screenshot where the other two do.
    • docs/BENCHMARK.md: the status lists what is built (interface and video workloads, desktop-impact mode, boot test, lordos-sysreport bench, two of three references) and what is not (the x86 PC reference, a checks subcommand); the CLI diagram shows report | fetch | bench; the design table notes xz instead of deflate and software-only video decoding; the Pi 5 reference is the 16 GB board; the desktop test's turning element stays still in this build (there is no transform style key; fixed later in this release with a rotate key); the Quick-run times (33 s, then 39 s and 46 s from a terminal) are told apart; "not yet measured with the monitor" and "about 20 % slower" are replaced by what was measured (a frame takes 15–16 ms instead of 7 ms under load, cause not measured).
    • docs/ROADMAP.md: the black-corners item under Release 0.8.6 is unticked (fixed on perf-frame, merged later in this release); "Pending before the final ISO" is marked done; LordBench's "Left:" adds detecting the Pi 5 firmware's throttling; the Pi 5 report item says what is still missing. docs/FLATPAK.md points to the roadmap item's current title.
    • docs/PAPER-VALIDATION.md: the Pi 5 load result is recorded as measured on the monitor with both changes; one Full and four Quick runs; 34 folders in apps/.
    • README.md: the "Measured, not promised" numbers are labelled as the pre-release 0.7.0 ISO test and joined by the released 0.8.5-beta's (592 MB whole system, first frame 7.4 s after the kernel); the repository map adds lord-bench.

Known issues and limitations

  • Raspberry Pi 5 under an all-core load: about 31 frames a second. Alone the desktop holds 60; with all four cores busy at the lowest priority it runs at 30.5–31.1 fps, up from 24, with the pointer reaching the screen in about 49 ms (docs/BENCHMARK.md, "The drag frame's cost"). The GPU's time per frame goes from 1.5 ms to 14–16 ms with no clock or thermal throttling; memory contention is the likeliest cause, not proven. The next steps are planned for 0.8.7 (docs/ROADMAP.md).
  • The frame pipeline and GPU drag are built but off by default. On the Pi 5 the pipeline gave 2–5 frames a second under load for 17–29 ms more pointer latency, and GPU drag, correct on V3D, gave no measurable gain. LORD_PIPELINE=1 (or 2) and LORD_GPU_DRAG=1 turn them on for testing. GPU drag does not yet cover the whole drag (where the Dock or the menu bar overlap the window); that is planned for 0.8.7.
  • LordBench's comparisons. There is no x86 PC reference yet (planned for 0.8.7), and video decoding is measured in software only. The reference machines' layout, paint and shaping values were measured before this version and are used only for results of the same LordOS version, so a 0.8.6 result leaves those four measurements out of its ×N until the references are measured again.
  • Raspberry Pi 5: one 1920×1080 HDMI monitor has been measured; two monitors and 4K have not.
  • llama.cpp on a GPU is not tested on real hardware yet (as in 0.8.5): only the CPU path and the backend loading (software Vulkan) were checked.
  • Legacy BIOS: a black rectangle between the boot menu and the splash while GRUB loads the kernel and the initrd, as in 0.8.5; barely visible on UEFI.
  • Searching "displays" in the search panel opens the Help article on touchpads and touch screens before Settings → Displays.
  • Settings → Displays: in the arrangement, the name of a small display is cut without an ellipsis.

Upgrading

  • From 0.8.5 (and 0.8.4.1 to 0.8.1): Store → Updates shows "LordOS 0.8.6 is available" with these notes; Update installs it and a restart finishes it. sudo apt update && sudo apt upgrade does the same. This version adds no migration (ls /var/lib/lordos/migrations is unchanged from 0.8.5); your settings are kept. Tested on 2026-10-30 with apt against lordos.org/apt-staging, on a live-session amd64 VM (no restart is possible there) and on the Raspberry Pi 5 with a restart: no migration ran, the settings files were unchanged, and the Pi came back with 0.8.6-beta build 20261030, its first frame 7.63 s after the kernel, in a release build with signed code. The Store's own package engine and the Store → Updates screen were not exercised, and a second reinstall was not run.
  • The compositor's CPU weight and real-time priority are set when lord-sessiond starts a session, so they apply after that restart.
  • Default apps you chose in Settings stay as they are; automatic defaults apply only to file types you never chose an app for.

Downloads

File What it is
lordos-live-0.8.6-amd64.iso Live and installable image for 64-bit PCs (UEFI and BIOS). Write it to a USB stick as is; Secure Boot off.
lordos-0.8.6-rpi5-arm64.img.xz Image for the Raspberry Pi 5, for the LordOS Imager (below) or Raspberry Pi Imager ("Use custom"), an SD card or an NVMe drive.
lordos_0.8.6_amd64.deb The LordOS package for an existing Debian 13 on a PC (installed by install.sh, or with apt install ./…).
lordos_0.8.6_arm64.deb The same package for Debian 13 on arm64.
lordos_imager_0.8.6_universal.dmg LordOS Imager for Mac (both processor families, signed and notarized): writes LordOS to an SD card or a USB stick and prepares the Raspberry Pi 5's first boot.
lordos_imager_0.8.6_x64_setup.exe LordOS Imager for Windows (64-bit installer; not code-signed yet, so the system asks before running it).
lordos_imager_0.8.6_amd64.deb LordOS Imager for Linux (Debian 13 and similar, amd64; needs udisks2).
install.sh The one-line installer for Debian 13: curl -fsSL https://lordos.org/install.sh | sh. It checks the signature before installing.
SHA256SUMS The SHA-256 of every file in this release.
SHA256SUMS.sig The Ed25519 signature of SHA256SUMS with the LordOS release key (base64).
lordos_0.8.6_<arch>.spdx.json Software bill of materials (SPDX 2.3) for each package: what it bundles and the Debian packages it depends on.

Verifying the downloads

The LordOS release public key is lordos-release.pub.pem (in the repository, or at https://lordos.org/lordos-release.pub.pem). Its fingerprint, the SHA-256 of the DER public key, is:

c7155b8e6bbbb180f907381994d06af76d333016cd6f3facf67cadd053ea108e

With OpenSSL 3, in the folder with the downloads:

openssl pkey -pubin -in lordos-release.pub.pem -outform DER | sha256sum   # must match the fingerprint
base64 -d SHA256SUMS.sig > SHA256SUMS.sig.bin
openssl pkeyutl -verify -pubin -inkey lordos-release.pub.pem -rawin -in SHA256SUMS -sigfile SHA256SUMS.sig.bin
sha256sum -c --ignore-missing SHA256SUMS

"Signature Verified Successfully" and "OK" for each downloaded file mean the files are the ones we published. Details: docs/SIGNING.md.

Links