Version: v0.1-draft Status: Design Target Release: v0.52+ Issue: #465 ABI Impact: New
ObType::Display(23), newObInfoClass(43), newObSetInfoClass(52–54)
NeoDOS can only draw to the screen from Ring 0, and only as a text console. There is no path for a Ring 3 process to produce pixels.
neodos-kernel/src/graphics/mod.rs is a kernel-only singleton:
pub struct FramebufferInfo {
pub base_address: u64,
pub size: usize,
pub width: usize,
pub height: usize,
pub stride: usize,
}
pub static RENDERER: Mutex<Option<Renderer>> = Mutex::new(None);The Renderer walks the GOP framebuffer obtained by the UEFI bootloader
(neodos-bootloader/src/main.rs) and exposes only put_pixel() (a
write_volatile) and clear(). It is consumed exclusively by
console/ and font.rs to render the 4 virtual terminals.
- No user-facing display object.
ObType(object/types.rs) has no Display/GraphicsDevice/Framebuffer variant.ObInfoClass/ObSetInfoClasshave no video classes. - No framebuffer mapping to Ring 3. The GOP framebuffer is mapped by the
kernel as UC- and, when above 4 GiB, through
map_phys_range_above_4g()(arch/x64/paging.rs). None of that is exposed to Ring 3. Sectionis not suitable.object/section.rsalready providesalloc_section/map_view, but:- sections are capped at
size <= 0x100000(1 MB) andMAX_SECTIONS = 32; - a view is backed by freshly allocated anonymous 4 KB pages
(
mmap_alloc_page), not by the physical scanout buffer; - there is no present/flip/scaling — a section is just shared memory.
- sections are capped at
- Per-pixel MMIO from Ring 3 would be unusable. Even if the raw FB were
mapped, uncacheable
write_volatileper pixel scales terribly (the console already avoids it withrep stosd), and it would expose the physical format and address to userland. - No format/resolution abstraction. Doom renders at 320×200 (or 640×400) with an 8-bit palette; the GOP mode is typically 800×600 XRGB. Something must own conversion + scaling + present.
- No VT/hardware-mode coordination. Section 11 of the source of truth gives each VT a text shadow buffer. A graphical client must be able to take over a VT, suppress text output on it, and restore the console afterwards.
| File | Role | Relevant to display? |
|---|---|---|
neodos-kernel/src/graphics/mod.rs |
FramebufferInfo, Renderer, RENDERER |
YES — kernel-only pixel path |
neodos-kernel/src/console/mod.rs |
Text rendering, scroll, cursor | YES — must yield FB in graphics mode |
neodos-kernel/src/graphics/font.rs |
Bitmap glyph blitting | No |
neodos-kernel/src/input/vt.rs |
Per-VT shadow buffers | YES — VT ownership/mode |
neodos-kernel/src/input/manager.rs |
active_vt, per-VT state |
YES — present gating |
neodos-kernel/src/object/section.rs |
Anonymous shared memory | Partial — not FB-backed |
neodos-kernel/src/arch/x64/paging.rs |
FB UC- mapping, mmap 4 KB demand paging | YES — map back buffer |
neodos-kernel/src/object/types.rs |
Ob types / info classes | YES — new variants |
neodos-bootloader/src/main.rs |
GOP FramebufferInfo |
No (already consumed) |
libneodos/ |
User-mode library | YES — new display module |
This document designs the display / framebuffer API for Ring 3. It does not
design a compositor, window manager, or GPU acceleration; those are follow-ups
(ROADMAP.md v3.x). The first delivery target is "a Ring 3 process can blit a
software surface to the screen at a stable rate" — sufficient for Doom.
+--------------------------------------------------------------+
| Ring 3 client (e.g. Doom) |
| display.open() -> info() -> map() -> [render] -> present() |
+------------------------------+-------------------------------+
| int 0x80 (ob_open/query/set_info)
v
+--------------------------------------------------------------+
| Display subsystem (Ring 0) |
| +----------------+ +-----------------+ +----------------+ |
| | \Device\Display | | Surface (back | | Present engine | |
| | ObType::Display | | buffer, frames) | | convert+scale | |
| +----------------+ +-----------------+ +----------------+ |
| | | | |
| +------+---------------------+--------------------+-------+ |
| | VT binding (vt_num, graphics mode) | |
| +----------------------------------------------------------+ |
+------------------------------+-------------------------------+
| kernel memcpy/scale
v
+--------------------------------------------------------------+
| GOP framebuffer (UC-, maybe >4 GiB) |
+--------------------------------------------------------------+
Key idea: the kernel owns the physical framebuffer; the client owns a
cacheable back buffer mapped into its mmap region; present copies/ converts /
scales back buffer → framebuffer.
A single kernel-created singleton object:
| Property | Value |
|---|---|
| Path | \Device\Display |
ObType |
Display = 23 |
| Created | display::init() at boot, after GOP info is available (Phase 3.86) |
| User-creatable | No (ob_create rejects it) |
| Open access | READ (info) / WRITE (map + present) |
Type-number allocation (resolved).
ObType::Display = 23. The Font Manager draft (docs/design/font-manager-design.md) has been renumbered toObType::Font = 24. The authoritative registry remainsobject/types.rs.
The Display owns one or more surfaces. A surface is:
- a set of physical frames allocated from the frame allocator
(
hal::alloc_page), owned by the Display per INV-5; - mapped into the opening process's mmap region
(
0x2000_0000..0x2200_0000) as writable cacheable user pages, satisfying Rule 3.2.3 (user-accessible PTEs only in allowed regions); - in a canonical format independent of the physical FB.
Canonical surface format for v1: XRGB8888 (32 bpp, little-endian
0x00RRGGBB). Rationale: matches the overwhelmingly common GOP format, avoids
palette handling in the kernel, and gives Doom a trivial 8→32 expansion.
Indexed8 (a single shared palette + 8-bit indices) is a possible later
surface format for low-memory clients; not in v1.
Surfaces are sized width * height * 4, contiguous virtual, page-granular
physical. MAX surface = 4 MB (covers 1024×768×4) for v1.
present moves pixels from the surface to the GOP framebuffer:
- Format conversion —
XRGB8888→ FB format. GOP exposes onlybase_address/size/width/height/stridetoday; the pixel format is assumedXRGB8888for v1 and validated/params extended later. - Scale — if
surface.width/height != fb.width/height, nearest-neighbor scale. This is the path Doom needs (320×200 → 800×600). - Copy — for the 1:1 case, one
memcpyper row (rep movsd-style, same primitive the console already uses) instead of per-pixel volatile writes. - Damage/dirty rect —
presentaccepts an optional rect. Full-screen present when the rect is null. Dirty rects are a v1.1 optimization.
Present is synchronous within the syscall for v1 (the caller blocks until the copy is issued). A queued/IRP path is future work (see §9).
The source-of-truth VT model (§11) is preserved:
- A client is bound to the VT it is running on (
eprocess.vt_num). - The first successful
DisplayMapBufferclaims the VT for graphics unless another owner holds it. - Each VT gains a mode:
pub enum VtMode {
Text,
Graphics { owner_pid: u32 },
}- Present gating:
presentrenders only whenactive_vtmatches the client's VT. When inactive, the present is a no-op (the surface is still writable) so background clients do not corrupt the visible VT. - Console suppression: while a VT is in
Graphics,consoleMUST NOT draw text to the physical framebuffer for that VT (cursor hidden, writes go to the shadow buffer only). On return toText, the console clears and redraws from the VT shadow buffer — reusing the existing switch redraw path. - VT switch away/back: switching away saves nothing extra (the surface is in RAM); switching back re-presents the last surface. No redraw of console over an active graphics VT.
- Release: on process exit or handle close, the surface is freed, views
unmapped, and the VT returns to
Text.
pub struct DisplayInfo {
pub fb_width: u32,
pub fb_height: u32,
pub fb_stride: u32, // in pixels
pub fb_format: u32, // DisplayFormat::Xrgb8888 = 1
pub surface_width: u32, // current back buffer size
pub surface_height: u32,
pub flags: u32, // bit0: double_buffered, bit1: front_owner
pub _pad: u32,
}
pub struct DisplayMap {
pub width: u32, // requested (0 = native fb)
pub height: u32,
pub format: u32, // must be Xrgb8888 in v1
pub _pad: u32,
}
pub struct DisplayPresent {
pub x: i32, // damage rect; 0,0,0,0 => full frame
pub y: i32,
pub width: u32,
pub height: u32,
pub flags: u32, // bit0: use damage rect
pub _pad: u32,
}pub struct Surface {
pub width: u32,
pub height: u32,
pub stride: u32,
pub format: u32,
pub frames: Vec<u64>, // physical frames (owned)
pub user_va: u64, // mapped base in mmap region (0 = unmapped)
pub size: u64,
}
impl Surface {
pub fn alloc(width: u32, height: u32, format: u32) -> Option<Surface>;
pub fn map_into_current(&mut self) -> Result<u64, DisplayError>;
pub fn unmap(&mut self);
pub fn free(self);
}pub fn present(surface: &Surface, fb: &FramebufferInfo, rect: Option<DamageRect>);
fn scale_nearest(src: &[u32], dst: *mut u32, sw: u32, sh: u32, dw: u32, dh: u32);
fn convert_xrgb(src: &[u32], dst: *mut u32, n: usize);pub struct DisplayObOps;
impl ObOperations for DisplayObOps {
fn on_destroy(&self, _id: ObId, _native_id: u64) {
// release VT, free surfaces, unmap views
}
}
pub static DISPLAY_OPS: DisplayObOps = DisplayObOps;Current highest ObInfoClass is 42; highest ObSetInfoClass is 51.
| Value | Name | Direction | Payload |
|---|---|---|---|
| 43 | DisplayInfo |
out | DisplayInfo (32 bytes) |
| Value | Name | Admin? | Payload |
|---|---|---|---|
| 52 | DisplayMapBuffer |
No | DisplayMap (in); returns surface VA |
| 53 | DisplayPresent |
No | DisplayPresent (in); returns 0 |
| 54 | DisplayFreeBuffer |
No | none; returns 0 |
DisplayMapBuffer/DisplayFreeBuffer are idempotent per handle: mapping twice
returns the existing VA; freeing twice is a no-op. present before mapping
returns -InvalidParam.
pub fn display_init(fb: FramebufferInfo); // boot, creates \Device\Display
pub fn display_info() -> DisplayInfo;
pub fn display_claim_vt(pid: u32, vt: u8) -> Result<(), DisplayError>;
pub fn display_release_vt(pid: u32);
pub fn display_present(pid: u32, rect: Option<DamageRect>) -> Result<(), DisplayError>;
pub fn display_vt_mode(vt: u8) -> VtMode;display_present checks vt_mode(eprocess.vt_num) == Graphics{owner: pid} and
active_vt == vt before touching the framebuffer.
New module libneodos/src/display.rs:
pub struct Display { fd: u8 }
pub struct Surface { pub base: *mut u32, pub width: u32, pub height: u32, pub stride: u32 }
impl Display {
pub fn open() -> Result<Display, i64>; // ob_open("\\Device\\Display", RW)
pub fn info(&self) -> Result<DisplayInfo, i64>; // ObInfoClass::DisplayInfo
pub fn map(&self, w: u32, h: u32) -> Result<Surface, i64>; // DisplayMapBuffer
pub fn present(&self, rect: Option<(i32,i32,u32,u32)>) -> Result<(), i64>;
pub fn free_buffer(&self) -> Result<(), i64>;
}Plus a convenience helper for software renderers:
pub fn blit_indexed8(palette: &[u32; 256], src: &[u8], dst: &mut [u32], count: usize);This is the exact primitive an 8-bit Doom renderer needs and keeps the palette conversion out of the kernel.
Phase 3.86 (after memory/paging and before the userland loader; near the Keyboard Manager at 3.875):
display_init(boot_info.fb_info)— storeFramebufferInfo, create\Device\Display(ObType::Display), installDISPLAY_OPS.- Initialize all VTs to
VtMode::Text.
Failure is non-fatal and consistent with graphics::init: if
fb.base_address == 0, the Display object is still created but
DisplayMapBuffer/present return -NoDev.
presentruns in syscall context, never in IRQ context (INV-2/INV-3).- The Display state (
Mutex<DisplayState>) is held only for bookkeeping, not across the pixel copy beyond what is required; the copy is not an allocation. - Frame ownership: surface frames are owned by the Display until
free(INV-5).presentonly reads them. - No dependency on scheduler, VFS, or block drivers (INV-1).
Map the GOP framebuffer UC- into the user mmap region and let apps write pixels directly.
Rejected because:
- Uncacheable per-pixel writes are slow; a 320×200×35 fps loop would crawl.
- Exposes the physical address and exact GOP pixel format to userland.
- No scaling, so every app must know the native mode.
- No VT gating: a background process could corrupt the visible VT.
- A malicious/buggy app can scribble over the whole scanout.
Allocate the surface as an anonymous Section and SectionMapView it.
Rejected because:
- 1 MB section cap (
size > 0x100000rejected) and only 32 slots; a 640×400 surface is exactly 1 MB and leaves no room. - Sections are RAM-backed, not tied to the FB, so a present/copy path is still required.
- No VT integration, no format/scale policy, no singleton display semantics.
Sections remain the right tool for generic shared memory; Display is a different abstraction.
sys_draw_rect(x, y, w, h, ptr) copying from a user buffer.
Rejected because:
- One syscall per blit adds overhead and forces the kernel to own the scale/convert of arbitrary user data.
- Wins only if userland cannot map memory — which it can (mmap).
- Can be layered later as a convenience, not as the base primitive.
Drive a VirtIO-GPU device instead of GOP.
Deferred: the platform currently targets GOP/OVMF. VirtIO GPU is tracked
with the VirtIO block (VIO-*) and is post-1.0 (ROADMAP.md v1.3).
| Subsystem | Change | Impact |
|---|---|---|
Object Manager (object/) |
ObType::Display=23; register \Device\Display; DisplayObOps |
Moderate |
Display (display/ — NEW) |
mod.rs, surface.rs, present.rs, object/display.rs |
New subsystem |
Graphics (graphics.rs) |
Expose FramebufferInfo accessors; no pixel path change |
Low |
Console (console/) |
Skip physical text draw when VT mode is Graphics | Moderate |
VT/Input (input/vt.rs, input/manager.rs) |
VtMode, per-VT graphics owner |
Moderate |
Paging (arch/x64/paging.rs) |
Map owned frames into mmap region as user-writable | Moderate |
Syscall (syscall/handlers.rs, ob/query, ob/set) |
Dispatch DisplayInfo (43), DisplayMapBuffer/Present/FreeBuffer (52–54) |
Moderate |
object/types.rs |
New ObType + info classes | Low |
libneodos |
New display.rs, export table entry |
Moderate |
Boot (main.rs) |
display::init() at Phase 3.86 |
Low |
| NeoDev | None (no new on-disk artifacts) | None |
| Docs | objects.md, syscalls.md, overview.md |
Low |
| Aspect | Detail |
|---|---|
| Path | \\Device\\Display |
| Access | READ for info, WRITE for map/present |
| Returns | fd to a ObType::Display object |
| Errors | -NoDev if the display is not initialized, -NotFound if path invalid |
| Aspect | Detail |
|---|---|
| Args | fd, class=43, buf, size |
| Returns | Bytes written (32) |
| Errors | -InvalidType if fd is not Display, -Fault if buf invalid, -InvalidParam if size < 32 |
| Preconditions | fd from ob_open on \Device\Display |
| Postconditions | buf contains DisplayInfo |
| Aspect | Detail |
|---|---|
| Args | fd, class=52, buf (DisplayMap), size |
| Returns | Surface base VA (≥ 1) in the caller's mmap region |
| Errors | -InvalidParam (bad size/format), -NoMem (allocation failed), -Busy (VT owned by another PID), -NoDev (no FB), -Perm (no WRITE access) |
| Preconditions | Caller has WRITE; size in 1..=1024×768; format Xrgb8888 |
| Postconditions | Frames allocated and mapped user-writable; VT claimed for caller |
| Aspect | Detail |
|---|---|
| Args | fd, class=53, buf (DisplayPresent), size |
| Returns | 0 |
| Errors | -InvalidParam (no surface mapped / bad rect), -NoDev, -Perm |
| Preconditions | DisplayMapBuffer succeeded; caller owns the VT |
| Postconditions | Surface blitted (converted/scaled) to the active FB, or no-op if VT inactive |
| Notes | Never blocks on IRQ/alloc; runs to completion in syscall context |
| Aspect | Detail |
|---|---|
| Args | fd, class=54, buf=NULL, size=0 |
| Returns | 0 |
| Errors | -Perm |
| Postconditions | Views unmapped, frames freed, VT released (→ Text) |
Closing the last Display handle for a PID (or sys_exit) MUST run the same
cleanup as DisplayFreeBuffer: unmap, free frames, release the VT
(ObOperations::on_destroy).
| Test | Expected |
|---|---|
display_object_created_at_boot |
ob_open("\\Device\\Display") succeeds |
display_info_fields |
DisplayInfo width/height/stride match BootInfo |
display_info_wrong_type |
Query class 43 on a Pipe fd → -InvalidType |
display_info_small_buffer |
size < 32 → -InvalidParam |
display_not_creatable |
ob_create(Display) → -InvalidType |
| Test | Expected |
|---|---|
display_map_native |
DisplayMap{0,0} returns VA and footprint fb_w*fb_h*4 |
display_map_custom |
320×200 returns a valid VA |
display_map_writable |
Write a pixel through the VA, read it back |
display_map_too_large |
4096×4096 → -NoMem |
display_map_bad_format |
format ≠ Xrgb8888 → -InvalidParam |
display_map_twice_idempotent |
Second map returns the same VA |
display_map_wrong_vt_owner |
Second PID on same VT → -Busy |
| Test | Expected |
|---|---|
display_present_unmapped |
Present before map → -InvalidParam |
display_present_native |
1:1 copy: FB pixel equals surface pixel |
display_present_scaled |
320×200 → 640×400: corner pixels map by nearest neighbor |
display_present_inactive_vt_nop |
Present while active_vt != vt: FB unchanged |
display_present_damage |
Only the damage rect changes |
display_present_format |
0x00RRGGBB lands correctly in FB (XRGB) |
| Test | Expected |
|---|---|
vt_mode_starts_text |
All VTs are Text at boot |
vt_mode_graphics_on_map |
Claimed VT becomes Graphics{owner} |
console_suppressed_in_graphics |
sys_write to console does not touch FB |
vt_switch_back_redraws |
Switching to a Text VT redraws from shadow buffer |
vt_release_on_free |
FreeBuffer returns VT to Text |
vt_release_on_exit |
Process exit releases the VT and frees frames |
| Test | Expected |
|---|---|
display_frames_freed_on_free |
Frame allocator bitmap returns to baseline |
display_frames_freed_on_exit |
No leak across process exit (INV-5) |
display_no_alloc_in_present |
Present path performs no heap allocation |
display_user_va_in_mmap |
Surface VA ∈ 0x2000_0000..0x2200_0000 (Rule 3.2.3) |
| Test | Expected |
|---|---|
display_fill_and_present |
Kernel/user test fills the surface with a pattern, presents, samples FB |
doom_smoke (userland) |
A .NXE fills 320×200 with moving pattern at 35 fps without tearing-induced faults |
| Path | Description |
|---|---|
neodos-kernel/src/display/mod.rs |
Display core: types, init, VT binding, public API |
neodos-kernel/src/display/surface.rs |
Surface allocation/mapping/free |
neodos-kernel/src/display/present.rs |
Format conversion + nearest-neighbor scaling + blit |
neodos-kernel/src/object/display.rs |
DisplayObOps lifecycle |
libneodos/src/display.rs |
User-mode display/surface API + blit_indexed8 |
| Path | Change |
|---|---|
neodos-kernel/src/object/types.rs |
ObType::Display=23; ObInfoClass::DisplayInfo=43; ObSetInfoClass::DisplayMapBuffer=52, DisplayPresent=53, DisplayFreeBuffer=54 |
neodos-kernel/src/syscall/handlers.rs |
Dispatch new info/set classes |
neodos-kernel/src/syscall/ob/query/mod.rs |
DisplayInfo query |
neodos-kernel/src/syscall/ob/set/mod.rs |
Map/Present/Free handlers |
neodos-kernel/src/graphics/mod.rs |
Accessors for FramebufferInfo; no longer the only writer of the FB |
neodos-kernel/src/console/mod.rs |
Respect VtMode::Graphics (suppress physical text) |
neodos-kernel/src/input/vt.rs |
VtMode definition |
neodos-kernel/src/input/manager.rs |
Per-VT mode + owner, present gating helpers |
neodos-kernel/src/arch/x64/paging.rs |
Map display frames into the mmap region as user pages |
neodos-kernel/src/boot/mod.rs |
mod display; + display::init(fb_info) at Phase 3.86 |
libneodos/src/lib.rs / export |
Register the display module/export entry |
docs/kernel/objects.md |
Display ObType, namespace, info classes |
docs/kernel/syscalls.md |
Document classes 43, 52–54 |
docs/architecture/overview.md |
Display subsystem in the map |
docs/development/testing.md |
New test groups |
- Add
ObType::Display = 23and the info/set class variants. - Create
display/mod.rswithDisplayInfo,DisplayMap,DisplayPresent,DisplayError. display_init(fb): store FB info, create\Device\Displayviaob_create_object_path, installDISPLAY_OPS.cargo build+ boot smoke (object visible viakobj).
- Implement
Surface::alloc(frame allocator, page-granular). - Implement
map_into_current(map frames into the mmap region user-writable cacheable) andunmap/free. - Tests from §6.2 (without VT yet).
- Implement
convert_xrgb(identity for v1) andscale_nearest. - Implement
present(surface, fb, rect)with 1:1memcpyfast path. - Tests from §6.3 (without VT gating).
- Wire
DisplayInfo(43),DisplayMapBuffer(52),DisplayPresent(53),DisplayFreeBuffer(54). - Handle cleanup on
close/exit viaDisplayObOps::on_destroy. - Tests from §6.1/6.5.
- Add
VtModetoinput/vt.rs, per-VT mode/owner toinput/manager.rs. - Gate present on
active_vt; suppress console text in Graphics mode. - Release on free/exit; restore text on VT switch back.
- Tests from §6.4.
display.rswithDisplay/Surfaceandblit_indexed8.- Export table entry + link.
- Build a small
.NXEsmoke test that fills/presents.
- Update
objects.md,syscalls.md,overview.md,testing.md. cargo buildinneodos-kernel/,neodev test,neodev check-deps,npx markdownlint '**/*.md' --config .markdownlint.json.display_fill_and_presentend-to-end on QEMU.
| Item | Priority | Complexity | Description |
|---|---|---|---|
| Dirty-rect present | High | Low | Present only changed regions |
| Double buffer / flip | High | Medium | Two surfaces, flip on present (tearing control) |
| Vsync / vblank hook | Medium | Medium | Tie present to the 1 kHz timer or a vblank IRQ |
Indexed8 surfaces |
Medium | Medium | Shared palette, 8-bit surfaces for low-memory clients |
| Cursor plane | Medium | Medium | Hardware/software cursor composited at present |
| Format negotiation | Medium | Low | Read GOP pixel format instead of assuming XRGB8888 |
| Async present via IRP | Low | High | Queue present and continue rendering |
| Damage via KWait/Event | Low | Medium | Signal vblank/complete through the event bus |
| GPU / VirtIO-GPU | Low | Very High | Hardware acceleration path (ROADMAP.md v1.3) |
| Multi-head | Experimental | Very High | Multiple \Device\DisplayN objects |
memory/ frame allocator (hal::alloc_page,map_page,unmap_page).arch/x64/paging(mmap region, user-accessible mapping).object(Ob types,ObOperations, namespace).graphics(FramebufferInfo, boot-provided FB).input(VT manager for ownership/gating).alloc(Vecfor surface frames).- MUST NOT depend on scheduler, VFS, or block drivers (INV-1).
- MUST NOT allocate or block in IRQ context (INV-2/INV-3).
- Surface format — is
XRGB8888sufficient for the GOP modes OVMF exposes, or should we read the format from the bootloader now? - Tearing — accept single-buffer tearing for v1, or require double buffering before shipping Doom?
- Ownership model — one graphics client per VT (proposed) or one global client with the others queueing?
- Present targets — should
presentbe restricted to the caller's VT, or allow an explicit VT/CRTC index for future multi-head?