"virtio gpu doesn't follow f8 menu default resolution" -- it did not, and now
that the head mirrors the console that is visible rather than academic.
vgpud sized itself from GET_DISPLAY_INFO, which answers with the size of the
*host's window*: a number set by dragging a corner, that says nothing about the
machine and everything about QEMU. Meanwhile the resolution the F8 menu offered
at boot is the one that was actually chosen, and it is the console's.
So picking 1024x768 at the menu gave a second display still at 1280x800, with
the console letterboxed inside it on the theme ground -- the right pixels in
the wrong mode, on a display that could simply have been the right size.
console_open() now runs *before* bring_up(), and bring_up() takes the console's
mode when there is a console. That ordering is the whole fix and it is not
cosmetic: the DMA allocation is sized from those numbers and fixed for the life
of the process, so a mode chosen after the fact could only ever be smaller than
the window already paid for. A machine with no console to mirror brings up
whatever the device reports, exactly as before.
Verified by driving the F8 menu over the serial line -- 'o', Resolution,
1024x768, boot -- and screendumping both heads over QMP: both come up 1024x768
and are byte-identical, where the virtio head used to stay 1280x800. `schmode
1024 768` from a 1280x800 boot still works. Both supported configurations: one
boot each, no traps, no panics, socktest and memfdtest green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
"but Virtio GPU is still non functional. it only shows logo" -- and that was
a fair reading of it. A second monitor showing a mark for ever is
indistinguishable from a second monitor that does not work.
The mark was the right answer to the question hybrid.md asked: H2 and H3 needed
a screenshot that proved a *userspace process* had drawn something, and that
the driver came back after being killed. Both are closed. What was left was a
display whose entire output was a logo, on a machine whose console was sitting
on the other head.
So vgpud mirrors the console onto it.
**A copy with a diff.** /dev/fb0 maps the console's framebuffer; the virtio
backing store is already mapped, because vgpud allocated it. The backing
therefore holds, at all times, exactly what was last sent to the device --
which means it *is* the previous frame, and "what changed" needs no third
buffer. Ten times a second: compare rows, skip the ones that match, copy the
ones that do not, transfer the one rectangle spanning the rows that changed.
Hence present() becoming present_rect(). Moving one line of text through a
full-screen transfer of a 1280x800 resource is most of a megabyte of copy for
a few hundred bytes of difference, and this loop runs whether or not anything
is happening.
Mapping /dev/fb0 does not take the screen from the console: the pages are the
ones the console draws into, and fb_mapped() only arranges a redraw when the fd
is closed. This one never is.
Sizes differ, so a head larger than the console centres it on the theme ground
the mark already draws, and a smaller head shows the top-left corner -- which
is where a terminal puts everything worth seeing. Scaling is the alternative
and it is the wrong one; text resampled to a non-integer ratio is text you
cannot read. The blocking accept() became poll() with the frame interval as its
timeout, because holding the device is no longer the only thing to do.
Not the endpoint: a compositor will want this head for itself, and when there
is one this goes away. Until then the honest thing for a working display to
show is the system that is running on it.
**And a bug older than any of it.** schmode reported "cannot reach vgpud on
/run/vgpud -- it is not running" about a process that was running perfectly
well. Every boot after the first, on any image whose root is writable, and
nothing to do with this change -- it reproduces on the previous commit, 3 runs
in 4.
bind() creates a node at the path, and /run is a directory on the root
filesystem rather than a tmpfs. The node outlived the process that bound it and
the boot it was bound in, so the next boot's vgpud claimed the device, drew,
held it -- and then failed bind() on a name that already existed. A driver
nobody could talk to, and an error message naming the wrong cause.
It unlinks before binding now, which is what a Unix daemon does. The usual
objection, that this lets a second instance steal a live socket, does not apply
here: two vgpuds cannot coexist, because the first holds the PCI claim and the
second is refused the device long before it reaches bind(). The claim is the
mutual exclusion; the socket node never was.
Verified by screendumping both heads over QMP: with the console at 1280x800 the
two are byte-identical, and after `schmode 1024 768` the virtio head carries
the console at the new mode. Three consecutive boots of one image all reach a
working control socket, where the previous commit managed one. Both supported
configurations: one boot each, no traps, no panics, socktest and memfdtest
green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Booting with a virtio-gpu as the *only* display reboot-looped. Not a hang, not
a panic: the firmware came back round to the boot menu, started the loader
again, and did it again, for ever, with nothing to say why.
virtio-gpu has no linear aperture, so OVMF's virtio GOP implements Blt and
nothing else. On a machine with one and nothing else, the firmware reports a
width and a height and a FrameBufferBase of **zero**:
schos: framebuffer 0x0000000000000000 0x500x0x320
Two places took that at face value.
**kmain() tested `if (bi.ptr && ...)` with bi.ptr already run through P2V()**,
and P2V(0) is KERNBASE, which is not zero. So the guard could not fail -- and
its own comment, three lines above, said "drawing into address zero is a worse
failure than staying quiet". The intent was exactly right and the test could
never enforce it. It reads the physical value first now and tests that.
**vga_fb_init() attached the scanout regardless**, one layer down, so fixing
kmain alone just moved the same write.
Either one had gfx_clear() write four megabytes over physical 0: the real-mode
IVT, the BIOS data area, the AP trampoline at 0x7000, the early page tables.
Hence a reset with no output -- there was nothing left to run.
Both refuse now, and the refusal says what it is:
vga: firmware reported a 1280x800 display with no framebuffer address;
no graphical console
A machine with no usable linear framebuffer gets no graphical console. That is
the truth about it, and the serial line still has everything.
**It still does not reach a shell on such a machine**, and that is recorded
rather than implied: the console path assumes a framebuffer -- vt_init()
reports a terminal of `0 by 0` cells -- and that path has never been exercised.
What this fixes is a machine that destroyed low memory and reset; what it does
not do is make a headless boot work. Supported remains stdvga, with virtio-gpu
optional beside it, which is what docs/architecture/graphics.md already said.
Both supported configurations verified: stdvga alone and stdvga with
virtio-gpu, one boot each, no traps, socktest and memfdtest green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Guru and SchCotie are gone. Shift is the only mark, and the crash screen draws
it where the face used to be.
Guru was an icon -- a pictogram with a face, denoting one state -- that lived
on the crash screen and nowhere else. Its argument was identity: `:(` is
recognisably Microsoft's, and a crash screen is the single screenshot most
likely to leave the machine, so it is the worst place to be wearing somebody
else's face. That part holds, and it is kept in the branding README because it
is the argument against any second mark. What does not follow is that the
answer is a *new* mark. Shift already signs the system, it is already what the
machine drew on the splash it started with, and a reader who has seen it twice
before the crash recognises it faster than a face they have never seen.
SchCotie was a mascot, and never existed: the file said so itself under "what
this does not cover". A brief for a character nobody is about to draw is not
worth carrying.
So `draw_guru()` and its SVG-space geometry leave panic.c, the two `guru.svg`
files are deleted, and PSOGM calls gfx_draw_logo() -- the same include/logo.h
mask the loader splash and the boot console composite. At its native 128 px:
there is no resampler in the crash path, and the face was scalable in a place
that only ever drew it at one size.
The name stays. "Purple Screen of Guru Meditation" is the Amiga nod, which
panic.c's own header already called out as a separate thing from the mark.
**And taking the screenshot to check it caught a regression from the TTF
console work.** `gfx_draw_char()` -- the pixel-positioned path, which only
PSOGM uses -- still walked `font_data[c][row]` for `row < CHAR_HEIGHT`.
font_data has exactly 16 rows per glyph; CHAR_HEIGHT is the *TrueType* cell
height now, about 21. So every line of the crash report drew five rows of the
next character's bitmap underneath itself, and it looked like each line had
been printed twice. The grid path had been updated when the cell size stopped
being a constant; this one had not, because nothing routine draws text in pixel
coordinates. Both go through one draw_glyph_px() now, and the bitmap's own
geometry is spelled BITMAP_W/BITMAP_H so it cannot be mistaken for a cell size
again.
Two stale claims corrected while in there, in panic.c's header and in
graphics.md: PSOGM has not dimmed-and-floated over the old screen for a while
-- it clears to full purple, and the file's own code comment says why -- and
there is no longer "a face that is obviously a cartoon".
The website's crash section shows the mark in flat white rather than its own
violets, which is what the kernel actually draws: PSOGM composites a coverage
mask in one colour, and those violets sit close enough to #4B1D9A to
half-disappear against it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
"Ą Ę Ś Ł Ż" drew as "?????" and the obvious diagnosis -- the font has no
glyphs for them -- was wrong twice over. `struct vtcell.ch` was **one byte**
holding a CP437 index, and the console mapped UTF-8 down to it on the way in.
U+0104 does not fit in a byte. No font could have fixed that; the cell had to
get wider first.
So: `ch` is a uint32 codepoint, the mapping on the way in is gone, and the cell
draws through kernel/dev/ttf.c against JetBrains Mono linked into the image.
Cell size comes from the font's measured advance instead of a hardcoded 8x16.
The grid costs 864 KB of BSS instead of 432 KB, which is the honest price of
holding a codepoint per cell for six terminals at 240x75.
The rasteriser needed three fixes before it could be trusted with this.
**21 KB of locals on a 4 KB kernel stack.** `ttf_render` held an outline (512
points as three arrays) and a raster (1024 edges) as automatics. KSTACKSIZE is
4096. They walked off the bottom of the stack into whatever was underneath,
which surfaced as user processes faulting on addresses they never touched and
then a panic inside freevm() on an overwritten page directory. Nothing pointed
anywhere near a font.
**And then again, recursively.** load_outline() held a 4736-byte outline as a
local *and recursed* for composite glyphs. Composite glyphs are exactly Ą, Ę,
Ś, Ł, Ż -- so the second crash arrived the moment the first fix let Unicode
reach the screen, looking like a fresh bug. Now a static outline per recursion
level with a depth cap of 4 (the font's deepest real nesting is 2), and the
leaf parse split into load_simple() so its 512-byte flag array is not
multiplied by the depth. Worst chain measured from the object file: ~2.3 KB.
Neither showed in tools/ttftest, which has an 8 MB stack. That is the standing
hazard of one source file built for host and kernel: the host is most forgiving
about exactly the resource the kernel is strictest about.
**Component transforms were being skipped**, on the stated theory that a wrong
transform is worse than none. It is not. `‘` in this font is the comma glyph
with a scale of -1 -- a 180-degree rotation -- and the offset that ships with
it only lands correctly after the rotation. Without it the mark went twelve
pixels into a ten-pixel cell and was clipped away entirely, so the character
read as missing from the font rather than as misplaced. F2Dot14 2x2 now, with
SCALED_COMPONENT_OFFSET honoured.
The glyph cache was ASCII-only, so every accented letter re-rasterised on every
repaint: a screenful of Polish did hundreds of bezier flattenings per scroll
where the same screenful of English did none. 95 reserved ASCII slots plus 224
shared slots keyed by codepoint, 64 KB pool. Verified 13680 cached renders
byte-identical to direct ones across collisions and a pool flush.
tools/ttftest speaks UTF-8 now. Iterating bytes meant it could only ever ask
for ASCII, and ASCII is entirely simple glyphs -- it could not reach the code
path that mattered.
Also: resolvconf's success line moves behind /proc/bootmode, same rule as
vgpud. A quiet boot opened with "resolvconf: nameserver 10.0.2.3" where the
one line was supposed to be "Welcome to SchOS!". Failures stay loud.
Verified on screen, not just on the host: Ą Ę Ś Ł Ż ć ń ó ź, Ω λ π Œ Ð Æ ß,
∞ ‘x’ → ≤ all render, panic=0 across four boots.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two things, both about the debugger being available at the moment you
actually need it rather than once a shell exists.
**Ctrl-G on the console runs every view.** No filesystem, no /dev, no
shell -- it is the last thing main() sets up before entering the scheduler,
so it answers from early in boot, which is exactly when boot problems
happen. It sits beside the older Ctrl-P, and the difference between them is
the interesting part: procdump() takes no lock at all, deliberately,
because a lock is a way to hang while reporting a hang. The views cannot
follow that rule -- a free list read torn is worse than not read -- so they
take locks, and a lock must never be taken from an interrupt handler that
may have interrupted its holder. The key therefore *requests* and a kernel
thread *performs*. A scheduler too wedged to run a thread will not answer
Ctrl-G; Ctrl-P still will, and that division is the honest one.
**PSOGM says where the report went.** Its closing line used to be an
addr2line command, which the call chain no longer needs -- it prints names
now. What replaced it is the thing nobody would guess:
This report is in /proc/kmsg after a restart.
schdbg, or Ctrl-G on the console, asks more.
PSOGM -- Purple Screen of Guru Meditation
"Restart and read it" beats "photograph this screen" by a wide margin, and
until the log outlived the kernel it was not true. Both panic.c and
graphics.md had claimed in writing that the screen was the only artefact of
a crash; neither says so now. The QR code stays, because it is still the
only channel that works when the machine will not come back at all.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VQ7Qx8E1q1M1bbCq8QLHS2
Two ANSI bugs, both found by running fastfetch rather than by reading the
parser, and both invisible to anything that spells its escapes out in full.
ESC[m with no parameters means ESC[0m. ECMA-48 is explicit that an omitted
parameter takes its default and SGR's default is 0, and sgr() iterated its
parameter list -- which is empty in exactly that case, so the loop body never
ran and the reset was silently dropped.
Programs emit the bare form constantly. fastfetch writes "\x1b[32m1%\x1b[m", so
everything after the first coloured character stayed green forever. With a
background colour it is much worse: its palette swatch sets a white background
and resets, the reset did nothing, and the console went white-on-white and
stayed there -- filling the screen with white rectangles as it scrolled, which
looks like a corrupted framebuffer rather than one missing branch.
OSC sequences were not a state at all, so ESC ] fell through and the payload
printed as text: a hyperlinked path arrived as "Disk (]8;;file:///\]8;;\)".
Nothing here acts on an operating-system command -- there is no window title and
no hyperlink to follow -- but *discarding* one is not optional, because the
alternative is printing somebody's URL into the middle of their output. ESC \
ends it, and an ESC followed by anything else is handed back to the escape state
rather than eaten, so a malformed OSC does not swallow the sequence after it.
Verified by screendumping the framebuffer after two consecutive fastfetch runs:
identical output both times, near-white coverage down from most of the screen to
2% (the palette swatch's own white cell), and the disk line reading "Disk (/)".
Also installs docs/branding/Ascii/ascii_50.txt as /usr/share/schos/logo.txt --
Shift as characters, at the width that leaves room for an info block on an
80-column console. Installed from the branding directory rather than copied into
sysroot by hand, so there is one source.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VQ7Qx8E1q1M1bbCq8QLHS2
docs/ was ten files where a plan, a description, a guide and a bug report all
sat next to each other. It is now architecture/ (what SchOS is), plan/ (what it
will be), guides/ (how to work on it) and history/ (why it is like this), with
an index that states the one rule holding it together: when something is
finished it moves from plan/ to architecture/ and is rewritten as a description
of the thing rather than a record of building it.
A ticked checkbox says the work happened. It does not say what the work
*produced*, and six months later the checkbox is all that is left. The network
stack proved the point: finished, verified end to end, and for a month the only
description of it anywhere was a checkbox four levels of indentation deep.
So roadmap.md went from 534 lines to 181 -- nothing deleted, the done work
graduated. Seven subsystems that were finished and undescribed now have
descriptions: boot, kernel, syscalls, filesystem, networking, graphics, usb,
init. Every one ends with "What it does not do", which is the section that pays;
a document listing only strengths is marketing, and the reader who needs it most
is the one about to trip over a limitation nobody wrote down.
schfs.md still opened with "phases 1 through 3 1/2 are done, phase 4 is next"
long after all five had landed. It is a description now, with the phase record
and the hard-won bugs moved to history/.
Three corrections found on the way, all load-bearing:
* projects/schcom/README.md claimed SCM_RIGHTS was done. There is no
sendmsg/recvmsg in the kernel at all, and an entire GUI plan rested on it.
* The root README said "No TCP/IP" months after the stack landed.
* history/kernel.md sec.7 was marked "in progress" on a problem that process
groups solved.
Also adds plan/1.0.md, which draws a finish line so the open list stops being
infinite, and marks schwm/ and schde/ superseded: in Wayland the compositor is
the window manager, so that split dissolves rather than being abandoned.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VQ7Qx8E1q1M1bbCq8QLHS2