SCFF: the live root ships compressed, with Xorg and Window Maker

SCFF, the SchOS Compressed File Format (include/fs/scff.h), is a block
image cut into 128 KB chunks, each its own zstd frame, behind an index.
tools/mkscff writes it from fs-ram.img on every core (-19, 11 s) and
checks each chunk decompresses to itself. The kernel carries the .scff
as its ramdisk and unpacks a chunk the first time a block in it is
read; zero blocks share one page. The kernel and the ISO are the
compressed size, and the root costs memory only for what is touched.
See docs/architecture/scff.md.

zstd 1.5.7 comes from ports/zstd like uACPI: the decoder is compiled
into the kernel at -O2, with a static context and a 64 KB stack of its
own, and mkscff builds the whole library from the same release.

world now has xorg-server, the fbdev and evdev drivers, xinit, Window
Maker, xterm and DejaVu, and /root/.xinitrc runs wmaker. That root is
358 MB, 102 MB as SCFF.

The ISO no longer carries the root a third time as an ISO 9660 tree,
which nothing read. schos.iso: 1389 MB -> 318 MB. El Torito keeps its
own copy of the EFI image, the layout real machines have booted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S1giNvrHTrPMPw7V6x1kQ4
This commit is contained in:
Nexoniarz 2026-10-24 21:17:31 +02:00
commit df1394eaa3
18 changed files with 832 additions and 38 deletions

190
tools/mkscff.c Normal file
View file

@ -0,0 +1,190 @@
// SPDX-License-Identifier: GPL-2.0-or-later
// mkscff: write a block image as SCFF, the SchOS Compressed File Format.
//
// mkscff IN.img OUT.scff [--chunk KB] [--level N] [--threads N]
//
// The layout is include/fs/scff.h. Chunks are compressed on every core, since
// zstd's high levels are slow and the image is hundreds of megabytes, and each
// one is decompressed again and compared before it is written: an image the
// kernel cannot read is found here rather than at boot.
//
// Built against the zstd release the kernel's decoder comes from (ports/zstd),
// not the host's libzstd, so both sides are one version and nothing needs to
// be installed for it.
#include <errno.h>
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include "fs/scff.h"
#include "zstd.h"
static unsigned char *img;
static uint64_t img_bytes;
static uint32_t chunk_bytes = 128 * 1024;
static int level = 19;
static uint32_t nchunks;
static unsigned char **out; // chunk i as stored, or 0 for an all-zero one
static size_t *outlen;
static uint32_t next_chunk;
static pthread_mutex_t next_lock = PTHREAD_MUTEX_INITIALIZER;
static void
die(const char *what)
{
fprintf(stderr, "mkscff: %s\n", what);
exit(1);
}
static int
all_zero(const unsigned char *p, size_t n)
{
for (size_t i = 0; i < n; i++)
if (p[i])
return 0;
return 1;
}
static void *
worker(void *arg)
{
(void)arg;
ZSTD_CCtx *cc = ZSTD_createCCtx();
ZSTD_DCtx *dc = ZSTD_createDCtx();
size_t cap = ZSTD_compressBound(chunk_bytes);
unsigned char *buf = malloc(cap), *back = malloc(chunk_bytes);
if (!cc || !dc || !buf || !back)
die("out of memory");
ZSTD_CCtx_setParameter(cc, ZSTD_c_compressionLevel, level);
ZSTD_CCtx_setParameter(cc, ZSTD_c_checksumFlag, 1);
for (;;) {
pthread_mutex_lock(&next_lock);
uint32_t i = next_chunk++;
pthread_mutex_unlock(&next_lock);
if (i >= nchunks)
break;
const unsigned char *src = img + (uint64_t)i * chunk_bytes;
size_t n = img_bytes - (uint64_t)i * chunk_bytes;
if (n > chunk_bytes)
n = chunk_bytes;
if (all_zero(src, n)) {
outlen[i] = 0;
continue;
}
size_t c = ZSTD_compress2(cc, buf, cap, src, n);
if (ZSTD_isError(c))
die(ZSTD_getErrorName(c));
if (c >= n) { // did not compress: stored as it is
out[i] = malloc(n);
memcpy(out[i], src, n);
outlen[i] = n;
continue;
}
size_t d = ZSTD_decompressDCtx(dc, back, chunk_bytes, buf, c);
if (ZSTD_isError(d) || d != n || memcmp(back, src, n) != 0)
die("a chunk did not decompress to itself");
out[i] = malloc(c);
memcpy(out[i], buf, c);
outlen[i] = c;
}
ZSTD_freeCCtx(cc);
ZSTD_freeDCtx(dc);
free(buf);
free(back);
return 0;
}
int
main(int argc, char **argv)
{
const char *in = 0, *outpath = 0;
long threads = sysconf(_SC_NPROCESSORS_ONLN);
for (int i = 1; i < argc; i++) {
if (!strcmp(argv[i], "--chunk") && i + 1 < argc)
chunk_bytes = (uint32_t)atoi(argv[++i]) * 1024;
else if (!strcmp(argv[i], "--level") && i + 1 < argc)
level = atoi(argv[++i]);
else if (!strcmp(argv[i], "--threads") && i + 1 < argc)
threads = atol(argv[++i]);
else if (!in)
in = argv[i];
else if (!outpath)
outpath = argv[i];
else
die("usage: mkscff IN.img OUT.scff [--chunk KB] [--level N] [--threads N]");
}
if (!in || !outpath)
die("usage: mkscff IN.img OUT.scff [--chunk KB] [--level N] [--threads N]");
if (chunk_bytes < 4096 || chunk_bytes > SCFF_CHUNK_MAX ||
(chunk_bytes & (chunk_bytes - 1)))
die("--chunk must be a power of two from 4 to 1024 KB");
if (threads < 1)
threads = 1;
FILE *f = fopen(in, "rb");
if (!f)
die(strerror(errno));
fseek(f, 0, SEEK_END);
img_bytes = (uint64_t)ftell(f);
fseek(f, 0, SEEK_SET);
if (img_bytes == 0 || img_bytes % 4096)
die("the image is not a whole number of 4 KB blocks");
if (!(img = malloc(img_bytes)) || fread(img, 1, img_bytes, f) != img_bytes)
die("cannot read the image");
fclose(f);
nchunks = (uint32_t)((img_bytes + chunk_bytes - 1) / chunk_bytes);
out = calloc(nchunks, sizeof(*out));
outlen = calloc(nchunks, sizeof(*outlen));
if (!out || !outlen)
die("out of memory");
pthread_t *t = calloc((size_t)threads, sizeof(*t));
for (long i = 0; i < threads; i++)
pthread_create(&t[i], 0, worker, 0);
for (long i = 0; i < threads; i++)
pthread_join(t[i], 0);
struct scff_header h;
memset(&h, 0, sizeof(h));
memcpy(h.magic, SCFF_MAGIC, 4);
h.version = SCFF_VERSION;
h.codec = SCFF_CODEC_ZSTD;
h.chunk_bytes = chunk_bytes;
h.nchunks = nchunks;
h.image_bytes = img_bytes;
h.index_off = sizeof(h);
uint64_t *index = malloc(((size_t)nchunks + 1) * sizeof(uint64_t));
uint64_t pos = sizeof(h) + ((uint64_t)nchunks + 1) * sizeof(uint64_t);
for (uint32_t i = 0; i < nchunks; i++) {
index[i] = pos;
pos += outlen[i];
}
index[nchunks] = pos;
h.stored_bytes = pos;
if (!(f = fopen(outpath, "wb")))
die(strerror(errno));
fwrite(&h, sizeof(h), 1, f);
fwrite(index, sizeof(uint64_t), (size_t)nchunks + 1, f);
for (uint32_t i = 0; i < nchunks; i++)
if (outlen[i])
fwrite(out[i], 1, outlen[i], f);
if (fclose(f) != 0)
die("cannot write the output");
printf("mkscff: %llu KB -> %llu KB (%.1f%%), %u chunks of %u KB, zstd -%d\n",
(unsigned long long)(img_bytes >> 10), (unsigned long long)(pos >> 10),
100.0 * (double)pos / (double)img_bytes, nchunks, chunk_bytes >> 10, level);
return 0;
}

View file

@ -109,6 +109,13 @@ cat > "$P/root/.xserverrc" <<'XEOF'
exec /usr/bin/X -nolisten tcp -dumbSched "$@"
XEOF
# What startx runs inside it: Window Maker, which world installs. Without this
# xinit falls back to twm and an xterm, neither of which is in the image.
cat > "$P/root/.xinitrc" <<'XEOF'
#!/bin/sh
exec wmaker
XEOF
# Mount points and runtime directories are alpine-baselayout's; an empty
# directory here only duplicates it.
for d in dev proc sys run tmp srv; do

View file

@ -3,11 +3,16 @@
#
# tools/posix/mkiso.sh [output.iso]
#
# The disc carries one El Torito boot entry and a real directory tree:
# The disc carries one El Torito boot entry and the loader and kernel as files:
#
# /boot/efiboot.img a FAT image holding the loader and the kernel
# /EFI/BOOT/BOOTX64.EFI the same loader, visible as a file on the disc
# /bin /etc /usr ... the staged sysroot, as an ordinary hierarchy
# /schos/kernel the same kernel, for chainloaders (below)
#
# The root filesystem is inside the kernel, as its ramdisk. The disc used to
# carry it a third time as an ISO 9660 tree, which nothing reads -- SchOS has
# no ISO 9660 driver -- and once Xorg was in the root that copy alone was
# 360 MB.
#
# --- Why only one entry -----------------------------------------------------
#
@ -69,7 +74,6 @@ cd "$repo"
BUILD=${BUILD:-build}
STAGING="$BUILD/iso"
SYSROOT=${SYSROOT:-sysroot}
KERNEL="$BUILD/kernel/kernel"
EFIAPP="$BUILD/uefi/BOOTX64.EFI"
@ -113,9 +117,6 @@ cp "$KERNEL" "$STAGING/schos/kernel"
# this FAT image -- not the ISO 9660 tree around it. Without the kernel in here
# the loader comes up and halts with EFI_NOT_FOUND, which is what it did.
#
# The ISO 9660 tree still carries the sysroot for a running system to read; the
# loader's own two files live in here because that is the volume it is handed.
#
# Sized from what actually goes in plus slack, rather than fixed: a hardcoded
# size is a thing that breaks quietly the first time the kernel grows.
EFI_IMG="$STAGING/boot/efiboot.img"
@ -145,15 +146,6 @@ if true; then
mcopy -i "$EFI_IMG" "$KERNEL" ::/schos/kernel
fi
# The sysroot, as an ordinary hierarchy. Copied rather than linked so that
# whatever ends up on the disc is a snapshot, not a view of a tree that may be
# rebuilt underneath it.
if [ -d "$SYSROOT" ]; then
for d in "$SYSROOT"/*; do
[ -e "$d" ] || continue
cp -a "$d" "$STAGING/"
done
fi
# xorriso if it is here, mkisofs otherwise. Both accept the El Torito flags
# below unchanged -- xorriso's -as mkisofs mode exists precisely to be
@ -173,6 +165,11 @@ if command -v xorriso >/dev/null 2>&1; then
# of type 0xEF is what every UEFI firmware looks for on a removable
# disk, MBR rather than GPT because the oldest ones read that best.
HYBRID="-append_partition 2 0xef $EFI_IMG"
# El Torito keeps its own copy of the image in the tree, the layout real
# machines have booted. Pointing it into the partition instead
# (--interval:appended_partition_2:all::, as Arch does) saves a copy, and
# an ASUS laptop stopped listing the stick the day that was tried. With
# the root compressed (SCFF) the copy is only the kernel's size.
elif command -v mkisofs >/dev/null 2>&1; then
MKISOFS="mkisofs"
HYBRID=""