implements PLN-0022

This commit is contained in:
bQUARKz 2026-04-17 17:49:18 +01:00
parent 240fe65da7
commit dd90ff812c
Signed by: bquarkz
SSH Key Fingerprint: SHA256:Z7dgqoglWwoK6j6u4QC87OveEq74WOhFN+gitsxtkf8
13 changed files with 179 additions and 111 deletions

View File

@ -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,
}

View File

@ -5,6 +5,7 @@ pub mod button;
pub mod cartridge; pub mod cartridge;
pub mod cartridge_loader; pub mod cartridge_loader;
pub mod color; pub mod color;
pub mod composer_status;
pub mod debugger_protocol; pub mod debugger_protocol;
pub mod gfx_bridge; pub mod gfx_bridge;
pub mod glyph; pub mod glyph;
@ -34,6 +35,7 @@ pub mod window;
pub use asset_bridge::AssetBridge; pub use asset_bridge::AssetBridge;
pub use audio_bridge::{AudioBridge, AudioOpStatus, LoopMode}; pub use audio_bridge::{AudioBridge, AudioOpStatus, LoopMode};
pub use composer_status::ComposerOpStatus;
pub use gfx_bridge::{BlendMode, GfxBridge, GfxOpStatus}; pub use gfx_bridge::{BlendMode, GfxBridge, GfxOpStatus};
pub use hardware_bridge::HardwareBridge; pub use hardware_bridge::HardwareBridge;
pub use host_context::{HostContext, HostContextProvider}; pub use host_context::{HostContext, HostContextProvider};

View File

@ -19,6 +19,7 @@ pub use resolver::{
/// Each Syscall has a unique 32-bit ID. The IDs are grouped by category: /// Each Syscall has a unique 32-bit ID. The IDs are grouped by category:
/// - **0x0xxx**: System & OS Control /// - **0x0xxx**: System & OS Control
/// - **0x1xxx**: Graphics (GFX) /// - **0x1xxx**: Graphics (GFX)
/// - **0x11xx**: Frame Composer orchestration
/// - **0x2xxx**: Reserved for legacy input syscalls (disabled for v1 VM-owned input) /// - **0x2xxx**: Reserved for legacy input syscalls (disabled for v1 VM-owned input)
/// - **0x3xxx**: Audio (PCM & Mixing) /// - **0x3xxx**: Audio (PCM & Mixing)
/// - **0x4xxx**: Filesystem (Sandboxed I/O) /// - **0x4xxx**: Filesystem (Sandboxed I/O)
@ -35,9 +36,12 @@ pub enum Syscall {
GfxDrawCircle = 0x1004, GfxDrawCircle = 0x1004,
GfxDrawDisc = 0x1005, GfxDrawDisc = 0x1005,
GfxDrawSquare = 0x1006, GfxDrawSquare = 0x1006,
GfxSetSprite = 0x1007,
GfxDrawText = 0x1008, GfxDrawText = 0x1008,
GfxClear565 = 0x1010, GfxClear565 = 0x1010,
ComposerBindScene = 0x1101,
ComposerUnbindScene = 0x1102,
ComposerSetCamera = 0x1103,
ComposerEmitSprite = 0x1104,
AudioPlaySample = 0x3001, AudioPlaySample = 0x3001,
AudioPlay = 0x3002, AudioPlay = 0x3002,
FsOpen = 0x4001, FsOpen = 0x4001,

View File

@ -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),
];

View File

@ -25,11 +25,6 @@ pub(crate) const ENTRIES: &[SyscallRegistryEntry] = &[
.args(6) .args(6)
.caps(caps::GFX) .caps(caps::GFX)
.cost(5), .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") SyscallRegistryEntry::builder(Syscall::GfxDrawText, "gfx", "draw_text")
.args(4) .args(4)
.caps(caps::GFX) .caps(caps::GFX)

View File

@ -1,6 +1,7 @@
mod asset; mod asset;
mod audio; mod audio;
mod bank; mod bank;
mod composer;
mod fs; mod fs;
mod gfx; mod gfx;
mod log; mod log;
@ -12,6 +13,7 @@ pub(crate) fn all_entries() -> impl Iterator<Item = &'static SyscallRegistryEntr
system::ENTRIES system::ENTRIES
.iter() .iter()
.chain(gfx::ENTRIES.iter()) .chain(gfx::ENTRIES.iter())
.chain(composer::ENTRIES.iter())
.chain(audio::ENTRIES.iter()) .chain(audio::ENTRIES.iter())
.chain(fs::ENTRIES.iter()) .chain(fs::ENTRIES.iter())
.chain(log::ENTRIES.iter()) .chain(log::ENTRIES.iter())

View File

@ -20,9 +20,12 @@ impl Syscall {
0x1004 => Some(Self::GfxDrawCircle), 0x1004 => Some(Self::GfxDrawCircle),
0x1005 => Some(Self::GfxDrawDisc), 0x1005 => Some(Self::GfxDrawDisc),
0x1006 => Some(Self::GfxDrawSquare), 0x1006 => Some(Self::GfxDrawSquare),
0x1007 => Some(Self::GfxSetSprite),
0x1008 => Some(Self::GfxDrawText), 0x1008 => Some(Self::GfxDrawText),
0x1010 => Some(Self::GfxClear565), 0x1010 => Some(Self::GfxClear565),
0x1101 => Some(Self::ComposerBindScene),
0x1102 => Some(Self::ComposerUnbindScene),
0x1103 => Some(Self::ComposerSetCamera),
0x1104 => Some(Self::ComposerEmitSprite),
0x3001 => Some(Self::AudioPlaySample), 0x3001 => Some(Self::AudioPlaySample),
0x3002 => Some(Self::AudioPlay), 0x3002 => Some(Self::AudioPlay),
0x4001 => Some(Self::FsOpen), 0x4001 => Some(Self::FsOpen),
@ -68,9 +71,12 @@ impl Syscall {
Self::GfxDrawCircle => "GfxDrawCircle", Self::GfxDrawCircle => "GfxDrawCircle",
Self::GfxDrawDisc => "GfxDrawDisc", Self::GfxDrawDisc => "GfxDrawDisc",
Self::GfxDrawSquare => "GfxDrawSquare", Self::GfxDrawSquare => "GfxDrawSquare",
Self::GfxSetSprite => "GfxSetSprite",
Self::GfxDrawText => "GfxDrawText", Self::GfxDrawText => "GfxDrawText",
Self::GfxClear565 => "GfxClear565", Self::GfxClear565 => "GfxClear565",
Self::ComposerBindScene => "ComposerBindScene",
Self::ComposerUnbindScene => "ComposerUnbindScene",
Self::ComposerSetCamera => "ComposerSetCamera",
Self::ComposerEmitSprite => "ComposerEmitSprite",
Self::AudioPlaySample => "AudioPlaySample", Self::AudioPlaySample => "AudioPlaySample",
Self::AudioPlay => "AudioPlay", Self::AudioPlay => "AudioPlay",
Self::FsOpen => "FsOpen", Self::FsOpen => "FsOpen",

View File

@ -194,10 +194,6 @@ fn status_first_syscall_signatures_are_pinned() {
assert_eq!(draw_square.arg_slots, 6); assert_eq!(draw_square.arg_slots, 6);
assert_eq!(draw_square.ret_slots, 0); 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); let draw_text = meta_for(Syscall::GfxDrawText);
assert_eq!(draw_text.arg_slots, 4); assert_eq!(draw_text.arg_slots, 4);
assert_eq!(draw_text.ret_slots, 0); 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.arg_slots, 1);
assert_eq!(clear_565.ret_slots, 0); 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); let audio_play_sample = meta_for(Syscall::AudioPlaySample);
assert_eq!(audio_play_sample.arg_slots, 5); assert_eq!(audio_play_sample.arg_slots, 5);
assert_eq!(audio_play_sample.ret_slots, 1); 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() { fn declared_resolver_rejects_legacy_status_first_signatures() {
let declared = vec![ let declared = vec![
prometeu_bytecode::SyscallDecl { prometeu_bytecode::SyscallDecl {
module: "gfx".into(), module: "composer".into(),
name: "set_sprite".into(), name: "bind_scene".into(),
version: 1, version: 1,
arg_slots: 10, arg_slots: 1,
ret_slots: 0, ret_slots: 0,
}, },
prometeu_bytecode::SyscallDecl { 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() { fn declared_resolver_accepts_mixed_status_first_surface_as_a_single_module() {
let declared = vec![ let declared = vec![
prometeu_bytecode::SyscallDecl { prometeu_bytecode::SyscallDecl {
module: "gfx".into(), module: "composer".into(),
name: "set_sprite".into(), name: "bind_scene".into(),
version: 1, 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, ret_slots: 1,
}, },
prometeu_bytecode::SyscallDecl { 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.len(), declared.len());
assert_eq!(resolved[0].meta.ret_slots, 1); assert_eq!(resolved[0].meta.ret_slots, 1);
assert_eq!(resolved[1].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[3].meta.ret_slots, 1);
assert_eq!(resolved[4].meta.ret_slots, 2);
assert_eq!(resolved[5].meta.ret_slots, 1);
} }
#[test] #[test]

View File

@ -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::asset::{AssetId, AssetOpStatus, BankType, SlotRef};
use prometeu_hal::cartridge::AppMode; use prometeu_hal::cartridge::AppMode;
use prometeu_hal::color::Color; use prometeu_hal::color::Color;
use prometeu_hal::glyph::Glyph;
use prometeu_hal::log::{LogLevel, LogSource}; use prometeu_hal::log::{LogLevel, LogSource};
use prometeu_hal::sprite::Sprite;
use prometeu_hal::syscalls::Syscall; use prometeu_hal::syscalls::Syscall;
use prometeu_hal::vm_fault::VmFault; use prometeu_hal::vm_fault::VmFault;
use prometeu_hal::{ use prometeu_hal::{
AudioOpStatus, GfxOpStatus, HostContext, HostReturn, NativeInterface, SyscallId, expect_bool, AudioOpStatus, HostContext, HostReturn, NativeInterface, SyscallId, expect_int,
expect_int,
}; };
use std::sync::atomic::Ordering; 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); hw.gfx_mut().draw_square(x, y, w, h, border_color, fill_color);
Ok(()) 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 => { Syscall::GfxDrawText => {
let x = expect_int(args, 0)? as i32; let x = expect_int(args, 0)? as i32;
let y = expect_int(args, 1)? 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)); hw.gfx_mut().clear(Color::from_raw(color_val as u16));
Ok(()) 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 => { Syscall::AudioPlaySample => {
let sample_id_raw = expect_int(args, 0)?; let sample_id_raw = expect_int(args, 0)?;
let voice_id_raw = expect_int(args, 1)?; let voice_id_raw = expect_int(args, 1)?;

View File

@ -2567,11 +2567,8 @@ mod tests {
#[test] #[test]
fn test_status_first_syscall_results_count_mismatch_panic() { fn test_status_first_syscall_results_count_mismatch_panic() {
// GfxSetSprite (0x1007) expects 1 result. // ComposerBindScene (0x1101) expects 1 result.
let code = assemble( let code = assemble("PUSH_I32 0\nSYSCALL 0x1101").expect("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");
struct BadNativeNoReturn; struct BadNativeNoReturn;
impl NativeInterface for BadNativeNoReturn { impl NativeInterface for BadNativeNoReturn {
@ -2921,10 +2918,24 @@ mod tests {
fn test_loader_patching_accepts_status_first_signatures() { fn test_loader_patching_accepts_status_first_signatures() {
let cases = vec![ let cases = vec![
SyscallDecl { SyscallDecl {
module: "gfx".into(), module: "composer".into(),
name: "set_sprite".into(), name: "bind_scene".into(),
version: 1, 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, ret_slots: 1,
}, },
SyscallDecl { SyscallDecl {
@ -2977,10 +2988,10 @@ mod tests {
fn test_loader_patching_rejects_legacy_status_first_ret_slots() { fn test_loader_patching_rejects_legacy_status_first_ret_slots() {
let cases = vec![ let cases = vec![
SyscallDecl { SyscallDecl {
module: "gfx".into(), module: "composer".into(),
name: "set_sprite".into(), name: "bind_scene".into(),
version: 1, version: 1,
arg_slots: 10, arg_slots: 1,
ret_slots: 0, ret_slots: 0,
}, },
SyscallDecl { SyscallDecl {

View File

@ -536,7 +536,12 @@ The system can measure:
## 19. Syscall Return and Fault Policy ## 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: Fault boundary:
@ -544,50 +549,50 @@ Fault boundary:
- `status`: operational failure; - `status`: operational failure;
- `Panic`: internal runtime invariant break only. - `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 | ### 19.2 `composer.emit_sprite`
| ------------------ | ------------- | ---------------------------------------------------- |
| `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 |
Only `gfx.set_sprite` is status-returning in v1. `composer.emit_sprite` returns `status:int`.
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`.
ABI: ABI:
1. `bank_id: int` — index of the tile bank 1. `glyph_id: int` — glyph index within the bank
2. `index: int` — sprite index (0..511) 2. `palette_id: int` — palette index
3. `x: int` — x coordinate 3. `x: int` — x coordinate
4. `y: int` — y coordinate 4. `y: int` — y coordinate
5. `tile_id: int` — tile index within the bank 5. `layer: int` — composition layer reference
6. `palette_id: int` — palette index (0..63) 6. `bank_id: int` — glyph bank index
7. `active: bool` — visibility toggle 7. `flip_x: bool` — horizontal flip
8. `flip_x: bool` — horizontal flip 8. `flip_y: bool` — vertical flip
9. `flip_y: bool` — vertical flip 9. `priority: int` — within-layer ordering priority
10. `priority: int` — layer priority (0..4)
Minimum status table: Minimum status table:
- `0` = `OK` - `0` = `OK`
- `2` = `INVALID_SPRITE_INDEX` - `1` = `SCENE_UNAVAILABLE`
- `3` = `INVALID_ARG_RANGE` - `2` = `INVALID_ARG_RANGE`
- `4` = `BANK_INVALID` - `3` = `BANK_INVALID`
- `4` = `LAYER_INVALID`
- `5` = `SPRITE_OVERFLOW`
Operational notes: Operational notes:
- no fallback to default bank when the sprite bank id cannot be resolved; - the canonical public sprite contract is frame-emission based;
- no silent no-op for invalid index/range; - no caller-provided sprite index exists in the v1 canonical ABI;
- `palette_id` and `priority` must be validated against runtime-supported ranges. - no `active` flag exists in the v1 canonical ABI;
- overflow remains non-fatal and must not escalate to trap in v1.

View File

@ -39,6 +39,7 @@ Example:
``` ```
("gfx", "present", 1) ("gfx", "present", 1)
("audio", "play", 2) ("audio", "play", 2)
("composer", "emit_sprite", 1)
``` ```
This identity is: This identity is:
@ -198,6 +199,24 @@ For `asset.load`:
- `slot` is the target slot index; - `slot` is the target slot index;
- bank kind is resolved from `asset_table` by `asset_id`, not supplied by the caller. - 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) ## 7 Syscalls as Callable Entities (Not First-Class)
Syscalls behave like call sites, not like first-class guest values. Syscalls behave like call sites, not like first-class guest values.

View File

@ -85,6 +85,9 @@ Example:
- `asset.load` currently resolves with `arg_slots = 2` and `ret_slots = 2`. - `asset.load` currently resolves with `arg_slots = 2` and `ret_slots = 2`.
- The canonical stack contract is `asset_id, slot -> status, handle`. - 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`. - 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 #### Canonical Intrinsic Registry Artifact