From dd90ff812cfb6a6137c3924193c559e489af3e26 Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Fri, 17 Apr 2026 17:49:18 +0100 Subject: [PATCH] implements PLN-0022 --- .../prometeu-hal/src/composer_status.rs | 10 +++ crates/console/prometeu-hal/src/lib.rs | 2 + crates/console/prometeu-hal/src/syscalls.rs | 6 +- .../src/syscalls/domains/composer.rs | 22 ++++++ .../prometeu-hal/src/syscalls/domains/gfx.rs | 5 -- .../prometeu-hal/src/syscalls/domains/mod.rs | 2 + .../prometeu-hal/src/syscalls/registry.rs | 10 ++- .../prometeu-hal/src/syscalls/tests.rs | 50 ++++++++++--- .../src/virtual_machine_runtime/dispatch.rs | 55 ++------------ .../prometeu-vm/src/virtual_machine.rs | 33 ++++++--- docs/specs/runtime/04-gfx-peripheral.md | 73 ++++++++++--------- .../specs/runtime/16-host-abi-and-syscalls.md | 19 +++++ docs/vm-arch/ISA_CORE.md | 3 + 13 files changed, 179 insertions(+), 111 deletions(-) create mode 100644 crates/console/prometeu-hal/src/composer_status.rs create mode 100644 crates/console/prometeu-hal/src/syscalls/domains/composer.rs diff --git a/crates/console/prometeu-hal/src/composer_status.rs b/crates/console/prometeu-hal/src/composer_status.rs new file mode 100644 index 00000000..2e02b7ed --- /dev/null +++ b/crates/console/prometeu-hal/src/composer_status.rs @@ -0,0 +1,10 @@ +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(i32)] +pub enum ComposerOpStatus { + Ok = 0, + SceneUnavailable = 1, + ArgRangeInvalid = 2, + BankInvalid = 3, + LayerInvalid = 4, + SpriteOverflow = 5, +} diff --git a/crates/console/prometeu-hal/src/lib.rs b/crates/console/prometeu-hal/src/lib.rs index abfc7d7c..cc8de0a1 100644 --- a/crates/console/prometeu-hal/src/lib.rs +++ b/crates/console/prometeu-hal/src/lib.rs @@ -5,6 +5,7 @@ pub mod button; pub mod cartridge; pub mod cartridge_loader; pub mod color; +pub mod composer_status; pub mod debugger_protocol; pub mod gfx_bridge; pub mod glyph; @@ -34,6 +35,7 @@ pub mod window; pub use asset_bridge::AssetBridge; pub use audio_bridge::{AudioBridge, AudioOpStatus, LoopMode}; +pub use composer_status::ComposerOpStatus; pub use gfx_bridge::{BlendMode, GfxBridge, GfxOpStatus}; pub use hardware_bridge::HardwareBridge; pub use host_context::{HostContext, HostContextProvider}; diff --git a/crates/console/prometeu-hal/src/syscalls.rs b/crates/console/prometeu-hal/src/syscalls.rs index c4a90381..3e9b269a 100644 --- a/crates/console/prometeu-hal/src/syscalls.rs +++ b/crates/console/prometeu-hal/src/syscalls.rs @@ -19,6 +19,7 @@ pub use resolver::{ /// Each Syscall has a unique 32-bit ID. The IDs are grouped by category: /// - **0x0xxx**: System & OS Control /// - **0x1xxx**: Graphics (GFX) +/// - **0x11xx**: Frame Composer orchestration /// - **0x2xxx**: Reserved for legacy input syscalls (disabled for v1 VM-owned input) /// - **0x3xxx**: Audio (PCM & Mixing) /// - **0x4xxx**: Filesystem (Sandboxed I/O) @@ -35,9 +36,12 @@ pub enum Syscall { GfxDrawCircle = 0x1004, GfxDrawDisc = 0x1005, GfxDrawSquare = 0x1006, - GfxSetSprite = 0x1007, GfxDrawText = 0x1008, GfxClear565 = 0x1010, + ComposerBindScene = 0x1101, + ComposerUnbindScene = 0x1102, + ComposerSetCamera = 0x1103, + ComposerEmitSprite = 0x1104, AudioPlaySample = 0x3001, AudioPlay = 0x3002, FsOpen = 0x4001, diff --git a/crates/console/prometeu-hal/src/syscalls/domains/composer.rs b/crates/console/prometeu-hal/src/syscalls/domains/composer.rs new file mode 100644 index 00000000..32a53a95 --- /dev/null +++ b/crates/console/prometeu-hal/src/syscalls/domains/composer.rs @@ -0,0 +1,22 @@ +use crate::syscalls::{Syscall, SyscallRegistryEntry, caps}; + +pub(crate) const ENTRIES: &[SyscallRegistryEntry] = &[ + SyscallRegistryEntry::builder(Syscall::ComposerBindScene, "composer", "bind_scene") + .args(1) + .rets(1) + .caps(caps::GFX) + .cost(5), + SyscallRegistryEntry::builder(Syscall::ComposerUnbindScene, "composer", "unbind_scene") + .rets(1) + .caps(caps::GFX) + .cost(2), + SyscallRegistryEntry::builder(Syscall::ComposerSetCamera, "composer", "set_camera") + .args(2) + .caps(caps::GFX) + .cost(2), + SyscallRegistryEntry::builder(Syscall::ComposerEmitSprite, "composer", "emit_sprite") + .args(9) + .rets(1) + .caps(caps::GFX) + .cost(5), +]; diff --git a/crates/console/prometeu-hal/src/syscalls/domains/gfx.rs b/crates/console/prometeu-hal/src/syscalls/domains/gfx.rs index 95998186..f6023957 100644 --- a/crates/console/prometeu-hal/src/syscalls/domains/gfx.rs +++ b/crates/console/prometeu-hal/src/syscalls/domains/gfx.rs @@ -25,11 +25,6 @@ pub(crate) const ENTRIES: &[SyscallRegistryEntry] = &[ .args(6) .caps(caps::GFX) .cost(5), - SyscallRegistryEntry::builder(Syscall::GfxSetSprite, "gfx", "set_sprite") - .args(10) - .rets(1) - .caps(caps::GFX) - .cost(5), SyscallRegistryEntry::builder(Syscall::GfxDrawText, "gfx", "draw_text") .args(4) .caps(caps::GFX) diff --git a/crates/console/prometeu-hal/src/syscalls/domains/mod.rs b/crates/console/prometeu-hal/src/syscalls/domains/mod.rs index 3b4d3b34..00e896f1 100644 --- a/crates/console/prometeu-hal/src/syscalls/domains/mod.rs +++ b/crates/console/prometeu-hal/src/syscalls/domains/mod.rs @@ -1,6 +1,7 @@ mod asset; mod audio; mod bank; +mod composer; mod fs; mod gfx; mod log; @@ -12,6 +13,7 @@ pub(crate) fn all_entries() -> impl Iterator Some(Self::GfxDrawCircle), 0x1005 => Some(Self::GfxDrawDisc), 0x1006 => Some(Self::GfxDrawSquare), - 0x1007 => Some(Self::GfxSetSprite), 0x1008 => Some(Self::GfxDrawText), 0x1010 => Some(Self::GfxClear565), + 0x1101 => Some(Self::ComposerBindScene), + 0x1102 => Some(Self::ComposerUnbindScene), + 0x1103 => Some(Self::ComposerSetCamera), + 0x1104 => Some(Self::ComposerEmitSprite), 0x3001 => Some(Self::AudioPlaySample), 0x3002 => Some(Self::AudioPlay), 0x4001 => Some(Self::FsOpen), @@ -68,9 +71,12 @@ impl Syscall { Self::GfxDrawCircle => "GfxDrawCircle", Self::GfxDrawDisc => "GfxDrawDisc", Self::GfxDrawSquare => "GfxDrawSquare", - Self::GfxSetSprite => "GfxSetSprite", Self::GfxDrawText => "GfxDrawText", Self::GfxClear565 => "GfxClear565", + Self::ComposerBindScene => "ComposerBindScene", + Self::ComposerUnbindScene => "ComposerUnbindScene", + Self::ComposerSetCamera => "ComposerSetCamera", + Self::ComposerEmitSprite => "ComposerEmitSprite", Self::AudioPlaySample => "AudioPlaySample", Self::AudioPlay => "AudioPlay", Self::FsOpen => "FsOpen", diff --git a/crates/console/prometeu-hal/src/syscalls/tests.rs b/crates/console/prometeu-hal/src/syscalls/tests.rs index 8f3f55af..9594c17b 100644 --- a/crates/console/prometeu-hal/src/syscalls/tests.rs +++ b/crates/console/prometeu-hal/src/syscalls/tests.rs @@ -194,10 +194,6 @@ fn status_first_syscall_signatures_are_pinned() { assert_eq!(draw_square.arg_slots, 6); assert_eq!(draw_square.ret_slots, 0); - let set_sprite = meta_for(Syscall::GfxSetSprite); - assert_eq!(set_sprite.arg_slots, 10); - assert_eq!(set_sprite.ret_slots, 1); - let draw_text = meta_for(Syscall::GfxDrawText); assert_eq!(draw_text.arg_slots, 4); assert_eq!(draw_text.ret_slots, 0); @@ -206,6 +202,22 @@ fn status_first_syscall_signatures_are_pinned() { assert_eq!(clear_565.arg_slots, 1); assert_eq!(clear_565.ret_slots, 0); + let bind_scene = meta_for(Syscall::ComposerBindScene); + assert_eq!(bind_scene.arg_slots, 1); + assert_eq!(bind_scene.ret_slots, 1); + + let unbind_scene = meta_for(Syscall::ComposerUnbindScene); + assert_eq!(unbind_scene.arg_slots, 0); + assert_eq!(unbind_scene.ret_slots, 1); + + let set_camera = meta_for(Syscall::ComposerSetCamera); + assert_eq!(set_camera.arg_slots, 2); + assert_eq!(set_camera.ret_slots, 0); + + let emit_sprite = meta_for(Syscall::ComposerEmitSprite); + assert_eq!(emit_sprite.arg_slots, 9); + assert_eq!(emit_sprite.ret_slots, 1); + let audio_play_sample = meta_for(Syscall::AudioPlaySample); assert_eq!(audio_play_sample.arg_slots, 5); assert_eq!(audio_play_sample.ret_slots, 1); @@ -231,10 +243,10 @@ fn status_first_syscall_signatures_are_pinned() { fn declared_resolver_rejects_legacy_status_first_signatures() { let declared = vec![ prometeu_bytecode::SyscallDecl { - module: "gfx".into(), - name: "set_sprite".into(), + module: "composer".into(), + name: "bind_scene".into(), version: 1, - arg_slots: 10, + arg_slots: 1, ret_slots: 0, }, prometeu_bytecode::SyscallDecl { @@ -306,10 +318,24 @@ fn declared_resolver_rejects_legacy_status_first_signatures() { fn declared_resolver_accepts_mixed_status_first_surface_as_a_single_module() { let declared = vec![ prometeu_bytecode::SyscallDecl { - module: "gfx".into(), - name: "set_sprite".into(), + module: "composer".into(), + name: "bind_scene".into(), version: 1, - arg_slots: 10, + arg_slots: 1, + ret_slots: 1, + }, + prometeu_bytecode::SyscallDecl { + module: "composer".into(), + name: "unbind_scene".into(), + version: 1, + arg_slots: 0, + ret_slots: 1, + }, + prometeu_bytecode::SyscallDecl { + module: "composer".into(), + name: "emit_sprite".into(), + version: 1, + arg_slots: 9, ret_slots: 1, }, prometeu_bytecode::SyscallDecl { @@ -342,8 +368,10 @@ fn declared_resolver_accepts_mixed_status_first_surface_as_a_single_module() { assert_eq!(resolved.len(), declared.len()); assert_eq!(resolved[0].meta.ret_slots, 1); assert_eq!(resolved[1].meta.ret_slots, 1); - assert_eq!(resolved[2].meta.ret_slots, 2); + assert_eq!(resolved[2].meta.ret_slots, 1); assert_eq!(resolved[3].meta.ret_slots, 1); + assert_eq!(resolved[4].meta.ret_slots, 2); + assert_eq!(resolved[5].meta.ret_slots, 1); } #[test] diff --git a/crates/console/prometeu-system/src/virtual_machine_runtime/dispatch.rs b/crates/console/prometeu-system/src/virtual_machine_runtime/dispatch.rs index e3ec62ef..95a00ecb 100644 --- a/crates/console/prometeu-system/src/virtual_machine_runtime/dispatch.rs +++ b/crates/console/prometeu-system/src/virtual_machine_runtime/dispatch.rs @@ -4,14 +4,11 @@ use prometeu_bytecode::{TRAP_INVALID_SYSCALL, TRAP_OOB, TRAP_TYPE, Value}; use prometeu_hal::asset::{AssetId, AssetOpStatus, BankType, SlotRef}; use prometeu_hal::cartridge::AppMode; use prometeu_hal::color::Color; -use prometeu_hal::glyph::Glyph; use prometeu_hal::log::{LogLevel, LogSource}; -use prometeu_hal::sprite::Sprite; use prometeu_hal::syscalls::Syscall; use prometeu_hal::vm_fault::VmFault; use prometeu_hal::{ - AudioOpStatus, GfxOpStatus, HostContext, HostReturn, NativeInterface, SyscallId, expect_bool, - expect_int, + AudioOpStatus, HostContext, HostReturn, NativeInterface, SyscallId, expect_int, }; use std::sync::atomic::Ordering; @@ -135,49 +132,6 @@ impl NativeInterface for VirtualMachineRuntime { hw.gfx_mut().draw_square(x, y, w, h, border_color, fill_color); Ok(()) } - Syscall::GfxSetSprite => { - let bank_id = expect_int(args, 0)? as u8; - let index = expect_int(args, 1)? as usize; - let x = expect_int(args, 2)? as i32; - let y = expect_int(args, 3)? as i32; - let glyph_id = expect_int(args, 4)? as u16; - let palette_id = expect_int(args, 5)? as u8; - let active = expect_bool(args, 6)?; - let flip_x = expect_bool(args, 7)?; - let flip_y = expect_bool(args, 8)?; - let priority = expect_int(args, 9)? as u8; - - if index >= 512 { - ret.push_int(GfxOpStatus::InvalidSpriteIndex as i64); - return Ok(()); - } - - if hw.assets().slot_info(SlotRef::gfx(bank_id as usize)).asset_id.is_none() { - ret.push_int(GfxOpStatus::BankInvalid as i64); - return Ok(()); - } - - if palette_id >= 64 || priority >= 5 { - ret.push_int(GfxOpStatus::ArgRangeInvalid as i64); - return Ok(()); - } - - if active { - hw.emit_sprite(Sprite { - glyph: Glyph { glyph_id, palette_id }, - x, - y, - layer: 0, - bank_id, - active: false, - flip_x, - flip_y, - priority, - }); - } - ret.push_int(GfxOpStatus::Ok as i64); - Ok(()) - } Syscall::GfxDrawText => { let x = expect_int(args, 0)? as i32; let y = expect_int(args, 1)? as i32; @@ -194,6 +148,13 @@ impl NativeInterface for VirtualMachineRuntime { hw.gfx_mut().clear(Color::from_raw(color_val as u16)); Ok(()) } + Syscall::ComposerBindScene + | Syscall::ComposerUnbindScene + | Syscall::ComposerSetCamera + | Syscall::ComposerEmitSprite => Err(VmFault::Trap( + TRAP_INVALID_SYSCALL, + "Composer syscall support is not implemented yet".into(), + )), Syscall::AudioPlaySample => { let sample_id_raw = expect_int(args, 0)?; let voice_id_raw = expect_int(args, 1)?; diff --git a/crates/console/prometeu-vm/src/virtual_machine.rs b/crates/console/prometeu-vm/src/virtual_machine.rs index 52bbb659..cef12bfa 100644 --- a/crates/console/prometeu-vm/src/virtual_machine.rs +++ b/crates/console/prometeu-vm/src/virtual_machine.rs @@ -2567,11 +2567,8 @@ mod tests { #[test] fn test_status_first_syscall_results_count_mismatch_panic() { - // GfxSetSprite (0x1007) expects 1 result. - let code = assemble( - "PUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nSYSCALL 0x1007", - ) - .expect("assemble"); + // ComposerBindScene (0x1101) expects 1 result. + let code = assemble("PUSH_I32 0\nSYSCALL 0x1101").expect("assemble"); struct BadNativeNoReturn; impl NativeInterface for BadNativeNoReturn { @@ -2921,10 +2918,24 @@ mod tests { fn test_loader_patching_accepts_status_first_signatures() { let cases = vec![ SyscallDecl { - module: "gfx".into(), - name: "set_sprite".into(), + module: "composer".into(), + name: "bind_scene".into(), version: 1, - arg_slots: 10, + arg_slots: 1, + ret_slots: 1, + }, + SyscallDecl { + module: "composer".into(), + name: "unbind_scene".into(), + version: 1, + arg_slots: 0, + ret_slots: 1, + }, + SyscallDecl { + module: "composer".into(), + name: "emit_sprite".into(), + version: 1, + arg_slots: 9, ret_slots: 1, }, SyscallDecl { @@ -2977,10 +2988,10 @@ mod tests { fn test_loader_patching_rejects_legacy_status_first_ret_slots() { let cases = vec![ SyscallDecl { - module: "gfx".into(), - name: "set_sprite".into(), + module: "composer".into(), + name: "bind_scene".into(), version: 1, - arg_slots: 10, + arg_slots: 1, ret_slots: 0, }, SyscallDecl { diff --git a/docs/specs/runtime/04-gfx-peripheral.md b/docs/specs/runtime/04-gfx-peripheral.md index 518744f2..d0e5186f 100644 --- a/docs/specs/runtime/04-gfx-peripheral.md +++ b/docs/specs/runtime/04-gfx-peripheral.md @@ -536,7 +536,12 @@ The system can measure: ## 19. Syscall Return and Fault Policy -`gfx` follows status-first policy for operations with operational failure modes. +Graphics-related public ABI in v1 is split between: + +- `gfx.*` for direct drawing/backend-oriented operations; +- `composer.*` for frame orchestration operations. + +Only operations with real operational rejection paths return explicit status values. Fault boundary: @@ -544,50 +549,50 @@ Fault boundary: - `status`: operational failure; - `Panic`: internal runtime invariant break only. -### 19.1 `gfx.set_sprite` +### 19.1 Return-shape matrix in v1 -Return-shape matrix in v1: +| Syscall | Return | Policy basis | +| ----------------------- | ------------- | --------------------------------------------------- | +| `gfx.clear` | `void` | no real operational failure path in v1 | +| `gfx.fill_rect` | `void` | no real operational failure path in v1 | +| `gfx.draw_line` | `void` | no real operational failure path in v1 | +| `gfx.draw_circle` | `void` | no real operational failure path in v1 | +| `gfx.draw_disc` | `void` | no real operational failure path in v1 | +| `gfx.draw_square` | `void` | no real operational failure path in v1 | +| `gfx.draw_text` | `void` | no real operational failure path in v1 | +| `gfx.clear_565` | `void` | no real operational failure path in v1 | +| `composer.bind_scene` | `status:int` | explicit orchestration-domain operational result | +| `composer.unbind_scene` | `status:int` | explicit orchestration-domain operational result | +| `composer.set_camera` | `void` | no real operational failure path in v1 | +| `composer.emit_sprite` | `status:int` | explicit orchestration-domain operational rejection | -| Syscall | Return | Policy basis | -| ------------------ | ------------- | ---------------------------------------------------- | -| `gfx.clear` | `void` | no real operational failure path in v1 | -| `gfx.fill_rect` | `void` | no real operational failure path in v1 | -| `gfx.draw_line` | `void` | no real operational failure path in v1 | -| `gfx.draw_circle` | `void` | no real operational failure path in v1 | -| `gfx.draw_disc` | `void` | no real operational failure path in v1 | -| `gfx.draw_square` | `void` | no real operational failure path in v1 | -| `gfx.set_sprite` | `status:int` | operational rejection must be explicit | -| `gfx.draw_text` | `void` | no real operational failure path in v1 | -| `gfx.clear_565` | `void` | no real operational failure path in v1 | +### 19.2 `composer.emit_sprite` -Only `gfx.set_sprite` is status-returning in v1. -All other `gfx` syscalls remain `void` unless a future domain revision introduces a real operational failure path. - -### 19.2 `gfx.set_sprite` - -`gfx.set_sprite` returns `status:int`. +`composer.emit_sprite` returns `status:int`. ABI: -1. `bank_id: int` — index of the tile bank -2. `index: int` — sprite index (0..511) +1. `glyph_id: int` — glyph index within the bank +2. `palette_id: int` — palette index 3. `x: int` — x coordinate 4. `y: int` — y coordinate -5. `tile_id: int` — tile index within the bank -6. `palette_id: int` — palette index (0..63) -7. `active: bool` — visibility toggle -8. `flip_x: bool` — horizontal flip -9. `flip_y: bool` — vertical flip -10. `priority: int` — layer priority (0..4) +5. `layer: int` — composition layer reference +6. `bank_id: int` — glyph bank index +7. `flip_x: bool` — horizontal flip +8. `flip_y: bool` — vertical flip +9. `priority: int` — within-layer ordering priority Minimum status table: - `0` = `OK` -- `2` = `INVALID_SPRITE_INDEX` -- `3` = `INVALID_ARG_RANGE` -- `4` = `BANK_INVALID` +- `1` = `SCENE_UNAVAILABLE` +- `2` = `INVALID_ARG_RANGE` +- `3` = `BANK_INVALID` +- `4` = `LAYER_INVALID` +- `5` = `SPRITE_OVERFLOW` Operational notes: -- no fallback to default bank when the sprite bank id cannot be resolved; -- no silent no-op for invalid index/range; -- `palette_id` and `priority` must be validated against runtime-supported ranges. +- the canonical public sprite contract is frame-emission based; +- no caller-provided sprite index exists in the v1 canonical ABI; +- no `active` flag exists in the v1 canonical ABI; +- overflow remains non-fatal and must not escalate to trap in v1. diff --git a/docs/specs/runtime/16-host-abi-and-syscalls.md b/docs/specs/runtime/16-host-abi-and-syscalls.md index 7ca43033..0e2b4c1b 100644 --- a/docs/specs/runtime/16-host-abi-and-syscalls.md +++ b/docs/specs/runtime/16-host-abi-and-syscalls.md @@ -39,6 +39,7 @@ Example: ``` ("gfx", "present", 1) ("audio", "play", 2) +("composer", "emit_sprite", 1) ``` This identity is: @@ -198,6 +199,24 @@ For `asset.load`: - `slot` is the target slot index; - bank kind is resolved from `asset_table` by `asset_id`, not supplied by the caller. +### Composition surface (`composer`, v1) + +The canonical frame-orchestration public ABI uses module `composer`. + +Canonical operations in v1 are: + +- `composer.bind_scene(bank_id) -> (status)` +- `composer.unbind_scene() -> (status)` +- `composer.set_camera(x, y) -> void` +- `composer.emit_sprite(glyph_id, palette_id, x, y, layer, bank_id, flip_x, flip_y, priority) -> (status)` + +For mutating composer operations: + +- `status` is a `ComposerOpStatus` value; +- `bind_scene`, `unbind_scene`, and `emit_sprite` are status-returning; +- `set_camera` remains `void` in v1; +- no caller-provided sprite index or `active` flag is part of the canonical contract. + ## 7 Syscalls as Callable Entities (Not First-Class) Syscalls behave like call sites, not like first-class guest values. diff --git a/docs/vm-arch/ISA_CORE.md b/docs/vm-arch/ISA_CORE.md index f2cea12d..d6f704d5 100644 --- a/docs/vm-arch/ISA_CORE.md +++ b/docs/vm-arch/ISA_CORE.md @@ -85,6 +85,9 @@ Example: - `asset.load` currently resolves with `arg_slots = 2` and `ret_slots = 2`. - The canonical stack contract is `asset_id, slot -> status, handle`. - Callers do not provide an explicit asset kind; the runtime derives it from `asset_table`. +- `composer.bind_scene` resolves with `arg_slots = 1` and `ret_slots = 1`. +- The canonical stack contract is `bank_id -> status`. +- `composer.emit_sprite` resolves with `arg_slots = 9` and `ret_slots = 1`. #### Canonical Intrinsic Registry Artifact