merge/master #7

Closed
bquarkz wants to merge 0 commits from merge/master into master
363 changed files with 14376 additions and 35278 deletions

View File

@ -1,29 +0,0 @@
name: CI
on:
push:
branches: [ main, master ]
pull_request:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt, clippy
- uses: Swatinem/rust-cache@v2
- name: Format check
run: cargo fmt -- --check
- name: Clippy
run: cargo clippy --workspace --all-features
- name: Test
run: cargo test --workspace --all-targets --all-features --no-fail-fast

7
.gitignore vendored
View File

@ -55,9 +55,4 @@ dist-staging
dist-staging/**
temp
temp/**
**/build/**
**/node_modules/**
AGENTS.md
temp/**

591
Cargo.lock generated
View File

@ -25,7 +25,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75"
dependencies = [
"cfg-if",
"getrandom 0.3.4",
"getrandom",
"once_cell",
"version_check",
"zerocopy",
@ -86,7 +86,7 @@ dependencies = [
"ndk-context",
"ndk-sys 0.6.0+11769913",
"num_enum",
"thiserror",
"thiserror 1.0.69",
]
[[package]]
@ -156,9 +156,9 @@ dependencies = [
[[package]]
name = "anyhow"
version = "1.0.101"
version = "1.0.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea"
checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
[[package]]
name = "arrayref"
@ -288,7 +288,7 @@ dependencies = [
"polling",
"rustix 0.38.44",
"slab",
"thiserror",
"thiserror 1.0.69",
]
[[package]]
@ -303,6 +303,15 @@ dependencies = [
"wayland-client",
]
[[package]]
name = "castaway"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a"
dependencies = [
"rustversion",
]
[[package]]
name = "cc"
version = "1.2.52"
@ -406,7 +415,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e"
dependencies = [
"termcolor",
"unicode-width",
"unicode-width 0.1.14",
]
[[package]]
@ -456,6 +465,20 @@ dependencies = [
"memchr",
]
[[package]]
name = "compact_str"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fdb1325a1cece981e8a296ab8f0f9b63ae357bd0784a9faaf548cc7b480707a"
dependencies = [
"castaway",
"cfg-if",
"itoa",
"rustversion",
"ryu",
"static_assertions",
]
[[package]]
name = "concurrent-queue"
version = "2.5.0"
@ -525,6 +548,12 @@ dependencies = [
"bindgen",
]
[[package]]
name = "cow-utils"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "417bef24afe1460300965a25ff4a24b8b45ad011948302ec221e8a0a81eb2c79"
[[package]]
name = "cpal"
version = "0.15.3"
@ -604,6 +633,12 @@ version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76"
[[package]]
name = "dragonbox_ecma"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a5577f010d4e1bb3f3c4d6081e05718eb6992cf20119cab4d3abadff198b5ae"
[[package]]
name = "either"
version = "1.15.0"
@ -626,6 +661,12 @@ dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "fastrand"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
[[package]]
name = "find-msvc-tools"
version = "0.1.7"
@ -669,17 +710,6 @@ dependencies = [
"windows-link",
]
[[package]]
name = "getrandom"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
dependencies = [
"cfg-if",
"libc",
"wasi",
]
[[package]]
name = "getrandom"
version = "0.3.4"
@ -757,7 +787,7 @@ checksum = "6f56f6318968d03c18e1bcf4857ff88c61157e9da8e47c5f29055d60e1228884"
dependencies = [
"log",
"presser",
"thiserror",
"thiserror 1.0.69",
"winapi",
"windows 0.52.0",
]
@ -797,6 +827,9 @@ name = "hashbrown"
version = "0.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
dependencies = [
"allocator-api2",
]
[[package]]
name = "hassle-rs"
@ -808,7 +841,7 @@ dependencies = [
"com",
"libc",
"libloading 0.8.9",
"thiserror",
"thiserror 1.0.69",
"widestring",
"winapi",
]
@ -873,7 +906,7 @@ dependencies = [
"combine",
"jni-sys",
"log",
"thiserror",
"thiserror 1.0.69",
"walkdir",
"windows-sys 0.45.0",
]
@ -890,7 +923,7 @@ version = "0.1.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33"
dependencies = [
"getrandom 0.3.4",
"getrandom",
"libc",
]
@ -1055,7 +1088,7 @@ dependencies = [
"rustc-hash 1.1.0",
"spirv",
"termcolor",
"thiserror",
"thiserror 1.0.69",
"unicode-xid",
]
@ -1070,7 +1103,7 @@ dependencies = [
"log",
"ndk-sys 0.5.0+25.2.9519653",
"num_enum",
"thiserror",
"thiserror 1.0.69",
]
[[package]]
@ -1085,7 +1118,7 @@ dependencies = [
"ndk-sys 0.6.0+11769913",
"num_enum",
"raw-window-handle",
"thiserror",
"thiserror 1.0.69",
]
[[package]]
@ -1122,6 +1155,22 @@ dependencies = [
"minimal-lexical",
]
[[package]]
name = "nonmax"
version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "610a5acd306ec67f907abe5567859a3c693fb9886eb1f012ab8f2a47bef3db51"
[[package]]
name = "num-bigint"
version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9"
dependencies = [
"num-integer",
"num-traits",
]
[[package]]
name = "num-derive"
version = "0.4.2"
@ -1133,6 +1182,15 @@ dependencies = [
"syn 2.0.114",
]
[[package]]
name = "num-integer"
version = "0.1.46"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f"
dependencies = [
"num-traits",
]
[[package]]
name = "num-traits"
version = "0.2.19"
@ -1440,6 +1498,212 @@ dependencies = [
"ttf-parser",
]
[[package]]
name = "owo-colors"
version = "4.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c6901729fa79e91a0913333229e9ca5dc725089d1c363b2f4b4760709dc4a52"
[[package]]
name = "oxc-miette"
version = "2.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60a7ba54c704edefead1f44e9ef09c43e5cfae666bdc33516b066011f0e6ebf7"
dependencies = [
"cfg-if",
"owo-colors",
"oxc-miette-derive",
"textwrap",
"thiserror 2.0.18",
"unicode-segmentation",
"unicode-width 0.2.2",
]
[[package]]
name = "oxc-miette-derive"
version = "2.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4faecb54d0971f948fbc1918df69b26007e6f279a204793669542e1e8b75eb3"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.114",
]
[[package]]
name = "oxc_allocator"
version = "0.110.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2174c7c8f77137b1bd1c653d7a5a531ae41f3b8fec1dd0251c801689784e7a2e"
dependencies = [
"allocator-api2",
"hashbrown 0.16.1",
"oxc_data_structures",
"rustc-hash 2.1.1",
]
[[package]]
name = "oxc_ast"
version = "0.110.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62f1902f97a5cac8767b76a1d8a1b3124e9db80c176ebbc98f75143dcc124a15"
dependencies = [
"bitflags 2.10.0",
"oxc_allocator",
"oxc_ast_macros",
"oxc_data_structures",
"oxc_diagnostics",
"oxc_estree",
"oxc_regular_expression",
"oxc_span",
"oxc_syntax",
]
[[package]]
name = "oxc_ast_macros"
version = "0.110.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c5a31bd55516a98a35b2d99fa5813a3d3a5b798bad3262c819dfe7344bc6f390"
dependencies = [
"phf",
"proc-macro2",
"quote",
"syn 2.0.114",
]
[[package]]
name = "oxc_ast_visit"
version = "0.110.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2c520a488c04ba5267223edd0bb245fb7f10e2358e8955802a5d962bb95b50a"
dependencies = [
"oxc_allocator",
"oxc_ast",
"oxc_span",
"oxc_syntax",
]
[[package]]
name = "oxc_data_structures"
version = "0.110.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a42840ce8d83a08a92823dda6189e4d97359feca24a4fa732f3256c4614bb5a4"
[[package]]
name = "oxc_diagnostics"
version = "0.110.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4f7b09c1563a67ede53af131f717b31ba89a992959ebad188b5158c21d4dc0a"
dependencies = [
"cow-utils",
"oxc-miette",
"percent-encoding",
]
[[package]]
name = "oxc_ecmascript"
version = "0.110.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4813b352bd5b0b05badf0c9e6c5ec7ea58a6a7ab49bec8d18ead262624c6ef8d"
dependencies = [
"cow-utils",
"num-bigint",
"num-traits",
"oxc_allocator",
"oxc_ast",
"oxc_regular_expression",
"oxc_span",
"oxc_syntax",
]
[[package]]
name = "oxc_estree"
version = "0.110.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e54fb3effe995e6538d68070bf0a450b5ffd11dd41b62f11a4d01efa1f40e278"
[[package]]
name = "oxc_index"
version = "4.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eb3e6120999627ec9703025eab7c9f410ebb7e95557632a8902ca48210416c2b"
dependencies = [
"nonmax",
"serde",
]
[[package]]
name = "oxc_parser"
version = "0.110.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5592bf8b64743944eb46528f9eabdde2b2435c8293cd502f5c183f9dff644e16"
dependencies = [
"bitflags 2.10.0",
"cow-utils",
"memchr",
"num-bigint",
"num-traits",
"oxc_allocator",
"oxc_ast",
"oxc_data_structures",
"oxc_diagnostics",
"oxc_ecmascript",
"oxc_regular_expression",
"oxc_span",
"oxc_syntax",
"rustc-hash 2.1.1",
"seq-macro",
]
[[package]]
name = "oxc_regular_expression"
version = "0.110.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09de7f7e0fb82f54750e3a95346a828fd354b9aeac00f131719008733e66a18d"
dependencies = [
"bitflags 2.10.0",
"oxc_allocator",
"oxc_ast_macros",
"oxc_diagnostics",
"oxc_span",
"phf",
"rustc-hash 2.1.1",
"unicode-id-start",
]
[[package]]
name = "oxc_span"
version = "0.110.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a42c0759b745eca0fe776890af46ce12e79e61796995e51a8eb9dcdf5516ab0"
dependencies = [
"compact_str",
"oxc-miette",
"oxc_allocator",
"oxc_ast_macros",
"oxc_estree",
]
[[package]]
name = "oxc_syntax"
version = "0.110.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b63eac2e04a75a10c5714aeb753cdfa06b1abc66bbaa748b7994700f52c9b184"
dependencies = [
"bitflags 2.10.0",
"cow-utils",
"dragonbox_ecma",
"nonmax",
"oxc_allocator",
"oxc_ast_macros",
"oxc_data_structures",
"oxc_estree",
"oxc_index",
"oxc_span",
"phf",
"unicode-id-start",
]
[[package]]
name = "parking_lot"
version = "0.12.5"
@ -1469,22 +1733,55 @@ version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
[[package]]
name = "pbxgen-stress"
version = "0.1.0"
dependencies = [
"anyhow",
"prometeu-bytecode",
"prometeu-hal",
"serde_json",
]
[[package]]
name = "percent-encoding"
version = "2.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
[[package]]
name = "phf"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf"
dependencies = [
"phf_macros",
"phf_shared",
"serde",
]
[[package]]
name = "phf_generator"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737"
dependencies = [
"fastrand",
"phf_shared",
]
[[package]]
name = "phf_macros"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef"
dependencies = [
"phf_generator",
"phf_shared",
"proc-macro2",
"quote",
"syn 2.0.114",
]
[[package]]
name = "phf_shared"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266"
dependencies = [
"siphasher",
]
[[package]]
name = "pin-project"
version = "1.1.10"
@ -1520,7 +1817,7 @@ dependencies = [
"bytemuck",
"pollster",
"raw-window-handle",
"thiserror",
"thiserror 1.0.69",
"ultraviolet",
"wgpu",
]
@ -1566,15 +1863,6 @@ dependencies = [
"portable-atomic",
]
[[package]]
name = "ppv-lite86"
version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
dependencies = [
"zerocopy",
]
[[package]]
name = "presser"
version = "0.3.1"
@ -1605,42 +1893,39 @@ version = "1.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3eb8486b569e12e2c32ad3e204dbaba5e4b5b216e9367044f25f1dba42341773"
[[package]]
name = "prometeu"
version = "0.1.0"
dependencies = [
"anyhow",
"clap",
"prometeu-compiler",
"prometeu-runtime-desktop",
]
[[package]]
name = "prometeu-bytecode"
version = "0.1.0"
dependencies = [
"serde",
]
[[package]]
name = "prometeu-cli"
name = "prometeu-compiler"
version = "0.1.0"
dependencies = [
"anyhow",
"clap",
"prometeu-host-desktop-winit",
]
[[package]]
name = "prometeu-drivers"
version = "0.1.0"
dependencies = [
"prometeu-hal",
"oxc_allocator",
"oxc_ast",
"oxc_ast_visit",
"oxc_parser",
"oxc_span",
"prometeu-bytecode",
"prometeu-core",
"serde",
"serde_json",
]
[[package]]
name = "prometeu-firmware"
version = "0.1.0"
dependencies = [
"prometeu-bytecode",
"prometeu-drivers",
"prometeu-hal",
"prometeu-system",
"prometeu-vm",
]
[[package]]
name = "prometeu-hal"
name = "prometeu-core"
version = "0.1.0"
dependencies = [
"prometeu-bytecode",
@ -1649,62 +1934,17 @@ dependencies = [
]
[[package]]
name = "prometeu-host-desktop-winit"
name = "prometeu-runtime-desktop"
version = "0.1.0"
dependencies = [
"clap",
"cpal",
"pixels",
"prometeu-drivers",
"prometeu-firmware",
"prometeu-hal",
"prometeu-system",
"prometeu-core",
"ringbuf",
"serde_json",
"winit",
]
[[package]]
name = "prometeu-layer-tests"
version = "0.1.0"
dependencies = [
"prometeu-bytecode",
"prometeu-hal",
"prometeu-test-support",
"prometeu-vm",
]
[[package]]
name = "prometeu-quality-checks"
version = "0.1.0"
[[package]]
name = "prometeu-system"
version = "0.1.0"
dependencies = [
"prometeu-bytecode",
"prometeu-drivers",
"prometeu-hal",
"prometeu-vm",
"serde_json",
]
[[package]]
name = "prometeu-test-support"
version = "0.1.0"
dependencies = [
"rand",
]
[[package]]
name = "prometeu-vm"
version = "0.1.0"
dependencies = [
"prometeu-bytecode",
"prometeu-hal",
"prometeu-test-support",
]
[[package]]
name = "quick-xml"
version = "0.38.4"
@ -1729,36 +1969,6 @@ version = "5.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
[[package]]
name = "rand"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
dependencies = [
"libc",
"rand_chacha",
"rand_core",
]
[[package]]
name = "rand_chacha"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
dependencies = [
"ppv-lite86",
"rand_core",
]
[[package]]
name = "rand_core"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
dependencies = [
"getrandom 0.2.17",
]
[[package]]
name = "range-alloc"
version = "0.1.4"
@ -1888,6 +2098,12 @@ version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
[[package]]
name = "ryu"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984"
[[package]]
name = "safe_arch"
version = "0.7.4"
@ -1931,6 +2147,12 @@ dependencies = [
"tiny-skia",
]
[[package]]
name = "seq-macro"
version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bc711410fbe7399f390ca1c3b60ad0f53f80e95c5eb935e52268a0e2cd49acc"
[[package]]
name = "serde"
version = "1.0.228"
@ -1980,6 +2202,12 @@ version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "siphasher"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d"
[[package]]
name = "slab"
version = "0.4.11"
@ -2001,6 +2229,12 @@ version = "1.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
[[package]]
name = "smawk"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c"
[[package]]
name = "smithay-client-toolkit"
version = "0.19.2"
@ -2015,7 +2249,7 @@ dependencies = [
"log",
"memmap2",
"rustix 0.38.44",
"thiserror",
"thiserror 1.0.69",
"wayland-backend",
"wayland-client",
"wayland-csd-frame",
@ -2093,13 +2327,33 @@ dependencies = [
"winapi-util",
]
[[package]]
name = "textwrap"
version = "0.16.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057"
dependencies = [
"smawk",
"unicode-linebreak",
"unicode-width 0.2.2",
]
[[package]]
name = "thiserror"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
dependencies = [
"thiserror-impl",
"thiserror-impl 1.0.69",
]
[[package]]
name = "thiserror"
version = "2.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
dependencies = [
"thiserror-impl 2.0.18",
]
[[package]]
@ -2113,6 +2367,17 @@ dependencies = [
"syn 2.0.114",
]
[[package]]
name = "thiserror-impl"
version = "2.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.114",
]
[[package]]
name = "tiny-skia"
version = "0.11.4"
@ -2161,9 +2426,9 @@ dependencies = [
[[package]]
name = "toml_parser"
version = "1.0.8+spec-1.1.0"
version = "1.0.6+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0742ff5ff03ea7e67c8ae6c93cac239e0d9784833362da3f9a9c1da8dfefcbdc"
checksum = "a3198b4b0a8e11f09dd03e133c0280504d0801269e9afa46362ffde1cbeebf44"
dependencies = [
"winnow",
]
@ -2199,12 +2464,24 @@ dependencies = [
"wide",
]
[[package]]
name = "unicode-id-start"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81b79ad29b5e19de4260020f8919b443b2ef0277d242ce532ec7b7a2cc8b6007"
[[package]]
name = "unicode-ident"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
[[package]]
name = "unicode-linebreak"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f"
[[package]]
name = "unicode-segmentation"
version = "1.12.0"
@ -2217,6 +2494,12 @@ version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af"
[[package]]
name = "unicode-width"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254"
[[package]]
name = "unicode-xid"
version = "0.2.6"
@ -2245,12 +2528,6 @@ dependencies = [
"winapi-util",
]
[[package]]
name = "wasi"
version = "0.11.1+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
[[package]]
name = "wasip2"
version = "1.0.1+wasi-0.2.4"
@ -2491,7 +2768,7 @@ dependencies = [
"raw-window-handle",
"rustc-hash 1.1.0",
"smallvec",
"thiserror",
"thiserror 1.0.69",
"web-sys",
"wgpu-hal",
"wgpu-types",
@ -2535,7 +2812,7 @@ dependencies = [
"renderdoc-sys",
"rustc-hash 1.1.0",
"smallvec",
"thiserror",
"thiserror 1.0.69",
"wasm-bindgen",
"web-sys",
"wgpu-types",

View File

@ -1,20 +1,10 @@
[workspace]
members = [
"crates/console/prometeu-bytecode",
"crates/console/prometeu-drivers",
"crates/console/prometeu-firmware",
"crates/console/prometeu-hal",
"crates/console/prometeu-system",
"crates/console/prometeu-vm",
"crates/host/prometeu-host-desktop-winit",
"crates/tools/prometeu-cli",
"crates/tools/pbxgen-stress",
"crates/dev/prometeu-test-support",
"crates/dev/prometeu-layer-tests",
"crates/dev/prometeu-quality-checks",
"crates/prometeu-core",
"crates/prometeu-runtime-desktop",
"crates/prometeu",
"crates/prometeu-bytecode",
"crates/prometeu-compiler",
]
resolver = "2"

View File

@ -1,32 +0,0 @@
.PHONY: fmt fmt-check clippy tes-local test-debugger-socket test ci cobertura
fmt:
cargo fmt
fmt-check:
cargo fmt -- --check
clippy:
cargo clippy --workspace --all-features
test-local:
cargo test --workspace --all-targets --all-features --no-fail-fast
test-debugger-socket:
cargo test -p prometeu-host-desktop-winit --lib -- --ignored
clean:
cargo llvm-cov clean --workspace
coverage:
cargo llvm-cov --workspace --all-features --html --output-dir target/llvm-cov
coverage-xml:
cargo llvm-cov report --cobertura --output-path target/llvm-cov/cobertura.xml
coverage-json:
cargo llvm-cov report --json --summary-only --output-path target/llvm-cov/summary.json
test: fmt-check clippy test-local test-debugger-socket
ci: clean fmt-check clippy coverage
cobertura: coverage-xml coverage-json

View File

@ -1,8 +1,8 @@
# PROMETEU
PROMETEU is an **educational and experimental fantasy handheld / fantasy console ecosystem** inspired by classic consoles, focusing on **teaching programming, system architecture, and hardware concepts through software**.
PROMETEU is an **educational and experimental ecosystem** inspired by classic consoles, focusing on **teaching programming, system architecture, and hardware concepts through software**.
> PROMETEU is a fantasy console with a simple, explicit, and educational VM/runtime inside it.
> PROMETEU is a simple, explicit, and educational virtual machine.
---
@ -12,7 +12,6 @@ PROMETEU is an **educational and experimental fantasy handheld / fantasy console
- **Deterministic Loop**: Ensure the same code produces the same result on any platform.
- **Total Portability**: The core does not depend on an operating system, allowing it to run from modern computers to dedicated hardware.
- **First-Class Tools**: Offer deep debugging and inspection as a central part of the experience.
- **DIY hardware affinity**: Keep the machine model close enough to handheld/console-era hardware that it can inform real embedded and homebrew-style implementations.
---
@ -23,17 +22,6 @@ PROMETEU is an **educational and experimental fantasy handheld / fantasy console
- **Deterministic**: same input → same result.
- **Hardware-first**: APIs model peripherals, not modern frameworks.
- **Portable by definition**: if it doesn't work on all platforms, it doesn't exist.
- **Console-era sensibility**: PROMETEU carries intentional influence from NES, SNES, Mega Drive, Game Boy, GBA, CPS-2, and adjacent DIY-friendly hardware thinking.
## 🧭 Canonical Architecture
PROMETEU is the machine. The VM/runtime is one subsystem inside that machine.
Authoritative documents:
- [`docs/runtime/virtual-machine/ARCHITECTURE.md`](docs/vm-arch/ARCHITECTURE.md) is normative for VM/runtime invariants.
- [`docs/runtime/specs/README.md`](docs/specs/runtime/README.md) describes the broader PROMETEU machine, hardware model, and fantasy console context.
- Supporting material under `docs/` may expand, explain, or propose changes, but it must not silently collapse the whole machine into the VM layer.
---
@ -42,9 +30,9 @@ Authoritative documents:
This repository is organized as a Rust workspace and contains several components:
- **[crates/](./crates)**: Software implementation in Rust.
- **[prometeu](crates/tools/prometeu)**: Unified command-line interface (CLI).
- **[prometeu-drivers](crates/console/prometeu-drivers)**: The virtual hardware (GPU, SPU, Input).
- **[prometeu-host-desktop-winit](crates/host/prometeu-host-desktop-winit)**: Host for execution on Desktop systems.
- **[prometeu](./crates/prometeu)**: Unified command-line interface (CLI).
- **[prometeu-core](./crates/prometeu-core)**: The logical core, VM, and internal OS.
- **[prometeu-runtime-desktop](crates/prometeu-runtime-desktop)**: Host for execution on Desktop systems.
- **[docs/](./docs)**: Technical documentation and system specifications.
- **[devtools-protocol/](devtools)**: Definition of the communication protocol for development tools.
- **[test-cartridges/](./test-cartridges)**: Cartridge examples and test suites.
@ -69,10 +57,10 @@ cargo build
To run an example cartridge:
```bash
./target/debug/prometeu run test-cartridges/color-square-ts
./target/debug/prometeu run test-cartridges/color-square
```
For more details on how to use the CLI, see the **[prometeu](crates/tools/prometeu)** README.
For more details on how to use the CLI, see the **[prometeu](./crates/prometeu)** README.
---
@ -93,3 +81,4 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file
## ✨ Final Note
PROMETEU is both a technical and pedagogical project. The idea is not to hide complexity, but to **expose the right complexity**, at the right level, so it can be understood, studied, and explored.

View File

@ -1 +1 @@
0.1.0

17
build/program.disasm.txt Normal file
View File

@ -0,0 +1,17 @@
00000000 Call U32(18) U32(0)
0000000A FrameSync
0000000C Jmp U32(0)
00000012 PushI32 U32(1) ; test_supported.ts:2
00000018 SetLocal U32(0) ; test_supported.ts:2
0000001E GetLocal U32(0) ; test_supported.ts:3
00000024 PushI32 U32(10) ; test_supported.ts:3
0000002A Lt ; test_supported.ts:3
0000002C JmpIfFalse U32(80) ; test_supported.ts:3
00000032 GetLocal U32(0) ; test_supported.ts:4
00000038 PushI32 U32(1) ; test_supported.ts:4
0000003E Add ; test_supported.ts:4
00000040 Dup ; test_supported.ts:4
00000042 SetLocal U32(0) ; test_supported.ts:4
00000048 Pop ; test_supported.ts:4
0000004A Jmp U32(80)
00000050 Ret

BIN
build/program.pbc Normal file

Binary file not shown.

74
build/symbols.json Normal file
View File

@ -0,0 +1,74 @@
[
{
"pc": 18,
"file": "test_supported.ts",
"line": 2,
"col": 13
},
{
"pc": 24,
"file": "test_supported.ts",
"line": 2,
"col": 9
},
{
"pc": 30,
"file": "test_supported.ts",
"line": 3,
"col": 9
},
{
"pc": 36,
"file": "test_supported.ts",
"line": 3,
"col": 13
},
{
"pc": 42,
"file": "test_supported.ts",
"line": 3,
"col": 9
},
{
"pc": 44,
"file": "test_supported.ts",
"line": 3,
"col": 5
},
{
"pc": 50,
"file": "test_supported.ts",
"line": 4,
"col": 13
},
{
"pc": 56,
"file": "test_supported.ts",
"line": 4,
"col": 17
},
{
"pc": 62,
"file": "test_supported.ts",
"line": 4,
"col": 13
},
{
"pc": 64,
"file": "test_supported.ts",
"line": 4,
"col": 9
},
{
"pc": 66,
"file": "test_supported.ts",
"line": 4,
"col": 9
},
{
"pc": 72,
"file": "test_supported.ts",
"line": 4,
"col": 9
}
]

View File

@ -1,50 +0,0 @@
//! This module defines the Application Binary Interface (ABI) of the Prometeu Virtual Machine.
//! It specifies how instructions are encoded in bytes and how they interact with memory.
/// Attempted to execute an unknown or invalid opcode.
pub const TRAP_ILLEGAL_INSTRUCTION: u32 = 0x0000_0001;
/// Program explicitly requested termination via the TRAP opcode.
pub const TRAP_EXPLICIT: u32 = 0x0000_0002;
/// Out-of-bounds access (e.g., stack/heap/local index out of range).
pub const TRAP_OOB: u32 = 0x0000_0003;
/// Type mismatch for the attempted operation (e.g., wrong operand type or syscall argument type).
pub const TRAP_TYPE: u32 = 0x0000_0004;
/// The syscall ID provided is not recognized by the system.
pub const TRAP_INVALID_SYSCALL: u32 = 0x0000_0007;
/// Not enough values on the operand stack for the requested operation/syscall.
pub const TRAP_STACK_UNDERFLOW: u32 = 0x0000_0008;
/// Attempted to access a local slot that is out of bounds for the current frame.
pub const TRAP_INVALID_LOCAL: u32 = 0x0000_0009;
/// Division or modulo by zero.
pub const TRAP_DIV_ZERO: u32 = 0x0000_000A;
/// Attempted to call a function that does not exist in the function table.
pub const TRAP_INVALID_FUNC: u32 = 0x0000_000B;
/// Executed RET with an incorrect stack height (mismatch with function metadata).
pub const TRAP_BAD_RET_SLOTS: u32 = 0x0000_000C;
/// The intrinsic ID provided is not recognized by the runtime, or its metadata is invalid.
pub const TRAP_INVALID_INTRINSIC: u32 = 0x0000_000D;
use serde::{Deserialize, Serialize};
/// Detailed information about a source code span.
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
pub struct SourceSpan {
pub file_id: u32,
pub start: u32,
pub end: u32,
}
/// Detailed information about a runtime trap.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TrapInfo {
/// The specific trap code (e.g., TRAP_OOB).
pub code: u32,
/// The numeric value of the opcode that triggered the trap.
pub opcode: u16,
/// A human-readable message explaining the trap.
pub message: String,
/// The absolute Program Counter (PC) address where the trap occurred.
pub pc: u32,
/// Optional source span information if debug symbols are available.
pub span: Option<SourceSpan>,
}

View File

@ -1,367 +0,0 @@
//! Minimal deterministic assembler for the canonical disassembly format.
//!
//! This is intended primarily for roundtrip tests: `bytes -> disassemble -> assemble -> bytes`.
//! It supports all mnemonics emitted by `disassembler.rs` and their operand formats.
use crate::isa::core::CoreOpCode;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AsmError {
EmptyLine,
UnknownMnemonic(String),
UnexpectedOperand(String),
MissingOperand(String),
InvalidOperand(String),
}
fn emit_u16(v: u16, out: &mut Vec<u8>) {
out.extend_from_slice(&v.to_le_bytes());
}
fn emit_u32(v: u32, out: &mut Vec<u8>) {
out.extend_from_slice(&v.to_le_bytes());
}
fn emit_i32(v: i32, out: &mut Vec<u8>) {
out.extend_from_slice(&v.to_le_bytes());
}
fn emit_i64(v: i64, out: &mut Vec<u8>) {
out.extend_from_slice(&v.to_le_bytes());
}
fn emit_f64_bits(bits: u64, out: &mut Vec<u8>) {
out.extend_from_slice(&bits.to_le_bytes());
}
fn parse_u32_any(s: &str) -> Result<u32, AsmError> {
let s = s.trim();
if let Some(rest) = s.strip_prefix("0x") {
u32::from_str_radix(rest, 16).map_err(|_| AsmError::InvalidOperand(s.into()))
} else {
s.parse::<u32>().map_err(|_| AsmError::InvalidOperand(s.into()))
}
}
fn parse_i32_any(s: &str) -> Result<i32, AsmError> {
s.trim().parse::<i32>().map_err(|_| AsmError::InvalidOperand(s.into()))
}
fn parse_i64_any(s: &str) -> Result<i64, AsmError> {
s.trim().parse::<i64>().map_err(|_| AsmError::InvalidOperand(s.into()))
}
fn parse_f64_bits(s: &str) -> Result<u64, AsmError> {
let s = s.trim();
let s = s.strip_prefix("f64:").ok_or_else(|| AsmError::InvalidOperand(s.into()))?;
let hex = s.strip_prefix("0x").ok_or_else(|| AsmError::InvalidOperand(s.into()))?;
if hex.len() != 16 {
return Err(AsmError::InvalidOperand(s.into()));
}
u64::from_str_radix(hex, 16).map_err(|_| AsmError::InvalidOperand(s.into()))
}
fn parse_keyvals(s: &str) -> Result<(&str, &str), AsmError> {
// Parses formats like: "fn=123, captures=2" or "fn=3, argc=1"
let mut parts = s.split(',');
let a = parts.next().ok_or_else(|| AsmError::MissingOperand(s.into()))?.trim();
let b = parts.next().ok_or_else(|| AsmError::MissingOperand(s.into()))?.trim();
if parts.next().is_some() {
return Err(AsmError::InvalidOperand(s.into()));
}
Ok((a, b))
}
fn parse_pair<'a>(a: &'a str, ka: &str, b: &'a str, kb: &str) -> Result<(u32, u32), AsmError> {
let (ka_l, va_s) = a.split_once('=').ok_or_else(|| AsmError::InvalidOperand(a.into()))?;
let (kb_l, vb_s) = b.split_once('=').ok_or_else(|| AsmError::InvalidOperand(b.into()))?;
if ka_l.trim() != ka || kb_l.trim() != kb {
return Err(AsmError::InvalidOperand(format!("expected keys {} and {}", ka, kb)));
}
let va = parse_u32_any(va_s)?;
let vb = parse_u32_any(vb_s)?;
Ok((va, vb))
}
fn parse_mnemonic(line: &str) -> (&str, &str) {
let line = line.trim();
if let Some(sp) = line.find(char::is_whitespace) {
let (mn, rest) = line.split_at(sp);
(mn, rest.trim())
} else {
(line, "")
}
}
pub fn assemble(src: &str) -> Result<Vec<u8>, AsmError> {
let mut out = Vec::new();
for raw_line in src.lines() {
let line = raw_line.trim();
if line.is_empty() {
continue;
}
let (mn, ops) = parse_mnemonic(line);
match mn {
// Zero-operand
"NOP" => {
emit_u16(CoreOpCode::Nop as u16, &mut out);
}
"HALT" => {
emit_u16(CoreOpCode::Halt as u16, &mut out);
}
"TRAP" => {
emit_u16(CoreOpCode::Trap as u16, &mut out);
}
"DUP" => {
emit_u16(CoreOpCode::Dup as u16, &mut out);
}
"SWAP" => {
emit_u16(CoreOpCode::Swap as u16, &mut out);
}
"ADD" => {
emit_u16(CoreOpCode::Add as u16, &mut out);
}
"SUB" => {
emit_u16(CoreOpCode::Sub as u16, &mut out);
}
"MUL" => {
emit_u16(CoreOpCode::Mul as u16, &mut out);
}
"DIV" => {
emit_u16(CoreOpCode::Div as u16, &mut out);
}
"MOD" => {
emit_u16(CoreOpCode::Mod as u16, &mut out);
}
"NEG" => {
emit_u16(CoreOpCode::Neg as u16, &mut out);
}
"EQ" => {
emit_u16(CoreOpCode::Eq as u16, &mut out);
}
"NEQ" => {
emit_u16(CoreOpCode::Neq as u16, &mut out);
}
"LT" => {
emit_u16(CoreOpCode::Lt as u16, &mut out);
}
"LTE" => {
emit_u16(CoreOpCode::Lte as u16, &mut out);
}
"GT" => {
emit_u16(CoreOpCode::Gt as u16, &mut out);
}
"GTE" => {
emit_u16(CoreOpCode::Gte as u16, &mut out);
}
"AND" => {
emit_u16(CoreOpCode::And as u16, &mut out);
}
"OR" => {
emit_u16(CoreOpCode::Or as u16, &mut out);
}
"NOT" => {
emit_u16(CoreOpCode::Not as u16, &mut out);
}
"BIT_AND" => {
emit_u16(CoreOpCode::BitAnd as u16, &mut out);
}
"BIT_OR" => {
emit_u16(CoreOpCode::BitOr as u16, &mut out);
}
"BIT_XOR" => {
emit_u16(CoreOpCode::BitXor as u16, &mut out);
}
"SHL" => {
emit_u16(CoreOpCode::Shl as u16, &mut out);
}
"SHR" => {
emit_u16(CoreOpCode::Shr as u16, &mut out);
}
"RET" => {
emit_u16(CoreOpCode::Ret as u16, &mut out);
}
"YIELD" => {
emit_u16(CoreOpCode::Yield as u16, &mut out);
}
"FRAME_SYNC" => {
emit_u16(CoreOpCode::FrameSync as u16, &mut out);
}
// One u32 immediate (decimal or hex accepted; SYSCALL/HOSTCALL commonly use hex/idx)
"JMP" => {
if ops.is_empty() {
return Err(AsmError::MissingOperand(line.into()));
}
emit_u16(CoreOpCode::Jmp as u16, &mut out);
emit_u32(parse_u32_any(ops)?, &mut out);
}
"JMP_IF_FALSE" => {
if ops.is_empty() {
return Err(AsmError::MissingOperand(line.into()));
}
emit_u16(CoreOpCode::JmpIfFalse as u16, &mut out);
emit_u32(parse_u32_any(ops)?, &mut out);
}
"JMP_IF_TRUE" => {
if ops.is_empty() {
return Err(AsmError::MissingOperand(line.into()));
}
emit_u16(CoreOpCode::JmpIfTrue as u16, &mut out);
emit_u32(parse_u32_any(ops)?, &mut out);
}
"PUSH_CONST" => {
if ops.is_empty() {
return Err(AsmError::MissingOperand(line.into()));
}
emit_u16(CoreOpCode::PushConst as u16, &mut out);
emit_u32(parse_u32_any(ops)?, &mut out);
}
"PUSH_I64" => {
if ops.is_empty() {
return Err(AsmError::MissingOperand(line.into()));
}
emit_u16(CoreOpCode::PushI64 as u16, &mut out);
emit_i64(parse_i64_any(ops)?, &mut out);
}
"PUSH_F64" => {
if ops.is_empty() {
return Err(AsmError::MissingOperand(line.into()));
}
emit_u16(CoreOpCode::PushF64 as u16, &mut out);
emit_f64_bits(parse_f64_bits(ops)?, &mut out);
}
"PUSH_BOOL" => {
if ops.is_empty() {
return Err(AsmError::MissingOperand(line.into()));
}
let v = parse_u32_any(ops)? as u8;
emit_u16(CoreOpCode::PushBool as u16, &mut out);
out.push(v);
}
"PUSH_I32" => {
if ops.is_empty() {
return Err(AsmError::MissingOperand(line.into()));
}
emit_u16(CoreOpCode::PushI32 as u16, &mut out);
emit_i32(parse_i32_any(ops)?, &mut out);
}
"POP_N" => {
if ops.is_empty() {
return Err(AsmError::MissingOperand(line.into()));
}
emit_u16(CoreOpCode::PopN as u16, &mut out);
emit_u32(parse_u32_any(ops)?, &mut out);
}
"GET_GLOBAL" => {
if ops.is_empty() {
return Err(AsmError::MissingOperand(line.into()));
}
emit_u16(CoreOpCode::GetGlobal as u16, &mut out);
emit_u32(parse_u32_any(ops)?, &mut out);
}
"SET_GLOBAL" => {
if ops.is_empty() {
return Err(AsmError::MissingOperand(line.into()));
}
emit_u16(CoreOpCode::SetGlobal as u16, &mut out);
emit_u32(parse_u32_any(ops)?, &mut out);
}
"GET_LOCAL" => {
if ops.is_empty() {
return Err(AsmError::MissingOperand(line.into()));
}
emit_u16(CoreOpCode::GetLocal as u16, &mut out);
emit_u32(parse_u32_any(ops)?, &mut out);
}
"SET_LOCAL" => {
if ops.is_empty() {
return Err(AsmError::MissingOperand(line.into()));
}
emit_u16(CoreOpCode::SetLocal as u16, &mut out);
emit_u32(parse_u32_any(ops)?, &mut out);
}
"CALL" => {
if ops.is_empty() {
return Err(AsmError::MissingOperand(line.into()));
}
emit_u16(CoreOpCode::Call as u16, &mut out);
emit_u32(parse_u32_any(ops)?, &mut out);
}
"CALL_CLOSURE" => {
if ops.is_empty() {
return Err(AsmError::MissingOperand(line.into()));
}
let (k, v) =
ops.split_once('=').ok_or_else(|| AsmError::InvalidOperand(ops.into()))?;
if k.trim() != "argc" {
return Err(AsmError::InvalidOperand(ops.into()));
}
emit_u16(CoreOpCode::CallClosure as u16, &mut out);
emit_u32(parse_u32_any(v)?, &mut out);
}
"MAKE_CLOSURE" => {
if ops.is_empty() {
return Err(AsmError::MissingOperand(line.into()));
}
let (a, b) = parse_keyvals(ops)?;
// Accept either order but require exact key names
let (fn_id, captures) = if a.starts_with("fn=") && b.starts_with("captures=") {
parse_pair(a, "fn", b, "captures")?
} else if a.starts_with("captures=") && b.starts_with("fn=") {
let (cap, fid) = parse_pair(a, "captures", b, "fn")?;
(fid, cap)
} else {
return Err(AsmError::InvalidOperand(ops.into()));
};
emit_u16(CoreOpCode::MakeClosure as u16, &mut out);
emit_u32(fn_id, &mut out);
emit_u32(captures, &mut out);
}
"SPAWN" => {
if ops.is_empty() {
return Err(AsmError::MissingOperand(line.into()));
}
let (a, b) = parse_keyvals(ops)?;
let (fn_id, argc) = if a.starts_with("fn=") && b.starts_with("argc=") {
parse_pair(a, "fn", b, "argc")?
} else if a.starts_with("argc=") && b.starts_with("fn=") {
let (ac, fid) = parse_pair(a, "argc", b, "fn")?;
(fid, ac)
} else {
return Err(AsmError::InvalidOperand(ops.into()));
};
emit_u16(CoreOpCode::Spawn as u16, &mut out);
emit_u32(fn_id, &mut out);
emit_u32(argc, &mut out);
}
"SLEEP" => {
if ops.is_empty() {
return Err(AsmError::MissingOperand(line.into()));
}
emit_u16(CoreOpCode::Sleep as u16, &mut out);
emit_u32(parse_u32_any(ops)?, &mut out);
}
"HOSTCALL" => {
if ops.is_empty() {
return Err(AsmError::MissingOperand(line.into()));
}
emit_u16(CoreOpCode::Hostcall as u16, &mut out);
emit_u32(parse_u32_any(ops)?, &mut out);
}
"SYSCALL" => {
if ops.is_empty() {
return Err(AsmError::MissingOperand(line.into()));
}
emit_u16(CoreOpCode::Syscall as u16, &mut out);
emit_u32(parse_u32_any(ops)?, &mut out);
}
"INTRINSIC" => {
if ops.is_empty() {
return Err(AsmError::MissingOperand(line.into()));
}
emit_u16(CoreOpCode::Intrinsic as u16, &mut out);
emit_u32(parse_u32_any(ops)?, &mut out);
}
other => return Err(AsmError::UnknownMnemonic(other.into())),
}
}
Ok(out)
}

View File

@ -1,133 +0,0 @@
//! Canonical bytecode decoder for Prometeu Bytecode (PBX payload).
//!
//! Single source of truth for instruction decoding used by compiler/linker/verifier/VM.
//!
//! Contract:
//! - Instructions are encoded as: [opcode: u16 LE][immediate: spec.imm_bytes]
//! - `decode_next(pc, bytes)` returns a typed `DecodedInstr` with canonical `next_pc`.
//! - Immediate helpers validate sizes deterministically and return explicit errors.
use crate::isa::core::{CoreOpCode, CoreOpCodeSpecExt, CoreOpcodeSpec};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DecodeError {
TruncatedOpcode { pc: usize },
UnknownOpcode { pc: usize, opcode: u16 },
TruncatedImmediate { pc: usize, opcode: CoreOpCode, need: usize, have: usize },
ImmediateSizeMismatch { pc: usize, opcode: CoreOpCode, expected: usize, actual: usize },
}
#[derive(Debug, Clone, Copy)]
pub struct DecodedInstr<'a> {
pub opcode: CoreOpCode,
pub pc: usize,
pub next_pc: usize,
/// Raw immediate bytes slice, guaranteed to have length `opcode.spec().imm_bytes`.
pub imm: &'a [u8],
}
impl<'a> DecodedInstr<'a> {
#[inline]
fn ensure_len(&self, expected: usize) -> Result<(), DecodeError> {
if self.imm.len() != expected {
return Err(DecodeError::ImmediateSizeMismatch {
pc: self.pc,
opcode: self.opcode,
expected,
actual: self.imm.len(),
});
}
Ok(())
}
#[inline]
pub fn imm_u8(&self) -> Result<u8, DecodeError> {
self.ensure_len(1)?;
Ok(self.imm[0])
}
#[inline]
pub fn imm_u16(&self) -> Result<u16, DecodeError> {
self.ensure_len(2)?;
Ok(u16::from_le_bytes(self.imm.try_into().unwrap()))
}
#[inline]
pub fn imm_u32(&self) -> Result<u32, DecodeError> {
self.ensure_len(4)?;
Ok(u32::from_le_bytes(self.imm.try_into().unwrap()))
}
#[inline]
pub fn imm_i32(&self) -> Result<i32, DecodeError> {
self.ensure_len(4)?;
Ok(i32::from_le_bytes(self.imm.try_into().unwrap()))
}
#[inline]
pub fn imm_i64(&self) -> Result<i64, DecodeError> {
self.ensure_len(8)?;
Ok(i64::from_le_bytes(self.imm.try_into().unwrap()))
}
#[inline]
pub fn imm_f64(&self) -> Result<f64, DecodeError> {
self.ensure_len(8)?;
Ok(f64::from_le_bytes(self.imm.try_into().unwrap()))
}
/// Helper for opcodes carrying two u32 values packed in 8 bytes (e.g., ALLOC meta).
#[inline]
pub fn imm_u32x2(&self) -> Result<(u32, u32), DecodeError> {
self.ensure_len(8)?;
let a = u32::from_le_bytes(self.imm[0..4].try_into().unwrap());
let b = u32::from_le_bytes(self.imm[4..8].try_into().unwrap());
Ok((a, b))
}
}
/// Decodes the instruction at program counter `pc` from `bytes`.
/// Returns the decoded instruction with canonical `next_pc`.
#[inline]
pub fn decode_next(pc: usize, bytes: &'_ [u8]) -> Result<DecodedInstr<'_>, DecodeError> {
if pc + 2 > bytes.len() {
return Err(DecodeError::TruncatedOpcode { pc });
}
let opcode_val = u16::from_le_bytes([bytes[pc], bytes[pc + 1]]);
let opcode = CoreOpCode::try_from(opcode_val)
.map_err(|_| DecodeError::UnknownOpcode { pc, opcode: opcode_val })?;
let spec: CoreOpcodeSpec = opcode.spec();
let imm_start = pc + 2;
let imm_end = imm_start + (spec.imm_bytes as usize);
if imm_end > bytes.len() {
return Err(DecodeError::TruncatedImmediate {
pc,
opcode,
need: spec.imm_bytes as usize,
have: bytes.len().saturating_sub(imm_start),
});
}
Ok(DecodedInstr { opcode, pc, next_pc: imm_end, imm: &bytes[imm_start..imm_end] })
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn unknown_opcode_is_reported_deterministically() {
// 0x0060 was previously a legacy opcode; now it must be unknown.
let bytes = vec![0x60, 0x00]; // little-endian u16 = 0x0060
match decode_next(0, &bytes) {
Err(DecodeError::UnknownOpcode { pc, opcode }) => {
assert_eq!(pc, 0);
assert_eq!(opcode, 0x0060);
}
other => panic!("expected UnknownOpcode, got {:?}", other),
}
}
}

View File

@ -1,120 +0,0 @@
//! Deterministic disassembler for Prometeu Bytecode (PBX payload).
//!
//! Goals:
//! - Stable formatting across platforms (snapshot-friendly).
//! - Complete coverage of the Core ISA, including closures/coroutines.
//! - Roundtrip-safe with the paired `assembler` module.
//!
//! Format (one instruction per line):
//! - `MNEMONIC` for zero-operand instructions.
//! - `MNEMONIC <imm>` for 1-operand instructions (decimal unless stated).
//! - Special operand formats:
//! - `PUSH_F64 f64:0xhhhhhhhhhhhhhhhh` — exact IEEE-754 bits in hex (little-endian to_bits()).
//! - `MAKE_CLOSURE fn=<u32>, captures=<u32>`
//! - `SPAWN fn=<u32>, argc=<u32>`
//! - `CALL_CLOSURE argc=<u32>`
//! - `HOSTCALL <index>` is printed in decimal because it is a `SYSC` table index.
//! - `SYSCALL` is printed as `SYSCALL 0xhhhh` (numeric id in hex) to avoid cross-crate deps.
//!
//! Notes:
//! - All integers are printed in base-10 except where explicitly noted.
//! - Floats use exact bit-pattern format to prevent locale/rounding differences.
//! - Ordering is the canonical decode order; no address prefixes are emitted.
use crate::decode_next;
use crate::isa::core::{CoreOpCode, CoreOpCodeSpecExt};
use crate::DecodeError;
fn fmt_f64_bits(bits: u64) -> String {
// Fixed-width 16 hex digits, lowercase.
format!("f64:0x{bits:016x}")
}
fn format_operand(op: CoreOpCode, imm: &[u8]) -> String {
match op {
CoreOpCode::Jmp | CoreOpCode::JmpIfFalse | CoreOpCode::JmpIfTrue => {
let v = u32::from_le_bytes(imm.try_into().unwrap());
format!("{}", v)
}
CoreOpCode::PushI64 => {
let v = i64::from_le_bytes(imm.try_into().unwrap());
format!("{}", v)
}
CoreOpCode::PushF64 => {
let v = u64::from_le_bytes(imm.try_into().unwrap());
fmt_f64_bits(v)
}
CoreOpCode::PushBool => {
let v = imm[0];
format!("{}", v)
}
CoreOpCode::PushI32 => {
let v = i32::from_le_bytes(imm.try_into().unwrap());
format!("{}", v)
}
CoreOpCode::PopN
| CoreOpCode::PushConst
| CoreOpCode::GetGlobal
| CoreOpCode::SetGlobal
| CoreOpCode::GetLocal
| CoreOpCode::SetLocal
| CoreOpCode::Call
| CoreOpCode::Sleep
| CoreOpCode::Hostcall => {
let v = u32::from_le_bytes(imm.try_into().unwrap());
format!("{}", v)
}
CoreOpCode::MakeClosure => {
let fn_id = u32::from_le_bytes(imm[0..4].try_into().unwrap());
let cap = u32::from_le_bytes(imm[4..8].try_into().unwrap());
format!("fn={}, captures={}", fn_id, cap)
}
CoreOpCode::CallClosure => {
let argc = u32::from_le_bytes(imm.try_into().unwrap());
format!("argc={}", argc)
}
CoreOpCode::Spawn => {
let fn_id = u32::from_le_bytes(imm[0..4].try_into().unwrap());
let argc = u32::from_le_bytes(imm[4..8].try_into().unwrap());
format!("fn={}, argc={}", fn_id, argc)
}
CoreOpCode::Syscall => {
let id = u32::from_le_bytes(imm.try_into().unwrap());
// Hex id stable, avoids dependency on HAL metadata.
format!("0x{:04x}", id)
}
CoreOpCode::Intrinsic => {
let id = u32::from_le_bytes(imm.try_into().unwrap());
format!("0x{:04x}", id)
}
_ => {
// Fallback: raw immediate hex (little-endian, as encoded)
let mut s = String::with_capacity(2 + imm.len() * 2);
s.push_str("0x");
for b in imm {
use core::fmt::Write as _;
let _ = write!(&mut s, "{:02x}", b);
}
s
}
}
}
/// Disassembles a contiguous byte slice (single function body) into deterministic text.
pub fn disassemble(bytes: &[u8]) -> Result<String, DecodeError> {
let mut pc = 0usize;
let mut out = Vec::new();
while pc < bytes.len() {
let instr = decode_next(pc, bytes)?;
let name = instr.opcode.spec().name;
let imm_len = instr.opcode.spec().imm_bytes as usize;
if imm_len == 0 {
out.push(name.to_string());
} else {
let ops = format_operand(instr.opcode, instr.imm);
out.push(format!("{} {}", name, ops));
}
pc = instr.next_pc;
}
Ok(out.join("\n"))
}

View File

@ -1,12 +0,0 @@
//! Core ISA profile (bytecode-level, stable, canonical).
//!
//! This profile is the canonical opcode surface used by decoder/disasm and
//! mirrors the runtime-visible instruction set implemented today, including
//! closures, coroutines, intrinsics, and hostcall patching semantics.
// For PR-1.4 we define the core profile as the current `OpCode` set. We re-export
// the opcode type and spec so downstream tools can import from a stable path:
// `prometeu_bytecode::isa::core::*`.
pub use crate::opcode::OpCode as CoreOpCode;
pub use crate::opcode_spec::{OpCodeSpecExt as CoreOpCodeSpecExt, OpcodeSpec as CoreOpcodeSpec};

View File

@ -1,6 +0,0 @@
//! ISA module tree
//!
//! This module defines stable ISA profiles. For PR-1.4 we expose the
//! minimal core ISA surface used by the encoder/decoder and (later) verifier.
pub mod core;

View File

@ -1,98 +0,0 @@
//! Shared bytecode layout utilities, used by both compiler (emitter/linker)
//! and the VM (verifier/loader). This ensures a single source of truth for
//! how function ranges, instruction boundaries, and pc→function lookups are
//! interpreted post-link.
use crate::model::FunctionMeta;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct FunctionLayout {
pub start: usize,
pub end: usize, // exclusive
}
/// Precompute canonical [start, end) ranges for all functions.
///
/// Contract:
/// - Ranges are computed by sorting functions by `code_offset` (stable),
/// then using the next function's start as the current end; the last
/// function ends at `code_len_total`.
/// - The returned vector is indexed by the original function indices.
pub fn compute_function_layouts(
functions: &[FunctionMeta],
code_len_total: usize,
) -> Vec<FunctionLayout> {
// Build index array and sort by start offset (stable to preserve relative order).
let mut idxs: Vec<usize> = (0..functions.len()).collect();
idxs.sort_by_key(|&i| functions[i].code_offset as usize);
// Optional guard: offsets should be strictly increasing (duplicates are suspicious).
for w in idxs.windows(2) {
if let [a, b] = *w {
let sa = functions[a].code_offset as usize;
let sb = functions[b].code_offset as usize;
debug_assert!(
sa < sb,
"Function code_offset must be strictly increasing: {} vs {} (indices {} and {})",
sa,
sb,
a,
b
);
}
}
let mut out = vec![FunctionLayout { start: 0, end: 0 }; functions.len()];
for (pos, &i) in idxs.iter().enumerate() {
let start = functions[i].code_offset as usize;
let end = if pos + 1 < idxs.len() {
functions[idxs[pos + 1]].code_offset as usize
} else {
code_len_total
};
out[i] = FunctionLayout { start, end };
}
out
}
#[cfg(test)]
mod tests {
use super::*;
fn build_funcs(offsets: &[usize], lens: Option<&[usize]>) -> Vec<FunctionMeta> {
let mut v = Vec::new();
for (i, off) in offsets.iter().copied().enumerate() {
let len_u32 = lens.and_then(|ls| ls.get(i).copied()).unwrap_or(0) as u32;
v.push(FunctionMeta {
code_offset: off as u32,
code_len: len_u32,
param_slots: 0,
local_slots: 0,
return_slots: 0,
max_stack_slots: 0,
});
}
v
}
#[test]
fn compute_function_layouts_end_is_next_start() {
// Synthetic functions with known offsets: 0, 10, 25; total_len = 40
let funcs = build_funcs(&[0, 10, 25], None);
let layouts = compute_function_layouts(&funcs, 40);
assert_eq!(layouts.len(), 3);
assert_eq!(layouts[0], FunctionLayout { start: 0, end: 10 });
assert_eq!(layouts[1], FunctionLayout { start: 10, end: 25 });
assert_eq!(layouts[2], FunctionLayout { start: 25, end: 40 });
for i in 0..3 {
let l = &layouts[i];
assert_eq!(
l.end - l.start,
(funcs.get(i + 1).map(|n| n.code_offset as usize).unwrap_or(40))
- (funcs[i].code_offset as usize)
);
}
}
}

View File

@ -1,24 +0,0 @@
mod abi;
pub mod assembler;
mod decoder;
mod disassembler;
pub mod isa; // canonical ISA boundary (core and future profiles)
mod layout;
pub mod model;
mod opcode;
mod opcode_spec;
mod program_image;
mod value;
pub use abi::{
TrapInfo, TRAP_BAD_RET_SLOTS, TRAP_DIV_ZERO, TRAP_EXPLICIT, TRAP_ILLEGAL_INSTRUCTION,
TRAP_INVALID_FUNC, TRAP_INVALID_INTRINSIC, TRAP_INVALID_LOCAL, TRAP_INVALID_SYSCALL, TRAP_OOB,
TRAP_STACK_UNDERFLOW, TRAP_TYPE,
};
pub use assembler::{assemble, AsmError};
pub use decoder::{decode_next, DecodeError};
pub use disassembler::disassemble;
pub use layout::{compute_function_layouts, FunctionLayout};
pub use model::{BytecodeLoader, FunctionMeta, LoadError, SyscallDecl};
pub use program_image::ProgramImage;
pub use value::{HeapRef, Value};

View File

@ -1,946 +0,0 @@
use crate::abi::SourceSpan;
use crate::opcode::OpCode;
use serde::{Deserialize, Serialize};
use std::collections::HashSet;
/// An entry in the Constant Pool.
///
/// The Constant Pool is a table of unique values used by the program.
/// Instead of embedding large data (like strings) directly in the instruction stream,
/// the bytecode uses `PushConst <index>` to load these values onto the stack.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum ConstantPoolEntry {
/// Reserved index (0). Represents a null/undefined value.
Null,
/// A 64-bit integer constant.
Int64(i64),
/// A 64-bit floating point constant.
Float64(f64),
/// A boolean constant.
Boolean(bool),
/// A UTF-8 string constant.
String(String),
/// A 32-bit integer constant.
Int32(i32),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum LoadError {
InvalidMagic,
InvalidVersion,
InvalidEndianness,
OverlappingSections,
SectionOutOfBounds,
InvalidOpcode,
InvalidConstIndex,
InvalidFunctionIndex,
MalformedHeader,
MalformedSection,
MissingSyscallSection,
DuplicateSyscallIdentity,
InvalidUtf8,
UnexpectedEof,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct FunctionMeta {
pub code_offset: u32,
pub code_len: u32,
pub param_slots: u16,
pub local_slots: u16,
pub return_slots: u16,
pub max_stack_slots: u16,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct DebugInfo {
pub pc_to_span: Vec<(u32, SourceSpan)>, // Sorted by PC
pub function_names: Vec<(u32, String)>, // (func_idx, name)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Export {
pub symbol: String,
pub func_idx: u32,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct SyscallDecl {
pub module: String,
pub name: String,
pub version: u16,
pub arg_slots: u16,
pub ret_slots: u16,
}
const SECTION_KIND_CONST_POOL: u32 = 0;
const SECTION_KIND_FUNCTIONS: u32 = 1;
const SECTION_KIND_CODE: u32 = 2;
const SECTION_KIND_DEBUG: u32 = 3;
const SECTION_KIND_EXPORTS: u32 = 4;
const SECTION_KIND_SYSCALLS: u32 = 5;
/// Represents the final serialized format of a PBS v0 module.
///
/// This structure is a pure data container for the PBS format. It does NOT
/// contain any linker-like logic (symbol resolution, patching, etc.).
/// All multi-module programs must be flattened and linked by the compiler
/// before being serialized into this format.
#[derive(Debug, Clone, PartialEq)]
pub struct BytecodeModule {
pub version: u16,
pub const_pool: Vec<ConstantPoolEntry>,
pub functions: Vec<FunctionMeta>,
pub code: Vec<u8>,
pub debug_info: Option<DebugInfo>,
pub exports: Vec<Export>,
pub syscalls: Vec<SyscallDecl>,
}
impl BytecodeModule {
pub fn serialize(&self) -> Vec<u8> {
let cp_data = self.serialize_const_pool();
let func_data = self.serialize_functions();
let code_data = self.code.clone();
let debug_data =
self.debug_info.as_ref().map(|di| self.serialize_debug(di)).unwrap_or_default();
let export_data = self.serialize_exports();
let syscall_data = self.serialize_syscalls();
let mut final_sections = Vec::new();
if !cp_data.is_empty() {
final_sections.push((SECTION_KIND_CONST_POOL, cp_data));
}
if !func_data.is_empty() {
final_sections.push((SECTION_KIND_FUNCTIONS, func_data));
}
if !code_data.is_empty() {
final_sections.push((SECTION_KIND_CODE, code_data));
}
if !debug_data.is_empty() {
final_sections.push((SECTION_KIND_DEBUG, debug_data));
}
if !export_data.is_empty() {
final_sections.push((SECTION_KIND_EXPORTS, export_data));
}
final_sections.push((SECTION_KIND_SYSCALLS, syscall_data));
let mut out = Vec::new();
// Magic "PBS\0"
out.extend_from_slice(b"PBS\0");
// Version 0
out.extend_from_slice(&0u16.to_le_bytes());
// Endianness 0 (Little Endian), Reserved
out.extend_from_slice(&[0u8, 0u8]);
// section_count
out.extend_from_slice(&(final_sections.len() as u32).to_le_bytes());
// padding to 32 bytes
out.extend_from_slice(&[0u8; 20]);
let mut current_offset = 32 + (final_sections.len() as u32 * 12);
// Write section table
for (kind, data) in &final_sections {
let k: u32 = *kind;
out.extend_from_slice(&k.to_le_bytes());
out.extend_from_slice(&current_offset.to_le_bytes());
out.extend_from_slice(&(data.len() as u32).to_le_bytes());
current_offset += data.len() as u32;
}
// Write section data
for (_, data) in final_sections {
out.extend_from_slice(&data);
}
out
}
fn serialize_const_pool(&self) -> Vec<u8> {
if self.const_pool.is_empty() {
return Vec::new();
}
let mut data = Vec::new();
data.extend_from_slice(&(self.const_pool.len() as u32).to_le_bytes());
for entry in &self.const_pool {
match entry {
ConstantPoolEntry::Null => data.push(0),
ConstantPoolEntry::Int64(v) => {
data.push(1);
data.extend_from_slice(&v.to_le_bytes());
}
ConstantPoolEntry::Float64(v) => {
data.push(2);
data.extend_from_slice(&v.to_le_bytes());
}
ConstantPoolEntry::Boolean(v) => {
data.push(3);
data.push(if *v { 1 } else { 0 });
}
ConstantPoolEntry::String(v) => {
data.push(4);
let s_bytes = v.as_bytes();
data.extend_from_slice(&(s_bytes.len() as u32).to_le_bytes());
data.extend_from_slice(s_bytes);
}
ConstantPoolEntry::Int32(v) => {
data.push(5);
data.extend_from_slice(&v.to_le_bytes());
}
}
}
data
}
fn serialize_functions(&self) -> Vec<u8> {
if self.functions.is_empty() {
return Vec::new();
}
let mut data = Vec::new();
data.extend_from_slice(&(self.functions.len() as u32).to_le_bytes());
for f in &self.functions {
data.extend_from_slice(&f.code_offset.to_le_bytes());
data.extend_from_slice(&f.code_len.to_le_bytes());
data.extend_from_slice(&f.param_slots.to_le_bytes());
data.extend_from_slice(&f.local_slots.to_le_bytes());
data.extend_from_slice(&f.return_slots.to_le_bytes());
data.extend_from_slice(&f.max_stack_slots.to_le_bytes());
}
data
}
fn serialize_debug(&self, di: &DebugInfo) -> Vec<u8> {
let mut data = Vec::new();
data.extend_from_slice(&(di.pc_to_span.len() as u32).to_le_bytes());
for (pc, span) in &di.pc_to_span {
data.extend_from_slice(&pc.to_le_bytes());
data.extend_from_slice(&span.file_id.to_le_bytes());
data.extend_from_slice(&span.start.to_le_bytes());
data.extend_from_slice(&span.end.to_le_bytes());
}
data.extend_from_slice(&(di.function_names.len() as u32).to_le_bytes());
for (idx, name) in &di.function_names {
data.extend_from_slice(&idx.to_le_bytes());
let n_bytes = name.as_bytes();
data.extend_from_slice(&(n_bytes.len() as u32).to_le_bytes());
data.extend_from_slice(n_bytes);
}
data
}
fn serialize_exports(&self) -> Vec<u8> {
if self.exports.is_empty() {
return Vec::new();
}
let mut data = Vec::new();
data.extend_from_slice(&(self.exports.len() as u32).to_le_bytes());
for exp in &self.exports {
data.extend_from_slice(&exp.func_idx.to_le_bytes());
let s_bytes = exp.symbol.as_bytes();
data.extend_from_slice(&(s_bytes.len() as u32).to_le_bytes());
data.extend_from_slice(s_bytes);
}
data
}
fn serialize_syscalls(&self) -> Vec<u8> {
let mut data = Vec::new();
data.extend_from_slice(&(self.syscalls.len() as u32).to_le_bytes());
for syscall in &self.syscalls {
let module = syscall.module.as_bytes();
let name = syscall.name.as_bytes();
assert!(u16::try_from(module.len()).is_ok(), "SYSC module name exceeds u16 length");
assert!(u16::try_from(name.len()).is_ok(), "SYSC syscall name exceeds u16 length");
data.extend_from_slice(&(module.len() as u16).to_le_bytes());
data.extend_from_slice(module);
data.extend_from_slice(&(name.len() as u16).to_le_bytes());
data.extend_from_slice(name);
data.extend_from_slice(&syscall.version.to_le_bytes());
data.extend_from_slice(&syscall.arg_slots.to_le_bytes());
data.extend_from_slice(&syscall.ret_slots.to_le_bytes());
}
data
}
}
pub struct BytecodeLoader;
impl BytecodeLoader {
pub fn load(bytes: &[u8]) -> Result<BytecodeModule, LoadError> {
if bytes.len() < 32 {
return Err(LoadError::UnexpectedEof);
}
// Magic "PBS\0"
if &bytes[0..4] != b"PBS\0" {
return Err(LoadError::InvalidMagic);
}
let version = u16::from_le_bytes([bytes[4], bytes[5]]);
if version != 0 {
return Err(LoadError::InvalidVersion);
}
let endianness = bytes[6];
if endianness != 0 {
// 0 = Little Endian
return Err(LoadError::InvalidEndianness);
}
let section_count = u32::from_le_bytes([bytes[8], bytes[9], bytes[10], bytes[11]]);
let mut sections = Vec::new();
let mut pos = 32;
for _ in 0..section_count {
if pos + 12 > bytes.len() {
return Err(LoadError::UnexpectedEof);
}
let kind =
u32::from_le_bytes([bytes[pos], bytes[pos + 1], bytes[pos + 2], bytes[pos + 3]]);
let offset = u32::from_le_bytes([
bytes[pos + 4],
bytes[pos + 5],
bytes[pos + 6],
bytes[pos + 7],
]);
let length = u32::from_le_bytes([
bytes[pos + 8],
bytes[pos + 9],
bytes[pos + 10],
bytes[pos + 11],
]);
// Basic bounds check
if (offset as usize) + (length as usize) > bytes.len() {
return Err(LoadError::SectionOutOfBounds);
}
sections.push((kind, offset, length));
pos += 12;
}
// Check for overlapping sections
for i in 0..sections.len() {
for j in i + 1..sections.len() {
let (_, o1, l1) = sections[i];
let (_, o2, l2) = sections[j];
if (o1 < o2 + l2) && (o2 < o1 + l1) {
return Err(LoadError::OverlappingSections);
}
}
}
let mut module = BytecodeModule {
version,
const_pool: Vec::new(),
functions: Vec::new(),
code: Vec::new(),
debug_info: None,
exports: Vec::new(),
syscalls: Vec::new(),
};
let mut has_syscalls = false;
for (kind, offset, length) in sections {
let section_data = &bytes[offset as usize..(offset + length) as usize];
match kind {
SECTION_KIND_CONST_POOL => {
// Const Pool
module.const_pool = parse_const_pool(section_data)?;
}
SECTION_KIND_FUNCTIONS => {
// Functions
module.functions = parse_functions(section_data)?;
}
SECTION_KIND_CODE => {
// Code
module.code = section_data.to_vec();
}
SECTION_KIND_DEBUG => {
// Debug Info
module.debug_info = Some(parse_debug_section(section_data)?);
}
SECTION_KIND_EXPORTS => {
// Exports
module.exports = parse_exports(section_data)?;
}
SECTION_KIND_SYSCALLS => {
module.syscalls = parse_syscalls(section_data)?;
has_syscalls = true;
}
_ => {} // Skip unknown or optional sections
}
}
if !has_syscalls {
return Err(LoadError::MissingSyscallSection);
}
// Additional validations
validate_module(&module)?;
Ok(module)
}
}
fn parse_const_pool(data: &[u8]) -> Result<Vec<ConstantPoolEntry>, LoadError> {
if data.is_empty() {
return Ok(Vec::new());
}
if data.len() < 4 {
return Err(LoadError::MalformedSection);
}
let count = u32::from_le_bytes([data[0], data[1], data[2], data[3]]) as usize;
let mut cp = Vec::with_capacity(count);
let mut pos = 4;
for _ in 0..count {
if pos >= data.len() {
return Err(LoadError::UnexpectedEof);
}
let tag = data[pos];
pos += 1;
match tag {
0 => cp.push(ConstantPoolEntry::Null),
1 => {
// Int64
if pos + 8 > data.len() {
return Err(LoadError::UnexpectedEof);
}
let val = i64::from_le_bytes(data[pos..pos + 8].try_into().unwrap());
cp.push(ConstantPoolEntry::Int64(val));
pos += 8;
}
2 => {
// Float64
if pos + 8 > data.len() {
return Err(LoadError::UnexpectedEof);
}
let val = f64::from_le_bytes(data[pos..pos + 8].try_into().unwrap());
cp.push(ConstantPoolEntry::Float64(val));
pos += 8;
}
3 => {
// Boolean
if pos >= data.len() {
return Err(LoadError::UnexpectedEof);
}
cp.push(ConstantPoolEntry::Boolean(data[pos] != 0));
pos += 1;
}
4 => {
// String
if pos + 4 > data.len() {
return Err(LoadError::UnexpectedEof);
}
let len = u32::from_le_bytes(data[pos..pos + 4].try_into().unwrap()) as usize;
pos += 4;
if pos + len > data.len() {
return Err(LoadError::UnexpectedEof);
}
let s = String::from_utf8_lossy(&data[pos..pos + len]).into_owned();
cp.push(ConstantPoolEntry::String(s));
pos += len;
}
5 => {
// Int32
if pos + 4 > data.len() {
return Err(LoadError::UnexpectedEof);
}
let val = i32::from_le_bytes(data[pos..pos + 4].try_into().unwrap());
cp.push(ConstantPoolEntry::Int32(val));
pos += 4;
}
_ => return Err(LoadError::MalformedSection),
}
}
Ok(cp)
}
fn parse_functions(data: &[u8]) -> Result<Vec<FunctionMeta>, LoadError> {
if data.is_empty() {
return Ok(Vec::new());
}
if data.len() < 4 {
return Err(LoadError::MalformedSection);
}
let count = u32::from_le_bytes([data[0], data[1], data[2], data[3]]) as usize;
let mut functions = Vec::with_capacity(count);
let mut pos = 4;
for _ in 0..count {
if pos + 16 > data.len() {
return Err(LoadError::UnexpectedEof);
}
let code_offset = u32::from_le_bytes(data[pos..pos + 4].try_into().unwrap());
let code_len = u32::from_le_bytes(data[pos + 4..pos + 8].try_into().unwrap());
let param_slots = u16::from_le_bytes(data[pos + 8..pos + 10].try_into().unwrap());
let local_slots = u16::from_le_bytes(data[pos + 10..pos + 12].try_into().unwrap());
let return_slots = u16::from_le_bytes(data[pos + 12..pos + 14].try_into().unwrap());
let max_stack_slots = u16::from_le_bytes(data[pos + 14..pos + 16].try_into().unwrap());
functions.push(FunctionMeta {
code_offset,
code_len,
param_slots,
local_slots,
return_slots,
max_stack_slots,
});
pos += 16;
}
Ok(functions)
}
fn parse_debug_section(data: &[u8]) -> Result<DebugInfo, LoadError> {
if data.is_empty() {
return Ok(DebugInfo::default());
}
if data.len() < 8 {
return Err(LoadError::MalformedSection);
}
let mut pos = 0;
// PC to Span table
let span_count = u32::from_le_bytes(data[pos..pos + 4].try_into().unwrap()) as usize;
pos += 4;
let mut pc_to_span = Vec::with_capacity(span_count);
for _ in 0..span_count {
if pos + 16 > data.len() {
return Err(LoadError::UnexpectedEof);
}
let pc = u32::from_le_bytes(data[pos..pos + 4].try_into().unwrap());
let file_id = u32::from_le_bytes(data[pos + 4..pos + 8].try_into().unwrap());
let start = u32::from_le_bytes(data[pos + 8..pos + 12].try_into().unwrap());
let end = u32::from_le_bytes(data[pos + 12..pos + 16].try_into().unwrap());
pc_to_span.push((pc, SourceSpan { file_id, start, end }));
pos += 16;
}
// Function names table
if pos + 4 > data.len() {
return Err(LoadError::UnexpectedEof);
}
let func_name_count = u32::from_le_bytes(data[pos..pos + 4].try_into().unwrap()) as usize;
pos += 4;
let mut function_names = Vec::with_capacity(func_name_count);
for _ in 0..func_name_count {
if pos + 8 > data.len() {
return Err(LoadError::UnexpectedEof);
}
let func_idx = u32::from_le_bytes(data[pos..pos + 4].try_into().unwrap());
let name_len = u32::from_le_bytes(data[pos + 4..pos + 8].try_into().unwrap()) as usize;
pos += 8;
if pos + name_len > data.len() {
return Err(LoadError::UnexpectedEof);
}
let name = String::from_utf8_lossy(&data[pos..pos + name_len]).into_owned();
function_names.push((func_idx, name));
pos += name_len;
}
Ok(DebugInfo { pc_to_span, function_names })
}
fn parse_exports(data: &[u8]) -> Result<Vec<Export>, LoadError> {
if data.is_empty() {
return Ok(Vec::new());
}
if data.len() < 4 {
return Err(LoadError::MalformedSection);
}
let count = u32::from_le_bytes([data[0], data[1], data[2], data[3]]) as usize;
let mut exports = Vec::with_capacity(count);
let mut pos = 4;
for _ in 0..count {
if pos + 8 > data.len() {
return Err(LoadError::UnexpectedEof);
}
let func_idx = u32::from_le_bytes(data[pos..pos + 4].try_into().unwrap());
let name_len = u32::from_le_bytes(data[pos + 4..pos + 8].try_into().unwrap()) as usize;
pos += 8;
if pos + name_len > data.len() {
return Err(LoadError::UnexpectedEof);
}
let symbol = String::from_utf8_lossy(&data[pos..pos + name_len]).into_owned();
exports.push(Export { symbol, func_idx });
pos += name_len;
}
Ok(exports)
}
fn parse_syscalls(data: &[u8]) -> Result<Vec<SyscallDecl>, LoadError> {
if data.len() < 4 {
return Err(LoadError::MalformedSection);
}
let count = u32::from_le_bytes([data[0], data[1], data[2], data[3]]) as usize;
let mut syscalls = Vec::with_capacity(count);
let mut pos = 4;
for _ in 0..count {
if pos + 2 > data.len() {
return Err(LoadError::UnexpectedEof);
}
let module_len = u16::from_le_bytes(data[pos..pos + 2].try_into().unwrap()) as usize;
pos += 2;
if pos + module_len > data.len() {
return Err(LoadError::UnexpectedEof);
}
let module = std::str::from_utf8(&data[pos..pos + module_len])
.map_err(|_| LoadError::InvalidUtf8)?;
pos += module_len;
if pos + 2 > data.len() {
return Err(LoadError::UnexpectedEof);
}
let name_len = u16::from_le_bytes(data[pos..pos + 2].try_into().unwrap()) as usize;
pos += 2;
if pos + name_len > data.len() {
return Err(LoadError::UnexpectedEof);
}
let name =
std::str::from_utf8(&data[pos..pos + name_len]).map_err(|_| LoadError::InvalidUtf8)?;
pos += name_len;
if pos + 6 > data.len() {
return Err(LoadError::UnexpectedEof);
}
let version = u16::from_le_bytes(data[pos..pos + 2].try_into().unwrap());
let arg_slots = u16::from_le_bytes(data[pos + 2..pos + 4].try_into().unwrap());
let ret_slots = u16::from_le_bytes(data[pos + 4..pos + 6].try_into().unwrap());
pos += 6;
syscalls.push(SyscallDecl {
module: module.to_owned(),
name: name.to_owned(),
version,
arg_slots,
ret_slots,
});
}
if pos != data.len() {
return Err(LoadError::MalformedSection);
}
Ok(syscalls)
}
fn validate_module(module: &BytecodeModule) -> Result<(), LoadError> {
let mut syscall_identities = HashSet::with_capacity(module.syscalls.len());
for syscall in &module.syscalls {
if !syscall_identities.insert((
syscall.module.clone(),
syscall.name.clone(),
syscall.version,
)) {
return Err(LoadError::DuplicateSyscallIdentity);
}
}
for func in &module.functions {
// Opcode stream bounds
if (func.code_offset as usize) + (func.code_len as usize) > module.code.len() {
return Err(LoadError::InvalidFunctionIndex);
}
}
// Basic opcode scan for const pool indices
let mut pos = 0;
while pos < module.code.len() {
if pos + 2 > module.code.len() {
break; // Unexpected EOF in middle of opcode, maybe should be error
}
let op_val = u16::from_le_bytes([module.code[pos], module.code[pos + 1]]);
let opcode = OpCode::try_from(op_val).map_err(|_| LoadError::InvalidOpcode)?;
pos += 2;
match opcode {
OpCode::PushConst => {
if pos + 4 > module.code.len() {
return Err(LoadError::UnexpectedEof);
}
let idx =
u32::from_le_bytes(module.code[pos..pos + 4].try_into().unwrap()) as usize;
if idx >= module.const_pool.len() {
return Err(LoadError::InvalidConstIndex);
}
pos += 4;
}
OpCode::PushI32
| OpCode::Jmp
| OpCode::JmpIfFalse
| OpCode::JmpIfTrue
| OpCode::GetGlobal
| OpCode::SetGlobal
| OpCode::GetLocal
| OpCode::SetLocal
| OpCode::PopN
| OpCode::Hostcall
| OpCode::Syscall
| OpCode::Intrinsic => {
pos += 4;
}
OpCode::PushI64 | OpCode::PushF64 => {
pos += 8;
}
OpCode::PushBool => {
pos += 1;
}
OpCode::Call => {
pos += 4;
}
_ => {}
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
fn create_header(section_count: u32) -> Vec<u8> {
let mut h = vec![0u8; 32];
h[0..4].copy_from_slice(b"PBS\0");
h[4..6].copy_from_slice(&0u16.to_le_bytes()); // version
h[6] = 0; // endianness
h[8..12].copy_from_slice(&section_count.to_le_bytes());
h
}
fn minimal_module() -> BytecodeModule {
BytecodeModule {
version: 0,
const_pool: vec![],
functions: vec![],
code: vec![],
debug_info: None,
exports: vec![],
syscalls: vec![],
}
}
fn build_pbs_with_sections(sections: Vec<(u32, Vec<u8>)>) -> Vec<u8> {
let mut data = create_header(sections.len() as u32);
let mut offset = 32 + (sections.len() as u32 * 12);
for (kind, section_data) in &sections {
data.extend_from_slice(&kind.to_le_bytes());
data.extend_from_slice(&offset.to_le_bytes());
data.extend_from_slice(&(section_data.len() as u32).to_le_bytes());
offset += section_data.len() as u32;
}
for (_, section_data) in sections {
data.extend_from_slice(&section_data);
}
data
}
#[test]
fn test_invalid_magic() {
let mut data = create_header(0);
data[0] = b'X';
assert_eq!(BytecodeLoader::load(&data), Err(LoadError::InvalidMagic));
}
#[test]
fn test_invalid_version() {
let mut data = create_header(0);
data[4] = 1;
assert_eq!(BytecodeLoader::load(&data), Err(LoadError::InvalidVersion));
}
#[test]
fn test_invalid_endianness() {
let mut data = create_header(0);
data[6] = 1;
assert_eq!(BytecodeLoader::load(&data), Err(LoadError::InvalidEndianness));
}
#[test]
fn test_overlapping_sections() {
let mut data = create_header(2);
// Section 1: Kind 0, Offset 64, Length 32
data.extend_from_slice(&0u32.to_le_bytes());
data.extend_from_slice(&64u32.to_le_bytes());
data.extend_from_slice(&32u32.to_le_bytes());
// Section 2: Kind 1, Offset 80, Length 32 (Overlaps with Section 1)
data.extend_from_slice(&1u32.to_le_bytes());
data.extend_from_slice(&80u32.to_le_bytes());
data.extend_from_slice(&32u32.to_le_bytes());
// Ensure data is long enough for the offsets
data.resize(256, 0);
assert_eq!(BytecodeLoader::load(&data), Err(LoadError::OverlappingSections));
}
#[test]
fn test_section_out_of_bounds() {
let mut data = create_header(1);
// Section 1: Kind 0, Offset 64, Length 1000
data.extend_from_slice(&0u32.to_le_bytes());
data.extend_from_slice(&64u32.to_le_bytes());
data.extend_from_slice(&1000u32.to_le_bytes());
data.resize(256, 0);
assert_eq!(BytecodeLoader::load(&data), Err(LoadError::SectionOutOfBounds));
}
#[test]
fn test_invalid_function_code_offset() {
let mut data = create_header(3);
// Section 1: Functions, Kind 1, Offset 80, Length 20 (Header 4 + 1 entry 16)
data.extend_from_slice(&1u32.to_le_bytes());
data.extend_from_slice(&80u32.to_le_bytes());
data.extend_from_slice(&20u32.to_le_bytes());
// Section 2: Code, Kind 2, Offset 128, Length 10
data.extend_from_slice(&2u32.to_le_bytes());
data.extend_from_slice(&128u32.to_le_bytes());
data.extend_from_slice(&10u32.to_le_bytes());
// Section 3: SYSC, Kind 5, Offset 160, Length 4 (empty)
data.extend_from_slice(&5u32.to_le_bytes());
data.extend_from_slice(&160u32.to_le_bytes());
data.extend_from_slice(&4u32.to_le_bytes());
data.resize(256, 0);
// Setup functions section
let func_data_start = 80;
data[func_data_start..func_data_start + 4].copy_from_slice(&1u32.to_le_bytes()); // 1 function
let entry_start = func_data_start + 4;
data[entry_start..entry_start + 4].copy_from_slice(&5u32.to_le_bytes()); // code_offset = 5
data[entry_start + 4..entry_start + 8].copy_from_slice(&10u32.to_le_bytes()); // code_len = 10
// 5 + 10 = 15 > 10 (code section length)
data[160..164].copy_from_slice(&0u32.to_le_bytes());
assert_eq!(BytecodeLoader::load(&data), Err(LoadError::InvalidFunctionIndex));
}
#[test]
fn test_invalid_const_index() {
let mut data = create_header(3);
// Section 1: Const Pool, Kind 0, Offset 80, Length 4 (Empty CP)
data.extend_from_slice(&0u32.to_le_bytes());
data.extend_from_slice(&80u32.to_le_bytes());
data.extend_from_slice(&4u32.to_le_bytes());
// Section 2: Code, Kind 2, Offset 128, Length 6 (PushConst 0)
data.extend_from_slice(&2u32.to_le_bytes());
data.extend_from_slice(&128u32.to_le_bytes());
data.extend_from_slice(&6u32.to_le_bytes());
// Section 3: SYSC, Kind 5, Offset 160, Length 4 (empty)
data.extend_from_slice(&5u32.to_le_bytes());
data.extend_from_slice(&160u32.to_le_bytes());
data.extend_from_slice(&4u32.to_le_bytes());
data.resize(256, 0);
// Setup empty CP
data[80..84].copy_from_slice(&0u32.to_le_bytes());
// Setup code with PushConst 0
data[128..130].copy_from_slice(&(OpCode::PushConst as u16).to_le_bytes());
data[130..134].copy_from_slice(&0u32.to_le_bytes());
data[160..164].copy_from_slice(&0u32.to_le_bytes());
assert_eq!(BytecodeLoader::load(&data), Err(LoadError::InvalidConstIndex));
}
#[test]
fn test_missing_sysc_section_is_rejected() {
let data = create_header(0);
assert_eq!(BytecodeLoader::load(&data), Err(LoadError::MissingSyscallSection));
}
#[test]
fn test_valid_minimal_load_with_empty_sysc() {
let data = minimal_module().serialize();
let module = BytecodeLoader::load(&data).unwrap();
assert_eq!(module.version, 0);
assert!(module.const_pool.is_empty());
assert!(module.functions.is_empty());
assert!(module.code.is_empty());
assert!(module.syscalls.is_empty());
}
#[test]
fn test_valid_sysc_roundtrip() {
let mut module = minimal_module();
module.syscalls = vec![SyscallDecl {
module: "gfx".into(),
name: "draw_line".into(),
version: 1,
arg_slots: 4,
ret_slots: 0,
}];
let data = module.serialize();
let loaded = BytecodeLoader::load(&data).unwrap();
assert_eq!(loaded.syscalls, module.syscalls);
}
#[test]
fn test_malformed_sysc_section_is_rejected() {
let data = build_pbs_with_sections(vec![(SECTION_KIND_SYSCALLS, vec![1, 0, 0])]);
assert_eq!(BytecodeLoader::load(&data), Err(LoadError::MalformedSection));
}
#[test]
fn test_invalid_utf8_in_sysc_section_is_rejected() {
let mut sysc = Vec::new();
sysc.extend_from_slice(&1u32.to_le_bytes());
sysc.extend_from_slice(&1u16.to_le_bytes());
sysc.push(0xFF);
sysc.extend_from_slice(&1u16.to_le_bytes());
sysc.push(b'x');
sysc.extend_from_slice(&1u16.to_le_bytes());
sysc.extend_from_slice(&0u16.to_le_bytes());
sysc.extend_from_slice(&0u16.to_le_bytes());
let data = build_pbs_with_sections(vec![(SECTION_KIND_SYSCALLS, sysc)]);
assert_eq!(BytecodeLoader::load(&data), Err(LoadError::InvalidUtf8));
}
#[test]
fn test_duplicate_sysc_identity_is_rejected() {
let mut module = minimal_module();
module.syscalls = vec![
SyscallDecl {
module: "system".into(),
name: "has_cart".into(),
version: 1,
arg_slots: 0,
ret_slots: 1,
},
SyscallDecl {
module: "system".into(),
name: "has_cart".into(),
version: 1,
arg_slots: 0,
ret_slots: 1,
},
];
let data = module.serialize();
assert_eq!(BytecodeLoader::load(&data), Err(LoadError::DuplicateSyscallIdentity));
}
}

View File

@ -1,324 +0,0 @@
/// Represents a single instruction in the Prometeu Virtual Machine.
///
/// Each OpCode is encoded as a 16-bit unsigned integer (u16) in the bytecode.
/// The PVM is a stack-based machine, meaning most instructions take their
/// operands from the top of the stack and push their results back onto it.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u16)]
pub enum OpCode {
// --- 6.1 Execution Control ---
/// No operation. Does nothing for 1 cycle.
Nop = 0x00,
/// Stops the Virtual Machine execution immediately.
Halt = 0x01,
/// Unconditional jump to a specific PC (Program Counter) address.
/// Operand: addr (u32)
Jmp = 0x02,
/// Jumps to `addr` if the value at the top of the stack is `false`.
/// Operand: addr (u32)
/// Stack: [bool] -> []
JmpIfFalse = 0x03,
/// Jumps to `addr` if the value at the top of the stack is `true`.
/// Operand: addr (u32)
/// Stack: [bool] -> []
JmpIfTrue = 0x04,
/// Triggers a software breakpoint. Used for debugging.
Trap = 0x05,
// --- 6.2 Stack Manipulation ---
/// Loads a constant from the Constant Pool into the stack.
/// Operand: index (u32)
/// Stack: [] -> [value]
PushConst = 0x10,
/// Removes the top value from the stack.
/// Stack: [val] -> []
Pop = 0x11,
/// Duplicates the top value of the stack.
/// Stack: [val] -> [val, val]
Dup = 0x12,
/// Swaps the two top values of the stack.
/// Stack: [a, b] -> [b, a]
Swap = 0x13,
/// Pushes a 64-bit integer literal onto the stack.
/// Operand: value (i64)
PushI64 = 0x14,
/// Pushes a 64-bit float literal onto the stack.
/// Operand: value (f64)
PushF64 = 0x15,
/// Pushes a boolean literal onto the stack (0=false, 1=true).
/// Operand: value (u8)
PushBool = 0x16,
/// Pushes a 32-bit integer literal onto the stack.
/// Operand: value (i32)
PushI32 = 0x17,
/// Removes `n` values from the stack.
/// Operand: n (u32)
PopN = 0x18,
// --- 6.3 Arithmetic ---
/// Adds the two top values (a + b).
/// Stack: [a, b] -> [result]
Add = 0x20,
/// Subtracts the top value from the second one (a - b).
/// Stack: [a, b] -> [result]
Sub = 0x21,
/// Multiplies the two top values (a * b).
/// Stack: [a, b] -> [result]
Mul = 0x22,
/// Divides the second top value by the top one (a / b).
/// Stack: [a, b] -> [result]
Div = 0x23,
/// Remainder of the division of the second top value by the top one (a % b).
/// Stack: [a, b] -> [result]
Mod = 0x24,
// --- 6.4 Comparison and Logic ---
/// Checks if a equals b.
/// Stack: [a, b] -> [bool]
Eq = 0x30,
/// Checks if a is not equal to b.
/// Stack: [a, b] -> [bool]
Neq = 0x31,
/// Checks if a is less than b.
/// Stack: [a, b] -> [bool]
Lt = 0x32,
/// Checks if a is greater than b.
/// Stack: [a, b] -> [bool]
Gt = 0x33,
/// Logical AND.
/// Stack: [bool, bool] -> [bool]
And = 0x34,
/// Logical OR.
/// Stack: [bool, bool] -> [bool]
Or = 0x35,
/// Logical NOT.
/// Stack: [bool] -> [bool]
Not = 0x36,
/// Bitwise AND.
/// Stack: [int, int] -> [int]
BitAnd = 0x37,
/// Bitwise OR.
/// Stack: [int, int] -> [int]
BitOr = 0x38,
/// Bitwise XOR.
/// Stack: [int, int] -> [int]
BitXor = 0x39,
/// Bitwise Shift Left.
/// Stack: [int, count] -> [int]
Shl = 0x3A,
/// Bitwise Shift Right.
/// Stack: [int, count] -> [int]
Shr = 0x3B,
/// Checks if a is less than or equal to b.
/// Stack: [a, b] -> [bool]
Lte = 0x3C,
/// Checks if a is greater than or equal to b.
/// Stack: [a, b] -> [bool]
Gte = 0x3D,
/// Negates a number (-a).
/// Stack: [num] -> [num]
Neg = 0x3E,
// --- 6.5 Variables ---
/// Loads a value from a global variable slot.
/// Operand: slot_index (u32)
/// Stack: [] -> [value]
GetGlobal = 0x40,
/// Stores the top value into a global variable slot.
/// Operand: slot_index (u32)
/// Stack: [value] -> []
SetGlobal = 0x41,
/// Loads a value from a local variable slot in the current frame.
/// Operand: slot_index (u32)
/// Stack: [] -> [value]
GetLocal = 0x42,
/// Stores the top value into a local variable slot in the current frame.
/// Operand: slot_index (u32)
/// Stack: [value] -> []
SetLocal = 0x43,
// --- 6.6 Functions ---
/// Calls a function by its index in the function table.
/// Operand: func_id (u32)
/// Stack: [arg0, arg1, ...] -> [return_slots...]
Call = 0x50,
/// Returns from the current function.
/// Stack: [return_val] -> [return_val]
Ret = 0x51,
/// Creates a closure capturing values from the operand stack (Model B).
/// Operands: fn_id (u32), capture_count (u32)
/// Stack before: [..., captured_N, ..., captured_1]
/// Pops capture_count values (top-first), preserves order as [captured_1..captured_N]
/// and stores them inside the closure environment. Pushes a HeapRef to the closure.
MakeClosure = 0x52,
/// Calls a closure value with hidden arg0 semantics (Model B).
/// Operand: arg_count (u32) — number of user-supplied args (excludes hidden arg0)
/// Stack before: [..., argN, ..., arg1, closure_ref]
/// Behavior:
/// - Pops `closure_ref` and validates it is a Closure.
/// - Pops `arg_count` user args.
/// - Fetches `fn_id` from the closure and creates a new call frame.
/// - Injects hidden arg0 = closure_ref, followed by user args as arg1..argN.
CallClosure = 0x53,
// --- 7.x Concurrency / Coroutines ---
/// Spawns a new coroutine to run a function with arguments.
/// Operands: fn_id (u32), arg_count (u32)
/// Semantics:
/// - Pops `arg_count` arguments from the current operand stack (top-first),
/// preserving user order as arg1..argN for the callee.
/// - Allocates a new Coroutine object with its own stack and a single entry frame
/// pointing at `fn_id`.
/// - Enqueues the coroutine into the scheduler ready queue.
/// - Does NOT switch execution immediately; current coroutine continues.
Spawn = 0x54,
/// Cooperatively yields the current coroutine. Execution continues
/// until the next VM safepoint (FRAME_SYNC), where the scheduler
/// may switch to another ready coroutine.
Yield = 0x55,
/// Suspends the current coroutine for a number of logical ticks.
/// Operand: duration_ticks (u32)
/// Semantics:
/// - Set the coroutine wake tick to `current_tick + duration_ticks`.
/// - End the current logical frame (as if reaching FRAME_SYNC).
/// - The coroutine will resume execution on or after the wake tick.
Sleep = 0x56,
// --- 6.8 Peripherals and System ---
/// Pre-load host binding call by `SYSC` table index.
/// Operand: sysc_index (u32)
/// This opcode is valid only in PBX artifact form and must be patched by the loader
/// into a final numeric `SYSCALL <id>` before verification or execution.
Hostcall = 0x71,
/// Invokes a final numeric system function (Firmware/OS).
/// Raw `SYSCALL` is valid only after loader patching and is rejected in PBX pre-load artifacts.
/// Operand: syscall_id (u32)
/// Stack: [args...] -> [results...] (depends on syscall)
Syscall = 0x70,
/// Invokes a VM-owned intrinsic by final numeric id.
/// Operand: intrinsic_id (u32)
/// Stack: [args...] -> [results...] (depends on intrinsic metadata)
Intrinsic = 0x72,
/// Synchronizes the VM with the hardware frame (usually 60Hz).
/// Execution pauses until the next VSync.
FrameSync = 0x80,
}
impl TryFrom<u16> for OpCode {
type Error = String;
fn try_from(value: u16) -> Result<Self, Self::Error> {
match value {
0x00 => Ok(OpCode::Nop),
0x01 => Ok(OpCode::Halt),
0x02 => Ok(OpCode::Jmp),
0x03 => Ok(OpCode::JmpIfFalse),
0x04 => Ok(OpCode::JmpIfTrue),
0x05 => Ok(OpCode::Trap),
0x10 => Ok(OpCode::PushConst),
0x11 => Ok(OpCode::Pop),
0x12 => Ok(OpCode::Dup),
0x13 => Ok(OpCode::Swap),
0x14 => Ok(OpCode::PushI64),
0x15 => Ok(OpCode::PushF64),
0x16 => Ok(OpCode::PushBool),
0x17 => Ok(OpCode::PushI32),
0x18 => Ok(OpCode::PopN),
0x20 => Ok(OpCode::Add),
0x21 => Ok(OpCode::Sub),
0x22 => Ok(OpCode::Mul),
0x23 => Ok(OpCode::Div),
0x24 => Ok(OpCode::Mod),
0x30 => Ok(OpCode::Eq),
0x31 => Ok(OpCode::Neq),
0x32 => Ok(OpCode::Lt),
0x33 => Ok(OpCode::Gt),
0x34 => Ok(OpCode::And),
0x35 => Ok(OpCode::Or),
0x36 => Ok(OpCode::Not),
0x37 => Ok(OpCode::BitAnd),
0x38 => Ok(OpCode::BitOr),
0x39 => Ok(OpCode::BitXor),
0x3A => Ok(OpCode::Shl),
0x3B => Ok(OpCode::Shr),
0x3C => Ok(OpCode::Lte),
0x3D => Ok(OpCode::Gte),
0x3E => Ok(OpCode::Neg),
0x40 => Ok(OpCode::GetGlobal),
0x41 => Ok(OpCode::SetGlobal),
0x42 => Ok(OpCode::GetLocal),
0x43 => Ok(OpCode::SetLocal),
0x50 => Ok(OpCode::Call),
0x51 => Ok(OpCode::Ret),
0x52 => Ok(OpCode::MakeClosure),
0x53 => Ok(OpCode::CallClosure),
0x54 => Ok(OpCode::Spawn),
0x55 => Ok(OpCode::Yield),
0x56 => Ok(OpCode::Sleep),
0x70 => Ok(OpCode::Syscall),
0x71 => Ok(OpCode::Hostcall),
0x72 => Ok(OpCode::Intrinsic),
0x80 => Ok(OpCode::FrameSync),
_ => Err(format!("Invalid OpCode: 0x{:04X}", value)),
}
}
}
impl OpCode {
/// Returns the cost of the instruction in VM cycles.
/// This is used for performance monitoring and resource limiting (Certification).
pub fn cycles(&self) -> u64 {
match self {
OpCode::Nop => 1,
OpCode::Halt => 1,
OpCode::Jmp => 2,
OpCode::JmpIfFalse => 3,
OpCode::JmpIfTrue => 3,
OpCode::Trap => 1,
OpCode::PushConst => 2,
OpCode::Pop => 1,
OpCode::PopN => 2,
OpCode::Dup => 1,
OpCode::Swap => 1,
OpCode::PushI64 => 2,
OpCode::PushF64 => 2,
OpCode::PushBool => 2,
OpCode::PushI32 => 2,
OpCode::Add => 2,
OpCode::Sub => 2,
OpCode::Mul => 4,
OpCode::Div => 6,
OpCode::Mod => 6,
OpCode::Eq => 2,
OpCode::Neq => 2,
OpCode::Lt => 2,
OpCode::Gt => 2,
OpCode::And => 2,
OpCode::Or => 2,
OpCode::Not => 1,
OpCode::BitAnd => 2,
OpCode::BitOr => 2,
OpCode::BitXor => 2,
OpCode::Shl => 2,
OpCode::Shr => 2,
OpCode::Lte => 2,
OpCode::Gte => 2,
OpCode::Neg => 1,
OpCode::GetGlobal => 3,
OpCode::SetGlobal => 3,
OpCode::GetLocal => 2,
OpCode::SetLocal => 2,
OpCode::Call => 5,
OpCode::Ret => 4,
OpCode::MakeClosure => 8,
OpCode::CallClosure => 6,
OpCode::Spawn => 6,
OpCode::Yield => 1,
OpCode::Sleep => 1,
OpCode::Syscall => 1,
OpCode::Hostcall => 1,
OpCode::Intrinsic => 1,
OpCode::FrameSync => 1,
}
}
}

View File

@ -1,567 +0,0 @@
use crate::opcode::OpCode;
/// Specification for a single OpCode.
/// All JMP/JMP_IF_* immediate are u32 absolute offsets from function start.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct OpcodeSpec {
pub name: &'static str,
pub imm_bytes: u8, // immediate payload size (decode)
pub pops: u16, // slots popped
pub pushes: u16, // slots pushed
pub is_branch: bool, // has a control-flow target
pub is_terminator: bool, // ends basic block: JMP/RET/TRAP/HALT
pub may_trap: bool, // runtime trap possible
/// Marks this opcode as a VM safepoint. Used by GC/scheduler policies.
pub is_safepoint: bool,
}
pub trait OpCodeSpecExt {
fn spec(&self) -> OpcodeSpec;
}
impl OpCodeSpecExt for OpCode {
fn spec(&self) -> OpcodeSpec {
match self {
OpCode::Nop => OpcodeSpec {
name: "NOP",
imm_bytes: 0,
pops: 0,
pushes: 0,
is_branch: false,
is_terminator: false,
may_trap: false,
is_safepoint: false,
},
OpCode::Halt => OpcodeSpec {
name: "HALT",
imm_bytes: 0,
pops: 0,
pushes: 0,
is_branch: false,
is_terminator: true,
may_trap: false,
is_safepoint: false,
},
OpCode::Jmp => OpcodeSpec {
name: "JMP",
imm_bytes: 4,
pops: 0,
pushes: 0,
is_branch: true,
is_terminator: true,
may_trap: false,
is_safepoint: false,
},
OpCode::JmpIfFalse => OpcodeSpec {
name: "JMP_IF_FALSE",
imm_bytes: 4,
pops: 1,
pushes: 0,
is_branch: true,
is_terminator: false,
may_trap: true,
is_safepoint: false,
},
OpCode::JmpIfTrue => OpcodeSpec {
name: "JMP_IF_TRUE",
imm_bytes: 4,
pops: 1,
pushes: 0,
is_branch: true,
is_terminator: false,
may_trap: true,
is_safepoint: false,
},
OpCode::Trap => OpcodeSpec {
name: "TRAP",
imm_bytes: 0,
pops: 0,
pushes: 0,
is_branch: false,
is_terminator: true,
may_trap: true,
is_safepoint: false,
},
OpCode::PushConst => OpcodeSpec {
name: "PUSH_CONST",
imm_bytes: 4,
pops: 0,
pushes: 1,
is_branch: false,
is_terminator: false,
may_trap: false,
is_safepoint: false,
},
OpCode::Pop => OpcodeSpec {
name: "POP",
imm_bytes: 0,
pops: 1,
pushes: 0,
is_branch: false,
is_terminator: false,
may_trap: false,
is_safepoint: false,
},
OpCode::PopN => OpcodeSpec {
name: "POP_N",
imm_bytes: 4,
pops: 0,
pushes: 0,
is_branch: false,
is_terminator: false,
may_trap: false,
is_safepoint: false,
},
OpCode::Dup => OpcodeSpec {
name: "DUP",
imm_bytes: 0,
pops: 1,
pushes: 2,
is_branch: false,
is_terminator: false,
may_trap: false,
is_safepoint: false,
},
OpCode::Swap => OpcodeSpec {
name: "SWAP",
imm_bytes: 0,
pops: 2,
pushes: 2,
is_branch: false,
is_terminator: false,
may_trap: false,
is_safepoint: false,
},
OpCode::PushI64 => OpcodeSpec {
name: "PUSH_I64",
imm_bytes: 8,
pops: 0,
pushes: 1,
is_branch: false,
is_terminator: false,
may_trap: false,
is_safepoint: false,
},
OpCode::PushF64 => OpcodeSpec {
name: "PUSH_F64",
imm_bytes: 8,
pops: 0,
pushes: 1,
is_branch: false,
is_terminator: false,
may_trap: false,
is_safepoint: false,
},
OpCode::PushBool => OpcodeSpec {
name: "PUSH_BOOL",
imm_bytes: 1,
pops: 0,
pushes: 1,
is_branch: false,
is_terminator: false,
may_trap: false,
is_safepoint: false,
},
OpCode::PushI32 => OpcodeSpec {
name: "PUSH_I32",
imm_bytes: 4,
pops: 0,
pushes: 1,
is_branch: false,
is_terminator: false,
may_trap: false,
is_safepoint: false,
},
OpCode::Add => OpcodeSpec {
name: "ADD",
imm_bytes: 0,
pops: 2,
pushes: 1,
is_branch: false,
is_terminator: false,
may_trap: true,
is_safepoint: false,
},
OpCode::Sub => OpcodeSpec {
name: "SUB",
imm_bytes: 0,
pops: 2,
pushes: 1,
is_branch: false,
is_terminator: false,
may_trap: true,
is_safepoint: false,
},
OpCode::Mul => OpcodeSpec {
name: "MUL",
imm_bytes: 0,
pops: 2,
pushes: 1,
is_branch: false,
is_terminator: false,
may_trap: true,
is_safepoint: false,
},
OpCode::Div => OpcodeSpec {
name: "DIV",
imm_bytes: 0,
pops: 2,
pushes: 1,
is_branch: false,
is_terminator: false,
may_trap: true,
is_safepoint: false,
},
OpCode::Mod => OpcodeSpec {
name: "MOD",
imm_bytes: 0,
pops: 2,
pushes: 1,
is_branch: false,
is_terminator: false,
may_trap: true,
is_safepoint: false,
},
OpCode::Eq => OpcodeSpec {
name: "EQ",
imm_bytes: 0,
pops: 2,
pushes: 1,
is_branch: false,
is_terminator: false,
may_trap: false,
is_safepoint: false,
},
OpCode::Neq => OpcodeSpec {
name: "NEQ",
imm_bytes: 0,
pops: 2,
pushes: 1,
is_branch: false,
is_terminator: false,
may_trap: false,
is_safepoint: false,
},
OpCode::Lt => OpcodeSpec {
name: "LT",
imm_bytes: 0,
pops: 2,
pushes: 1,
is_branch: false,
is_terminator: false,
may_trap: false,
is_safepoint: false,
},
OpCode::Gt => OpcodeSpec {
name: "GT",
imm_bytes: 0,
pops: 2,
pushes: 1,
is_branch: false,
is_terminator: false,
may_trap: false,
is_safepoint: false,
},
OpCode::And => OpcodeSpec {
name: "AND",
imm_bytes: 0,
pops: 2,
pushes: 1,
is_branch: false,
is_terminator: false,
may_trap: false,
is_safepoint: false,
},
OpCode::Or => OpcodeSpec {
name: "OR",
imm_bytes: 0,
pops: 2,
pushes: 1,
is_branch: false,
is_terminator: false,
may_trap: false,
is_safepoint: false,
},
OpCode::Not => OpcodeSpec {
name: "NOT",
imm_bytes: 0,
pops: 1,
pushes: 1,
is_branch: false,
is_terminator: false,
may_trap: false,
is_safepoint: false,
},
OpCode::BitAnd => OpcodeSpec {
name: "BIT_AND",
imm_bytes: 0,
pops: 2,
pushes: 1,
is_branch: false,
is_terminator: false,
may_trap: false,
is_safepoint: false,
},
OpCode::BitOr => OpcodeSpec {
name: "BIT_OR",
imm_bytes: 0,
pops: 2,
pushes: 1,
is_branch: false,
is_terminator: false,
may_trap: false,
is_safepoint: false,
},
OpCode::BitXor => OpcodeSpec {
name: "BIT_XOR",
imm_bytes: 0,
pops: 2,
pushes: 1,
is_branch: false,
is_terminator: false,
may_trap: false,
is_safepoint: false,
},
OpCode::Shl => OpcodeSpec {
name: "SHL",
imm_bytes: 0,
pops: 2,
pushes: 1,
is_branch: false,
is_terminator: false,
may_trap: false,
is_safepoint: false,
},
OpCode::Shr => OpcodeSpec {
name: "SHR",
imm_bytes: 0,
pops: 2,
pushes: 1,
is_branch: false,
is_terminator: false,
may_trap: false,
is_safepoint: false,
},
OpCode::Lte => OpcodeSpec {
name: "LTE",
imm_bytes: 0,
pops: 2,
pushes: 1,
is_branch: false,
is_terminator: false,
may_trap: false,
is_safepoint: false,
},
OpCode::Gte => OpcodeSpec {
name: "GTE",
imm_bytes: 0,
pops: 2,
pushes: 1,
is_branch: false,
is_terminator: false,
may_trap: false,
is_safepoint: false,
},
OpCode::Neg => OpcodeSpec {
name: "NEG",
imm_bytes: 0,
pops: 1,
pushes: 1,
is_branch: false,
is_terminator: false,
may_trap: false,
is_safepoint: false,
},
OpCode::GetGlobal => OpcodeSpec {
name: "GET_GLOBAL",
imm_bytes: 4,
pops: 0,
pushes: 1,
is_branch: false,
is_terminator: false,
may_trap: false,
is_safepoint: false,
},
OpCode::SetGlobal => OpcodeSpec {
name: "SET_GLOBAL",
imm_bytes: 4,
pops: 1,
pushes: 0,
is_branch: false,
is_terminator: false,
may_trap: false,
is_safepoint: false,
},
OpCode::GetLocal => OpcodeSpec {
name: "GET_LOCAL",
imm_bytes: 4,
pops: 0,
pushes: 1,
is_branch: false,
is_terminator: false,
may_trap: false,
is_safepoint: false,
},
OpCode::SetLocal => OpcodeSpec {
name: "SET_LOCAL",
imm_bytes: 4,
pops: 1,
pushes: 0,
is_branch: false,
is_terminator: false,
may_trap: false,
is_safepoint: false,
},
OpCode::Call => OpcodeSpec {
name: "CALL",
imm_bytes: 4,
pops: 0,
pushes: 0,
is_branch: false,
is_terminator: false,
may_trap: true,
is_safepoint: false,
},
OpCode::Ret => OpcodeSpec {
name: "RET",
imm_bytes: 0,
pops: 0,
pushes: 0,
is_branch: false,
is_terminator: true,
may_trap: false,
is_safepoint: false,
},
OpCode::MakeClosure => OpcodeSpec {
name: "MAKE_CLOSURE",
// Two u32 immediates: fn_id and capture_count
imm_bytes: 8,
// Dynamic, depends on capture_count; keep 0 here for verifier-free spec
pops: 0,
pushes: 1,
is_branch: false,
is_terminator: false,
may_trap: false,
is_safepoint: false,
},
OpCode::CallClosure => OpcodeSpec {
name: "CALL_CLOSURE",
// One u32 immediate: arg_count (user args, excludes hidden arg0)
imm_bytes: 4,
// Dynamic: pops closure_ref + arg_count; keep 0 in spec layer
pops: 0,
pushes: 0,
is_branch: false,
is_terminator: false,
may_trap: true,
is_safepoint: false,
},
OpCode::Spawn => OpcodeSpec {
name: "SPAWN",
// Two u32 immediates: fn_id and arg_count
imm_bytes: 8,
// Dynamic pops depends on arg_count; keep 0 here in spec layer
pops: 0,
pushes: 0,
is_branch: false,
is_terminator: false,
may_trap: false,
is_safepoint: false,
},
OpCode::Yield => OpcodeSpec {
name: "YIELD",
imm_bytes: 0,
pops: 0,
pushes: 0,
is_branch: false,
// Not a block terminator; effect realized at safepoint
is_terminator: false,
may_trap: false,
// Treated as a safepoint marker for cooperative scheduling
is_safepoint: true,
},
OpCode::Sleep => OpcodeSpec {
name: "SLEEP",
// One u32 immediate: duration_ticks
imm_bytes: 4,
pops: 0,
pushes: 0,
is_branch: false,
// Ends execution at safepoint after instruction completes
is_terminator: false,
may_trap: false,
// Considered a safepoint since it forces a frame boundary
is_safepoint: true,
},
OpCode::Hostcall => OpcodeSpec {
name: "HOSTCALL",
imm_bytes: 4,
pops: 0,
pushes: 0,
is_branch: false,
is_terminator: false,
may_trap: false,
is_safepoint: false,
},
OpCode::Syscall => OpcodeSpec {
name: "SYSCALL",
imm_bytes: 4,
pops: 0,
pushes: 0,
is_branch: false,
is_terminator: false,
may_trap: true,
is_safepoint: false,
},
OpCode::Intrinsic => OpcodeSpec {
name: "INTRINSIC",
imm_bytes: 4,
pops: 0,
pushes: 0,
is_branch: false,
is_terminator: false,
may_trap: true,
is_safepoint: false,
},
OpCode::FrameSync => OpcodeSpec {
name: "FRAME_SYNC",
imm_bytes: 0,
pops: 0,
pushes: 0,
is_branch: false,
is_terminator: false,
may_trap: false,
is_safepoint: true,
},
}
}
}
#[cfg(test)]
mod tests {
use super::*;
// Infer the numeric range from the TryFrom<u16> mapping used in opcode.rs tests
// by scanning a plausible range (0..1024) and keeping all successful decodes.
#[test]
fn every_opcode_has_spec_and_imm_defined() {
let mut count = 0usize;
for val in 0u16..=1023u16 {
if let Ok(op) = OpCode::try_from(val) {
let spec = op.spec();
// Access all fields to ensure they are present and not optimized away
let _ = (
spec.name,
spec.imm_bytes,
spec.pops,
spec.pushes,
spec.is_branch,
spec.is_terminator,
spec.may_trap,
);
count += 1;
}
}
assert!(count > 0, "No opcodes were found via OpCode::try_from");
}
}

View File

@ -1,124 +0,0 @@
use crate::abi::TrapInfo;
use crate::model::{BytecodeModule, ConstantPoolEntry, DebugInfo, Export, FunctionMeta};
use crate::value::Value;
use std::collections::HashMap;
use std::sync::Arc;
/// Represents a fully linked, executable PBS program image.
///
/// Under the Prometeu architecture, the ProgramImage is a "closed-world" artifact
/// produced by the compiler. All linking, relocation, and symbol resolution
/// MUST be performed by the compiler before this image is created.
///
/// The runtime (VM) assumes this image is authoritative and performs no
/// additional linking or fixups.
#[derive(Debug, Clone, Default)]
pub struct ProgramImage {
pub rom: Arc<[u8]>,
pub constant_pool: Arc<[Value]>,
pub functions: Arc<[FunctionMeta]>,
pub debug_info: Option<DebugInfo>,
pub exports: Arc<HashMap<String, u32>>,
}
impl ProgramImage {
pub fn new(
rom: Vec<u8>,
constant_pool: Vec<Value>,
functions: Vec<FunctionMeta>,
debug_info: Option<DebugInfo>,
exports: HashMap<String, u32>,
) -> Self {
Self {
rom: Arc::from(rom),
constant_pool: Arc::from(constant_pool),
functions: Arc::from(functions),
debug_info,
exports: Arc::new(exports),
}
}
pub fn create_trap(&self, code: u32, opcode: u16, mut message: String, pc: u32) -> TrapInfo {
let span = self
.debug_info
.as_ref()
.and_then(|di| di.pc_to_span.iter().find(|(p, _)| *p == pc).map(|(_, s)| s.clone()));
if let Some(func_idx) = self.find_function_index(pc) {
if let Some(func_name) = self.get_function_name(func_idx) {
message = format!("{} (in function {})", message, func_name);
}
}
TrapInfo { code, opcode, message, pc, span }
}
pub fn find_function_index(&self, pc: u32) -> Option<usize> {
self.functions.iter().position(|f| pc >= f.code_offset && pc < (f.code_offset + f.code_len))
}
pub fn get_function_name(&self, func_idx: usize) -> Option<&str> {
self.debug_info
.as_ref()
.and_then(|di| di.function_names.iter().find(|(idx, _)| *idx as usize == func_idx))
.map(|(_, name)| name.as_str())
}
}
impl From<BytecodeModule> for ProgramImage {
fn from(module: BytecodeModule) -> Self {
let constant_pool: Vec<Value> = module
.const_pool
.iter()
.map(|entry| match entry {
ConstantPoolEntry::Null => Value::Null,
ConstantPoolEntry::Int64(v) => Value::Int64(*v),
ConstantPoolEntry::Float64(v) => Value::Float(*v),
ConstantPoolEntry::Boolean(v) => Value::Boolean(*v),
ConstantPoolEntry::String(v) => Value::String(v.clone()),
ConstantPoolEntry::Int32(v) => Value::Int32(*v),
})
.collect();
let mut exports = HashMap::new();
for export in module.exports {
exports.insert(export.symbol, export.func_idx);
}
ProgramImage::new(module.code, constant_pool, module.functions, module.debug_info, exports)
}
}
impl From<ProgramImage> for BytecodeModule {
fn from(program: ProgramImage) -> Self {
let const_pool = program
.constant_pool
.iter()
.map(|v| match v {
Value::Null => ConstantPoolEntry::Null,
Value::Int64(v) => ConstantPoolEntry::Int64(*v),
Value::Float(v) => ConstantPoolEntry::Float64(*v),
Value::Boolean(v) => ConstantPoolEntry::Boolean(*v),
Value::String(v) => ConstantPoolEntry::String(v.clone()),
Value::Int32(v) => ConstantPoolEntry::Int32(*v),
Value::HeapRef(_) => ConstantPoolEntry::Null,
})
.collect();
let exports = program
.exports
.iter()
.map(|(symbol, &func_idx)| Export { symbol: symbol.clone(), func_idx })
.collect();
BytecodeModule {
version: 0,
const_pool,
functions: program.functions.as_ref().to_vec(),
code: program.rom.as_ref().to_vec(),
debug_info: program.debug_info.clone(),
exports,
syscalls: Vec::new(),
}
}
}

View File

@ -1,46 +0,0 @@
use prometeu_bytecode::isa::core::CoreOpCode;
use prometeu_bytecode::{assemble, disassemble};
fn emit(op: CoreOpCode, imm: Option<&[u8]>, out: &mut Vec<u8>) {
out.extend_from_slice(&(op as u16).to_le_bytes());
if let Some(bytes) = imm {
out.extend_from_slice(bytes);
}
}
#[test]
fn roundtrip_disasm_assemble_byte_equal_with_closures_and_coroutines() {
// Program: PUSH_I32 7; MAKE_CLOSURE fn=1,captures=0; CALL_CLOSURE argc=1;
// SPAWN fn=2,argc=1; YIELD; SLEEP 3; HOSTCALL 2; SYSCALL 0x1003; FRAME_SYNC; HALT
let mut prog = Vec::new();
emit(CoreOpCode::PushI32, Some(&7i32.to_le_bytes()), &mut prog);
// MAKE_CLOSURE (fn=1, captures=0)
let mut mc = [0u8; 8];
mc[0..4].copy_from_slice(&1u32.to_le_bytes());
mc[4..8].copy_from_slice(&0u32.to_le_bytes());
emit(CoreOpCode::MakeClosure, Some(&mc), &mut prog);
// CALL_CLOSURE argc=1
emit(CoreOpCode::CallClosure, Some(&1u32.to_le_bytes()), &mut prog);
// SPAWN (fn=2, argc=1)
let mut sp = [0u8; 8];
sp[0..4].copy_from_slice(&2u32.to_le_bytes());
sp[4..8].copy_from_slice(&1u32.to_le_bytes());
emit(CoreOpCode::Spawn, Some(&sp), &mut prog);
// YIELD
emit(CoreOpCode::Yield, None, &mut prog);
// SLEEP 3
emit(CoreOpCode::Sleep, Some(&3u32.to_le_bytes()), &mut prog);
// HOSTCALL sysc[2]
emit(CoreOpCode::Hostcall, Some(&2u32.to_le_bytes()), &mut prog);
// SYSCALL gfx.draw_line (0x1003)
emit(CoreOpCode::Syscall, Some(&0x1003u32.to_le_bytes()), &mut prog);
// FRAME_SYNC
emit(CoreOpCode::FrameSync, None, &mut prog);
// HALT
emit(CoreOpCode::Halt, None, &mut prog);
let text = disassemble(&prog).expect("disasm ok");
let rebuilt = assemble(&text).expect("assemble ok");
assert_eq!(rebuilt, prog, "re-assembled bytes must match original");
}

View File

@ -1,43 +0,0 @@
use prometeu_bytecode::disassemble;
use prometeu_bytecode::isa::core::CoreOpCode;
fn emit(op: CoreOpCode, imm: Option<&[u8]>, out: &mut Vec<u8>) {
out.extend_from_slice(&(op as u16).to_le_bytes());
if let Some(bytes) = imm {
out.extend_from_slice(bytes);
}
}
#[test]
fn snapshot_representative_program_is_stable() {
let mut prog = Vec::new();
emit(CoreOpCode::PushI32, Some(&7i32.to_le_bytes()), &mut prog);
// MAKE_CLOSURE (fn=1, captures=0)
let mut mc = [0u8; 8];
mc[0..4].copy_from_slice(&1u32.to_le_bytes());
mc[4..8].copy_from_slice(&0u32.to_le_bytes());
emit(CoreOpCode::MakeClosure, Some(&mc), &mut prog);
// CALL_CLOSURE argc=1
emit(CoreOpCode::CallClosure, Some(&1u32.to_le_bytes()), &mut prog);
// SPAWN (fn=2, argc=1)
let mut sp = [0u8; 8];
sp[0..4].copy_from_slice(&2u32.to_le_bytes());
sp[4..8].copy_from_slice(&1u32.to_le_bytes());
emit(CoreOpCode::Spawn, Some(&sp), &mut prog);
// YIELD
emit(CoreOpCode::Yield, None, &mut prog);
// SLEEP 3
emit(CoreOpCode::Sleep, Some(&3u32.to_le_bytes()), &mut prog);
// HOSTCALL 2
emit(CoreOpCode::Hostcall, Some(&2u32.to_le_bytes()), &mut prog);
// SYSCALL 0x1003
emit(CoreOpCode::Syscall, Some(&0x1003u32.to_le_bytes()), &mut prog);
// FRAME_SYNC
emit(CoreOpCode::FrameSync, None, &mut prog);
// HALT
emit(CoreOpCode::Halt, None, &mut prog);
let text = disassemble(&prog).expect("disasm ok");
let expected = "PUSH_I32 7\nMAKE_CLOSURE fn=1, captures=0\nCALL_CLOSURE argc=1\nSPAWN fn=2, argc=1\nYIELD\nSLEEP 3\nHOSTCALL 2\nSYSCALL 0x1003\nFRAME_SYNC\nHALT";
assert_eq!(text, expected);
}

View File

@ -1,160 +0,0 @@
use prometeu_bytecode::decode_next;
use prometeu_bytecode::isa::core::{CoreOpCode, CoreOpCodeSpecExt};
fn encode_instr(op: CoreOpCode, imm: Option<&[u8]>) -> Vec<u8> {
let mut out = Vec::new();
let code = op as u16;
out.extend_from_slice(&code.to_le_bytes());
let spec = op.spec();
let need = spec.imm_bytes as usize;
match (need, imm) {
(0, None) => {}
(n, Some(bytes)) if bytes.len() == n => out.extend_from_slice(bytes),
(n, Some(bytes)) => {
panic!("immediate size mismatch for {:?}: expected {}, got {}", op, n, bytes.len())
}
(n, None) => panic!("missing immediate for {:?}: need {} bytes", op, n),
}
out
}
fn disasm(bytes: &[u8]) -> String {
// Minimal test-only disasm: NAME [operands]
let mut pc = 0usize;
let mut lines = Vec::new();
while pc < bytes.len() {
match decode_next(pc, bytes) {
Ok(instr) => {
let name = instr.opcode.spec().name;
let mut line = String::from(name);
let imm_len = instr.opcode.spec().imm_bytes as usize;
if imm_len > 0 {
// Heuristic formatting based on known op immediates
line.push(' ');
let s = match instr.opcode {
CoreOpCode::Jmp | CoreOpCode::JmpIfFalse | CoreOpCode::JmpIfTrue => {
format!("{}", instr.imm_u32().unwrap())
}
CoreOpCode::PushI64 => format!("{}", instr.imm_i64().unwrap()),
CoreOpCode::PushF64 => format!("{}", instr.imm_f64().unwrap()),
CoreOpCode::PushBool => format!("{}", instr.imm_u8().unwrap()),
CoreOpCode::PushI32 => format!("{}", instr.imm_i32().unwrap()),
CoreOpCode::PopN | CoreOpCode::PushConst | CoreOpCode::Hostcall => {
format!("{}", instr.imm_u32().unwrap())
}
CoreOpCode::Syscall | CoreOpCode::Intrinsic => {
format!("0x{}", hex::encode(instr.imm))
}
_ => format!("0x{}", hex::encode(instr.imm)),
};
line.push_str(&s);
}
lines.push(line);
pc = instr.next_pc;
}
Err(_) => break,
}
}
lines.join("\n")
}
#[test]
fn encode_decode_roundtrip_preserves_structure() {
// Program: PUSH_I32 42; PUSH_I32 100; ADD; PUSH_BOOL 1; JMP 12; NOP; HALT
let mut prog = Vec::new();
prog.extend(encode_instr(CoreOpCode::PushI32, Some(&42i32.to_le_bytes())));
prog.extend(encode_instr(CoreOpCode::PushI32, Some(&100i32.to_le_bytes())));
prog.extend(encode_instr(CoreOpCode::Add, None));
prog.extend(encode_instr(CoreOpCode::PushBool, Some(&[1u8])));
// Jump to the HALT (compute absolute PC within this byte slice)
// Current pc after previous: 2+4 + 2+4 + 2 + 2+1 = 17 bytes
// Next we place: JMP (2+4), NOP (2), HALT (2)
// We want JMP target to land at the HALT's pc
let jmp_target: u32 = 17 + 2 + 4 + 2; // pc where HALT starts
prog.extend(encode_instr(CoreOpCode::Jmp, Some(&jmp_target.to_le_bytes())));
prog.extend(encode_instr(CoreOpCode::Nop, None));
prog.extend(encode_instr(CoreOpCode::Halt, None));
// Decode sequentially and check opcodes and immediates
let mut pc = 0usize;
let mut seen = Vec::new();
while pc < prog.len() {
let instr = decode_next(pc, &prog).expect("decode ok");
seen.push(instr);
pc = instr.next_pc;
}
assert_eq!(seen.len(), 7);
assert_eq!(seen[0].opcode, CoreOpCode::PushI32);
assert_eq!(seen[0].imm_i32().unwrap(), 42);
assert_eq!(seen[1].opcode, CoreOpCode::PushI32);
assert_eq!(seen[1].imm_i32().unwrap(), 100);
assert_eq!(seen[2].opcode, CoreOpCode::Add);
assert_eq!(seen[3].opcode, CoreOpCode::PushBool);
assert_eq!(seen[3].imm_u8().unwrap(), 1);
assert_eq!(seen[4].opcode, CoreOpCode::Jmp);
assert_eq!(seen[4].imm_u32().unwrap(), jmp_target);
assert_eq!(seen[5].opcode, CoreOpCode::Nop);
assert_eq!(seen[6].opcode, CoreOpCode::Halt);
}
#[test]
fn disasm_contains_expected_mnemonics_and_operands() {
// Tiny deterministic sample: NOP; PUSH_I32 -7; PUSH_BOOL 0; ADD; HALT
let mut prog = Vec::new();
prog.extend(encode_instr(CoreOpCode::Nop, None));
prog.extend(encode_instr(CoreOpCode::PushI32, Some(&(-7i32).to_le_bytes())));
prog.extend(encode_instr(CoreOpCode::PushBool, Some(&[0u8])));
prog.extend(encode_instr(CoreOpCode::Add, None));
prog.extend(encode_instr(CoreOpCode::Halt, None));
let text = disasm(&prog);
// Must contain stable opcode names and operand text
assert!(text.contains("NOP"));
assert!(text.contains("PUSH_I32 -7"));
assert!(text.contains("PUSH_BOOL 0"));
assert!(text.contains("ADD"));
assert!(text.contains("HALT"));
}
#[test]
fn hostcall_roundtrips_with_decimal_index() {
let mut prog = Vec::new();
prog.extend(encode_instr(CoreOpCode::Hostcall, Some(&7u32.to_le_bytes())));
prog.extend(encode_instr(CoreOpCode::Halt, None));
let text = disasm(&prog);
assert!(text.contains("HOSTCALL 7"));
let rebuilt = prometeu_bytecode::assemble(&text).expect("assemble hostcall");
assert_eq!(rebuilt, prog);
}
#[test]
fn intrinsic_roundtrips_with_hex_id() {
let mut prog = Vec::new();
prog.extend(encode_instr(CoreOpCode::Intrinsic, Some(&0x1000u32.to_le_bytes())));
prog.extend(encode_instr(CoreOpCode::Halt, None));
let text = prometeu_bytecode::disassemble(&prog).expect("disasm intrinsic");
assert!(text.contains("INTRINSIC 0x1000"));
let rebuilt = prometeu_bytecode::assemble(&text).expect("assemble intrinsic");
assert_eq!(rebuilt, prog);
}
// Minimal hex helper to avoid extra deps in tests
mod hex {
pub fn encode(bytes: &[u8]) -> String {
let mut s = String::with_capacity(bytes.len() * 2);
const HEX: &[u8; 16] = b"0123456789abcdef";
for &b in bytes {
s.push(HEX[(b >> 4) as usize] as char);
s.push(HEX[(b & 0x0f) as usize] as char);
}
s
}
}

View File

@ -1,9 +0,0 @@
[package]
name = "prometeu-drivers"
version = "0.1.0"
edition = "2024"
license.workspace = true
[dependencies]
serde_json = "1.0.149"
prometeu-hal = { path = "../prometeu-hal" }

File diff suppressed because it is too large Load Diff

View File

@ -1,332 +0,0 @@
use prometeu_hal::{AudioBridge, AudioOpStatus};
use std::sync::Arc;
/// Maximum number of simultaneous audio voices supported by the hardware.
pub const MAX_CHANNELS: usize = 16;
/// Standard sample rate for the final audio output.
pub const OUTPUT_SAMPLE_RATE: u32 = 48000;
use crate::memory_banks::SoundBankPoolAccess;
/// Looping mode for samples (re-exported from the hardware contract).
pub use prometeu_hal::LoopMode;
use prometeu_hal::sample::Sample;
/// State of a single playback voice (channel).
///
/// The Core maintains this state to provide information to the App (e.g., is_playing),
/// but the actual real-time mixing is performed by the Host using commands.
pub struct Channel {
/// The actual sample data being played.
pub sample: Option<Arc<Sample>>,
/// Whether this channel is currently active.
pub active: bool,
/// Current playback position within the sample (fractional for pitch shifting).
pub pos: f64,
/// Playback speed multiplier (1.0 = original speed).
pub pitch: f64,
/// Voice volume (0-255).
pub volume: u8,
/// Stereo panning (0=Full Left, 127=Center, 255=Full Right).
pub pan: u8,
/// Loop configuration for this voice.
pub loop_mode: LoopMode,
/// Playback priority (used for voice stealing policies).
pub priority: u8,
}
impl Default for Channel {
fn default() -> Self {
Self {
sample: None,
active: false,
pos: 0.0,
pitch: 1.0,
volume: 255,
pan: 127,
loop_mode: LoopMode::Off,
priority: 0,
}
}
}
/// Commands sent from the Core to the Host audio backend.
///
/// Because the Core logic runs at 60Hz and Audio is generated at 48kHz,
/// we use an asynchronous command queue to synchronize them.
pub enum AudioCommand {
/// Start playing a sample on a specific voice.
Play {
sample: Arc<Sample>,
voice_id: usize,
volume: u8,
pan: u8,
pitch: f64,
priority: u8,
loop_mode: LoopMode,
},
/// Immediately stop playback on a voice.
Stop { voice_id: usize },
/// Update volume of an ongoing playback.
SetVolume { voice_id: usize, volume: u8 },
/// Update panning of an ongoing playback.
SetPan { voice_id: usize, pan: u8 },
/// Update pitch of an ongoing playback.
SetPitch { voice_id: usize, pitch: f64 },
/// Pause all audio processing.
MasterPause,
/// Resume audio processing.
MasterResume,
}
/// PROMETEU Audio Subsystem.
///
/// Models a multi-channel PCM sampler (SPU).
/// The audio system in Prometeu is **command-based**. This means the Core
/// doesn't generate raw audio samples; instead, it sends high-level commands
/// (like `Play`, `Stop`, `SetVolume`) to a queue. The physical host backend
/// (e.g., CPAL on desktop) then consumes these commands and performs the
/// actual mixing at the native hardware sample rate.
///
/// ### Key Features:
/// - **16 Simultaneous Voices**: Independent volume, pan, and pitch.
/// - **Sample-based Synthesis**: Plays PCM data stored in SoundBanks.
/// - **Stereo Output**: 48kHz output target.
pub struct Audio {
/// Local state of the hardware voices. This state is used for logic
/// (e.g., checking if a sound is still playing) and is synchronized with the Host.
pub voices: [Channel; MAX_CHANNELS],
/// Queue of pending commands to be processed by the Host mixer.
/// This queue is typically flushed and sent to the Host once per frame.
pub commands: Vec<AudioCommand>,
/// Interface to access sound memory banks (ARAM).
pub sound_banks: Arc<dyn SoundBankPoolAccess>,
}
impl AudioBridge for Audio {
fn play(
&mut self,
bank_id: u8,
sample_id: u16,
voice_id: usize,
volume: u8,
pan: u8,
pitch: f64,
priority: u8,
loop_mode: prometeu_hal::LoopMode,
) -> AudioOpStatus {
let lm = match loop_mode {
prometeu_hal::LoopMode::Off => LoopMode::Off,
prometeu_hal::LoopMode::On => LoopMode::On,
};
self.play(bank_id, sample_id, voice_id, volume, pan, pitch, priority, lm)
}
fn play_sample(
&mut self,
sample: Arc<Sample>,
voice_id: usize,
volume: u8,
pan: u8,
pitch: f64,
priority: u8,
loop_mode: prometeu_hal::LoopMode,
) -> AudioOpStatus {
let lm = match loop_mode {
prometeu_hal::LoopMode::Off => LoopMode::Off,
prometeu_hal::LoopMode::On => LoopMode::On,
};
self.play_sample(sample, voice_id, volume, pan, pitch, priority, lm)
}
fn stop(&mut self, voice_id: usize) {
self.stop(voice_id)
}
fn set_volume(&mut self, voice_id: usize, volume: u8) {
self.set_volume(voice_id, volume)
}
fn set_pan(&mut self, voice_id: usize, pan: u8) {
self.set_pan(voice_id, pan)
}
fn set_pitch(&mut self, voice_id: usize, pitch: f64) {
self.set_pitch(voice_id, pitch)
}
fn is_playing(&self, voice_id: usize) -> bool {
self.is_playing(voice_id)
}
fn clear_commands(&mut self) {
self.clear_commands()
}
}
impl Audio {
/// Initializes the audio system with empty voices and sound bank access.
pub fn new(sound_banks: Arc<dyn SoundBankPoolAccess>) -> Self {
Self {
voices: std::array::from_fn(|_| Channel::default()),
commands: Vec::new(),
sound_banks,
}
}
#[allow(clippy::too_many_arguments)]
pub fn play(
&mut self,
bank_id: u8,
sample_id: u16,
voice_id: usize,
volume: u8,
pan: u8,
pitch: f64,
priority: u8,
loop_mode: LoopMode,
) -> AudioOpStatus {
if voice_id >= MAX_CHANNELS {
return AudioOpStatus::VoiceInvalid;
}
// Resolve the sample from the hardware pools
let bank_slot = self.sound_banks.sound_bank_slot(bank_id as usize);
if bank_slot.is_none() {
return AudioOpStatus::BankInvalid;
}
let sample =
bank_slot.and_then(|bank| bank.samples.get(sample_id as usize).map(Arc::clone));
if let Some(s) = sample {
// println!(
// "[Audio] Resolved sample from bank {} sample {}. Playing on voice {}.",
// bank_id, sample_id, voice_id
// );
self.play_sample(s, voice_id, volume, pan, pitch, priority, loop_mode)
} else {
AudioOpStatus::SampleNotFound
}
}
#[allow(clippy::too_many_arguments)]
pub fn play_sample(
&mut self,
sample: Arc<Sample>,
voice_id: usize,
volume: u8,
pan: u8,
pitch: f64,
priority: u8,
loop_mode: LoopMode,
) -> AudioOpStatus {
if voice_id >= MAX_CHANNELS {
return AudioOpStatus::VoiceInvalid;
}
// Update local state
self.voices[voice_id] = Channel {
sample: Some(Arc::clone(&sample)),
active: true,
pos: 0.0,
pitch,
volume,
pan,
loop_mode,
priority,
};
// Push command to the host
self.commands.push(AudioCommand::Play {
sample,
voice_id,
volume,
pan,
pitch,
priority,
loop_mode,
});
AudioOpStatus::Ok
}
pub fn stop(&mut self, voice_id: usize) {
if voice_id < MAX_CHANNELS {
self.voices[voice_id].active = false;
self.voices[voice_id].sample = None;
self.commands.push(AudioCommand::Stop { voice_id });
}
}
pub fn set_volume(&mut self, voice_id: usize, volume: u8) {
if voice_id < MAX_CHANNELS {
self.voices[voice_id].volume = volume;
self.commands.push(AudioCommand::SetVolume { voice_id, volume });
}
}
pub fn set_pan(&mut self, voice_id: usize, pan: u8) {
if voice_id < MAX_CHANNELS {
self.voices[voice_id].pan = pan;
self.commands.push(AudioCommand::SetPan { voice_id, pan });
}
}
pub fn set_pitch(&mut self, voice_id: usize, pitch: f64) {
if voice_id < MAX_CHANNELS {
self.voices[voice_id].pitch = pitch;
self.commands.push(AudioCommand::SetPitch { voice_id, pitch });
}
}
pub fn is_playing(&self, voice_id: usize) -> bool {
if voice_id < MAX_CHANNELS { self.voices[voice_id].active } else { false }
}
/// Clears the command queue. The Host should consume this every frame.
pub fn clear_commands(&mut self) {
self.commands.clear();
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::memory_banks::{MemoryBanks, SoundBankPoolAccess, SoundBankPoolInstaller};
use prometeu_hal::sound_bank::SoundBank;
fn sample() -> Arc<Sample> {
Arc::new(Sample::new(44_100, vec![0, 1, 0, -1]))
}
#[test]
fn play_returns_voice_invalid_for_out_of_range_voice() {
let banks = Arc::new(MemoryBanks::new());
let sound_banks = Arc::clone(&banks) as Arc<dyn SoundBankPoolAccess>;
let mut audio = Audio::new(sound_banks);
let status = audio.play(0, 0, MAX_CHANNELS, 255, 128, 1.0, 0, LoopMode::Off);
assert_eq!(status, AudioOpStatus::VoiceInvalid);
assert!(audio.commands.is_empty());
}
#[test]
fn play_returns_sample_not_found_when_sample_is_missing() {
let banks = Arc::new(MemoryBanks::new());
let installer = Arc::clone(&banks) as Arc<dyn SoundBankPoolInstaller>;
installer.install_sound_bank(0, Arc::new(SoundBank::new(vec![sample()])));
let sound_banks = Arc::clone(&banks) as Arc<dyn SoundBankPoolAccess>;
let mut audio = Audio::new(sound_banks);
let status = audio.play(0, 1, 0, 255, 128, 1.0, 0, LoopMode::Off);
assert_eq!(status, AudioOpStatus::SampleNotFound);
assert!(audio.commands.is_empty());
}
#[test]
fn play_sample_returns_voice_invalid_without_side_effects() {
let banks = Arc::new(MemoryBanks::new());
let sound_banks = Arc::clone(&banks) as Arc<dyn SoundBankPoolAccess>;
let mut audio = Audio::new(sound_banks);
let status = audio.play_sample(sample(), MAX_CHANNELS, 255, 128, 1.0, 0, LoopMode::Off);
assert_eq!(status, AudioOpStatus::VoiceInvalid);
assert!(!audio.voices.iter().any(|voice| voice.active));
assert!(audio.commands.is_empty());
}
}

View File

@ -1,719 +0,0 @@
use crate::memory_banks::SceneBankPoolAccess;
use prometeu_hal::GfxBridge;
use prometeu_hal::glyph::Glyph;
use prometeu_hal::scene_bank::SceneBank;
use prometeu_hal::scene_viewport_cache::SceneViewportCache;
use prometeu_hal::scene_viewport_resolver::{CacheRefreshRequest, SceneViewportResolver};
use prometeu_hal::sprite::Sprite;
use std::sync::Arc;
const EMPTY_SPRITE: Sprite = Sprite {
glyph: Glyph { glyph_id: 0, palette_id: 0 },
x: 0,
y: 0,
layer: 0,
bank_id: 0,
active: false,
flip_x: false,
flip_y: false,
priority: 0,
};
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum SceneStatus {
#[default]
Unbound,
Available {
scene_bank_id: usize,
},
}
#[derive(Clone, Debug)]
pub struct SpriteController {
sprites: [Sprite; 512],
sprite_count: usize,
frame_counter: u64,
dropped_sprites: usize,
layer_buckets: [Vec<usize>; 4],
}
impl Default for SpriteController {
fn default() -> Self {
Self::new()
}
}
impl SpriteController {
pub fn new() -> Self {
Self {
sprites: [EMPTY_SPRITE; 512],
sprite_count: 0,
frame_counter: 0,
dropped_sprites: 0,
layer_buckets: std::array::from_fn(|_| Vec::with_capacity(128)),
}
}
pub fn sprites(&self) -> &[Sprite; 512] {
&self.sprites
}
pub fn sprites_mut(&mut self) -> &mut [Sprite; 512] {
&mut self.sprites
}
pub fn begin_frame(&mut self) {
self.frame_counter = self.frame_counter.wrapping_add(1);
self.sprite_count = 0;
self.dropped_sprites = 0;
for bucket in &mut self.layer_buckets {
bucket.clear();
}
}
pub fn emit_sprite(&mut self, mut sprite: Sprite) -> bool {
let Some(bucket) = self.layer_buckets.get_mut(sprite.layer as usize) else {
self.dropped_sprites += 1;
return false;
};
if self.sprite_count >= self.sprites.len() {
self.dropped_sprites += 1;
return false;
}
sprite.active = true;
let index = self.sprite_count;
self.sprites[index] = sprite;
self.sprite_count += 1;
bucket.push(index);
true
}
pub fn sprite_count(&self) -> usize {
self.sprite_count
}
pub fn frame_counter(&self) -> u64 {
self.frame_counter
}
pub fn dropped_sprites(&self) -> usize {
self.dropped_sprites
}
pub fn ordered_sprites(&self) -> Vec<Sprite> {
let mut ordered = Vec::with_capacity(self.sprite_count);
for bucket in &self.layer_buckets {
let mut indices = bucket.clone();
indices.sort_by_key(|&index| self.sprites[index].priority);
for index in indices {
ordered.push(self.sprites[index]);
}
}
ordered
}
}
pub struct FrameComposer {
scene_bank_pool: Arc<dyn SceneBankPoolAccess>,
viewport_width_px: usize,
viewport_height_px: usize,
active_scene_id: Option<usize>,
active_scene: Option<Arc<SceneBank>>,
scene_status: SceneStatus,
camera_x_px: i32,
camera_y_px: i32,
cache: Option<SceneViewportCache>,
resolver: Option<SceneViewportResolver>,
sprite_controller: SpriteController,
}
impl FrameComposer {
pub fn new(
viewport_width_px: usize,
viewport_height_px: usize,
scene_bank_pool: Arc<dyn SceneBankPoolAccess>,
) -> Self {
Self {
scene_bank_pool,
viewport_width_px,
viewport_height_px,
active_scene_id: None,
active_scene: None,
scene_status: SceneStatus::Unbound,
camera_x_px: 0,
camera_y_px: 0,
cache: None,
resolver: None,
sprite_controller: SpriteController::new(),
}
}
pub fn viewport_size(&self) -> (usize, usize) {
(self.viewport_width_px, self.viewport_height_px)
}
pub fn scene_bank_pool(&self) -> &Arc<dyn SceneBankPoolAccess> {
&self.scene_bank_pool
}
pub fn scene_bank_slot(&self, slot: usize) -> Option<Arc<SceneBank>> {
self.scene_bank_pool.scene_bank_slot(slot)
}
pub fn scene_bank_slot_count(&self) -> usize {
self.scene_bank_pool.scene_bank_slot_count()
}
pub fn active_scene_id(&self) -> Option<usize> {
self.active_scene_id
}
pub fn active_scene(&self) -> Option<&Arc<SceneBank>> {
self.active_scene.as_ref()
}
pub fn scene_status(&self) -> SceneStatus {
self.scene_status
}
pub fn camera(&self) -> (i32, i32) {
(self.camera_x_px, self.camera_y_px)
}
pub fn bind_scene(&mut self, scene_bank_id: usize) -> bool {
let Some(scene) = self.scene_bank_pool.scene_bank_slot(scene_bank_id) else {
self.unbind_scene();
return false;
};
let (cache, resolver) =
Self::build_scene_runtime(self.viewport_width_px, self.viewport_height_px, &scene);
self.active_scene_id = Some(scene_bank_id);
self.active_scene = Some(scene);
self.scene_status = SceneStatus::Available { scene_bank_id };
self.cache = Some(cache);
self.resolver = Some(resolver);
true
}
pub fn unbind_scene(&mut self) {
self.active_scene_id = None;
self.active_scene = None;
self.scene_status = SceneStatus::Unbound;
self.cache = None;
self.resolver = None;
}
pub fn set_camera(&mut self, x: i32, y: i32) {
self.camera_x_px = x;
self.camera_y_px = y;
}
pub fn cache(&self) -> Option<&SceneViewportCache> {
self.cache.as_ref()
}
pub fn resolver(&self) -> Option<&SceneViewportResolver> {
self.resolver.as_ref()
}
pub fn sprite_controller(&self) -> &SpriteController {
&self.sprite_controller
}
pub fn sprite_controller_mut(&mut self) -> &mut SpriteController {
&mut self.sprite_controller
}
pub fn begin_frame(&mut self) {
self.sprite_controller.begin_frame();
}
pub fn emit_sprite(&mut self, sprite: Sprite) -> bool {
self.sprite_controller.emit_sprite(sprite)
}
pub fn ordered_sprites(&self) -> Vec<Sprite> {
self.sprite_controller.ordered_sprites()
}
pub fn render_frame(&mut self, gfx: &mut dyn GfxBridge) {
let ordered_sprites = self.ordered_sprites();
gfx.load_frame_sprites(&ordered_sprites);
if let (Some(scene), Some(cache), Some(resolver)) =
(self.active_scene.as_deref(), self.cache.as_mut(), self.resolver.as_mut())
{
let update = resolver.update(scene, self.camera_x_px, self.camera_y_px);
Self::apply_refresh_requests(cache, scene, &update.refresh_requests);
// `FrameComposer` owns only canonical game-frame composition.
// Deferred `gfx.*` primitives are drained later by a separate
// overlay/debug stage outside this service boundary.
gfx.render_scene_from_cache(cache, &update);
return;
}
// No-scene frames still stop at canonical game composition. Final
// overlay/debug work remains outside `FrameComposer`.
gfx.render_no_scene_frame();
}
fn build_scene_runtime(
viewport_width_px: usize,
viewport_height_px: usize,
scene: &SceneBank,
) -> (SceneViewportCache, SceneViewportResolver) {
let min_tile_px =
scene.layers.iter().map(|layer| layer.tile_size as usize).min().unwrap_or(8);
let cache_width_tiles = viewport_width_px.div_ceil(min_tile_px) + 5;
let cache_height_tiles = viewport_height_px.div_ceil(min_tile_px) + 4;
let hysteresis_safe_px = min_tile_px.saturating_sub(4) as i32;
let hysteresis_trigger_px = (min_tile_px + 4) as i32;
(
SceneViewportCache::new(scene, cache_width_tiles, cache_height_tiles),
SceneViewportResolver::new(
viewport_width_px as i32,
viewport_height_px as i32,
cache_width_tiles,
cache_height_tiles,
hysteresis_safe_px,
hysteresis_trigger_px,
),
)
}
fn apply_refresh_requests(
cache: &mut SceneViewportCache,
scene: &SceneBank,
refresh_requests: &[CacheRefreshRequest],
) {
for request in refresh_requests {
match *request {
CacheRefreshRequest::InvalidateLayer { layer_index } => {
cache.layers[layer_index].invalidate_all();
}
CacheRefreshRequest::RefreshLine { layer_index, cache_y } => {
cache.refresh_layer_line(scene, layer_index, cache_y);
}
CacheRefreshRequest::RefreshColumn { layer_index, cache_x } => {
cache.refresh_layer_column(scene, layer_index, cache_x);
}
CacheRefreshRequest::RefreshRegion { layer_index, region } => {
cache.refresh_layer_region(scene, layer_index, region);
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::gfx::Gfx;
use crate::memory_banks::{
GlyphBankPoolAccess, GlyphBankPoolInstaller, MemoryBanks, SceneBankPoolInstaller,
};
use prometeu_hal::color::Color;
use prometeu_hal::glyph_bank::{GlyphBank, TileSize};
use prometeu_hal::scene_layer::{ParallaxFactor, SceneLayer};
use prometeu_hal::tile::Tile;
use prometeu_hal::tilemap::TileMap;
fn make_scene() -> SceneBank {
make_scene_with_palette(1, 1, TileSize::Size8)
}
fn make_scene_with_palette(
glyph_bank_id: u8,
palette_id: u8,
tile_size: TileSize,
) -> SceneBank {
let layer = SceneLayer {
active: true,
glyph_bank_id,
tile_size,
parallax_factor: ParallaxFactor { x: 1.0, y: 0.5 },
tilemap: TileMap {
width: 2,
height: 2,
tiles: vec![
Tile {
active: true,
glyph: Glyph { glyph_id: 0, palette_id },
flip_x: false,
flip_y: false,
};
4
],
},
};
SceneBank { layers: std::array::from_fn(|_| layer.clone()) }
}
fn make_glyph_bank(tile_size: TileSize, palette_id: u8, color: Color) -> GlyphBank {
let size = tile_size as usize;
let mut bank = GlyphBank::new(tile_size, size, size);
bank.palettes[palette_id as usize][1] = color;
for pixel in &mut bank.pixel_indices {
*pixel = 1;
}
bank
}
#[test]
fn frame_composer_starts_unbound_with_empty_owned_state() {
let frame_composer = FrameComposer::new(320, 180, Arc::new(MemoryBanks::new()));
assert_eq!(frame_composer.viewport_size(), (320, 180));
assert_eq!(frame_composer.active_scene_id(), None);
assert!(frame_composer.active_scene().is_none());
assert_eq!(frame_composer.scene_status(), SceneStatus::Unbound);
assert_eq!(frame_composer.camera(), (0, 0));
assert!(frame_composer.cache().is_none());
assert!(frame_composer.resolver().is_none());
assert_eq!(frame_composer.sprite_controller().sprites().len(), 512);
assert_eq!(frame_composer.sprite_controller().sprite_count(), 0);
assert_eq!(frame_composer.sprite_controller().dropped_sprites(), 0);
}
#[test]
fn frame_composer_exposes_shared_scene_bank_access() {
let banks = Arc::new(MemoryBanks::new());
banks.install_scene_bank(3, Arc::new(make_scene()));
let frame_composer = FrameComposer::new(320, 180, banks);
let scene =
frame_composer.scene_bank_slot(3).expect("scene bank slot 3 should be resident");
assert_eq!(frame_composer.scene_bank_slot_count(), 16);
assert_eq!(scene.layers[0].tile_size, TileSize::Size8);
assert_eq!(scene.layers[0].parallax_factor.y, 0.5);
}
#[test]
fn bind_scene_stores_scene_identity_and_shared_reference() {
let banks = Arc::new(MemoryBanks::new());
banks.install_scene_bank(3, Arc::new(make_scene()));
let expected_scene =
banks.scene_bank_slot(3).expect("scene bank slot 3 should be resident");
let mut frame_composer = FrameComposer::new(320, 180, banks);
assert!(frame_composer.bind_scene(3));
assert_eq!(frame_composer.active_scene_id(), Some(3));
assert!(Arc::ptr_eq(
frame_composer.active_scene().expect("active scene should exist"),
&expected_scene,
));
assert_eq!(frame_composer.scene_status(), SceneStatus::Available { scene_bank_id: 3 });
assert!(frame_composer.cache().is_some());
assert!(frame_composer.resolver().is_some());
}
#[test]
fn unbind_scene_clears_scene_and_cache_state() {
let banks = Arc::new(MemoryBanks::new());
banks.install_scene_bank(1, Arc::new(make_scene()));
let mut frame_composer = FrameComposer::new(320, 180, banks);
assert!(frame_composer.bind_scene(1));
frame_composer.unbind_scene();
assert_eq!(frame_composer.active_scene_id(), None);
assert!(frame_composer.active_scene().is_none());
assert_eq!(frame_composer.scene_status(), SceneStatus::Unbound);
assert!(frame_composer.cache().is_none());
assert!(frame_composer.resolver().is_none());
}
#[test]
fn set_camera_stores_top_left_pixel_coordinates() {
let mut frame_composer = FrameComposer::new(320, 180, Arc::new(MemoryBanks::new()));
frame_composer.set_camera(-12, 48);
assert_eq!(frame_composer.camera(), (-12, 48));
}
#[test]
fn bind_scene_derives_cache_and_resolver_from_eight_pixel_layers() {
let banks = Arc::new(MemoryBanks::new());
banks.install_scene_bank(0, Arc::new(make_scene()));
let mut frame_composer = FrameComposer::new(320, 180, banks);
assert!(frame_composer.bind_scene(0));
let cache = frame_composer.cache().expect("cache should exist for bound scene");
assert_eq!((cache.width(), cache.height()), (45, 27));
}
#[test]
fn missing_scene_binding_falls_back_to_no_scene_state() {
let mut frame_composer = FrameComposer::new(320, 180, Arc::new(MemoryBanks::new()));
assert!(!frame_composer.bind_scene(7));
assert_eq!(frame_composer.scene_status(), SceneStatus::Unbound);
assert!(frame_composer.cache().is_none());
assert!(frame_composer.resolver().is_none());
}
#[test]
fn sprite_controller_begin_frame_resets_sprite_count_and_buckets() {
let mut controller = SpriteController::new();
let emitted = controller.emit_sprite(Sprite {
glyph: Glyph { glyph_id: 1, palette_id: 2 },
x: 4,
y: 5,
layer: 2,
bank_id: 3,
active: false,
flip_x: false,
flip_y: false,
priority: 1,
});
assert!(emitted);
controller.begin_frame();
assert_eq!(controller.frame_counter(), 1);
assert_eq!(controller.sprite_count(), 0);
assert_eq!(controller.dropped_sprites(), 0);
assert!(controller.ordered_sprites().is_empty());
}
#[test]
fn sprite_controller_orders_by_layer_then_priority_then_fifo() {
let mut controller = SpriteController::new();
controller.begin_frame();
assert!(controller.emit_sprite(Sprite {
glyph: Glyph { glyph_id: 10, palette_id: 0 },
x: 0,
y: 0,
layer: 1,
bank_id: 0,
active: false,
flip_x: false,
flip_y: false,
priority: 2,
}));
assert!(controller.emit_sprite(Sprite {
glyph: Glyph { glyph_id: 11, palette_id: 0 },
x: 0,
y: 0,
layer: 0,
bank_id: 0,
active: false,
flip_x: false,
flip_y: false,
priority: 3,
}));
assert!(controller.emit_sprite(Sprite {
glyph: Glyph { glyph_id: 12, palette_id: 0 },
x: 0,
y: 0,
layer: 1,
bank_id: 0,
active: false,
flip_x: false,
flip_y: false,
priority: 1,
}));
assert!(controller.emit_sprite(Sprite {
glyph: Glyph { glyph_id: 13, palette_id: 0 },
x: 0,
y: 0,
layer: 1,
bank_id: 0,
active: false,
flip_x: false,
flip_y: false,
priority: 2,
}));
let ordered = controller.ordered_sprites();
let ordered_ids: Vec<u16> = ordered.iter().map(|sprite| sprite.glyph.glyph_id).collect();
assert_eq!(ordered_ids, vec![11, 12, 10, 13]);
assert!(ordered.iter().all(|sprite| sprite.active));
}
#[test]
fn sprite_controller_drops_overflow_without_panicking() {
let mut controller = SpriteController::new();
controller.begin_frame();
for glyph_id in 0..512 {
assert!(controller.emit_sprite(Sprite {
glyph: Glyph { glyph_id, palette_id: 0 },
x: 0,
y: 0,
layer: 0,
bank_id: 0,
active: false,
flip_x: false,
flip_y: false,
priority: 0,
}));
}
let overflowed = controller.emit_sprite(Sprite {
glyph: Glyph { glyph_id: 999, palette_id: 0 },
x: 0,
y: 0,
layer: 0,
bank_id: 0,
active: false,
flip_x: false,
flip_y: false,
priority: 0,
});
assert!(!overflowed);
assert_eq!(controller.sprite_count(), 512);
assert_eq!(controller.dropped_sprites(), 1);
}
#[test]
fn frame_composer_emits_ordered_sprites_for_rendering() {
let mut frame_composer = FrameComposer::new(320, 180, Arc::new(MemoryBanks::new()));
frame_composer.begin_frame();
assert!(frame_composer.emit_sprite(Sprite {
glyph: Glyph { glyph_id: 21, palette_id: 0 },
x: 0,
y: 0,
layer: 2,
bank_id: 1,
active: false,
flip_x: false,
flip_y: false,
priority: 1,
}));
assert!(frame_composer.emit_sprite(Sprite {
glyph: Glyph { glyph_id: 20, palette_id: 0 },
x: 0,
y: 0,
layer: 1,
bank_id: 1,
active: false,
flip_x: false,
flip_y: false,
priority: 0,
}));
let ordered = frame_composer.ordered_sprites();
assert_eq!(ordered.len(), 2);
assert_eq!(ordered[0].glyph.glyph_id, 20);
assert_eq!(ordered[1].glyph.glyph_id, 21);
}
#[test]
fn render_frame_without_scene_uses_sprite_only_path() {
let banks = Arc::new(MemoryBanks::new());
banks.install_glyph_bank(1, Arc::new(make_glyph_bank(TileSize::Size8, 3, Color::WHITE)));
let mut frame_composer =
FrameComposer::new(16, 16, Arc::clone(&banks) as Arc<dyn SceneBankPoolAccess>);
frame_composer.begin_frame();
assert!(frame_composer.emit_sprite(Sprite {
glyph: Glyph { glyph_id: 0, palette_id: 3 },
x: 0,
y: 0,
layer: 0,
bank_id: 1,
active: false,
flip_x: false,
flip_y: false,
priority: 0,
}));
let mut gfx = Gfx::new(16, 16, Arc::clone(&banks) as Arc<dyn GlyphBankPoolAccess>);
gfx.scene_fade_level = 31;
gfx.hud_fade_level = 31;
frame_composer.render_frame(&mut gfx);
gfx.present();
assert_eq!(gfx.front_buffer()[0], Color::WHITE.raw());
}
#[test]
fn render_frame_with_scene_applies_refreshes_before_composition() {
let banks = Arc::new(MemoryBanks::new());
banks.install_glyph_bank(0, Arc::new(make_glyph_bank(TileSize::Size8, 2, Color::BLUE)));
banks.install_scene_bank(0, Arc::new(make_scene_with_palette(0, 2, TileSize::Size8)));
let mut frame_composer =
FrameComposer::new(16, 16, Arc::clone(&banks) as Arc<dyn SceneBankPoolAccess>);
assert!(frame_composer.bind_scene(0));
let mut gfx = Gfx::new(16, 16, Arc::clone(&banks) as Arc<dyn GlyphBankPoolAccess>);
gfx.scene_fade_level = 31;
gfx.hud_fade_level = 31;
frame_composer.render_frame(&mut gfx);
gfx.present();
assert!(
frame_composer
.cache()
.expect("cache should exist")
.layers
.iter()
.all(|layer| layer.valid)
);
assert_eq!(gfx.front_buffer()[0], Color::BLUE.raw());
}
#[test]
fn render_frame_survives_scene_transition_through_unbind_and_rebind() {
let banks = Arc::new(MemoryBanks::new());
banks.install_glyph_bank(0, Arc::new(make_glyph_bank(TileSize::Size8, 1, Color::RED)));
banks.install_glyph_bank(1, Arc::new(make_glyph_bank(TileSize::Size8, 2, Color::BLUE)));
banks.install_glyph_bank(2, Arc::new(make_glyph_bank(TileSize::Size8, 3, Color::WHITE)));
banks.install_scene_bank(0, Arc::new(make_scene_with_palette(0, 1, TileSize::Size8)));
banks.install_scene_bank(1, Arc::new(make_scene_with_palette(1, 2, TileSize::Size8)));
let mut frame_composer =
FrameComposer::new(16, 16, Arc::clone(&banks) as Arc<dyn SceneBankPoolAccess>);
let mut gfx = Gfx::new(16, 16, Arc::clone(&banks) as Arc<dyn GlyphBankPoolAccess>);
gfx.scene_fade_level = 31;
gfx.hud_fade_level = 31;
assert!(frame_composer.bind_scene(0));
frame_composer.render_frame(&mut gfx);
gfx.present();
assert_eq!(gfx.front_buffer()[0], Color::RED.raw());
frame_composer.unbind_scene();
frame_composer.begin_frame();
assert!(frame_composer.emit_sprite(Sprite {
glyph: Glyph { glyph_id: 0, palette_id: 3 },
x: 0,
y: 0,
layer: 0,
bank_id: 2,
active: false,
flip_x: false,
flip_y: false,
priority: 0,
}));
frame_composer.render_frame(&mut gfx);
gfx.present();
assert_eq!(gfx.front_buffer()[0], Color::WHITE.raw());
frame_composer.begin_frame();
assert!(frame_composer.bind_scene(1));
frame_composer.render_frame(&mut gfx);
gfx.present();
assert_eq!(gfx.front_buffer()[0], Color::BLUE.raw());
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,39 +0,0 @@
use crate::gfx::BlendMode;
use prometeu_hal::color::Color;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum OverlayCommand {
FillRectBlend { x: i32, y: i32, w: i32, h: i32, color: Color, mode: BlendMode },
DrawLine { x0: i32, y0: i32, x1: i32, y1: i32, color: Color },
DrawCircle { x: i32, y: i32, r: i32, color: Color },
DrawDisc { x: i32, y: i32, r: i32, border_color: Color, fill_color: Color },
DrawSquare { x: i32, y: i32, w: i32, h: i32, border_color: Color, fill_color: Color },
DrawText { x: i32, y: i32, text: String, color: Color },
}
#[derive(Debug, Clone, Default)]
pub struct DeferredGfxOverlay {
commands: Vec<OverlayCommand>,
}
impl DeferredGfxOverlay {
pub fn begin_frame(&mut self) {
self.commands.clear();
}
pub fn push(&mut self, command: OverlayCommand) {
self.commands.push(command);
}
pub fn commands(&self) -> &[OverlayCommand] {
&self.commands
}
pub fn command_count(&self) -> usize {
self.commands.len()
}
pub fn take_commands(&mut self) -> Vec<OverlayCommand> {
std::mem::take(&mut self.commands)
}
}

View File

@ -1,240 +0,0 @@
use crate::asset::AssetManager;
use crate::audio::Audio;
use crate::frame_composer::FrameComposer;
use crate::gfx::Gfx;
use crate::memory_banks::{
GlyphBankPoolAccess, GlyphBankPoolInstaller, MemoryBanks, SceneBankPoolAccess,
SceneBankPoolInstaller, SoundBankPoolAccess, SoundBankPoolInstaller,
};
use crate::pad::Pad;
use crate::touch::Touch;
use prometeu_hal::cartridge::AssetsPayloadSource;
use prometeu_hal::sprite::Sprite;
use prometeu_hal::{AssetBridge, AudioBridge, GfxBridge, HardwareBridge, PadBridge, TouchBridge};
use std::sync::Arc;
/// Aggregate structure for all virtual hardware peripherals.
///
/// This struct represents the "Mainboard" of the PROMETEU console.
/// It acts as a container for all hardware subsystems. In the Prometeu
/// architecture, hardware is decoupled from the OS and VM, allowing
/// for easier testing and different host implementations (Desktop, Web, etc.).
///
/// ### Console Specifications:
/// - **Resolution**: 320x180 (16:9 Aspect Ratio).
/// - **Color Depth**: RGB565 (16-bit).
/// - **Audio**: Stereo, Command-based mixing.
/// - **Input**: 12-button Digital Gamepad + Absolute Touch/Mouse.
pub struct Hardware {
/// The Graphics Processing Unit (GPU). Handles drawing primitives, sprites, and tilemaps.
pub gfx: Gfx,
/// Canonical frame orchestration owner for scene/camera/cache/resolver/sprites.
pub frame_composer: FrameComposer,
/// The Sound Processing Unit (SPU). Manages sample playback and volume control.
pub audio: Audio,
/// The standard digital gamepad. Provides state for D-Pad, face buttons, and triggers.
pub pad: Pad,
/// The absolute pointer input device (Mouse/Touchscreen).
pub touch: Touch,
/// The Asset Management system (DMA). Handles loading data into VRAM (GlyphBanks) and ARAM (SoundBanks).
pub assets: AssetManager,
}
impl Default for Hardware {
fn default() -> Self {
Self::new()
}
}
impl HardwareBridge for Hardware {
fn begin_frame(&mut self) {
self.gfx.begin_overlay_frame();
self.frame_composer.begin_frame();
}
fn bind_scene(&mut self, scene_bank_id: usize) -> bool {
self.frame_composer.bind_scene(scene_bank_id)
}
fn unbind_scene(&mut self) {
self.frame_composer.unbind_scene();
}
fn set_camera(&mut self, x: i32, y: i32) {
self.frame_composer.set_camera(x, y);
}
fn emit_sprite(&mut self, sprite: Sprite) -> bool {
self.frame_composer.emit_sprite(sprite)
}
fn render_frame(&mut self) {
self.frame_composer.render_frame(&mut self.gfx);
self.gfx.drain_overlay_debug();
}
fn has_glyph_bank(&self, bank_id: usize) -> bool {
self.gfx.glyph_banks.glyph_bank_slot(bank_id).is_some()
}
fn gfx(&self) -> &dyn GfxBridge {
&self.gfx
}
fn gfx_mut(&mut self) -> &mut dyn GfxBridge {
&mut self.gfx
}
fn audio(&self) -> &dyn AudioBridge {
&self.audio
}
fn audio_mut(&mut self) -> &mut dyn AudioBridge {
&mut self.audio
}
fn pad(&self) -> &dyn PadBridge {
&self.pad
}
fn pad_mut(&mut self) -> &mut dyn PadBridge {
&mut self.pad
}
fn touch(&self) -> &dyn TouchBridge {
&self.touch
}
fn touch_mut(&mut self) -> &mut dyn TouchBridge {
&mut self.touch
}
fn assets(&self) -> &dyn AssetBridge {
&self.assets
}
fn assets_mut(&mut self) -> &mut dyn AssetBridge {
&mut self.assets
}
}
impl Hardware {
/// Internal hardware width in pixels.
pub const W: usize = 320;
/// Internal hardware height in pixels.
pub const H: usize = 180;
/// Creates a fresh hardware instance with default settings.
pub fn new() -> Self {
Self::new_with_memory_banks(Arc::new(MemoryBanks::new()))
}
/// Creates hardware with explicit shared bank ownership.
pub fn new_with_memory_banks(memory_banks: Arc<MemoryBanks>) -> Self {
Self {
gfx: Gfx::new(
Self::W,
Self::H,
Arc::clone(&memory_banks) as Arc<dyn GlyphBankPoolAccess>,
),
frame_composer: FrameComposer::new(
Self::W,
Self::H,
Arc::clone(&memory_banks) as Arc<dyn SceneBankPoolAccess>,
),
audio: Audio::new(Arc::clone(&memory_banks) as Arc<dyn SoundBankPoolAccess>),
pad: Pad::default(),
touch: Touch::default(),
assets: AssetManager::new(
vec![],
AssetsPayloadSource::empty(),
Arc::clone(&memory_banks) as Arc<dyn GlyphBankPoolInstaller>,
Arc::clone(&memory_banks) as Arc<dyn SoundBankPoolInstaller>,
Arc::clone(&memory_banks) as Arc<dyn SceneBankPoolInstaller>,
),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::memory_banks::{
GlyphBankPoolInstaller, SceneBankPoolAccess, SceneBankPoolInstaller,
};
use prometeu_hal::color::Color;
use prometeu_hal::glyph::Glyph;
use prometeu_hal::glyph_bank::{GlyphBank, TileSize};
use prometeu_hal::scene_bank::SceneBank;
use prometeu_hal::scene_layer::{ParallaxFactor, SceneLayer};
use prometeu_hal::scene_viewport_cache::SceneViewportCache;
use prometeu_hal::scene_viewport_resolver::SceneViewportResolver;
use prometeu_hal::tile::Tile;
use prometeu_hal::tilemap::TileMap;
fn make_glyph_bank() -> GlyphBank {
let mut bank = GlyphBank::new(TileSize::Size8, 8, 8);
bank.palettes[0][1] = Color::RED;
for pixel in &mut bank.pixel_indices {
*pixel = 1;
}
bank
}
fn make_scene() -> SceneBank {
let layer = SceneLayer {
active: true,
glyph_bank_id: 0,
tile_size: TileSize::Size8,
parallax_factor: ParallaxFactor { x: 1.0, y: 1.0 },
tilemap: TileMap {
width: 4,
height: 4,
tiles: vec![
Tile {
active: true,
glyph: Glyph { glyph_id: 0, palette_id: 0 },
flip_x: false,
flip_y: false,
};
16
],
},
};
SceneBank { layers: std::array::from_fn(|_| layer.clone()) }
}
#[test]
fn hardware_can_render_scene_from_shared_scene_bank_pipeline() {
let banks = Arc::new(MemoryBanks::new());
banks.install_glyph_bank(0, Arc::new(make_glyph_bank()));
banks.install_scene_bank(0, Arc::new(make_scene()));
let mut hardware = Hardware::new_with_memory_banks(Arc::clone(&banks));
let scene = banks.scene_bank_slot(0).expect("scene bank slot 0 should be resident");
let mut cache = SceneViewportCache::new(&scene, 4, 4);
cache.materialize_all_layers(&scene);
let mut resolver = SceneViewportResolver::new(16, 16, 4, 4, 12, 20);
let update = resolver.update(&scene, 0, 0);
hardware.gfx.scene_fade_level = 31;
hardware.gfx.hud_fade_level = 31;
hardware.gfx.render_scene_from_cache(&cache, &update);
hardware.gfx.present();
assert_eq!(hardware.gfx.front_buffer()[0], Color::RED.raw());
}
#[test]
fn hardware_constructs_frame_composer_with_shared_scene_bank_access() {
let banks = Arc::new(MemoryBanks::new());
banks.install_scene_bank(2, Arc::new(make_scene()));
let hardware = Hardware::new_with_memory_banks(banks);
let scene = hardware
.frame_composer
.scene_bank_slot(2)
.expect("scene bank slot 2 should be resident");
assert_eq!(hardware.frame_composer.viewport_size(), (Hardware::W, Hardware::H));
assert_eq!(hardware.frame_composer.scene_bank_slot_count(), 16);
assert_eq!(scene.layers[0].tile_size, TileSize::Size8);
}
}

View File

@ -1,20 +0,0 @@
mod asset;
mod audio;
mod frame_composer;
mod gfx;
mod gfx_overlay;
pub mod hardware;
mod memory_banks;
mod pad;
mod touch;
pub use crate::asset::AssetManager;
pub use crate::audio::{Audio, AudioCommand, Channel, MAX_CHANNELS, OUTPUT_SAMPLE_RATE};
pub use crate::frame_composer::{FrameComposer, SceneStatus, SpriteController};
pub use crate::gfx::Gfx;
pub use crate::gfx_overlay::{DeferredGfxOverlay, OverlayCommand};
pub use crate::memory_banks::{
GlyphBankPoolAccess, GlyphBankPoolInstaller, MemoryBanks, SceneBankPoolAccess,
SceneBankPoolInstaller, SoundBankPoolAccess, SoundBankPoolInstaller,
};
pub use crate::pad::Pad;

View File

@ -1,134 +0,0 @@
use prometeu_hal::glyph_bank::GlyphBank;
use prometeu_hal::scene_bank::SceneBank;
use prometeu_hal::sound_bank::SoundBank;
use std::sync::{Arc, RwLock};
/// Non-generic interface for peripherals to access graphical glyph banks.
pub trait GlyphBankPoolAccess: Send + Sync {
/// Returns a reference to the resident GlyphBank in the specified slot, if any.
fn glyph_bank_slot(&self, slot: usize) -> Option<Arc<GlyphBank>>;
/// Returns the total number of slots available in this bank.
fn glyph_bank_slot_count(&self) -> usize;
}
/// Non-generic interface for the AssetManager to install graphical glyph banks.
pub trait GlyphBankPoolInstaller: Send + Sync {
/// Atomically swaps the resident GlyphBank in the specified slot.
fn install_glyph_bank(&self, slot: usize, bank: Arc<GlyphBank>);
}
/// Non-generic interface for peripherals to access sound banks.
pub trait SoundBankPoolAccess: Send + Sync {
/// Returns a reference to the resident SoundBank in the specified slot, if any.
fn sound_bank_slot(&self, slot: usize) -> Option<Arc<SoundBank>>;
/// Returns the total number of slots available in this bank.
fn sound_bank_slot_count(&self) -> usize;
}
/// Non-generic interface for the AssetManager to install sound banks.
pub trait SoundBankPoolInstaller: Send + Sync {
/// Atomically swaps the resident SoundBank in the specified slot.
fn install_sound_bank(&self, slot: usize, bank: Arc<SoundBank>);
}
/// Non-generic interface for peripherals to access scene banks.
pub trait SceneBankPoolAccess: Send + Sync {
/// Returns a reference to the resident SceneBank in the specified slot, if any.
fn scene_bank_slot(&self, slot: usize) -> Option<Arc<SceneBank>>;
/// Returns the total number of slots available in this bank.
fn scene_bank_slot_count(&self) -> usize;
}
/// Non-generic interface for the AssetManager to install scene banks.
pub trait SceneBankPoolInstaller: Send + Sync {
/// Atomically swaps the resident SceneBank in the specified slot.
fn install_scene_bank(&self, slot: usize, bank: Arc<SceneBank>);
}
/// Centralized container for all hardware memory banks.
///
/// MemoryBanks represent the actual hardware slot state.
/// Peripherals consume this state via narrow, non-generic traits.
/// AssetManager coordinates residency and installs assets into these slots.
pub struct MemoryBanks {
glyph_bank_pool: Arc<RwLock<[Option<Arc<GlyphBank>>; 16]>>,
sound_bank_pool: Arc<RwLock<[Option<Arc<SoundBank>>; 16]>>,
scene_bank_pool: Arc<RwLock<[Option<Arc<SceneBank>>; 16]>>,
}
impl Default for MemoryBanks {
fn default() -> Self {
Self::new()
}
}
impl MemoryBanks {
/// Creates a new set of memory banks with empty slots.
pub fn new() -> Self {
Self {
glyph_bank_pool: Arc::new(RwLock::new(std::array::from_fn(|_| None))),
sound_bank_pool: Arc::new(RwLock::new(std::array::from_fn(|_| None))),
scene_bank_pool: Arc::new(RwLock::new(std::array::from_fn(|_| None))),
}
}
}
impl GlyphBankPoolAccess for MemoryBanks {
fn glyph_bank_slot(&self, slot: usize) -> Option<Arc<GlyphBank>> {
let pool = self.glyph_bank_pool.read().unwrap();
pool.get(slot).and_then(|s| s.as_ref().map(Arc::clone))
}
fn glyph_bank_slot_count(&self) -> usize {
16
}
}
impl GlyphBankPoolInstaller for MemoryBanks {
fn install_glyph_bank(&self, slot: usize, bank: Arc<GlyphBank>) {
let mut pool = self.glyph_bank_pool.write().unwrap();
if slot < 16 {
pool[slot] = Some(bank);
}
}
}
impl SoundBankPoolAccess for MemoryBanks {
fn sound_bank_slot(&self, slot: usize) -> Option<Arc<SoundBank>> {
let pool = self.sound_bank_pool.read().unwrap();
pool.get(slot).and_then(|s| s.as_ref().map(Arc::clone))
}
fn sound_bank_slot_count(&self) -> usize {
16
}
}
impl SoundBankPoolInstaller for MemoryBanks {
fn install_sound_bank(&self, slot: usize, bank: Arc<SoundBank>) {
let mut pool = self.sound_bank_pool.write().unwrap();
if slot < 16 {
pool[slot] = Some(bank);
}
}
}
impl SceneBankPoolAccess for MemoryBanks {
fn scene_bank_slot(&self, slot: usize) -> Option<Arc<SceneBank>> {
let pool = self.scene_bank_pool.read().unwrap();
pool.get(slot).and_then(|s| s.as_ref().map(Arc::clone))
}
fn scene_bank_slot_count(&self) -> usize {
16
}
}
impl SceneBankPoolInstaller for MemoryBanks {
fn install_scene_bank(&self, slot: usize, bank: Arc<SceneBank>) {
let mut pool = self.scene_bank_pool.write().unwrap();
if slot < 16 {
pool[slot] = Some(bank);
}
}
}

View File

@ -1,33 +0,0 @@
use prometeu_hal::button::Button;
use prometeu_hal::{InputSignals, TouchBridge};
#[derive(Default, Clone, Copy, Debug)]
pub struct Touch {
pub f: Button,
pub x: i32,
pub y: i32,
}
impl Touch {
/// Transient flags should last only 1 frame.
pub fn begin_frame(&mut self, signals: &InputSignals) {
self.f.begin_frame(signals.f_signal);
self.x = signals.x_pos;
self.y = signals.y_pos;
}
}
impl TouchBridge for Touch {
fn begin_frame(&mut self, signals: &InputSignals) {
self.begin_frame(signals)
}
fn f(&self) -> &Button {
&self.f
}
fn x(&self) -> i32 {
self.x
}
fn y(&self) -> i32 {
self.y
}
}

View File

@ -1,14 +0,0 @@
[package]
name = "prometeu-firmware"
version = "0.1.0"
edition = "2024"
license.workspace = true
[dependencies]
prometeu-vm = { path = "../prometeu-vm" }
prometeu-system = { path = "../prometeu-system" }
prometeu-hal = { path = "../prometeu-hal" }
[dev-dependencies]
prometeu-drivers = { path = "../prometeu-drivers" }
prometeu-bytecode = { path = "../prometeu-bytecode" }

View File

@ -1,10 +0,0 @@
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub enum BootTarget {
#[default]
Hub,
Cartridge {
path: String,
debug: bool,
debug_port: u16,
},
}

View File

@ -1,271 +0,0 @@
use crate::firmware::boot_target::BootTarget;
use crate::firmware::firmware_state::{FirmwareState, LoadCartridgeStep, ResetStep};
use crate::firmware::prometeu_context::PrometeuContext;
use prometeu_hal::cartridge::Cartridge;
use prometeu_hal::telemetry::CertificationConfig;
use prometeu_hal::{HardwareBridge, InputSignals};
use prometeu_system::{PrometeuHub, VirtualMachineRuntime};
use prometeu_vm::VirtualMachine;
/// PROMETEU Firmware.
///
/// The central orchestrator of the console. The firmware acts as the
/// "Control Unit", managing the high-level state machine of the system.
///
/// It is responsible for transitioning between different modes of operation,
/// such as showing the splash screen, running the Hub (launcher), or
/// executing a game/app.
///
/// ### Execution Loop:
/// The firmware is designed to be ticked once per frame (60Hz). During each
/// tick, it:
/// 1. Updates peripherals with the latest input signals.
/// 2. Delegates the logic update to the current active state.
/// 3. Handles state transitions (e.g., from Loading to Playing).
pub struct Firmware {
/// The execution engine (PVM) for user applications.
pub vm: VirtualMachine,
/// The underlying OS services (Syscalls, Filesystem, Telemetry).
pub os: VirtualMachineRuntime,
/// The internal state of the system launcher (Hub).
pub hub: PrometeuHub,
/// The current operational state (e.g., Reset, SplashScreen, GameRunning).
pub state: FirmwareState,
/// The desired application to run after boot (Hub or specific Cartridge).
pub boot_target: BootTarget,
/// State-machine lifecycle tracker.
state_initialized: bool,
}
impl Firmware {
/// Initializes the firmware in the `Reset` state.
pub fn new(cap_config: Option<CertificationConfig>) -> Self {
Self {
vm: VirtualMachine::default(),
os: VirtualMachineRuntime::new(cap_config),
hub: PrometeuHub::new(),
state: FirmwareState::Reset(ResetStep),
boot_target: BootTarget::Hub,
state_initialized: false,
}
}
/// The main entry point for the Host to advance the system logic.
///
/// This method is called exactly once per Host frame (60Hz).
/// It updates peripheral signals and delegates the logic to the current state.
pub fn tick(&mut self, signals: &InputSignals, hw: &mut dyn HardwareBridge) {
// 0. Process asset commits at the beginning of the frame boundary.
hw.assets_mut().apply_commits();
// 1. Update the peripheral state using the latest signals from the Host.
// This ensures input is consistent throughout the entire update.
hw.pad_mut().begin_frame(signals);
hw.touch_mut().begin_frame(signals);
// 2. State machine lifecycle management.
if !self.state_initialized {
self.on_enter(signals, hw);
self.state_initialized = true;
}
// 3. Update the current state and check for transitions.
if let Some(next_state) = self.on_update(signals, hw) {
self.change_state(next_state, signals, hw);
}
}
/// Transitions the system to a new state, handling lifecycle hooks.
pub fn change_state(
&mut self,
new_state: FirmwareState,
signals: &InputSignals,
hw: &mut dyn HardwareBridge,
) {
self.on_exit(signals, hw);
self.state = new_state;
self.state_initialized = false;
// Enter the new state immediately to avoid "empty" frames during transitions.
self.on_enter(signals, hw);
self.state_initialized = true;
}
/// Dispatches the `on_enter` event to the current state implementation.
fn on_enter(&mut self, signals: &InputSignals, hw: &mut dyn HardwareBridge) {
let mut req = PrometeuContext {
vm: &mut self.vm,
os: &mut self.os,
hub: &mut self.hub,
boot_target: &self.boot_target,
signals,
hw,
};
match &mut self.state {
FirmwareState::Reset(s) => s.on_enter(&mut req),
FirmwareState::SplashScreen(s) => s.on_enter(&mut req),
FirmwareState::LaunchHub(s) => s.on_enter(&mut req),
FirmwareState::HubHome(s) => s.on_enter(&mut req),
FirmwareState::LoadCartridge(s) => s.on_enter(&mut req),
FirmwareState::GameRunning(s) => s.on_enter(&mut req),
FirmwareState::AppCrashes(s) => s.on_enter(&mut req),
}
}
/// Dispatches the `on_update` event to the current state implementation.
/// Returns an optional `FirmwareState` if a transition is requested.
fn on_update(
&mut self,
signals: &InputSignals,
hw: &mut dyn HardwareBridge,
) -> Option<FirmwareState> {
let mut req = PrometeuContext {
vm: &mut self.vm,
os: &mut self.os,
hub: &mut self.hub,
boot_target: &self.boot_target,
signals,
hw,
};
match &mut self.state {
FirmwareState::Reset(s) => s.on_update(&mut req),
FirmwareState::SplashScreen(s) => s.on_update(&mut req),
FirmwareState::LaunchHub(s) => s.on_update(&mut req),
FirmwareState::HubHome(s) => s.on_update(&mut req),
FirmwareState::LoadCartridge(s) => s.on_update(&mut req),
FirmwareState::GameRunning(s) => s.on_update(&mut req),
FirmwareState::AppCrashes(s) => s.on_update(&mut req),
}
}
/// Dispatches the `on_exit` event to the current state implementation.
fn on_exit(&mut self, signals: &InputSignals, hw: &mut dyn HardwareBridge) {
let mut req = PrometeuContext {
vm: &mut self.vm,
os: &mut self.os,
hub: &mut self.hub,
boot_target: &self.boot_target,
signals,
hw,
};
match &mut self.state {
FirmwareState::Reset(s) => s.on_exit(&mut req),
FirmwareState::SplashScreen(s) => s.on_exit(&mut req),
FirmwareState::LaunchHub(s) => s.on_exit(&mut req),
FirmwareState::HubHome(s) => s.on_exit(&mut req),
FirmwareState::LoadCartridge(s) => s.on_exit(&mut req),
FirmwareState::GameRunning(s) => s.on_exit(&mut req),
FirmwareState::AppCrashes(s) => s.on_exit(&mut req),
}
}
pub fn load_cartridge(&mut self, cartridge: Cartridge) {
self.state = FirmwareState::LoadCartridge(LoadCartridgeStep::new(cartridge));
self.state_initialized = false;
}
}
#[cfg(test)]
mod tests {
use super::*;
use prometeu_bytecode::assembler::assemble;
use prometeu_bytecode::model::{BytecodeModule, FunctionMeta, SyscallDecl};
use prometeu_drivers::hardware::Hardware;
use prometeu_hal::cartridge::{AppMode, AssetsPayloadSource};
use prometeu_hal::syscalls::caps;
use prometeu_system::CrashReport;
fn invalid_game_cartridge() -> Cartridge {
Cartridge {
app_id: 7,
title: "Broken Cart".into(),
app_version: "1.0.0".into(),
app_mode: AppMode::Game,
capabilities: 0,
program: vec![0, 0, 0, 0],
assets: AssetsPayloadSource::empty(),
asset_table: vec![],
preload: vec![],
}
}
fn trapping_game_cartridge() -> Cartridge {
let code = assemble("PUSH_BOOL 1\nHOSTCALL 0\nHALT").expect("assemble");
let program = BytecodeModule {
version: 0,
const_pool: vec![],
functions: vec![FunctionMeta {
code_offset: 0,
code_len: code.len() as u32,
..Default::default()
}],
code,
debug_info: None,
exports: vec![],
syscalls: vec![SyscallDecl {
module: "gfx".into(),
name: "clear".into(),
version: 1,
arg_slots: 1,
ret_slots: 0,
}],
}
.serialize();
Cartridge {
app_id: 8,
title: "Trap Cart".into(),
app_version: "1.0.0".into(),
app_mode: AppMode::Game,
capabilities: caps::GFX,
program,
assets: AssetsPayloadSource::empty(),
asset_table: vec![],
preload: vec![],
}
}
#[test]
fn load_cartridge_transitions_to_app_crashes_when_vm_init_fails() {
let mut firmware = Firmware::new(None);
let mut hardware = Hardware::new();
let signals = InputSignals::default();
firmware.load_cartridge(invalid_game_cartridge());
firmware.tick(&signals, &mut hardware);
match &firmware.state {
FirmwareState::AppCrashes(step) => match &step.report {
CrashReport::VmInit { error } => {
assert!(matches!(error, prometeu_vm::VmInitError::InvalidFormat));
}
other => panic!("expected VmInit crash report, got {:?}", other),
},
other => panic!("expected AppCrashes state, got {:?}", other),
}
}
#[test]
fn game_running_transitions_to_app_crashes_when_runtime_surfaces_trap() {
let mut firmware = Firmware::new(None);
let mut hardware = Hardware::new();
let signals = InputSignals::default();
firmware.load_cartridge(trapping_game_cartridge());
firmware.tick(&signals, &mut hardware);
assert!(matches!(firmware.state, FirmwareState::GameRunning(_)));
firmware.tick(&signals, &mut hardware);
match &firmware.state {
FirmwareState::AppCrashes(step) => match &step.report {
CrashReport::VmTrap { trap } => {
assert!(trap.message.contains("Expected integer at index 0"));
}
other => panic!("expected VmTrap crash report, got {:?}", other),
},
other => panic!("expected AppCrashes state, got {:?}", other),
}
}
}

View File

@ -1,67 +0,0 @@
use crate::firmware::firmware_state::{FirmwareState, LaunchHubStep};
use crate::firmware::prometeu_context::PrometeuContext;
use prometeu_hal::color::Color;
use prometeu_hal::log::{LogLevel, LogSource};
use prometeu_system::CrashReport;
#[derive(Debug, Clone)]
pub struct AppCrashesStep {
pub report: CrashReport,
}
impl AppCrashesStep {
pub fn log_message(&self) -> String {
format!("App Crashed: {}", self.report)
}
pub fn on_enter(&mut self, ctx: &mut PrometeuContext) {
ctx.os.log(LogLevel::Error, LogSource::Pos, self.report.log_tag(), self.log_message());
}
pub fn on_update(&mut self, ctx: &mut PrometeuContext) -> Option<FirmwareState> {
// Update peripherals for input on the crash screen
ctx.hw.pad_mut().begin_frame(ctx.signals);
// Error screen: red background, white text
ctx.hw.gfx_mut().clear(Color::RED);
// For now we just log or show something simple
// In the future, use draw_text
ctx.hw.gfx_mut().present();
// If START is pressed, return to the Hub
if ctx.hw.pad().start().down {
return Some(FirmwareState::LaunchHub(LaunchHubStep));
}
None
}
pub fn on_exit(&mut self, _ctx: &mut PrometeuContext) {}
}
#[cfg(test)]
mod tests {
use super::*;
use prometeu_bytecode::TrapInfo;
#[test]
fn crash_step_formats_trap_from_structured_report() {
let step = AppCrashesStep {
report: CrashReport::VmTrap {
trap: TrapInfo {
code: 0xAB,
opcode: 0x22,
message: "type mismatch".into(),
pc: 0x44,
span: None,
},
},
};
let msg = step.log_message();
assert!(msg.contains("PVM Trap 0x000000AB"));
assert!(msg.contains("PC 0x44"));
assert!(msg.contains("opcode 0x0022"));
assert!(msg.contains("type mismatch"));
}
}

View File

@ -1,3 +0,0 @@
pub mod firmware;
pub use firmware::*;

View File

@ -1,163 +0,0 @@
use serde::{Deserialize, Serialize};
pub type HandleId = u32;
pub type AssetId = i32;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Deserialize, Serialize)]
#[allow(non_camel_case_types)]
pub enum BankType {
GLYPH,
SOUNDS,
SCENE,
// TILEMAPS,
// BLOBS,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Deserialize, Serialize)]
pub enum AssetCodec {
#[serde(rename = "NONE")]
None,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct AssetEntry {
pub asset_id: AssetId,
pub asset_name: String,
pub bank_type: BankType,
pub offset: u64,
pub size: u64,
pub decoded_size: u64,
pub codec: AssetCodec,
pub metadata: serde_json::Value,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct GlyphBankMetadata {
pub tile_size: u32,
pub width: u32,
pub height: u32,
pub palette_count: u32,
pub palette_authored: u32,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct SoundBankMetadata {
pub sample_rate: u32,
pub channels: u32,
}
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
pub struct SceneBankMetadata {}
pub const SCENE_PAYLOAD_MAGIC_V1: [u8; 4] = *b"SCNE";
pub const SCENE_PAYLOAD_VERSION_V1: u16 = 1;
pub const SCENE_LAYER_COUNT_V1: usize = 4;
pub const SCENE_HEADER_BYTES_V1: usize = 12;
pub const SCENE_LAYER_HEADER_BYTES_V1: usize = 28;
pub const SCENE_TILE_RECORD_BYTES_V1: usize = 4;
pub const SCENE_DECODED_LAYER_OVERHEAD_BYTES_V1: usize = 16;
impl AssetEntry {
pub fn metadata_as_glyph_bank(&self) -> Result<GlyphBankMetadata, String> {
if self.bank_type != BankType::GLYPH {
return Err(format!(
"Asset {} is not a GLYPH bank (type: {:?})",
self.asset_id, self.bank_type
));
}
serde_json::from_value(self.metadata.clone())
.map_err(|e| format!("Invalid GLYPH metadata for asset {}: {}", self.asset_id, e))
}
pub fn metadata_as_sound_bank(&self) -> Result<SoundBankMetadata, String> {
if self.bank_type != BankType::SOUNDS {
return Err(format!(
"Asset {} is not a SOUNDS bank (type: {:?})",
self.asset_id, self.bank_type
));
}
serde_json::from_value(self.metadata.clone())
.map_err(|e| format!("Invalid SOUNDS metadata for asset {}: {}", self.asset_id, e))
}
pub fn metadata_as_scene_bank(&self) -> Result<SceneBankMetadata, String> {
if self.bank_type != BankType::SCENE {
return Err(format!(
"Asset {} is not a SCENE bank (type: {:?})",
self.asset_id, self.bank_type
));
}
serde_json::from_value(self.metadata.clone())
.map_err(|e| format!("Invalid SCENE metadata for asset {}: {}", self.asset_id, e))
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct PreloadEntry {
pub asset_id: AssetId,
pub slot: usize,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[repr(i32)]
pub enum LoadStatus {
PENDING = 0,
LOADING = 1,
READY = 2,
COMMITTED = 3,
CANCELED = 4,
ERROR = 5,
UnknownHandle = 6,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(i32)]
pub enum AssetLoadError {
AssetNotFound = 3,
SlotKindMismatch = 4,
SlotIndexInvalid = 5,
BackendError = 6,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(i32)]
pub enum AssetOpStatus {
Ok = 0,
UnknownHandle = 1,
InvalidState = 2,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BankTelemetry {
pub bank_type: BankType,
pub used_slots: usize,
pub total_slots: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SlotStats {
pub asset_id: Option<AssetId>,
pub asset_name: Option<String>,
pub generation: u32,
pub resident_bytes: usize,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct SlotRef {
pub asset_type: BankType,
pub index: usize,
}
impl SlotRef {
pub fn gfx(index: usize) -> Self {
Self { asset_type: BankType::GLYPH, index }
}
pub fn audio(index: usize) -> Self {
Self { asset_type: BankType::SOUNDS, index }
}
pub fn scene(index: usize) -> Self {
Self { asset_type: BankType::SCENE, index }
}
}

View File

@ -1,22 +0,0 @@
use crate::asset::{
AssetEntry, AssetId, AssetLoadError, AssetOpStatus, BankTelemetry, HandleId, LoadStatus,
PreloadEntry, SlotRef, SlotStats,
};
use crate::cartridge::AssetsPayloadSource;
pub trait AssetBridge {
fn initialize_for_cartridge(
&self,
assets: Vec<AssetEntry>,
preload: Vec<PreloadEntry>,
assets_data: AssetsPayloadSource,
);
fn load(&self, asset_id: AssetId, slot_index: usize) -> Result<HandleId, AssetLoadError>;
fn status(&self, handle: HandleId) -> LoadStatus;
fn commit(&self, handle: HandleId) -> AssetOpStatus;
fn cancel(&self, handle: HandleId) -> AssetOpStatus;
fn apply_commits(&self);
fn bank_telemetry(&self) -> Vec<BankTelemetry>;
fn slot_info(&self, slot: SlotRef) -> SlotStats;
fn shutdown(&self);
}

View File

@ -1,52 +0,0 @@
use crate::sample::Sample;
use std::sync::Arc;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LoopMode {
Off,
On,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(i32)]
pub enum AudioOpStatus {
Ok = 0,
VoiceInvalid = 1,
SampleNotFound = 2,
ArgRangeInvalid = 3,
AssetNotFound = 4,
NoEffect = 5,
BankInvalid = 6,
}
pub trait AudioBridge {
#[allow(clippy::too_many_arguments)]
fn play(
&mut self,
bank_id: u8,
sample_id: u16,
voice_id: usize,
volume: u8,
pan: u8,
pitch: f64,
priority: u8,
loop_mode: LoopMode,
) -> AudioOpStatus;
#[allow(clippy::too_many_arguments)]
fn play_sample(
&mut self,
sample: Arc<Sample>,
voice_id: usize,
volume: u8,
pan: u8,
pitch: f64,
priority: u8,
loop_mode: LoopMode,
) -> AudioOpStatus;
fn stop(&mut self, voice_id: usize);
fn set_volume(&mut self, voice_id: usize, volume: u8);
fn set_pan(&mut self, voice_id: usize, pan: u8);
fn set_pitch(&mut self, voice_id: usize, pitch: f64);
fn is_playing(&self, voice_id: usize) -> bool;
fn clear_commands(&mut self);
}

View File

@ -1,312 +0,0 @@
use crate::asset::{AssetEntry, PreloadEntry};
use crate::syscalls::CapFlags;
use serde::{Deserialize, Serialize};
use std::fs::File;
use std::io::{self, Cursor, Read, Seek, SeekFrom};
use std::path::PathBuf;
use std::sync::Arc;
pub const ASSETS_PA_MAGIC: [u8; 4] = *b"ASPA";
pub const ASSETS_PA_SCHEMA_VERSION: u32 = 1;
pub const ASSETS_PA_PRELUDE_SIZE: usize = 32;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
pub enum AppMode {
Game,
System,
}
#[derive(Debug, Clone)]
pub struct Cartridge {
pub app_id: u32,
pub title: String,
pub app_version: String,
pub app_mode: AppMode,
pub capabilities: CapFlags,
pub program: Vec<u8>,
pub assets: AssetsPayloadSource,
pub asset_table: Vec<AssetEntry>,
pub preload: Vec<PreloadEntry>,
}
#[derive(Debug, Clone)]
pub struct CartridgeDTO {
pub app_id: u32,
pub title: String,
pub app_version: String,
pub app_mode: AppMode,
pub capabilities: CapFlags,
pub program: Vec<u8>,
pub assets: AssetsPayloadSource,
pub asset_table: Vec<AssetEntry>,
pub preload: Vec<PreloadEntry>,
}
impl From<CartridgeDTO> for Cartridge {
fn from(dto: CartridgeDTO) -> Self {
Self {
app_id: dto.app_id,
title: dto.title,
app_version: dto.app_version,
app_mode: dto.app_mode,
capabilities: dto.capabilities,
program: dto.program,
assets: dto.assets,
asset_table: dto.asset_table,
preload: dto.preload,
}
}
}
#[derive(Debug, Clone)]
pub enum AssetsPayloadSource {
Memory(Arc<[u8]>),
File(Arc<FileAssetsPayloadSource>),
}
impl AssetsPayloadSource {
pub fn empty() -> Self {
Self::Memory(Arc::<[u8]>::from(Vec::<u8>::new()))
}
pub fn from_bytes(bytes: Vec<u8>) -> Self {
Self::Memory(Arc::<[u8]>::from(bytes))
}
pub fn from_file(path: PathBuf, payload_offset: u64, payload_len: u64) -> Self {
Self::File(Arc::new(FileAssetsPayloadSource { path, payload_offset, payload_len }))
}
pub fn is_empty(&self) -> bool {
match self {
Self::Memory(bytes) => bytes.is_empty(),
Self::File(source) => source.payload_len == 0,
}
}
pub fn open_slice(&self, offset: u64, size: u64) -> io::Result<AssetsPayloadSlice> {
match self {
Self::Memory(bytes) => {
let start =
usize::try_from(offset).map_err(|_| invalid_input("asset offset overflow"))?;
let len =
usize::try_from(size).map_err(|_| invalid_input("asset size overflow"))?;
let end =
start.checked_add(len).ok_or_else(|| invalid_input("asset range overflow"))?;
if end > bytes.len() {
return Err(invalid_input("asset range out of bounds"));
}
Ok(AssetsPayloadSlice::Memory { bytes: Arc::clone(bytes), start, len })
}
Self::File(source) => {
let end = offset
.checked_add(size)
.ok_or_else(|| invalid_input("asset range overflow"))?;
if end > source.payload_len {
return Err(invalid_input("asset range out of bounds"));
}
Ok(AssetsPayloadSlice::File { source: Arc::clone(source), offset, size })
}
}
}
}
#[derive(Debug)]
pub struct FileAssetsPayloadSource {
pub path: PathBuf,
pub payload_offset: u64,
pub payload_len: u64,
}
#[derive(Debug, Clone)]
pub enum AssetsPayloadSlice {
Memory { bytes: Arc<[u8]>, start: usize, len: usize },
File { source: Arc<FileAssetsPayloadSource>, offset: u64, size: u64 },
}
impl AssetsPayloadSlice {
pub fn open_reader(&self) -> io::Result<AssetsPayloadReader> {
match self {
Self::Memory { bytes, start, len } => {
let data = Arc::<[u8]>::from(bytes[*start..*start + *len].to_vec());
Ok(AssetsPayloadReader::Memory(Cursor::new(data)))
}
Self::File { source, offset, size } => {
let mut file = File::open(&source.path)?;
let absolute_start = source.payload_offset + offset;
file.seek(SeekFrom::Start(absolute_start))?;
Ok(AssetsPayloadReader::File(FileSliceReader {
file,
start: absolute_start,
len: *size,
position: 0,
}))
}
}
}
pub fn read_all(&self) -> io::Result<Vec<u8>> {
let mut reader = self.open_reader()?;
let mut buffer = Vec::new();
reader.read_to_end(&mut buffer)?;
Ok(buffer)
}
}
pub enum AssetsPayloadReader {
Memory(Cursor<Arc<[u8]>>),
File(FileSliceReader),
}
impl Read for AssetsPayloadReader {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
match self {
Self::Memory(reader) => reader.read(buf),
Self::File(reader) => reader.read(buf),
}
}
}
impl Seek for AssetsPayloadReader {
fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
match self {
Self::Memory(reader) => reader.seek(pos),
Self::File(reader) => reader.seek(pos),
}
}
}
pub struct FileSliceReader {
file: File,
start: u64,
len: u64,
position: u64,
}
impl Read for FileSliceReader {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
if self.position >= self.len {
return Ok(0);
}
let remaining = (self.len - self.position) as usize;
let to_read = remaining.min(buf.len());
let read = self.file.read(&mut buf[..to_read])?;
self.position += read as u64;
Ok(read)
}
}
impl Seek for FileSliceReader {
fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
let next = match pos {
SeekFrom::Start(offset) => offset as i128,
SeekFrom::Current(delta) => self.position as i128 + delta as i128,
SeekFrom::End(delta) => self.len as i128 + delta as i128,
};
if next < 0 || next as u64 > self.len {
return Err(invalid_input("slice seek out of bounds"));
}
let next = next as u64;
self.file.seek(SeekFrom::Start(self.start + next))?;
self.position = next;
Ok(self.position)
}
}
fn invalid_input(message: &'static str) -> io::Error {
io::Error::new(io::ErrorKind::InvalidInput, message)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct AssetsPackPrelude {
pub magic: [u8; 4],
pub schema_version: u32,
pub header_len: u32,
pub payload_offset: u64,
pub flags: u32,
pub reserved: u32,
pub header_checksum: u32,
}
impl AssetsPackPrelude {
pub fn from_bytes(bytes: &[u8]) -> Option<Self> {
if bytes.len() < ASSETS_PA_PRELUDE_SIZE {
return None;
}
let mut magic = [0_u8; 4];
magic.copy_from_slice(&bytes[0..4]);
Some(Self {
magic,
schema_version: u32::from_le_bytes(bytes[4..8].try_into().ok()?),
header_len: u32::from_le_bytes(bytes[8..12].try_into().ok()?),
payload_offset: u64::from_le_bytes(bytes[12..20].try_into().ok()?),
flags: u32::from_le_bytes(bytes[20..24].try_into().ok()?),
reserved: u32::from_le_bytes(bytes[24..28].try_into().ok()?),
header_checksum: u32::from_le_bytes(bytes[28..32].try_into().ok()?),
})
}
pub fn to_bytes(self) -> [u8; ASSETS_PA_PRELUDE_SIZE] {
let mut bytes = [0_u8; ASSETS_PA_PRELUDE_SIZE];
bytes[0..4].copy_from_slice(&self.magic);
bytes[4..8].copy_from_slice(&self.schema_version.to_le_bytes());
bytes[8..12].copy_from_slice(&self.header_len.to_le_bytes());
bytes[12..20].copy_from_slice(&self.payload_offset.to_le_bytes());
bytes[20..24].copy_from_slice(&self.flags.to_le_bytes());
bytes[24..28].copy_from_slice(&self.reserved.to_le_bytes());
bytes[28..32].copy_from_slice(&self.header_checksum.to_le_bytes());
bytes
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct AssetsPackHeader {
#[serde(default)]
pub asset_table: Vec<AssetEntry>,
#[serde(default)]
pub preload: Vec<PreloadEntry>,
}
#[derive(Debug)]
pub enum CartridgeError {
NotFound,
InvalidFormat,
InvalidManifest,
UnsupportedVersion,
MissingProgram,
MissingAssets,
IoError,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Deserialize, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum Capability {
None,
System,
Gfx,
Audio,
Fs,
Log,
Asset,
Bank,
All,
}
#[derive(Deserialize)]
pub struct CartridgeManifest {
pub magic: String,
pub cartridge_version: u32,
pub app_id: u32,
pub title: String,
pub app_version: String,
pub app_mode: AppMode,
#[serde(default)]
pub capabilities: Vec<Capability>,
}

View File

@ -1,586 +0,0 @@
use crate::asset::{AssetEntry, BankType};
use crate::cartridge::{
ASSETS_PA_MAGIC, ASSETS_PA_PRELUDE_SIZE, ASSETS_PA_SCHEMA_VERSION, AssetsPackHeader,
AssetsPackPrelude, AssetsPayloadSource, Capability, Cartridge, CartridgeDTO, CartridgeError,
CartridgeManifest,
};
use crate::syscalls::{CapFlags, caps};
use std::collections::HashSet;
use std::fs;
use std::io::{Read, Seek, SeekFrom};
use std::path::Path;
pub struct CartridgeLoader;
impl CartridgeLoader {
pub fn load(path: impl AsRef<Path>) -> Result<Cartridge, CartridgeError> {
let path = path.as_ref();
if !path.exists() {
return Err(CartridgeError::NotFound);
}
if path.is_dir() {
DirectoryCartridgeLoader::load(path)
} else if path.extension().is_some_and(|ext| ext == "pmc") {
PackedCartridgeLoader::load(path)
} else {
Err(CartridgeError::InvalidFormat)
}
}
}
pub struct DirectoryCartridgeLoader;
impl DirectoryCartridgeLoader {
pub fn load(path: &Path) -> Result<Cartridge, CartridgeError> {
let manifest_path = path.join("manifest.json");
if !manifest_path.exists() {
return Err(CartridgeError::InvalidManifest);
}
let manifest_content =
fs::read_to_string(manifest_path).map_err(|_| CartridgeError::IoError)?;
let manifest: CartridgeManifest =
serde_json::from_str(&manifest_content).map_err(|_| CartridgeError::InvalidManifest)?;
// Additional validation as per requirements
if manifest.magic != "PMTU" {
return Err(CartridgeError::InvalidManifest);
}
if manifest.cartridge_version != 1 {
return Err(CartridgeError::UnsupportedVersion);
}
let capabilities = normalize_capabilities(&manifest.capabilities)?;
let program_path = path.join("program.pbx");
if !program_path.exists() {
return Err(CartridgeError::MissingProgram);
}
let program = fs::read(program_path).map_err(|_| CartridgeError::IoError)?;
let assets_pa_path = path.join("assets.pa");
let (assets, asset_table, preload) = if assets_pa_path.exists() {
let parsed = parse_assets_pack(&assets_pa_path)?;
(
AssetsPayloadSource::from_file(
assets_pa_path.clone(),
parsed.payload_offset,
parsed.payload_len,
),
parsed.header.asset_table,
parsed.header.preload,
)
} else {
if capabilities & caps::ASSET != 0 {
return Err(CartridgeError::MissingAssets);
}
(AssetsPayloadSource::empty(), Vec::new(), Vec::new())
};
let dto = CartridgeDTO {
app_id: manifest.app_id,
title: manifest.title,
app_version: manifest.app_version,
app_mode: manifest.app_mode,
capabilities,
program,
assets,
asset_table,
preload,
};
Ok(Cartridge::from(dto))
}
}
pub struct PackedCartridgeLoader;
impl PackedCartridgeLoader {
pub fn load(_path: &Path) -> Result<Cartridge, CartridgeError> {
Err(CartridgeError::InvalidFormat)
}
}
fn normalize_capabilities(capabilities: &[Capability]) -> Result<CapFlags, CartridgeError> {
let mut seen = HashSet::new();
let mut normalized = caps::NONE;
for capability in capabilities {
if !seen.insert(*capability) {
return Err(CartridgeError::InvalidManifest);
}
normalized |= match capability {
Capability::None => caps::NONE,
Capability::System => caps::SYSTEM,
Capability::Gfx => caps::GFX,
Capability::Audio => caps::AUDIO,
Capability::Fs => caps::FS,
Capability::Log => caps::LOG,
Capability::Asset => caps::ASSET,
Capability::Bank => caps::BANK,
Capability::All => caps::ALL,
};
}
Ok(normalized)
}
struct ParsedAssetsPack {
header: AssetsPackHeader,
payload_offset: u64,
payload_len: u64,
}
fn parse_assets_pack(path: &Path) -> Result<ParsedAssetsPack, CartridgeError> {
let mut file = fs::File::open(path).map_err(|_| CartridgeError::IoError)?;
let mut prelude_bytes = [0_u8; ASSETS_PA_PRELUDE_SIZE];
file.read_exact(&mut prelude_bytes).map_err(|_| CartridgeError::InvalidFormat)?;
let prelude =
AssetsPackPrelude::from_bytes(&prelude_bytes).ok_or(CartridgeError::InvalidFormat)?;
if prelude.magic != ASSETS_PA_MAGIC || prelude.schema_version != ASSETS_PA_SCHEMA_VERSION {
return Err(CartridgeError::InvalidFormat);
}
let header_start = ASSETS_PA_PRELUDE_SIZE;
let header_len =
usize::try_from(prelude.header_len).map_err(|_| CartridgeError::InvalidFormat)?;
let header_end = header_start.checked_add(header_len).ok_or(CartridgeError::InvalidFormat)?;
let payload_offset =
usize::try_from(prelude.payload_offset).map_err(|_| CartridgeError::InvalidFormat)?;
let file_len = usize::try_from(file.metadata().map_err(|_| CartridgeError::IoError)?.len())
.map_err(|_| CartridgeError::InvalidFormat)?;
if payload_offset < header_start || header_end > file_len || payload_offset > file_len {
return Err(CartridgeError::InvalidFormat);
}
if header_end != payload_offset {
return Err(CartridgeError::InvalidFormat);
}
file.seek(SeekFrom::Start(header_start as u64)).map_err(|_| CartridgeError::IoError)?;
let mut header_bytes = vec![0_u8; header_len];
file.read_exact(&mut header_bytes).map_err(|_| CartridgeError::InvalidFormat)?;
let header: AssetsPackHeader =
serde_json::from_slice(&header_bytes).map_err(|_| CartridgeError::InvalidFormat)?;
validate_preload(&header.asset_table, &header.preload)?;
let payload_len =
u64::try_from(file_len - payload_offset).map_err(|_| CartridgeError::InvalidFormat)?;
Ok(ParsedAssetsPack { header, payload_offset: prelude.payload_offset, payload_len })
}
fn validate_preload(
asset_table: &[AssetEntry],
preload: &[crate::asset::PreloadEntry],
) -> Result<(), CartridgeError> {
let mut claimed_slots = HashSet::<(BankType, usize)>::new();
for item in preload {
let entry = asset_table
.iter()
.find(|entry| entry.asset_id == item.asset_id)
.ok_or(CartridgeError::InvalidFormat)?;
if !claimed_slots.insert((entry.bank_type, item.slot)) {
return Err(CartridgeError::InvalidFormat);
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::asset::{AssetCodec, AssetEntry, BankType, PreloadEntry};
use crate::cartridge::{ASSETS_PA_MAGIC, ASSETS_PA_SCHEMA_VERSION, AssetsPackPrelude};
use crate::glyph_bank::GLYPH_BANK_PALETTE_COUNT_V1;
use serde_json::json;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{SystemTime, UNIX_EPOCH};
static TEST_DIR_COUNTER: AtomicU64 = AtomicU64::new(0);
struct TestCartridgeDir {
path: PathBuf,
}
impl TestCartridgeDir {
fn new(manifest: serde_json::Value) -> Self {
let unique = TEST_DIR_COUNTER.fetch_add(1, Ordering::Relaxed);
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("system time must be after unix epoch")
.as_nanos();
let path = std::env::temp_dir()
.join(format!("prometeu-hal-cartridge-loader-{}-{}", timestamp, unique));
fs::create_dir_all(&path).expect("must create temporary cartridge directory");
fs::write(
path.join("manifest.json"),
serde_json::to_vec_pretty(&manifest).expect("manifest must serialize"),
)
.expect("must write manifest.json");
fs::write(path.join("program.pbx"), [0x01_u8, 0x02, 0x03]).expect("must write program");
Self { path }
}
fn path(&self) -> &Path {
&self.path
}
fn write_assets_pa(
&self,
asset_table: Vec<AssetEntry>,
preload: Vec<PreloadEntry>,
payload: &[u8],
) {
let header = serde_json::to_vec(&AssetsPackHeader { asset_table, preload })
.expect("assets header must serialize");
let payload_offset = (ASSETS_PA_PRELUDE_SIZE + header.len()) as u64;
let prelude = AssetsPackPrelude {
magic: ASSETS_PA_MAGIC,
schema_version: ASSETS_PA_SCHEMA_VERSION,
header_len: header.len() as u32,
payload_offset,
flags: 0,
reserved: 0,
header_checksum: 0,
};
let mut bytes = prelude.to_bytes().to_vec();
bytes.extend_from_slice(&header);
bytes.extend_from_slice(payload);
fs::write(self.path.join("assets.pa"), bytes).expect("must write assets.pa");
}
}
impl Drop for TestCartridgeDir {
fn drop(&mut self) {
let _ = fs::remove_dir_all(&self.path);
}
}
fn manifest_with_capabilities(capabilities: Option<Vec<&str>>) -> serde_json::Value {
let mut manifest = json!({
"magic": "PMTU",
"cartridge_version": 1,
"app_id": 1001,
"title": "Example",
"app_version": "1.0.0",
"app_mode": "Game"
});
if let Some(capabilities) = capabilities {
manifest["capabilities"] = json!(capabilities);
}
manifest
}
#[test]
fn load_without_capabilities_defaults_to_none() {
let dir = TestCartridgeDir::new(manifest_with_capabilities(None));
let cartridge = DirectoryCartridgeLoader::load(dir.path()).expect("cartridge must load");
assert_eq!(cartridge.capabilities, caps::NONE);
}
#[test]
fn load_with_single_capability_normalizes_to_flag() {
let dir = TestCartridgeDir::new(manifest_with_capabilities(Some(vec!["gfx"])));
let cartridge = DirectoryCartridgeLoader::load(dir.path()).expect("cartridge must load");
assert_eq!(cartridge.capabilities, caps::GFX);
}
#[test]
fn load_with_multiple_capabilities_combines_flags() {
let dir = TestCartridgeDir::new(manifest_with_capabilities(Some(vec!["gfx", "audio"])));
let cartridge = DirectoryCartridgeLoader::load(dir.path()).expect("cartridge must load");
assert_eq!(cartridge.capabilities, caps::GFX | caps::AUDIO);
}
#[test]
fn load_with_all_capability_normalizes_to_all_flags() {
let dir = TestCartridgeDir::new(manifest_with_capabilities(Some(vec!["all"])));
dir.write_assets_pa(vec![], vec![], &[]);
let cartridge = DirectoryCartridgeLoader::load(dir.path()).expect("cartridge must load");
assert_eq!(cartridge.capabilities, caps::ALL);
}
#[test]
fn load_with_none_capability_keeps_zero_flags() {
let dir = TestCartridgeDir::new(manifest_with_capabilities(Some(vec!["none"])));
let cartridge = DirectoryCartridgeLoader::load(dir.path()).expect("cartridge must load");
assert_eq!(cartridge.capabilities, caps::NONE);
}
#[test]
fn load_with_duplicate_capabilities_fails() {
let dir = TestCartridgeDir::new(manifest_with_capabilities(Some(vec!["gfx", "gfx"])));
let error = DirectoryCartridgeLoader::load(dir.path()).unwrap_err();
assert!(matches!(error, CartridgeError::InvalidManifest));
}
#[test]
fn load_with_unknown_capability_fails() {
let dir = TestCartridgeDir::new(manifest_with_capabilities(Some(vec!["network"])));
let error = DirectoryCartridgeLoader::load(dir.path()).unwrap_err();
assert!(matches!(error, CartridgeError::InvalidManifest));
}
#[test]
fn load_with_legacy_input_capability_fails() {
let dir = TestCartridgeDir::new(manifest_with_capabilities(Some(vec!["input"])));
let error = DirectoryCartridgeLoader::load(dir.path()).unwrap_err();
assert!(matches!(error, CartridgeError::InvalidManifest));
}
fn test_asset_entry(offset: u64, size: u64) -> AssetEntry {
AssetEntry {
asset_id: 7,
asset_name: "tiles".to_string(),
bank_type: BankType::GLYPH,
offset,
size,
decoded_size: 16 * 16 + (GLYPH_BANK_PALETTE_COUNT_V1 as u64 * 16 * 2),
codec: AssetCodec::None,
metadata: json!({
"tile_size": 16,
"width": 16,
"height": 16,
"palette_count": GLYPH_BANK_PALETTE_COUNT_V1,
"palette_authored": GLYPH_BANK_PALETTE_COUNT_V1
}),
}
}
#[test]
fn load_with_asset_capability_requires_assets_pa() {
let dir = TestCartridgeDir::new(manifest_with_capabilities(Some(vec!["asset"])));
let error = DirectoryCartridgeLoader::load(dir.path()).unwrap_err();
assert!(matches!(error, CartridgeError::MissingAssets));
}
#[test]
fn load_without_asset_capability_accepts_missing_assets_pa() {
let dir = TestCartridgeDir::new(manifest_with_capabilities(Some(vec!["gfx"])));
let cartridge = DirectoryCartridgeLoader::load(dir.path()).expect("cartridge must load");
assert!(cartridge.assets.is_empty());
assert!(cartridge.asset_table.is_empty());
assert!(cartridge.preload.is_empty());
}
#[test]
fn load_reads_asset_table_and_preload_from_assets_pa_header() {
let dir = TestCartridgeDir::new(manifest_with_capabilities(Some(vec!["asset"])));
let payload = vec![1_u8, 2, 3, 4];
dir.write_assets_pa(
vec![test_asset_entry(0, payload.len() as u64)],
vec![PreloadEntry { asset_id: 7, slot: 2 }],
&payload,
);
let cartridge = DirectoryCartridgeLoader::load(dir.path()).expect("cartridge must load");
let slice = cartridge
.assets
.open_slice(0, payload.len() as u64)
.expect("payload slice must open")
.read_all()
.expect("payload slice must read");
assert_eq!(slice, payload);
assert_eq!(cartridge.asset_table.len(), 1);
assert_eq!(cartridge.asset_table[0].asset_name, "tiles");
assert_eq!(cartridge.preload.len(), 1);
assert_eq!(cartridge.preload[0].asset_id, 7);
assert_eq!(cartridge.preload[0].slot, 2);
}
#[test]
fn load_rejects_preload_with_missing_asset_id() {
let dir = TestCartridgeDir::new(manifest_with_capabilities(Some(vec!["asset"])));
dir.write_assets_pa(
vec![test_asset_entry(0, 4)],
vec![PreloadEntry { asset_id: 999, slot: 2 }],
&[1_u8, 2, 3, 4],
);
let error = DirectoryCartridgeLoader::load(dir.path()).unwrap_err();
assert!(matches!(error, CartridgeError::InvalidFormat));
}
#[test]
fn load_rejects_preload_slot_clash_per_bank_type() {
let dir = TestCartridgeDir::new(manifest_with_capabilities(Some(vec!["asset"])));
let asset_table = vec![
test_asset_entry(0, 4),
AssetEntry {
asset_id: 8,
asset_name: "other_tiles".to_string(),
bank_type: BankType::GLYPH,
offset: 4,
size: 4,
decoded_size: 16 * 16 + (GLYPH_BANK_PALETTE_COUNT_V1 as u64 * 16 * 2),
codec: AssetCodec::None,
metadata: json!({
"tile_size": 16,
"width": 16,
"height": 16,
"palette_count": GLYPH_BANK_PALETTE_COUNT_V1,
"palette_authored": GLYPH_BANK_PALETTE_COUNT_V1
}),
},
];
let preload =
vec![PreloadEntry { asset_id: 7, slot: 2 }, PreloadEntry { asset_id: 8, slot: 2 }];
dir.write_assets_pa(asset_table, preload, &[1_u8, 2, 3, 4, 5, 6, 7, 8]);
let error = DirectoryCartridgeLoader::load(dir.path()).unwrap_err();
assert!(matches!(error, CartridgeError::InvalidFormat));
}
#[test]
fn load_rejects_invalid_assets_pa_prelude() {
let dir = TestCartridgeDir::new(manifest_with_capabilities(Some(vec!["asset"])));
fs::write(dir.path().join("assets.pa"), b"not-a-valid-pack").expect("must write assets.pa");
let error = DirectoryCartridgeLoader::load(dir.path()).unwrap_err();
assert!(matches!(error, CartridgeError::InvalidFormat));
}
#[test]
fn load_rejects_invalid_assets_pa_header_json() {
let dir = TestCartridgeDir::new(manifest_with_capabilities(Some(vec!["asset"])));
let prelude = AssetsPackPrelude {
magic: ASSETS_PA_MAGIC,
schema_version: ASSETS_PA_SCHEMA_VERSION,
header_len: 4,
payload_offset: (ASSETS_PA_PRELUDE_SIZE + 4) as u64,
flags: 0,
reserved: 0,
header_checksum: 0,
};
let mut bytes = prelude.to_bytes().to_vec();
bytes.extend_from_slice(b"{no}");
fs::write(dir.path().join("assets.pa"), bytes).expect("must write assets.pa");
let error = DirectoryCartridgeLoader::load(dir.path()).unwrap_err();
assert!(matches!(error, CartridgeError::InvalidFormat));
}
#[test]
fn load_rejects_unknown_codec_string_in_assets_pa_header() {
let dir = TestCartridgeDir::new(manifest_with_capabilities(Some(vec!["asset"])));
let header = serde_json::json!({
"asset_table": [{
"asset_id": 7,
"asset_name": "tiles",
"bank_type": "GLYPH",
"offset": 0,
"size": 4,
"decoded_size": 768,
"codec": "LZ4",
"metadata": {
"tile_size": 16,
"width": 16,
"height": 16,
"palette_count": GLYPH_BANK_PALETTE_COUNT_V1,
"palette_authored": GLYPH_BANK_PALETTE_COUNT_V1
}
}],
"preload": []
});
let header_bytes = serde_json::to_vec(&header).expect("header must serialize");
let prelude = AssetsPackPrelude {
magic: ASSETS_PA_MAGIC,
schema_version: ASSETS_PA_SCHEMA_VERSION,
header_len: header_bytes.len() as u32,
payload_offset: (ASSETS_PA_PRELUDE_SIZE + header_bytes.len()) as u64,
flags: 0,
reserved: 0,
header_checksum: 0,
};
let mut bytes = prelude.to_bytes().to_vec();
bytes.extend_from_slice(&header_bytes);
bytes.extend_from_slice(&[1_u8, 2, 3, 4]);
fs::write(dir.path().join("assets.pa"), bytes).expect("must write assets.pa");
let error = DirectoryCartridgeLoader::load(dir.path()).unwrap_err();
assert!(matches!(error, CartridgeError::InvalidFormat));
}
#[test]
fn load_rejects_legacy_raw_codec_string_in_assets_pa_header() {
let dir = TestCartridgeDir::new(manifest_with_capabilities(Some(vec!["asset"])));
let header = serde_json::json!({
"asset_table": [{
"asset_id": 7,
"asset_name": "tiles",
"bank_type": "GLYPH",
"offset": 0,
"size": 4,
"decoded_size": 768,
"codec": "RAW",
"metadata": {
"tile_size": 16,
"width": 16,
"height": 16,
"palette_count": GLYPH_BANK_PALETTE_COUNT_V1,
"palette_authored": GLYPH_BANK_PALETTE_COUNT_V1
}
}],
"preload": []
});
let header_bytes = serde_json::to_vec(&header).expect("header must serialize");
let prelude = AssetsPackPrelude {
magic: ASSETS_PA_MAGIC,
schema_version: ASSETS_PA_SCHEMA_VERSION,
header_len: header_bytes.len() as u32,
payload_offset: (ASSETS_PA_PRELUDE_SIZE + header_bytes.len()) as u64,
flags: 0,
reserved: 0,
header_checksum: 0,
};
let mut bytes = prelude.to_bytes().to_vec();
bytes.extend_from_slice(&header_bytes);
bytes.extend_from_slice(&[1_u8, 2, 3, 4]);
fs::write(dir.path().join("assets.pa"), bytes).expect("must write assets.pa");
let error = DirectoryCartridgeLoader::load(dir.path()).unwrap_err();
assert!(matches!(error, CartridgeError::InvalidFormat));
}
}

View File

@ -1,10 +0,0 @@
#[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

@ -1,80 +0,0 @@
use crate::color::Color;
use crate::scene_viewport_cache::SceneViewportCache;
use crate::scene_viewport_resolver::ResolverUpdate;
use crate::sprite::Sprite;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BlendMode {
None,
Half,
HalfPlus,
HalfMinus,
Full,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(i32)]
pub enum GfxOpStatus {
Ok = 0,
AssetNotFound = 1,
InvalidSpriteIndex = 2,
ArgRangeInvalid = 3,
BankInvalid = 4,
}
pub trait GfxBridge {
fn size(&self) -> (usize, usize);
fn front_buffer(&self) -> &[u16];
fn clear(&mut self, color: Color);
fn fill_rect_blend(&mut self, x: i32, y: i32, w: i32, h: i32, color: Color, mode: BlendMode);
fn fill_rect(&mut self, x: i32, y: i32, w: i32, h: i32, color: Color);
fn draw_pixel(&mut self, x: i32, y: i32, color: Color);
fn draw_line(&mut self, x0: i32, y0: i32, x1: i32, y1: i32, color: Color);
fn draw_circle(&mut self, xc: i32, yc: i32, r: i32, color: Color);
fn draw_circle_points(&mut self, xc: i32, yc: i32, x: i32, y: i32, color: Color);
fn fill_circle(&mut self, xc: i32, yc: i32, r: i32, color: Color);
fn draw_circle_lines(&mut self, xc: i32, yc: i32, x: i32, y: i32, color: Color);
fn draw_disc(&mut self, x: i32, y: i32, r: i32, border_color: Color, fill_color: Color);
fn draw_rect(&mut self, x: i32, y: i32, w: i32, h: i32, color: Color);
fn draw_square(
&mut self,
x: i32,
y: i32,
w: i32,
h: i32,
border_color: Color,
fill_color: Color,
);
fn draw_horizontal_line(&mut self, x0: i32, x1: i32, y: i32, color: Color);
fn draw_vertical_line(&mut self, x: i32, y0: i32, y1: i32, color: Color);
fn present(&mut self);
/// Render the canonical game frame with no bound scene.
///
/// Deferred `gfx.*` overlay/debug primitives are intentionally outside this
/// contract and are drained by a separate final overlay stage.
fn render_no_scene_frame(&mut self);
/// Render the canonical scene-backed game frame from cache/resolver state.
///
/// Deferred `gfx.*` overlay/debug primitives are intentionally outside this
/// contract and are drained by a separate final overlay stage.
fn render_scene_from_cache(&mut self, cache: &SceneViewportCache, update: &ResolverUpdate);
fn load_frame_sprites(&mut self, sprites: &[Sprite]);
/// Submit text into the `gfx.*` primitive path.
///
/// Under the accepted runtime contract this is not the canonical game
/// composition path; it belongs to the deferred final overlay/debug family.
fn draw_text(&mut self, x: i32, y: i32, text: &str, color: Color);
fn draw_char(&mut self, x: i32, y: i32, c: char, color: Color);
fn sprite(&self, index: usize) -> &Sprite;
fn sprite_mut(&mut self, index: usize) -> &mut Sprite;
fn scene_fade_level(&self) -> u8;
fn set_scene_fade_level(&mut self, level: u8);
fn scene_fade_color(&self) -> Color;
fn set_scene_fade_color(&mut self, color: Color);
fn hud_fade_level(&self) -> u8;
fn set_hud_fade_level(&mut self, level: u8);
fn hud_fade_color(&self) -> Color;
fn set_hud_fade_color(&mut self, color: Color);
}

View File

@ -1,7 +0,0 @@
use serde::{Deserialize, Serialize};
#[derive(Clone, Copy, Debug, Default, Deserialize, Serialize)]
pub struct Glyph {
pub glyph_id: u16,
pub palette_id: u8,
}

View File

@ -1,31 +0,0 @@
use crate::asset_bridge::AssetBridge;
use crate::audio_bridge::AudioBridge;
use crate::gfx_bridge::GfxBridge;
use crate::pad_bridge::PadBridge;
use crate::sprite::Sprite;
use crate::touch_bridge::TouchBridge;
pub trait HardwareBridge {
fn begin_frame(&mut self);
fn bind_scene(&mut self, scene_bank_id: usize) -> bool;
fn unbind_scene(&mut self);
fn set_camera(&mut self, x: i32, y: i32);
fn emit_sprite(&mut self, sprite: Sprite) -> bool;
fn render_frame(&mut self);
fn has_glyph_bank(&self, bank_id: usize) -> bool;
fn gfx(&self) -> &dyn GfxBridge;
fn gfx_mut(&mut self) -> &mut dyn GfxBridge;
fn audio(&self) -> &dyn AudioBridge;
fn audio_mut(&mut self) -> &mut dyn AudioBridge;
fn pad(&self) -> &dyn PadBridge;
fn pad_mut(&mut self) -> &mut dyn PadBridge;
fn touch(&self) -> &dyn TouchBridge;
fn touch_mut(&mut self) -> &mut dyn TouchBridge;
fn assets(&self) -> &dyn AssetBridge;
fn assets_mut(&mut self) -> &mut dyn AssetBridge;
}

View File

@ -1,24 +0,0 @@
use crate::hardware_bridge::HardwareBridge;
use crate::vm_fault::VmFault;
pub struct HostContext<'a> {
pub hw: Option<&'a mut dyn HardwareBridge>,
}
impl<'a> HostContext<'a> {
pub fn new(hw: Option<&'a mut dyn HardwareBridge>) -> Self {
Self { hw }
}
#[inline]
pub fn require_hw(&mut self) -> Result<&mut dyn HardwareBridge, VmFault> {
match &mut self.hw {
Some(hw) => Ok(*hw),
None => Err(VmFault::Unavailable),
}
}
}
pub trait HostContextProvider {
fn make_ctx(&'_ mut self) -> HostContext<'_>;
}

View File

@ -1,27 +0,0 @@
use prometeu_bytecode::{HeapRef, Value};
pub struct HostReturn<'a> {
stack: &'a mut Vec<Value>,
}
impl<'a> HostReturn<'a> {
pub fn new(stack: &'a mut Vec<Value>) -> Self {
Self { stack }
}
pub fn push_bool(&mut self, v: bool) {
self.stack.push(Value::Boolean(v));
}
pub fn push_int(&mut self, v: i64) {
self.stack.push(Value::Int64(v));
}
pub fn push_null(&mut self) {
self.stack.push(Value::Null);
}
pub fn push_gate(&mut self, g: usize) {
// Temporary: cast incoming handle/HeapRef index. Real allocator will provide proper handles.
self.stack.push(Value::HeapRef(HeapRef(g as u32)));
}
pub fn push_string(&mut self, s: String) {
self.stack.push(Value::String(s));
}
}

View File

@ -1,47 +0,0 @@
pub mod asset;
pub mod asset_bridge;
pub mod audio_bridge;
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;
pub mod glyph_bank;
pub mod hardware_bridge;
pub mod host_context;
pub mod host_return;
pub mod input_signals;
pub mod log;
pub mod native_helpers;
pub mod native_interface;
pub mod pad_bridge;
pub mod sample;
pub mod scene_bank;
pub mod scene_layer;
pub mod scene_viewport_cache;
pub mod scene_viewport_resolver;
pub mod sound_bank;
pub mod sprite;
pub mod syscalls;
pub mod telemetry;
pub mod tile;
pub mod tilemap;
pub mod touch_bridge;
pub mod vm_fault;
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};
pub use host_return::HostReturn;
pub use input_signals::InputSignals;
pub use native_helpers::{expect_bool, expect_int};
pub use native_interface::{NativeInterface, SyscallId};
pub use pad_bridge::PadBridge;
pub use touch_bridge::TouchBridge;

View File

@ -1,17 +0,0 @@
use crate::vm_fault::VmFault;
use prometeu_bytecode::{TRAP_TYPE, Value};
pub fn expect_int(args: &[Value], idx: usize) -> Result<i64, VmFault> {
args.get(idx)
.and_then(|v| v.as_integer())
.ok_or_else(|| VmFault::Trap(TRAP_TYPE, format!("Expected integer at index {}", idx)))
}
pub fn expect_bool(args: &[Value], idx: usize) -> Result<bool, VmFault> {
args.get(idx)
.and_then(|v| match v {
Value::Boolean(b) => Some(*b),
_ => None,
})
.ok_or_else(|| VmFault::Trap(TRAP_TYPE, format!("Expected boolean at index {}", idx)))
}

View File

@ -1,21 +0,0 @@
use crate::host_context::HostContext;
use crate::host_return::HostReturn;
use crate::vm_fault::VmFault;
use prometeu_bytecode::Value;
pub type SyscallId = u32;
pub trait NativeInterface {
/// Dispatches a syscall from the Virtual Machine to the native implementation.
///
/// ABI Rule: Arguments for the syscall are passed in `args`.
///
/// Returns are written via `ret`.
fn syscall(
&mut self,
id: SyscallId,
args: &[Value],
ret: &mut HostReturn,
ctx: &mut HostContext,
) -> Result<(), VmFault>;
}

View File

@ -1,20 +0,0 @@
use crate::button::Button;
use crate::input_signals::InputSignals;
pub trait PadBridge {
fn begin_frame(&mut self, signals: &InputSignals);
fn any(&self) -> bool;
fn up(&self) -> &Button;
fn down(&self) -> &Button;
fn left(&self) -> &Button;
fn right(&self) -> &Button;
fn a(&self) -> &Button;
fn b(&self) -> &Button;
fn x(&self) -> &Button;
fn y(&self) -> &Button;
fn l(&self) -> &Button;
fn r(&self) -> &Button;
fn start(&self) -> &Button;
fn select(&self) -> &Button;
}

View File

@ -1,50 +0,0 @@
use crate::scene_layer::SceneLayer;
#[derive(Clone, Debug)]
pub struct SceneBank {
pub layers: [SceneLayer; 4],
}
#[cfg(test)]
mod tests {
use super::*;
use crate::glyph::Glyph;
use crate::glyph_bank::TileSize;
use crate::scene_layer::ParallaxFactor;
use crate::tile::Tile;
use crate::tilemap::TileMap;
fn layer(glyph_bank_id: u8, parallax_x: f32, parallax_y: f32, glyph_id: u16) -> SceneLayer {
SceneLayer {
active: true,
glyph_bank_id,
tile_size: TileSize::Size16,
parallax_factor: ParallaxFactor { x: parallax_x, y: parallax_y },
tilemap: TileMap {
width: 1,
height: 1,
tiles: vec![Tile {
active: true,
glyph: Glyph { glyph_id, palette_id: glyph_bank_id },
flip_x: false,
flip_y: false,
}],
},
}
}
#[test]
fn scene_bank_owns_exactly_four_layers() {
let scene = SceneBank {
layers: [
layer(0, 1.0, 1.0, 10),
layer(1, 0.5, 1.0, 11),
layer(2, 1.0, 0.5, 12),
layer(3, 0.25, 0.25, 13),
],
};
assert_eq!(scene.layers.len(), 4);
assert_eq!(scene.layers[3].tilemap.tiles[0].glyph.glyph_id, 13);
}
}

View File

@ -1,59 +0,0 @@
use crate::glyph_bank::TileSize;
use crate::tilemap::TileMap;
#[derive(Clone, Copy, Debug)]
pub struct ParallaxFactor {
pub x: f32,
pub y: f32,
}
#[derive(Clone, Debug)]
pub struct SceneLayer {
pub active: bool,
pub glyph_bank_id: u8,
pub tile_size: TileSize,
pub parallax_factor: ParallaxFactor,
pub tilemap: TileMap,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::glyph::Glyph;
use crate::tile::Tile;
#[test]
fn scene_layer_preserves_parallax_factor_and_tilemap_ownership() {
let layer = SceneLayer {
active: true,
glyph_bank_id: 7,
tile_size: TileSize::Size16,
parallax_factor: ParallaxFactor { x: 0.5, y: 0.75 },
tilemap: TileMap {
width: 2,
height: 1,
tiles: vec![
Tile {
active: true,
glyph: Glyph { glyph_id: 21, palette_id: 3 },
flip_x: false,
flip_y: false,
},
Tile {
active: false,
glyph: Glyph { glyph_id: 22, palette_id: 4 },
flip_x: true,
flip_y: false,
},
],
},
};
assert_eq!(layer.glyph_bank_id, 7);
assert_eq!(layer.parallax_factor.x, 0.5);
assert_eq!(layer.parallax_factor.y, 0.75);
assert_eq!(layer.tilemap.width, 2);
assert_eq!(layer.tilemap.tiles[1].glyph.glyph_id, 22);
assert!(layer.tilemap.tiles[1].flip_x);
}
}

View File

@ -1,565 +0,0 @@
use crate::glyph_bank::TileSize;
use crate::scene_bank::SceneBank;
use crate::scene_layer::SceneLayer;
use crate::tile::Tile;
const FLAG_FLIP_X: u8 = 0b0000_0001;
const FLAG_FLIP_Y: u8 = 0b0000_0010;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct CachedTileEntry {
pub active: bool,
pub glyph_id: u16,
pub palette_id: u8,
pub flags: u8,
pub glyph_bank_id: u8,
}
impl CachedTileEntry {
pub fn flip_x(self) -> bool {
(self.flags & FLAG_FLIP_X) != 0
}
pub fn flip_y(self) -> bool {
(self.flags & FLAG_FLIP_Y) != 0
}
fn from_tile(layer: &SceneLayer, tile: Tile) -> Self {
let mut flags = 0_u8;
if tile.flip_x {
flags |= FLAG_FLIP_X;
}
if tile.flip_y {
flags |= FLAG_FLIP_Y;
}
Self {
active: tile.active,
glyph_id: tile.glyph.glyph_id,
palette_id: tile.glyph.palette_id,
flags,
glyph_bank_id: layer.glyph_bank_id,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct ViewportRegion {
pub x: usize,
pub y: usize,
pub width: usize,
pub height: usize,
}
impl ViewportRegion {
pub const fn new(x: usize, y: usize, width: usize, height: usize) -> Self {
Self { x, y, width, height }
}
}
#[derive(Clone, Debug)]
pub struct SceneViewportLayerCache {
width: usize,
height: usize,
logical_origin_x: i32,
logical_origin_y: i32,
ring_origin_x: usize,
ring_origin_y: usize,
pub glyph_bank_id: u8,
pub tile_size: TileSize,
entries: Vec<CachedTileEntry>,
pub valid: bool,
}
impl SceneViewportLayerCache {
pub fn new(layer: &SceneLayer, width: usize, height: usize) -> Self {
Self {
width,
height,
logical_origin_x: 0,
logical_origin_y: 0,
ring_origin_x: 0,
ring_origin_y: 0,
glyph_bank_id: layer.glyph_bank_id,
tile_size: layer.tile_size,
entries: vec![CachedTileEntry::default(); width * height],
valid: false,
}
}
pub fn width(&self) -> usize {
self.width
}
pub fn height(&self) -> usize {
self.height
}
pub fn logical_origin(&self) -> (i32, i32) {
(self.logical_origin_x, self.logical_origin_y)
}
pub fn ring_origin(&self) -> (usize, usize) {
(self.ring_origin_x, self.ring_origin_y)
}
pub fn entry(&self, cache_x: usize, cache_y: usize) -> CachedTileEntry {
self.entries[self.physical_index(cache_x, cache_y)]
}
pub fn invalidate_all(&mut self) {
self.entries.fill(CachedTileEntry::default());
self.valid = false;
}
pub fn move_window_to(&mut self, origin_x: i32, origin_y: i32) {
let delta_x = origin_x - self.logical_origin_x;
let delta_y = origin_y - self.logical_origin_y;
self.logical_origin_x = origin_x;
self.logical_origin_y = origin_y;
self.ring_origin_x = Self::wrapped_origin(self.ring_origin_x, delta_x, self.width);
self.ring_origin_y = Self::wrapped_origin(self.ring_origin_y, delta_y, self.height);
}
pub fn move_window_by(&mut self, delta_x: i32, delta_y: i32) {
self.move_window_to(self.logical_origin_x + delta_x, self.logical_origin_y + delta_y);
}
pub fn refresh_line(&mut self, layer: &SceneLayer, cache_y: usize) {
self.refresh_region(layer, ViewportRegion::new(0, cache_y, self.width, 1));
}
pub fn refresh_column(&mut self, layer: &SceneLayer, cache_x: usize) {
self.refresh_region(layer, ViewportRegion::new(cache_x, 0, 1, self.height));
}
pub fn refresh_region(&mut self, layer: &SceneLayer, region: ViewportRegion) {
self.glyph_bank_id = layer.glyph_bank_id;
self.tile_size = layer.tile_size;
let max_x = region.x.saturating_add(region.width).min(self.width);
let max_y = region.y.saturating_add(region.height).min(self.height);
for cache_y in region.y..max_y {
for cache_x in region.x..max_x {
let entry = self.materialize_entry(layer, cache_x, cache_y);
let idx = self.physical_index(cache_x, cache_y);
self.entries[idx] = entry;
}
}
self.valid = true;
}
pub fn refresh_all(&mut self, layer: &SceneLayer) {
self.refresh_region(layer, ViewportRegion::new(0, 0, self.width, self.height));
}
fn materialize_entry(
&self,
layer: &SceneLayer,
cache_x: usize,
cache_y: usize,
) -> CachedTileEntry {
let scene_x = self.logical_origin_x + cache_x as i32;
let scene_y = self.logical_origin_y + cache_y as i32;
if scene_x < 0 || scene_y < 0 {
return CachedTileEntry::default();
}
let tile_x = scene_x as usize;
let tile_y = scene_y as usize;
if tile_x >= layer.tilemap.width || tile_y >= layer.tilemap.height {
return CachedTileEntry::default();
}
let tile = layer.tilemap.tiles[tile_y * layer.tilemap.width + tile_x];
CachedTileEntry::from_tile(layer, tile)
}
fn physical_index(&self, cache_x: usize, cache_y: usize) -> usize {
let physical_x = (self.ring_origin_x + cache_x) % self.width;
let physical_y = (self.ring_origin_y + cache_y) % self.height;
physical_y * self.width + physical_x
}
fn wrapped_origin(current: usize, delta: i32, span: usize) -> usize {
if span == 0 {
return 0;
}
let span_i32 = span as i32;
let current_i32 = current as i32;
(current_i32 + delta).rem_euclid(span_i32) as usize
}
}
#[derive(Clone, Debug)]
pub struct SceneViewportCache {
width: usize,
height: usize,
pub layers: [SceneViewportLayerCache; 4],
}
impl SceneViewportCache {
pub fn new(scene: &SceneBank, width: usize, height: usize) -> Self {
Self {
width,
height,
layers: std::array::from_fn(|i| {
SceneViewportLayerCache::new(&scene.layers[i], width, height)
}),
}
}
pub fn width(&self) -> usize {
self.width
}
pub fn height(&self) -> usize {
self.height
}
pub fn invalidate_all(&mut self) {
for layer in &mut self.layers {
layer.invalidate_all();
}
}
pub fn move_layer_window_to(&mut self, layer_idx: usize, origin_x: i32, origin_y: i32) {
self.layers[layer_idx].move_window_to(origin_x, origin_y);
}
pub fn move_layer_window_by(&mut self, layer_idx: usize, delta_x: i32, delta_y: i32) {
self.layers[layer_idx].move_window_by(delta_x, delta_y);
}
pub fn refresh_layer_line(&mut self, scene: &SceneBank, layer_idx: usize, cache_y: usize) {
self.layers[layer_idx].refresh_line(&scene.layers[layer_idx], cache_y);
}
pub fn refresh_layer_column(&mut self, scene: &SceneBank, layer_idx: usize, cache_x: usize) {
self.layers[layer_idx].refresh_column(&scene.layers[layer_idx], cache_x);
}
pub fn refresh_layer_region(
&mut self,
scene: &SceneBank,
layer_idx: usize,
region: ViewportRegion,
) {
self.layers[layer_idx].refresh_region(&scene.layers[layer_idx], region);
}
pub fn refresh_layer_all(&mut self, scene: &SceneBank, layer_idx: usize) {
self.layers[layer_idx].refresh_all(&scene.layers[layer_idx]);
}
pub fn materialize_all_layers(&mut self, scene: &SceneBank) {
for layer_idx in 0..self.layers.len() {
self.refresh_layer_all(scene, layer_idx);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::glyph::Glyph;
use crate::glyph_bank::TileSize;
use crate::scene_layer::ParallaxFactor;
use crate::tile::Tile;
use crate::tilemap::TileMap;
fn make_tile(glyph_id: u16, palette_id: u8, flip_x: bool, flip_y: bool) -> Tile {
Tile { active: true, glyph: Glyph { glyph_id, palette_id }, flip_x, flip_y }
}
fn make_layer(glyph_bank_id: u8, base_glyph: u16) -> SceneLayer {
let mut tiles = Vec::new();
for y in 0..4 {
for x in 0..4 {
tiles.push(make_tile(
base_glyph + (y * 4 + x) as u16,
glyph_bank_id,
x % 2 == 0,
y % 2 == 1,
));
}
}
SceneLayer {
active: true,
glyph_bank_id,
tile_size: TileSize::Size16,
parallax_factor: ParallaxFactor { x: 1.0, y: 1.0 },
tilemap: TileMap { width: 4, height: 4, tiles },
}
}
fn make_scene() -> SceneBank {
SceneBank {
layers: [
make_layer(1, 100),
make_layer(2, 200),
make_layer(3, 300),
make_layer(4, 400),
],
}
}
#[test]
fn layer_cache_wraps_ring_origin_under_window_movement() {
let scene = make_scene();
let mut cache = SceneViewportCache::new(&scene, 3, 3);
cache.move_layer_window_by(0, 1, 2);
assert_eq!(cache.layers[0].logical_origin(), (1, 2));
assert_eq!(cache.layers[0].ring_origin(), (1, 2));
cache.move_layer_window_by(0, 3, 2);
assert_eq!(cache.layers[0].logical_origin(), (4, 4));
assert_eq!(cache.layers[0].ring_origin(), (1, 1));
}
#[test]
fn layer_cache_wraps_ring_origin_for_negative_and_large_movements() {
let scene = make_scene();
let mut cache = SceneViewportCache::new(&scene, 3, 3);
cache.move_layer_window_by(0, -1, -4);
assert_eq!(cache.layers[0].logical_origin(), (-1, -4));
assert_eq!(cache.layers[0].ring_origin(), (2, 2));
cache.move_layer_window_by(0, 7, 8);
assert_eq!(cache.layers[0].logical_origin(), (6, 4));
assert_eq!(cache.layers[0].ring_origin(), (0, 1));
}
#[test]
fn move_window_to_matches_incremental_ring_movement() {
let scene = make_scene();
let mut direct = SceneViewportCache::new(&scene, 4, 4);
let mut incremental = SceneViewportCache::new(&scene, 4, 4);
direct.move_layer_window_to(0, 9, -6);
incremental.move_layer_window_by(0, 5, -2);
incremental.move_layer_window_by(0, 4, -4);
assert_eq!(direct.layers[0].logical_origin(), incremental.layers[0].logical_origin());
assert_eq!(direct.layers[0].ring_origin(), incremental.layers[0].ring_origin());
}
#[test]
fn cache_entry_fields_are_derived_from_scene_tiles() {
let scene = make_scene();
let mut cache = SceneViewportCache::new(&scene, 2, 2);
cache.refresh_layer_all(&scene, 0);
let entry = cache.layers[0].entry(1, 1);
assert!(entry.active);
assert_eq!(entry.glyph_id, 105);
assert_eq!(entry.palette_id, 1);
assert_eq!(entry.glyph_bank_id, 1);
assert!(!entry.flip_x());
assert!(entry.flip_y());
}
#[test]
fn line_refresh_only_updates_the_requested_line() {
let scene = make_scene();
let mut cache = SceneViewportCache::new(&scene, 3, 3);
cache.refresh_layer_line(&scene, 0, 1);
assert_eq!(cache.layers[0].entry(0, 0), CachedTileEntry::default());
assert_eq!(cache.layers[0].entry(0, 1).glyph_id, 104);
assert_eq!(cache.layers[0].entry(2, 1).glyph_id, 106);
assert_eq!(cache.layers[0].entry(0, 2), CachedTileEntry::default());
}
#[test]
fn column_refresh_only_updates_the_requested_column() {
let scene = make_scene();
let mut cache = SceneViewportCache::new(&scene, 3, 3);
cache.refresh_layer_column(&scene, 1, 2);
assert_eq!(cache.layers[1].entry(0, 0), CachedTileEntry::default());
assert_eq!(cache.layers[1].entry(2, 0).glyph_id, 202);
assert_eq!(cache.layers[1].entry(2, 2).glyph_id, 210);
assert_eq!(cache.layers[1].entry(1, 2), CachedTileEntry::default());
}
#[test]
fn region_refresh_only_updates_the_requested_area() {
let scene = make_scene();
let mut cache = SceneViewportCache::new(&scene, 3, 3);
cache.refresh_layer_region(&scene, 2, ViewportRegion::new(1, 1, 2, 2));
assert_eq!(cache.layers[2].entry(0, 0), CachedTileEntry::default());
assert_eq!(cache.layers[2].entry(1, 1).glyph_id, 305);
assert_eq!(cache.layers[2].entry(2, 2).glyph_id, 310);
assert_eq!(cache.layers[2].entry(0, 2), CachedTileEntry::default());
}
#[test]
fn scene_swap_invalidation_clears_all_layers() {
let scene = make_scene();
let mut cache = SceneViewportCache::new(&scene, 2, 2);
cache.materialize_all_layers(&scene);
cache.invalidate_all();
for layer in &cache.layers {
assert!(!layer.valid);
for y in 0..cache.height() {
for x in 0..cache.width() {
assert_eq!(layer.entry(x, y), CachedTileEntry::default());
}
}
}
}
#[test]
fn corner_style_region_update_does_not_touch_outside_tiles() {
let scene = make_scene();
let mut cache = SceneViewportCache::new(&scene, 4, 4);
cache.materialize_all_layers(&scene);
let before = cache.layers[3].entry(1, 1);
cache.layers[3].invalidate_all();
cache.refresh_layer_region(&scene, 3, ViewportRegion::new(2, 2, 2, 2));
assert_eq!(cache.layers[3].entry(0, 0), CachedTileEntry::default());
assert_eq!(cache.layers[3].entry(1, 1), CachedTileEntry::default());
assert_ne!(cache.layers[3].entry(2, 2), CachedTileEntry::default());
assert_eq!(before.glyph_id, 405);
assert_eq!(cache.layers[3].entry(3, 3).glyph_id, 415);
}
#[test]
fn refresh_after_wrapped_window_move_materializes_new_logical_tiles() {
let scene = make_scene();
let mut cache = SceneViewportCache::new(&scene, 3, 3);
cache.refresh_layer_all(&scene, 0);
cache.move_layer_window_to(0, 1, 2);
cache.refresh_layer_all(&scene, 0);
assert_eq!(cache.layers[0].logical_origin(), (1, 2));
assert_eq!(cache.layers[0].ring_origin(), (1, 2));
assert_eq!(cache.layers[0].entry(0, 0).glyph_id, 109);
assert_eq!(cache.layers[0].entry(1, 0).glyph_id, 110);
assert_eq!(cache.layers[0].entry(2, 0).glyph_id, 111);
assert_eq!(cache.layers[0].entry(0, 1).glyph_id, 113);
assert_eq!(cache.layers[0].entry(2, 1).glyph_id, 115);
assert_eq!(cache.layers[0].entry(0, 2), CachedTileEntry::default());
}
#[test]
fn partial_refresh_uses_wrapped_physical_slots_after_window_move() {
let scene = make_scene();
let mut cache = SceneViewportCache::new(&scene, 3, 3);
cache.move_layer_window_to(0, 1, 0);
cache.refresh_layer_column(&scene, 0, 0);
assert_eq!(cache.layers[0].ring_origin(), (1, 0));
assert_eq!(cache.layers[0].entry(0, 0).glyph_id, 101);
assert_eq!(cache.layers[0].entry(0, 1).glyph_id, 105);
assert_eq!(cache.layers[0].entry(0, 2).glyph_id, 109);
assert_eq!(cache.layers[0].entry(1, 0), CachedTileEntry::default());
assert_eq!(cache.layers[0].entry(2, 0), CachedTileEntry::default());
}
#[test]
fn out_of_bounds_logical_origins_materialize_default_entries_after_wrap() {
let scene = make_scene();
let mut cache = SceneViewportCache::new(&scene, 2, 2);
cache.move_layer_window_to(0, -2, 3);
cache.refresh_layer_all(&scene, 0);
for y in 0..2 {
for x in 0..2 {
assert_eq!(cache.layers[0].entry(x, y), CachedTileEntry::default());
}
}
}
#[test]
fn ringbuffer_preserves_logical_tile_mapping_across_long_mixed_movements() {
let scene = make_scene();
let mut cache = SceneViewportCache::new(&scene, 3, 3);
let motions = [
(1, 0),
(0, 1),
(2, 2),
(-1, 0),
(0, -2),
(4, 1),
(-3, 3),
(5, -4),
(-6, 2),
(3, -3),
(7, 7),
(-8, -5),
];
for &(dx, dy) in &motions {
cache.move_layer_window_by(0, dx, dy);
cache.refresh_layer_all(&scene, 0);
let (origin_x, origin_y) = cache.layers[0].logical_origin();
for cache_y in 0..cache.height() {
for cache_x in 0..cache.width() {
let expected_scene_x = origin_x + cache_x as i32;
let expected_scene_y = origin_y + cache_y as i32;
let expected = if expected_scene_x < 0
|| expected_scene_y < 0
|| expected_scene_x as usize >= scene.layers[0].tilemap.width
|| expected_scene_y as usize >= scene.layers[0].tilemap.height
{
CachedTileEntry::default()
} else {
let tile_x = expected_scene_x as usize;
let tile_y = expected_scene_y as usize;
let tile = scene.layers[0].tilemap.tiles
[tile_y * scene.layers[0].tilemap.width + tile_x];
CachedTileEntry::from_tile(&scene.layers[0], tile)
};
assert_eq!(
cache.layers[0].entry(cache_x, cache_y),
expected,
"mismatch at logical origin ({}, {}), cache ({}, {})",
origin_x,
origin_y,
cache_x,
cache_y
);
}
}
}
}
#[test]
fn materialization_populates_all_four_layers() {
let scene = make_scene();
let mut cache = SceneViewportCache::new(&scene, 2, 2);
cache.materialize_all_layers(&scene);
assert_eq!(cache.layers[0].entry(0, 0).glyph_id, 100);
assert_eq!(cache.layers[1].entry(0, 0).glyph_id, 200);
assert_eq!(cache.layers[2].entry(1, 1).glyph_id, 305);
assert_eq!(cache.layers[3].entry(1, 0).glyph_id, 401);
}
}

View File

@ -1,536 +0,0 @@
use crate::glyph_bank::TileSize;
use crate::scene_bank::SceneBank;
use crate::scene_viewport_cache::ViewportRegion;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct TileAnchor {
pub x: i32,
pub y: i32,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum CacheRefreshRequest {
InvalidateLayer { layer_index: usize },
RefreshLine { layer_index: usize, cache_y: usize },
RefreshColumn { layer_index: usize, cache_x: usize },
RefreshRegion { layer_index: usize, region: ViewportRegion },
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct LayerCopyRequest {
pub layer_index: usize,
pub tile_size: TileSize,
pub viewport_width_px: i32,
pub viewport_height_px: i32,
pub source_offset_x_px: i32,
pub source_offset_y_px: i32,
pub cache_origin_tile_x: i32,
pub cache_origin_tile_y: i32,
pub camera_x_px: i32,
pub camera_y_px: i32,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ResolverUpdate {
pub master_anchor: TileAnchor,
pub layer_anchors: [TileAnchor; 4],
pub refresh_requests: Vec<CacheRefreshRequest>,
pub copy_requests: [LayerCopyRequest; 4],
}
#[derive(Clone, Debug)]
pub struct SceneViewportResolver {
viewport_width_px: i32,
viewport_height_px: i32,
cache_width_tiles: usize,
cache_height_tiles: usize,
hysteresis_safe_px: i32,
hysteresis_trigger_px: i32,
initialized: bool,
master_anchor: TileAnchor,
layer_anchors: [TileAnchor; 4],
}
impl SceneViewportResolver {
pub fn new(
viewport_width_px: i32,
viewport_height_px: i32,
cache_width_tiles: usize,
cache_height_tiles: usize,
hysteresis_safe_px: i32,
hysteresis_trigger_px: i32,
) -> Self {
Self {
viewport_width_px,
viewport_height_px,
cache_width_tiles,
cache_height_tiles,
hysteresis_safe_px,
hysteresis_trigger_px,
initialized: false,
master_anchor: TileAnchor::default(),
layer_anchors: [TileAnchor::default(); 4],
}
}
pub fn reset_scene(&mut self) -> Vec<CacheRefreshRequest> {
self.initialized = false;
vec![
CacheRefreshRequest::InvalidateLayer { layer_index: 0 },
CacheRefreshRequest::InvalidateLayer { layer_index: 1 },
CacheRefreshRequest::InvalidateLayer { layer_index: 2 },
CacheRefreshRequest::InvalidateLayer { layer_index: 3 },
]
}
pub fn update(
&mut self,
scene: &SceneBank,
camera_x_px: i32,
camera_y_px: i32,
) -> ResolverUpdate {
let mut refresh_requests = Vec::new();
let camera_center_x = camera_x_px + self.viewport_width_px / 2;
let camera_center_y = camera_y_px + self.viewport_height_px / 2;
let layer_inputs: [(i32, i32, i32, i32, i32); 4] = std::array::from_fn(|i| {
let layer = &scene.layers[i];
let tile_size_px = layer.tile_size as i32;
let layer_camera_x_px = ((camera_x_px as f32) * layer.parallax_factor.x).floor() as i32;
let layer_camera_y_px = ((camera_y_px as f32) * layer.parallax_factor.y).floor() as i32;
let layer_center_x_px = layer_camera_x_px + self.viewport_width_px / 2;
let layer_center_y_px = layer_camera_y_px + self.viewport_height_px / 2;
(
tile_size_px,
layer_camera_x_px,
layer_camera_y_px,
layer_center_x_px,
layer_center_y_px,
)
});
if !self.initialized {
self.master_anchor = self.initial_anchor(
scene.layers[0].tilemap.width,
scene.layers[0].tilemap.height,
scene.layers[0].tile_size as i32,
camera_center_x,
camera_center_y,
);
for (layer_index, layer) in scene.layers.iter().enumerate() {
let (_, _, _, layer_center_x_px, layer_center_y_px) = layer_inputs[layer_index];
self.layer_anchors[layer_index] = self.initial_anchor(
layer.tilemap.width,
layer.tilemap.height,
layer.tile_size as i32,
layer_center_x_px,
layer_center_y_px,
);
refresh_requests.push(CacheRefreshRequest::RefreshRegion {
layer_index,
region: ViewportRegion::new(
0,
0,
self.cache_width_tiles,
self.cache_height_tiles,
),
});
}
self.initialized = true;
} else {
let layer0 = &scene.layers[0];
self.master_anchor = self.advance_anchor(
self.master_anchor,
camera_center_x,
camera_center_y,
layer0.tile_size as i32,
layer0.tilemap.width,
layer0.tilemap.height,
);
for (layer_index, layer) in scene.layers.iter().enumerate() {
let previous = self.layer_anchors[layer_index];
let (tile_size_px, _, _, layer_center_x_px, layer_center_y_px) =
layer_inputs[layer_index];
let next = self.advance_anchor(
previous,
layer_center_x_px,
layer_center_y_px,
tile_size_px,
layer.tilemap.width,
layer.tilemap.height,
);
self.layer_anchors[layer_index] = next;
self.emit_refresh_requests(layer_index, previous, next, &mut refresh_requests);
}
}
let copy_requests = std::array::from_fn(|layer_index| {
let layer = &scene.layers[layer_index];
let (tile_size_px, layer_camera_x_px, layer_camera_y_px, _, _) =
layer_inputs[layer_index];
let anchor = self.layer_anchors[layer_index];
let cache_origin_tile_x = anchor.x - (self.cache_width_tiles as i32 / 2);
let cache_origin_tile_y = anchor.y - (self.cache_height_tiles as i32 / 2);
let cache_origin_x_px = cache_origin_tile_x * tile_size_px;
let cache_origin_y_px = cache_origin_tile_y * tile_size_px;
LayerCopyRequest {
layer_index,
tile_size: layer.tile_size,
viewport_width_px: self.viewport_width_px,
viewport_height_px: self.viewport_height_px,
source_offset_x_px: layer_camera_x_px - cache_origin_x_px,
source_offset_y_px: layer_camera_y_px - cache_origin_y_px,
cache_origin_tile_x,
cache_origin_tile_y,
camera_x_px: layer_camera_x_px,
camera_y_px: layer_camera_y_px,
}
});
ResolverUpdate {
master_anchor: self.master_anchor,
layer_anchors: self.layer_anchors,
refresh_requests,
copy_requests,
}
}
fn initial_anchor(
&self,
scene_width_tiles: usize,
scene_height_tiles: usize,
tile_size_px: i32,
camera_center_x_px: i32,
camera_center_y_px: i32,
) -> TileAnchor {
let proposed = TileAnchor {
x: camera_center_x_px.div_euclid(tile_size_px),
y: camera_center_y_px.div_euclid(tile_size_px),
};
self.clamp_anchor(proposed, scene_width_tiles, scene_height_tiles)
}
fn advance_anchor(
&self,
current: TileAnchor,
camera_center_x_px: i32,
camera_center_y_px: i32,
tile_size_px: i32,
scene_width_tiles: usize,
scene_height_tiles: usize,
) -> TileAnchor {
let mut next = current;
loop {
let center_x_px = next.x * tile_size_px + tile_size_px / 2;
let drift_x = camera_center_x_px - center_x_px;
if drift_x.abs() <= self.hysteresis_safe_px {
break;
}
if drift_x > self.hysteresis_trigger_px {
next.x += 1;
continue;
}
if drift_x < -self.hysteresis_trigger_px {
next.x -= 1;
continue;
}
break;
}
loop {
let center_y_px = next.y * tile_size_px + tile_size_px / 2;
let drift_y = camera_center_y_px - center_y_px;
if drift_y.abs() <= self.hysteresis_safe_px {
break;
}
if drift_y > self.hysteresis_trigger_px {
next.y += 1;
continue;
}
if drift_y < -self.hysteresis_trigger_px {
next.y -= 1;
continue;
}
break;
}
self.clamp_anchor(next, scene_width_tiles, scene_height_tiles)
}
fn clamp_anchor(
&self,
proposed: TileAnchor,
scene_width_tiles: usize,
scene_height_tiles: usize,
) -> TileAnchor {
TileAnchor {
x: self.clamp_axis(proposed.x, scene_width_tiles, self.cache_width_tiles),
y: self.clamp_axis(proposed.y, scene_height_tiles, self.cache_height_tiles),
}
}
fn clamp_axis(&self, proposed: i32, scene_tiles: usize, cache_tiles: usize) -> i32 {
let half = (cache_tiles / 2) as i32;
if scene_tiles <= cache_tiles {
return half;
}
let min = half;
let max = (scene_tiles - cache_tiles) as i32 + half;
proposed.clamp(min, max)
}
fn emit_refresh_requests(
&self,
layer_index: usize,
previous: TileAnchor,
next: TileAnchor,
requests: &mut Vec<CacheRefreshRequest>,
) {
let delta_x = next.x - previous.x;
let delta_y = next.y - previous.y;
if delta_x == 0 && delta_y == 0 {
return;
}
if delta_x == 0 {
self.emit_line_requests(layer_index, delta_y, requests);
return;
}
if delta_y == 0 {
self.emit_column_requests(layer_index, delta_x, requests);
return;
}
self.emit_corner_region_requests(layer_index, delta_x, delta_y, requests);
}
fn emit_line_requests(
&self,
layer_index: usize,
delta_y: i32,
requests: &mut Vec<CacheRefreshRequest>,
) {
let count = delta_y.unsigned_abs() as usize;
if delta_y > 0 {
for offset in 0..count {
requests.push(CacheRefreshRequest::RefreshLine {
layer_index,
cache_y: self.cache_height_tiles - count + offset,
});
}
} else {
for cache_y in 0..count {
requests.push(CacheRefreshRequest::RefreshLine { layer_index, cache_y });
}
}
}
fn emit_column_requests(
&self,
layer_index: usize,
delta_x: i32,
requests: &mut Vec<CacheRefreshRequest>,
) {
let count = delta_x.unsigned_abs() as usize;
if delta_x > 0 {
for offset in 0..count {
requests.push(CacheRefreshRequest::RefreshColumn {
layer_index,
cache_x: self.cache_width_tiles - count + offset,
});
}
} else {
for cache_x in 0..count {
requests.push(CacheRefreshRequest::RefreshColumn { layer_index, cache_x });
}
}
}
fn emit_corner_region_requests(
&self,
layer_index: usize,
delta_x: i32,
delta_y: i32,
requests: &mut Vec<CacheRefreshRequest>,
) {
let width = delta_x.unsigned_abs() as usize;
let height = delta_y.unsigned_abs() as usize;
let primary_x = if delta_x > 0 { self.cache_width_tiles - width } else { 0 };
requests.push(CacheRefreshRequest::RefreshRegion {
layer_index,
region: ViewportRegion::new(primary_x, 0, width, self.cache_height_tiles),
});
let secondary_y = if delta_y > 0 { self.cache_height_tiles - height } else { 0 };
let secondary_x = if delta_x > 0 { 0 } else { width };
let secondary_width = self.cache_width_tiles.saturating_sub(width);
if secondary_width > 0 {
requests.push(CacheRefreshRequest::RefreshRegion {
layer_index,
region: ViewportRegion::new(secondary_x, secondary_y, secondary_width, height),
});
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::glyph::Glyph;
use crate::glyph_bank::TileSize;
use crate::scene_layer::{ParallaxFactor, SceneLayer};
use crate::tile::Tile;
use crate::tilemap::TileMap;
fn make_layer(
tile_size: TileSize,
parallax_x: f32,
parallax_y: f32,
width: usize,
height: usize,
) -> SceneLayer {
let mut tiles = Vec::new();
for i in 0..(width * height) {
tiles.push(Tile {
active: true,
glyph: Glyph { glyph_id: i as u16, palette_id: 0 },
flip_x: false,
flip_y: false,
});
}
SceneLayer {
active: true,
glyph_bank_id: 1,
tile_size,
parallax_factor: ParallaxFactor { x: parallax_x, y: parallax_y },
tilemap: TileMap { width, height, tiles },
}
}
fn make_scene() -> SceneBank {
SceneBank {
layers: [
make_layer(TileSize::Size16, 1.0, 1.0, 64, 64),
make_layer(TileSize::Size16, 0.5, 0.5, 64, 64),
make_layer(TileSize::Size16, 1.0, 0.75, 64, 64),
make_layer(TileSize::Size16, 0.25, 1.0, 64, 64),
],
}
}
#[test]
fn first_update_initializes_master_and_layer_anchors() {
let scene = make_scene();
let mut resolver = SceneViewportResolver::new(320, 180, 25, 16, 12, 20);
let update = resolver.update(&scene, 0, 0);
assert_eq!(update.master_anchor, TileAnchor { x: 12, y: 8 });
assert_eq!(update.layer_anchors[0], TileAnchor { x: 12, y: 8 });
assert_eq!(update.layer_anchors[1], TileAnchor { x: 12, y: 8 });
assert_eq!(update.refresh_requests.len(), 4);
}
#[test]
fn per_layer_copy_requests_follow_parallax_factor() {
let scene = make_scene();
let mut resolver = SceneViewportResolver::new(320, 180, 25, 16, 12, 20);
let update = resolver.update(&scene, 32, 48);
assert_eq!(update.copy_requests[0].camera_x_px, 32);
assert_eq!(update.copy_requests[0].camera_y_px, 48);
assert_eq!(update.copy_requests[1].camera_x_px, 16);
assert_eq!(update.copy_requests[1].camera_y_px, 24);
assert_eq!(update.copy_requests[3].camera_x_px, 8);
assert_eq!(update.copy_requests[3].camera_y_px, 48);
}
#[test]
fn hysteresis_prevents_small_back_and_forth_refresh_churn() {
let scene = make_scene();
let mut resolver = SceneViewportResolver::new(320, 180, 25, 16, 12, 20);
let _ = resolver.update(&scene, 0, 0);
let update = resolver.update(&scene, 8, 0);
assert!(update.refresh_requests.is_empty());
assert_eq!(update.master_anchor, TileAnchor { x: 12, y: 8 });
}
#[test]
fn repeated_high_speed_movement_advances_in_tile_steps() {
let scene = make_scene();
let mut resolver = SceneViewportResolver::new(320, 180, 25, 16, 12, 20);
let _ = resolver.update(&scene, 0, 0);
let update = resolver.update(&scene, 64, 0);
assert!(update.master_anchor.x > 12);
assert!(update.refresh_requests.iter().any(|request| matches!(
request,
CacheRefreshRequest::RefreshColumn { layer_index: 0, .. }
| CacheRefreshRequest::RefreshRegion { layer_index: 0, .. }
)));
}
#[test]
fn anchors_clamp_near_scene_edges() {
let scene = make_scene();
let mut resolver = SceneViewportResolver::new(320, 180, 25, 16, 12, 20);
let update = resolver.update(&scene, 10_000, 10_000);
assert_eq!(update.master_anchor, TileAnchor { x: 51, y: 56 });
assert_eq!(update.layer_anchors[0], TileAnchor { x: 51, y: 56 });
}
#[test]
fn corner_trigger_converts_to_non_overlapping_region_requests() {
let scene = make_scene();
let mut resolver = SceneViewportResolver::new(320, 180, 25, 16, 12, 20);
let _ = resolver.update(&scene, 0, 0);
let update = resolver.update(&scene, 64, 80);
let regions: Vec<_> = update
.refresh_requests
.iter()
.filter_map(|request| match request {
CacheRefreshRequest::RefreshRegion { layer_index: 0, region } => Some(*region),
_ => None,
})
.collect();
assert_eq!(regions.len(), 2);
assert_eq!(regions[0].x + regions[0].width, 25);
assert_eq!(regions[0].height, 16);
assert_eq!(regions[1].width, 24);
assert_eq!(regions[1].height, 1);
}
#[test]
fn reset_scene_requests_full_invalidation_for_all_layers() {
let mut resolver = SceneViewportResolver::new(320, 180, 25, 16, 12, 20);
let requests = resolver.reset_scene();
assert_eq!(requests.len(), 4);
assert!(
requests
.iter()
.all(|request| matches!(request, CacheRefreshRequest::InvalidateLayer { .. }))
);
}
}

View File

@ -1,161 +0,0 @@
mod domains;
mod registry;
mod resolver;
#[cfg(test)]
mod tests;
pub mod caps;
pub use resolver::{
DeclaredLoadError, LoadError, SyscallIdentity, SyscallResolved,
resolve_declared_program_syscalls, resolve_program_syscalls, resolve_syscall,
};
/// Enumeration of all System Calls (Syscalls) available in the Prometeu environment.
///
/// Syscalls are the primary mechanism for a program running in the Virtual Machine
/// to interact with the outside world (Hardware, OS, Filesystem).
///
/// 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)
/// - **0x5xxx**: Logging & Debugging
/// - **0x6xxx**: Asset Loading & Memory Banks
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u32)]
pub enum Syscall {
SystemHasCart = 0x0001,
SystemRunCart = 0x0002,
GfxClear = 0x1001,
GfxFillRect = 0x1002,
GfxDrawLine = 0x1003,
GfxDrawCircle = 0x1004,
GfxDrawDisc = 0x1005,
GfxDrawSquare = 0x1006,
GfxDrawText = 0x1008,
GfxClear565 = 0x1010,
ComposerBindScene = 0x1101,
ComposerUnbindScene = 0x1102,
ComposerSetCamera = 0x1103,
ComposerEmitSprite = 0x1104,
AudioPlaySample = 0x3001,
AudioPlay = 0x3002,
FsOpen = 0x4001,
FsRead = 0x4002,
FsWrite = 0x4003,
FsClose = 0x4004,
FsListDir = 0x4005,
FsExists = 0x4006,
FsDelete = 0x4007,
MemSlotCount = 0x4201,
MemSlotStat = 0x4202,
MemSlotRead = 0x4203,
MemSlotWrite = 0x4204,
MemSlotCommit = 0x4205,
MemSlotClear = 0x4206,
LogWrite = 0x5001,
LogWriteTag = 0x5002,
AssetLoad = 0x6001,
AssetStatus = 0x6002,
AssetCommit = 0x6003,
AssetCancel = 0x6004,
BankInfo = 0x6101,
BankSlotInfo = 0x6102,
}
/// Canonical metadata describing a syscall using the unified slot-based ABI.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SyscallMeta {
pub id: u32,
pub module: &'static str,
pub name: &'static str,
pub version: u16,
pub arg_slots: u8,
pub ret_slots: u16,
pub caps: CapFlags,
pub determinism: Determinism,
pub may_allocate: bool,
pub cost_hint: u32,
}
/// Bitflags representing capabilities required to invoke a syscall.
pub type CapFlags = u64;
/// Determinism flags for a syscall.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Determinism {
Unknown,
Deterministic,
NonDeterministic,
}
/// Pairing of a strongly-typed syscall and its metadata.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SyscallRegistryEntry {
pub syscall: Syscall,
pub meta: SyscallMeta,
}
impl SyscallRegistryEntry {
/// Starts the builder with mandatory fields and sensible default values.
pub const fn builder(syscall: Syscall, module: &'static str, name: &'static str) -> Self {
Self {
syscall,
meta: SyscallMeta {
id: syscall as u32,
module,
name,
version: 1, // Default for new syscalls
arg_slots: 0,
ret_slots: 0,
caps: 0,
determinism: Determinism::Deterministic,
may_allocate: false,
cost_hint: 1,
},
}
}
pub const fn version(mut self, n: u16) -> Self {
self.meta.version = n;
self
}
pub const fn args(mut self, n: u8) -> Self {
self.meta.arg_slots = n;
self
}
pub const fn rets(mut self, n: u16) -> Self {
self.meta.ret_slots = n;
self
}
pub const fn caps(mut self, caps: CapFlags) -> Self {
self.meta.caps = caps;
self
}
pub const fn non_deterministic(mut self) -> Self {
self.meta.determinism = Determinism::NonDeterministic;
self
}
pub const fn may_allocate(mut self) -> Self {
self.meta.may_allocate = true;
self
}
pub const fn cost(mut self, cost: u32) -> Self {
self.meta.cost_hint = cost;
self
}
}
pub fn meta_for(syscall: Syscall) -> &'static SyscallMeta {
registry::meta_for(syscall)
}

View File

@ -1,11 +0,0 @@
use super::CapFlags;
pub const NONE: CapFlags = 0;
pub const SYSTEM: CapFlags = 1 << 0;
pub const GFX: CapFlags = 1 << 1;
pub const AUDIO: CapFlags = 1 << 2;
pub const FS: CapFlags = 1 << 3;
pub const LOG: CapFlags = 1 << 4;
pub const ASSET: CapFlags = 1 << 5;
pub const BANK: CapFlags = 1 << 6;
pub const ALL: CapFlags = SYSTEM | GFX | AUDIO | FS | LOG | ASSET | BANK;

View File

@ -1,27 +0,0 @@
use crate::syscalls::{Syscall, SyscallRegistryEntry, caps};
pub(crate) const ENTRIES: &[SyscallRegistryEntry] = &[
SyscallRegistryEntry::builder(Syscall::AssetLoad, "asset", "load")
.args(2)
.rets(2)
.caps(caps::ASSET)
.non_deterministic()
.cost(20),
SyscallRegistryEntry::builder(Syscall::AssetStatus, "asset", "status")
.args(1)
.rets(1)
.caps(caps::ASSET)
.non_deterministic(),
SyscallRegistryEntry::builder(Syscall::AssetCommit, "asset", "commit")
.args(1)
.rets(1)
.caps(caps::ASSET)
.non_deterministic()
.cost(20),
SyscallRegistryEntry::builder(Syscall::AssetCancel, "asset", "cancel")
.args(1)
.rets(1)
.caps(caps::ASSET)
.non_deterministic()
.cost(20),
];

View File

@ -1,14 +0,0 @@
use crate::syscalls::{Syscall, SyscallRegistryEntry, caps};
pub(crate) const ENTRIES: &[SyscallRegistryEntry] = &[
SyscallRegistryEntry::builder(Syscall::AudioPlaySample, "audio", "play_sample")
.args(5)
.rets(1)
.caps(caps::AUDIO)
.cost(5),
SyscallRegistryEntry::builder(Syscall::AudioPlay, "audio", "play")
.args(7)
.rets(1)
.caps(caps::AUDIO)
.cost(5),
];

View File

@ -1,12 +0,0 @@
use crate::syscalls::{Syscall, SyscallRegistryEntry, caps};
pub(crate) const ENTRIES: &[SyscallRegistryEntry] = &[
SyscallRegistryEntry::builder(Syscall::BankInfo, "bank", "info")
.args(1)
.rets(1)
.caps(caps::BANK),
SyscallRegistryEntry::builder(Syscall::BankSlotInfo, "bank", "slot_info")
.args(2)
.rets(1)
.caps(caps::BANK),
];

View File

@ -1,22 +0,0 @@
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

@ -1,68 +0,0 @@
use crate::syscalls::{Syscall, SyscallRegistryEntry, caps};
pub(crate) const ENTRIES: &[SyscallRegistryEntry] = &[
SyscallRegistryEntry::builder(Syscall::FsOpen, "fs", "open")
.args(1)
.rets(1)
.caps(caps::FS)
.non_deterministic()
.cost(20),
SyscallRegistryEntry::builder(Syscall::FsRead, "fs", "read")
.args(1)
.rets(1)
.caps(caps::FS)
.non_deterministic()
.cost(20),
SyscallRegistryEntry::builder(Syscall::FsWrite, "fs", "write")
.args(2)
.rets(1)
.caps(caps::FS)
.non_deterministic()
.cost(20),
SyscallRegistryEntry::builder(Syscall::FsClose, "fs", "close").args(1).caps(caps::FS).cost(5),
SyscallRegistryEntry::builder(Syscall::FsListDir, "fs", "list_dir")
.args(1)
.rets(1)
.caps(caps::FS)
.non_deterministic()
.cost(20),
SyscallRegistryEntry::builder(Syscall::FsExists, "fs", "exists").args(1).rets(1).caps(caps::FS),
SyscallRegistryEntry::builder(Syscall::FsDelete, "fs", "delete")
.args(1)
.caps(caps::FS)
.non_deterministic()
.cost(20),
SyscallRegistryEntry::builder(Syscall::MemSlotCount, "mem", "slot_count")
.rets(2)
.caps(caps::FS),
SyscallRegistryEntry::builder(Syscall::MemSlotStat, "mem", "slot_stat")
.args(1)
.rets(5)
.caps(caps::FS)
.non_deterministic()
.cost(5),
SyscallRegistryEntry::builder(Syscall::MemSlotRead, "mem", "slot_read")
.args(3)
.rets(3)
.caps(caps::FS)
.non_deterministic()
.cost(20),
SyscallRegistryEntry::builder(Syscall::MemSlotWrite, "mem", "slot_write")
.args(3)
.rets(2)
.caps(caps::FS)
.non_deterministic()
.cost(20),
SyscallRegistryEntry::builder(Syscall::MemSlotCommit, "mem", "slot_commit")
.args(1)
.rets(1)
.caps(caps::FS)
.non_deterministic()
.cost(20),
SyscallRegistryEntry::builder(Syscall::MemSlotClear, "mem", "slot_clear")
.args(1)
.rets(1)
.caps(caps::FS)
.non_deterministic()
.cost(20),
];

View File

@ -1,36 +0,0 @@
use crate::syscalls::{Syscall, SyscallRegistryEntry, caps};
pub(crate) const ENTRIES: &[SyscallRegistryEntry] = &[
SyscallRegistryEntry::builder(Syscall::GfxClear, "gfx", "clear")
.args(1)
.caps(caps::GFX)
.cost(20),
SyscallRegistryEntry::builder(Syscall::GfxFillRect, "gfx", "fill_rect")
.args(5)
.caps(caps::GFX)
.cost(20),
SyscallRegistryEntry::builder(Syscall::GfxDrawLine, "gfx", "draw_line")
.args(5)
.caps(caps::GFX)
.cost(5),
SyscallRegistryEntry::builder(Syscall::GfxDrawCircle, "gfx", "draw_circle")
.args(4)
.caps(caps::GFX)
.cost(5),
SyscallRegistryEntry::builder(Syscall::GfxDrawDisc, "gfx", "draw_disc")
.args(5)
.caps(caps::GFX)
.cost(5),
SyscallRegistryEntry::builder(Syscall::GfxDrawSquare, "gfx", "draw_square")
.args(6)
.caps(caps::GFX)
.cost(5),
SyscallRegistryEntry::builder(Syscall::GfxDrawText, "gfx", "draw_text")
.args(4)
.caps(caps::GFX)
.cost(20),
SyscallRegistryEntry::builder(Syscall::GfxClear565, "gfx", "clear_565")
.args(1)
.caps(caps::GFX)
.cost(20),
];

View File

@ -1,14 +0,0 @@
use crate::syscalls::{Syscall, SyscallRegistryEntry, caps};
pub(crate) const ENTRIES: &[SyscallRegistryEntry] = &[
SyscallRegistryEntry::builder(Syscall::LogWrite, "log", "write")
.args(2)
.caps(caps::LOG)
.non_deterministic()
.cost(5),
SyscallRegistryEntry::builder(Syscall::LogWriteTag, "log", "write_tag")
.args(3)
.caps(caps::LOG)
.non_deterministic()
.cost(5),
];

View File

@ -1,22 +0,0 @@
mod asset;
mod audio;
mod bank;
mod composer;
mod fs;
mod gfx;
mod log;
mod system;
use super::SyscallRegistryEntry;
pub(crate) fn all_entries() -> impl Iterator<Item = &'static SyscallRegistryEntry> {
system::ENTRIES
.iter()
.chain(gfx::ENTRIES.iter())
.chain(composer::ENTRIES.iter())
.chain(audio::ENTRIES.iter())
.chain(fs::ENTRIES.iter())
.chain(log::ENTRIES.iter())
.chain(asset::ENTRIES.iter())
.chain(bank::ENTRIES.iter())
}

View File

@ -1,11 +0,0 @@
use crate::syscalls::{Syscall, SyscallRegistryEntry, caps};
pub(crate) const ENTRIES: &[SyscallRegistryEntry] = &[
SyscallRegistryEntry::builder(Syscall::SystemHasCart, "system", "has_cart")
.rets(1)
.caps(caps::SYSTEM),
SyscallRegistryEntry::builder(Syscall::SystemRunCart, "system", "run_cart")
.caps(caps::SYSTEM)
.non_deterministic()
.cost(50),
];

View File

@ -1,105 +0,0 @@
use super::{Syscall, SyscallMeta, domains};
pub(crate) fn meta_for(syscall: Syscall) -> &'static SyscallMeta {
for entry in domains::all_entries() {
if entry.syscall == syscall {
return &entry.meta;
}
}
panic!("Missing SyscallMeta for {:?}", syscall);
}
impl Syscall {
pub fn from_u32(id: u32) -> Option<Self> {
match id {
0x0001 => Some(Self::SystemHasCart),
0x0002 => Some(Self::SystemRunCart),
0x1001 => Some(Self::GfxClear),
0x1002 => Some(Self::GfxFillRect),
0x1003 => Some(Self::GfxDrawLine),
0x1004 => Some(Self::GfxDrawCircle),
0x1005 => Some(Self::GfxDrawDisc),
0x1006 => Some(Self::GfxDrawSquare),
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),
0x4002 => Some(Self::FsRead),
0x4003 => Some(Self::FsWrite),
0x4004 => Some(Self::FsClose),
0x4005 => Some(Self::FsListDir),
0x4006 => Some(Self::FsExists),
0x4007 => Some(Self::FsDelete),
0x4201 => Some(Self::MemSlotCount),
0x4202 => Some(Self::MemSlotStat),
0x4203 => Some(Self::MemSlotRead),
0x4204 => Some(Self::MemSlotWrite),
0x4205 => Some(Self::MemSlotCommit),
0x4206 => Some(Self::MemSlotClear),
0x5001 => Some(Self::LogWrite),
0x5002 => Some(Self::LogWriteTag),
0x6001 => Some(Self::AssetLoad),
0x6002 => Some(Self::AssetStatus),
0x6003 => Some(Self::AssetCommit),
0x6004 => Some(Self::AssetCancel),
0x6101 => Some(Self::BankInfo),
0x6102 => Some(Self::BankSlotInfo),
_ => None,
}
}
pub fn args_count(&self) -> usize {
super::meta_for(*self).arg_slots as usize
}
pub fn results_count(&self) -> usize {
super::meta_for(*self).ret_slots as usize
}
pub fn name(&self) -> &'static str {
match self {
Self::SystemHasCart => "SystemHasCart",
Self::SystemRunCart => "SystemRunCart",
Self::GfxClear => "GfxClear",
Self::GfxFillRect => "GfxFillRect",
Self::GfxDrawLine => "GfxDrawLine",
Self::GfxDrawCircle => "GfxDrawCircle",
Self::GfxDrawDisc => "GfxDrawDisc",
Self::GfxDrawSquare => "GfxDrawSquare",
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",
Self::FsRead => "FsRead",
Self::FsWrite => "FsWrite",
Self::FsClose => "FsClose",
Self::FsListDir => "FsListDir",
Self::FsExists => "FsExists",
Self::FsDelete => "FsDelete",
Self::MemSlotCount => "MemSlotCount",
Self::MemSlotStat => "MemSlotStat",
Self::MemSlotRead => "MemSlotRead",
Self::MemSlotWrite => "MemSlotWrite",
Self::MemSlotCommit => "MemSlotCommit",
Self::MemSlotClear => "MemSlotClear",
Self::LogWrite => "LogWrite",
Self::LogWriteTag => "LogWriteTag",
Self::AssetLoad => "AssetLoad",
Self::AssetStatus => "AssetStatus",
Self::AssetCommit => "AssetCommit",
Self::AssetCancel => "AssetCancel",
Self::BankInfo => "BankInfo",
Self::BankSlotInfo => "BankSlotInfo",
}
}
}

View File

@ -1,156 +0,0 @@
use super::{CapFlags, SyscallMeta, domains};
/// Canonical identity triple for a syscall.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SyscallIdentity {
pub module: &'static str,
pub name: &'static str,
pub version: u16,
}
impl SyscallIdentity {
pub fn key(&self) -> (&'static str, &'static str, u16) {
(self.module, self.name, self.version)
}
}
/// Resolved syscall information provided to the loader/VM.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SyscallResolved {
pub id: u32,
pub meta: SyscallMeta,
}
/// Load-time error for syscall resolution.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LoadError {
UnknownSyscall {
module: &'static str,
name: &'static str,
version: u16,
},
MissingCapability {
required: CapFlags,
provided: CapFlags,
module: &'static str,
name: &'static str,
version: u16,
},
}
/// Load-time error for PBX-declared syscall resolution.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DeclaredLoadError {
UnknownSyscall {
module: String,
name: String,
version: u16,
},
MissingCapability {
required: CapFlags,
provided: CapFlags,
module: String,
name: String,
version: u16,
},
AbiMismatch {
module: String,
name: String,
version: u16,
declared_arg_slots: u16,
declared_ret_slots: u16,
expected_arg_slots: u16,
expected_ret_slots: u16,
},
}
fn resolve_syscall_impl(module: &str, name: &str, version: u16) -> Option<SyscallResolved> {
for entry in domains::all_entries() {
if entry.meta.module == module && entry.meta.name == name && entry.meta.version == version {
return Some(SyscallResolved { id: entry.meta.id, meta: entry.meta });
}
}
None
}
pub fn resolve_syscall(
module: &'static str,
name: &'static str,
version: u16,
) -> Option<SyscallResolved> {
resolve_syscall_impl(module, name, version)
}
pub fn resolve_program_syscalls(
declared: &[SyscallIdentity],
caps: CapFlags,
) -> Result<Vec<SyscallResolved>, LoadError> {
let mut out = Vec::with_capacity(declared.len());
for ident in declared {
let Some(res) = resolve_syscall(ident.module, ident.name, ident.version) else {
return Err(LoadError::UnknownSyscall {
module: ident.module,
name: ident.name,
version: ident.version,
});
};
let missing = res.meta.caps & !caps;
if missing != 0 {
return Err(LoadError::MissingCapability {
required: res.meta.caps,
provided: caps,
module: ident.module,
name: ident.name,
version: ident.version,
});
}
out.push(res);
}
Ok(out)
}
pub fn resolve_declared_program_syscalls(
declared: &[prometeu_bytecode::SyscallDecl],
caps: CapFlags,
) -> Result<Vec<SyscallResolved>, DeclaredLoadError> {
let mut out = Vec::with_capacity(declared.len());
for decl in declared {
let Some(res) = resolve_syscall_impl(&decl.module, &decl.name, decl.version) else {
return Err(DeclaredLoadError::UnknownSyscall {
module: decl.module.clone(),
name: decl.name.clone(),
version: decl.version,
});
};
let missing = res.meta.caps & !caps;
if missing != 0 {
return Err(DeclaredLoadError::MissingCapability {
required: res.meta.caps,
provided: caps,
module: decl.module.clone(),
name: decl.name.clone(),
version: decl.version,
});
}
let expected_arg_slots = u16::from(res.meta.arg_slots);
let expected_ret_slots = res.meta.ret_slots;
if decl.arg_slots != expected_arg_slots || decl.ret_slots != expected_ret_slots {
return Err(DeclaredLoadError::AbiMismatch {
module: decl.module.clone(),
name: decl.name.clone(),
version: decl.version,
declared_arg_slots: decl.arg_slots,
declared_ret_slots: decl.ret_slots,
expected_arg_slots,
expected_ret_slots,
});
}
out.push(res);
}
Ok(out)
}

View File

@ -1,416 +0,0 @@
use super::*;
fn all_syscalls() -> Vec<Syscall> {
domains::all_entries().map(|entry| entry.syscall).collect()
}
#[test]
fn every_syscall_has_metadata() {
for sc in all_syscalls() {
let m = meta_for(sc);
assert_eq!(m.id, sc as u32, "id mismatch for {:?}", sc);
assert!(!m.module.is_empty(), "module must be non-empty for id=0x{:08X}", m.id);
assert!(!m.name.is_empty(), "name must be non-empty for id=0x{:08X}", m.id);
assert!(m.version > 0, "version must be > 0 for id=0x{:08X}", m.id);
}
use std::collections::HashSet;
let mut ids = HashSet::new();
let mut identities = HashSet::new();
let mut count = 0usize;
for entry in domains::all_entries() {
count += 1;
assert!(ids.insert(entry.meta.id), "duplicate syscall id 0x{:08X}", entry.meta.id);
let parsed = Syscall::from_u32(entry.meta.id).expect("id not recognized by enum mapping");
assert_eq!(parsed as u32, entry.meta.id);
let key = (entry.meta.module, entry.meta.name, entry.meta.version);
assert!(
identities.insert(key),
"duplicate canonical identity: ({}.{}, v{})",
entry.meta.module,
entry.meta.name,
entry.meta.version
);
}
assert_eq!(count, all_syscalls().len());
}
#[test]
fn resolver_returns_expected_id_for_known_identity() {
let id = resolve_syscall("gfx", "clear", 1).expect("known identity must resolve");
assert_eq!(id.id, 0x1001);
assert_eq!(id.meta.module, "gfx");
assert_eq!(id.meta.name, "clear");
assert_eq!(id.meta.version, 1);
}
#[test]
fn resolver_rejects_unknown_identity() {
let res = resolve_syscall("gfx", "nonexistent", 1);
assert!(res.is_none());
let requested = [SyscallIdentity { module: "gfx", name: "nonexistent", version: 1 }];
let err = resolve_program_syscalls(&requested, 0).unwrap_err();
match err {
LoadError::UnknownSyscall { module, name, version } => {
assert_eq!(module, "gfx");
assert_eq!(name, "nonexistent");
assert_eq!(version, 1);
}
_ => panic!("expected UnknownSyscall error"),
}
}
#[test]
fn resolver_rejects_removed_legacy_gfx_set_sprite_identity() {
assert!(resolve_syscall("gfx", "set_sprite", 1).is_none());
let requested = [SyscallIdentity { module: "gfx", name: "set_sprite", version: 1 }];
let err = resolve_program_syscalls(&requested, caps::ALL).unwrap_err();
assert_eq!(
err,
LoadError::UnknownSyscall { module: "gfx".into(), name: "set_sprite".into(), version: 1 }
);
}
#[test]
fn resolver_enforces_capabilities() {
let requested = [SyscallIdentity { module: "gfx", name: "clear", version: 1 }];
let err = resolve_program_syscalls(&requested, 0).unwrap_err();
match err {
LoadError::MissingCapability { required, provided, module, name, version } => {
assert_eq!(module, "gfx");
assert_eq!(name, "clear");
assert_eq!(version, 1);
assert_ne!(required, 0);
assert_eq!(provided, 0);
}
_ => panic!("expected MissingCapability error"),
}
let ok = resolve_program_syscalls(&requested, caps::GFX).expect("must resolve with caps");
assert_eq!(ok.len(), 1);
assert_eq!(ok[0].id, 0x1001);
}
#[test]
fn declared_resolver_returns_expected_id_for_known_identity() {
let declared = [prometeu_bytecode::SyscallDecl {
module: "gfx".into(),
name: "clear".into(),
version: 1,
arg_slots: 1,
ret_slots: 0,
}];
let ok =
resolve_declared_program_syscalls(&declared, caps::GFX).expect("must resolve with ABI");
assert_eq!(ok.len(), 1);
assert_eq!(ok[0].id, 0x1001);
}
#[test]
fn declared_resolver_rejects_unknown_identity() {
let declared = [prometeu_bytecode::SyscallDecl {
module: "gfx".into(),
name: "nonexistent".into(),
version: 1,
arg_slots: 1,
ret_slots: 0,
}];
let err = resolve_declared_program_syscalls(&declared, caps::GFX).unwrap_err();
assert_eq!(
err,
DeclaredLoadError::UnknownSyscall {
module: "gfx".into(),
name: "nonexistent".into(),
version: 1,
}
);
}
#[test]
fn declared_resolver_rejects_missing_capability() {
let declared = [prometeu_bytecode::SyscallDecl {
module: "gfx".into(),
name: "clear".into(),
version: 1,
arg_slots: 1,
ret_slots: 0,
}];
let err = resolve_declared_program_syscalls(&declared, caps::NONE).unwrap_err();
assert_eq!(
err,
DeclaredLoadError::MissingCapability {
required: caps::GFX,
provided: caps::NONE,
module: "gfx".into(),
name: "clear".into(),
version: 1,
}
);
}
#[test]
fn declared_resolver_rejects_abi_mismatch() {
let declared = [prometeu_bytecode::SyscallDecl {
module: "gfx".into(),
name: "draw_line".into(),
version: 1,
arg_slots: 4,
ret_slots: 0,
}];
let err = resolve_declared_program_syscalls(&declared, caps::GFX).unwrap_err();
assert_eq!(
err,
DeclaredLoadError::AbiMismatch {
module: "gfx".into(),
name: "draw_line".into(),
version: 1,
declared_arg_slots: 4,
declared_ret_slots: 0,
expected_arg_slots: 5,
expected_ret_slots: 0,
}
);
}
#[test]
fn status_first_syscall_signatures_are_pinned() {
let clear = meta_for(Syscall::GfxClear);
assert_eq!(clear.arg_slots, 1);
assert_eq!(clear.ret_slots, 0);
let fill_rect = meta_for(Syscall::GfxFillRect);
assert_eq!(fill_rect.arg_slots, 5);
assert_eq!(fill_rect.ret_slots, 0);
let draw_line = meta_for(Syscall::GfxDrawLine);
assert_eq!(draw_line.arg_slots, 5);
assert_eq!(draw_line.ret_slots, 0);
let draw_circle = meta_for(Syscall::GfxDrawCircle);
assert_eq!(draw_circle.arg_slots, 4);
assert_eq!(draw_circle.ret_slots, 0);
let draw_disc = meta_for(Syscall::GfxDrawDisc);
assert_eq!(draw_disc.arg_slots, 5);
assert_eq!(draw_disc.ret_slots, 0);
let draw_square = meta_for(Syscall::GfxDrawSquare);
assert_eq!(draw_square.arg_slots, 6);
assert_eq!(draw_square.ret_slots, 0);
let draw_text = meta_for(Syscall::GfxDrawText);
assert_eq!(draw_text.arg_slots, 4);
assert_eq!(draw_text.ret_slots, 0);
let clear_565 = meta_for(Syscall::GfxClear565);
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);
let audio_play = meta_for(Syscall::AudioPlay);
assert_eq!(audio_play.arg_slots, 7);
assert_eq!(audio_play.ret_slots, 1);
let asset_load = meta_for(Syscall::AssetLoad);
assert_eq!(asset_load.arg_slots, 2);
assert_eq!(asset_load.ret_slots, 2);
let asset_commit = meta_for(Syscall::AssetCommit);
assert_eq!(asset_commit.arg_slots, 1);
assert_eq!(asset_commit.ret_slots, 1);
let asset_cancel = meta_for(Syscall::AssetCancel);
assert_eq!(asset_cancel.arg_slots, 1);
assert_eq!(asset_cancel.ret_slots, 1);
}
#[test]
fn declared_resolver_rejects_legacy_status_first_signatures() {
let declared = vec![
prometeu_bytecode::SyscallDecl {
module: "composer".into(),
name: "bind_scene".into(),
version: 1,
arg_slots: 1,
ret_slots: 0,
},
prometeu_bytecode::SyscallDecl {
module: "audio".into(),
name: "play_sample".into(),
version: 1,
arg_slots: 5,
ret_slots: 0,
},
prometeu_bytecode::SyscallDecl {
module: "audio".into(),
name: "play".into(),
version: 1,
arg_slots: 7,
ret_slots: 0,
},
prometeu_bytecode::SyscallDecl {
module: "asset".into(),
name: "load".into(),
version: 1,
arg_slots: 3,
ret_slots: 2,
},
prometeu_bytecode::SyscallDecl {
module: "asset".into(),
name: "commit".into(),
version: 1,
arg_slots: 1,
ret_slots: 0,
},
prometeu_bytecode::SyscallDecl {
module: "asset".into(),
name: "cancel".into(),
version: 1,
arg_slots: 1,
ret_slots: 0,
},
];
for decl in declared {
let err = resolve_declared_program_syscalls(std::slice::from_ref(&decl), caps::ALL)
.expect_err("legacy signature must be rejected");
match err {
DeclaredLoadError::AbiMismatch {
module,
name,
version,
declared_arg_slots,
declared_ret_slots,
expected_arg_slots,
expected_ret_slots,
} => {
assert_eq!(module, decl.module);
assert_eq!(name, decl.name);
assert_eq!(version, decl.version);
assert_eq!(declared_arg_slots, decl.arg_slots);
assert_eq!(declared_ret_slots, decl.ret_slots);
assert!(
expected_arg_slots != declared_arg_slots
|| expected_ret_slots != declared_ret_slots
);
}
other => panic!("expected AbiMismatch, got {:?}", other),
}
}
}
#[test]
fn declared_resolver_accepts_mixed_status_first_surface_as_a_single_module() {
let declared = vec![
prometeu_bytecode::SyscallDecl {
module: "composer".into(),
name: "bind_scene".into(),
version: 1,
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 {
module: "audio".into(),
name: "play".into(),
version: 1,
arg_slots: 7,
ret_slots: 1,
},
prometeu_bytecode::SyscallDecl {
module: "asset".into(),
name: "load".into(),
version: 1,
arg_slots: 2,
ret_slots: 2,
},
prometeu_bytecode::SyscallDecl {
module: "asset".into(),
name: "commit".into(),
version: 1,
arg_slots: 1,
ret_slots: 1,
},
];
let resolved =
resolve_declared_program_syscalls(&declared, caps::GFX | caps::AUDIO | caps::ASSET)
.expect("mixed status-first surface must resolve together");
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, 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]
fn memcard_syscall_signatures_are_pinned() {
let slot_count = meta_for(Syscall::MemSlotCount);
assert_eq!(slot_count.module, "mem");
assert_eq!(slot_count.name, "slot_count");
assert_eq!(slot_count.arg_slots, 0);
assert_eq!(slot_count.ret_slots, 2);
let slot_stat = meta_for(Syscall::MemSlotStat);
assert_eq!(slot_stat.arg_slots, 1);
assert_eq!(slot_stat.ret_slots, 5);
let slot_read = meta_for(Syscall::MemSlotRead);
assert_eq!(slot_read.arg_slots, 3);
assert_eq!(slot_read.ret_slots, 3);
let slot_write = meta_for(Syscall::MemSlotWrite);
assert_eq!(slot_write.arg_slots, 3);
assert_eq!(slot_write.ret_slots, 2);
let slot_commit = meta_for(Syscall::MemSlotCommit);
assert_eq!(slot_commit.arg_slots, 1);
assert_eq!(slot_commit.ret_slots, 1);
let slot_clear = meta_for(Syscall::MemSlotClear);
assert_eq!(slot_clear.arg_slots, 1);
assert_eq!(slot_clear.ret_slots, 1);
}

View File

@ -1,308 +0,0 @@
use crate::log::{LogLevel, LogService, LogSource};
use std::sync::Arc;
use std::sync::atomic::{AtomicU32, AtomicU64, AtomicUsize, Ordering};
#[derive(Debug, Clone, Copy, Default)]
pub struct TelemetryFrame {
pub frame_index: u64,
pub vm_steps: u32,
pub cycles_used: u64,
pub cycles_budget: u64,
pub syscalls: u32,
pub host_cpu_time_us: u64,
pub completed_logical_frames: u32,
pub violations: u32,
// Bank telemetry
pub glyph_slots_used: u32,
pub glyph_slots_total: u32,
pub sound_slots_used: u32,
pub sound_slots_total: u32,
// RAM (Heap)
pub heap_used_bytes: usize,
pub heap_max_bytes: usize,
// Log Pressure from the last completed logical frame
pub logs_count: u32,
}
/// Thread-safe, atomic telemetry storage for real-time monitoring by the host.
/// This follows the push-based model from DEC-0005 to avoid expensive scans or locks.
#[derive(Debug, Default)]
pub struct AtomicTelemetry {
pub frame_index: AtomicU64,
pub cycles_used: AtomicU64,
pub cycles_budget: AtomicU64,
pub syscalls: AtomicU32,
pub host_cpu_time_us: AtomicU64,
pub vm_steps: AtomicU32,
pub completed_logical_frames: AtomicU32,
pub violations: AtomicU32,
// Bank telemetry
pub glyph_slots_used: AtomicU32,
pub glyph_slots_total: AtomicU32,
pub sound_slots_used: AtomicU32,
pub sound_slots_total: AtomicU32,
// RAM (Heap)
pub heap_used_bytes: AtomicUsize,
pub heap_max_bytes: AtomicUsize,
// Transient in-flight log counter for the current logical frame
pub current_logs_count: Arc<AtomicU32>,
// Persisted log count from the last completed logical frame
pub logs_count: AtomicU32,
}
impl AtomicTelemetry {
pub fn new(current_logs_count: Arc<AtomicU32>) -> Self {
Self { current_logs_count, ..Default::default() }
}
/// Snapshots the current atomic state into a TelemetryFrame.
pub fn snapshot(&self) -> TelemetryFrame {
TelemetryFrame {
frame_index: self.frame_index.load(Ordering::Relaxed),
cycles_used: self.cycles_used.load(Ordering::Relaxed),
cycles_budget: self.cycles_budget.load(Ordering::Relaxed),
syscalls: self.syscalls.load(Ordering::Relaxed),
host_cpu_time_us: self.host_cpu_time_us.load(Ordering::Relaxed),
completed_logical_frames: self.completed_logical_frames.load(Ordering::Relaxed),
violations: self.violations.load(Ordering::Relaxed),
glyph_slots_used: self.glyph_slots_used.load(Ordering::Relaxed),
glyph_slots_total: self.glyph_slots_total.load(Ordering::Relaxed),
sound_slots_used: self.sound_slots_used.load(Ordering::Relaxed),
sound_slots_total: self.sound_slots_total.load(Ordering::Relaxed),
heap_used_bytes: self.heap_used_bytes.load(Ordering::Relaxed),
heap_max_bytes: self.heap_max_bytes.load(Ordering::Relaxed),
logs_count: self.logs_count.load(Ordering::Relaxed),
vm_steps: self.vm_steps.load(Ordering::Relaxed),
}
}
pub fn reset(&self) {
self.frame_index.store(0, Ordering::Relaxed);
self.cycles_used.store(0, Ordering::Relaxed);
self.syscalls.store(0, Ordering::Relaxed);
self.host_cpu_time_us.store(0, Ordering::Relaxed);
self.completed_logical_frames.store(0, Ordering::Relaxed);
self.violations.store(0, Ordering::Relaxed);
self.glyph_slots_used.store(0, Ordering::Relaxed);
self.glyph_slots_total.store(0, Ordering::Relaxed);
self.sound_slots_used.store(0, Ordering::Relaxed);
self.sound_slots_total.store(0, Ordering::Relaxed);
self.heap_used_bytes.store(0, Ordering::Relaxed);
self.vm_steps.store(0, Ordering::Relaxed);
self.logs_count.store(0, Ordering::Relaxed);
self.current_logs_count.store(0, Ordering::Relaxed);
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct CertificationConfig {
pub enabled: bool,
pub cycles_budget_per_frame: Option<u64>,
pub max_syscalls_per_frame: Option<u32>,
pub max_host_cpu_us_per_frame: Option<u64>,
pub max_glyph_slots_used: Option<u32>,
pub max_sound_slots_used: Option<u32>,
pub max_heap_bytes: Option<usize>,
pub max_logs_per_frame: Option<u32>,
}
pub struct Certifier {
pub config: CertificationConfig,
}
impl Certifier {
pub fn new(config: CertificationConfig) -> Self {
Self { config }
}
pub fn evaluate(
&self,
telemetry: &TelemetryFrame,
log_service: &mut LogService,
ts_ms: u64,
) -> usize {
if !self.config.enabled {
return 0;
}
let mut violations = 0;
// 1. Cycles
if let Some(budget) = self.config.cycles_budget_per_frame
&& telemetry.cycles_used > budget
{
log_service.log(
ts_ms,
telemetry.frame_index,
LogLevel::Warn,
LogSource::Pos,
0xCA01,
format!(
"Cert: cycles_used exceeded budget ({} > {})",
telemetry.cycles_used, budget
),
);
violations += 1;
}
// 2. Syscalls
if let Some(limit) = self.config.max_syscalls_per_frame
&& telemetry.syscalls > limit
{
log_service.log(
ts_ms,
telemetry.frame_index,
LogLevel::Warn,
LogSource::Pos,
0xCA02,
format!(
"Cert: syscalls per frame exceeded limit ({} > {})",
telemetry.syscalls, limit
),
);
violations += 1;
}
// 3. CPU Time
if let Some(limit) = self.config.max_host_cpu_us_per_frame
&& telemetry.host_cpu_time_us > limit
{
log_service.log(
ts_ms,
telemetry.frame_index,
LogLevel::Warn,
LogSource::Pos,
0xCA03,
format!(
"Cert: host_cpu_time_us exceeded limit ({} > {})",
telemetry.host_cpu_time_us, limit
),
);
violations += 1;
}
// 4. GLYPH bank slots
if let Some(limit) = self.config.max_glyph_slots_used
&& telemetry.glyph_slots_used > limit
{
log_service.log(
ts_ms,
telemetry.frame_index,
LogLevel::Warn,
LogSource::Pos,
0xCA04,
format!(
"Cert: GLYPH bank exceeded slot limit ({} > {})",
telemetry.glyph_slots_used, limit
),
);
violations += 1;
}
// 5. SOUNDS bank slots
if let Some(limit) = self.config.max_sound_slots_used
&& telemetry.sound_slots_used > limit
{
log_service.log(
ts_ms,
telemetry.frame_index,
LogLevel::Warn,
LogSource::Pos,
0xCA05,
format!(
"Cert: SOUNDS bank exceeded slot limit ({} > {})",
telemetry.sound_slots_used, limit
),
);
violations += 1;
}
// 6. Heap Memory
if let Some(limit) = self.config.max_heap_bytes
&& telemetry.heap_used_bytes > limit
{
log_service.log(
ts_ms,
telemetry.frame_index,
LogLevel::Warn,
LogSource::Pos,
0xCA06,
format!(
"Cert: Heap memory exceeded limit ({} > {})",
telemetry.heap_used_bytes, limit
),
);
violations += 1;
}
// 7. Log Pressure
if let Some(limit) = self.config.max_logs_per_frame
&& telemetry.logs_count > limit
{
log_service.log(
ts_ms,
telemetry.frame_index,
LogLevel::Warn,
LogSource::Pos,
0xCA07,
format!("Cert: Log pressure exceeded limit ({} > {})", telemetry.logs_count, limit),
);
violations += 1;
}
violations
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::log::LogService;
#[test]
fn test_certifier_violations() {
let mut ls = LogService::new(10);
let config = CertificationConfig {
enabled: true,
cycles_budget_per_frame: Some(100),
max_syscalls_per_frame: Some(5),
max_host_cpu_us_per_frame: Some(1000),
max_glyph_slots_used: Some(1),
..Default::default()
};
let cert = Certifier::new(config);
let mut tel = TelemetryFrame::default();
tel.cycles_used = 150;
tel.syscalls = 10;
tel.host_cpu_time_us = 500;
tel.glyph_slots_used = 2;
let violations = cert.evaluate(&tel, &mut ls, 1000);
assert_eq!(violations, 3);
let logs = ls.get_recent(10);
assert_eq!(logs.len(), 3);
assert!(logs[0].msg.contains("cycles_used"));
assert!(logs[1].msg.contains("syscalls"));
assert!(logs[2].msg.contains("GLYPH bank"));
}
#[test]
fn snapshot_uses_persisted_last_frame_logs() {
let current = Arc::new(AtomicU32::new(7));
let tel = AtomicTelemetry::new(Arc::clone(&current));
tel.logs_count.store(3, Ordering::Relaxed);
let snapshot = tel.snapshot();
assert_eq!(snapshot.logs_count, 3);
assert_eq!(current.load(Ordering::Relaxed), 7);
}
}

View File

@ -1,10 +0,0 @@
use crate::glyph::Glyph;
use serde::{Deserialize, Serialize};
#[derive(Clone, Copy, Debug, Default, Deserialize, Serialize)]
pub struct Tile {
pub active: bool,
pub glyph: Glyph,
pub flip_x: bool,
pub flip_y: bool,
}

View File

@ -1,33 +0,0 @@
use crate::tile::Tile;
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct TileMap {
pub width: usize,
pub height: usize,
pub tiles: Vec<Tile>,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::glyph::Glyph;
#[test]
fn tilemap_tile_write_and_read_remain_canonical() {
let mut map = TileMap { width: 2, height: 2, tiles: vec![Tile::default(); 4] };
let index = 3;
map.tiles[index] = Tile {
active: true,
glyph: Glyph { glyph_id: 99, palette_id: 5 },
flip_x: true,
flip_y: false,
};
assert_eq!(map.tiles[index].glyph.glyph_id, 99);
assert_eq!(map.tiles[index].glyph.palette_id, 5);
assert!(map.tiles[index].flip_x);
assert!(map.tiles[index].active);
}
}

View File

@ -1,9 +0,0 @@
use crate::button::Button;
use crate::input_signals::InputSignals;
pub trait TouchBridge {
fn begin_frame(&mut self, signals: &InputSignals);
fn f(&self) -> &Button;
fn x(&self) -> i32;
fn y(&self) -> i32;
}

View File

@ -1,6 +0,0 @@
#[derive(Debug, PartialEq, Clone)]
pub enum VmFault {
Trap(u32, String),
Panic(String),
Unavailable,
}

View File

@ -1,14 +0,0 @@
[package]
name = "prometeu-system"
version = "0.1.0"
edition = "2024"
license.workspace = true
[dependencies]
serde_json = "1.0.149"
prometeu-vm = { path = "../prometeu-vm" }
prometeu-bytecode = { path = "../prometeu-bytecode" }
prometeu-hal = { path = "../prometeu-hal" }
[dev-dependencies]
prometeu-drivers = { path = "../prometeu-drivers" }

View File

@ -1,55 +0,0 @@
use prometeu_bytecode::TrapInfo;
use prometeu_vm::VmInitError;
use std::fmt;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CrashReport {
VmTrap { trap: TrapInfo },
VmPanic { message: String, pc: Option<u32> },
VmInit { error: VmInitError },
}
impl CrashReport {
pub fn kind(&self) -> &'static str {
match self {
Self::VmTrap { .. } => "vm_trap",
Self::VmPanic { .. } => "vm_panic",
Self::VmInit { .. } => "vm_init",
}
}
pub fn pc(&self) -> Option<u32> {
match self {
Self::VmTrap { trap } => Some(trap.pc),
Self::VmPanic { pc, .. } => *pc,
Self::VmInit { .. } => None,
}
}
pub fn summary(&self) -> String {
match self {
Self::VmTrap { trap } => format!(
"PVM Trap 0x{:08X} at PC 0x{:X} (opcode 0x{:04X}): {}",
trap.code, trap.pc, trap.opcode, trap.message
),
Self::VmPanic { message, pc: Some(pc) } => {
format!("PVM Panic at PC 0x{:X}: {}", pc, message)
}
Self::VmPanic { message, pc: None } => format!("PVM Panic: {}", message),
Self::VmInit { error } => format!("PVM Init Error: {:?}", error),
}
}
pub fn log_tag(&self) -> u16 {
match self {
Self::VmTrap { trap } => trap.code as u16,
Self::VmPanic { .. } | Self::VmInit { .. } => 0,
}
}
}
impl fmt::Display for CrashReport {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.summary())
}
}

View File

@ -1,9 +0,0 @@
mod crash_report;
mod programs;
mod services;
mod virtual_machine_runtime;
pub use crash_report::CrashReport;
pub use programs::PrometeuHub;
pub use services::fs;
pub use virtual_machine_runtime::VirtualMachineRuntime;

View File

@ -1,3 +0,0 @@
mod prometeu_hub;
pub use prometeu_hub::PrometeuHub;

View File

@ -1,5 +0,0 @@
#[allow(clippy::module_inception)]
mod prometeu_hub;
mod window_manager;
pub use prometeu_hub::PrometeuHub;

View File

@ -1,78 +0,0 @@
use crate::VirtualMachineRuntime;
use crate::programs::prometeu_hub::window_manager::WindowManager;
use prometeu_hal::HardwareBridge;
use prometeu_hal::color::Color;
use prometeu_hal::log::{LogLevel, LogSource};
use prometeu_hal::window::Rect;
/// PrometeuHub: Launcher and system UI environment.
pub struct PrometeuHub {
pub window_manager: WindowManager,
}
impl Default for PrometeuHub {
fn default() -> Self {
Self::new()
}
}
impl PrometeuHub {
pub fn new() -> Self {
Self { window_manager: WindowManager::new() }
}
pub fn init(&mut self) {
// Initializes the Window System and lists apps
}
pub fn gui_update(&mut self, os: &mut VirtualMachineRuntime, hw: &mut dyn HardwareBridge) {
hw.gfx_mut().clear(Color::BLACK);
let mut next_window = None;
if hw.pad().a().pressed {
os.log(LogLevel::Debug, LogSource::Hub, 0, "window A opened".to_string());
next_window = Some((
"Green Window".to_string(),
Rect { x: 0, y: 0, w: 160, h: 90 },
Color::GREEN,
));
} else if hw.pad().b().pressed {
os.log(LogLevel::Debug, LogSource::Hub, 0, "window B opened".to_string());
next_window = Some((
"Indigo Window".to_string(),
Rect { x: 160, y: 0, w: 160, h: 90 },
Color::INDIGO,
));
} else if hw.pad().x().pressed {
os.log(LogLevel::Debug, LogSource::Hub, 0, "window X opened".to_string());
next_window = Some((
"Yellow Window".to_string(),
Rect { x: 0, y: 90, w: 160, h: 90 },
Color::YELLOW,
));
} else if hw.pad().y().pressed {
os.log(LogLevel::Debug, LogSource::Hub, 0, "window Y opened".to_string());
next_window =
Some(("Red Window".to_string(), Rect { x: 160, y: 90, w: 160, h: 90 }, Color::RED));
}
if let Some((title, rect, color)) = next_window {
self.window_manager.remove_all_windows();
let id = self.window_manager.add_window(title, rect, color);
self.window_manager.set_focus(id);
}
}
pub fn render(&mut self, _os: &mut VirtualMachineRuntime, hw: &mut dyn HardwareBridge) {
for window in &self.window_manager.windows {
hw.gfx_mut().fill_rect(
window.viewport.x,
window.viewport.y,
window.viewport.w,
window.viewport.h,
window.color,
);
}
}
}

View File

@ -1,83 +0,0 @@
use prometeu_hal::color::Color;
use prometeu_hal::window::{Rect, Window, WindowId};
/// PROMETEU Window Manager.
pub struct WindowManager {
pub windows: Vec<Window>,
pub focused: Option<WindowId>,
}
impl WindowManager {
pub fn new() -> Self {
Self { windows: Vec::new(), focused: None }
}
pub fn add_window(&mut self, title: String, viewport: Rect, color: Color) -> WindowId {
let id = WindowId(self.windows.len() as u32);
let window = Window { id, viewport, has_focus: false, title, color };
self.windows.push(window);
id
}
pub fn remove_window(&mut self, id: WindowId) {
self.windows.retain(|w| w.id != id);
if self.focused == Some(id) {
self.focused = None;
}
}
pub fn remove_all_windows(&mut self) {
self.windows.clear();
self.focused = None;
}
pub fn set_focus(&mut self, id: WindowId) {
self.focused = Some(id);
for window in &mut self.windows {
window.has_focus = window.id == id;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_window_manager_focus() {
let mut wm = WindowManager::new();
let id1 =
wm.add_window("Window 1".to_string(), Rect { x: 0, y: 0, w: 10, h: 10 }, Color::WHITE);
let id2 = wm.add_window(
"Window 2".to_string(),
Rect { x: 10, y: 10, w: 10, h: 10 },
Color::WHITE,
);
assert_eq!(wm.windows.len(), 2);
assert_eq!(wm.focused, None);
wm.set_focus(id1);
assert_eq!(wm.focused, Some(id1));
assert!(wm.windows[0].has_focus);
assert!(!wm.windows[1].has_focus);
wm.set_focus(id2);
assert_eq!(wm.focused, Some(id2));
assert!(!wm.windows[0].has_focus);
assert!(wm.windows[1].has_focus);
}
#[test]
fn test_window_manager_remove_window() {
let mut wm = WindowManager::new();
let id =
wm.add_window("Window".to_string(), Rect { x: 0, y: 0, w: 10, h: 10 }, Color::WHITE);
wm.set_focus(id);
assert_eq!(wm.focused, Some(id));
wm.remove_window(id);
assert_eq!(wm.windows.len(), 0);
assert_eq!(wm.focused, None);
}
}

View File

@ -1,245 +0,0 @@
use crate::fs::{FsBackend, FsEntry, FsError};
/// Virtual Filesystem (VFS) interface for Prometeu.
///
/// The VFS provides a sandboxed, unified path interface for user applications.
/// Instead of interacting directly with the host's disk, the VM uses
/// normalized paths (e.g., `/user/save.dat`).
///
/// The actual storage is provided by an `FsBackend`, which can be a real
/// directory on disk, an in-memory map, or even a network resource.
pub struct VirtualFS {
/// The active storage implementation.
backend: Option<Box<dyn FsBackend>>,
}
impl Default for VirtualFS {
fn default() -> Self {
Self::new()
}
}
impl VirtualFS {
pub fn new() -> Self {
Self { backend: None }
}
pub fn mount(&mut self, mut backend: Box<dyn FsBackend>) -> Result<(), FsError> {
backend.mount()?;
self.backend = Some(backend);
Ok(())
}
pub fn unmount(&mut self) {
if let Some(mut backend) = self.backend.take() {
backend.unmount();
}
}
pub fn is_mounted(&self) -> bool {
self.backend.is_some()
}
fn normalize_path(path: &str) -> Result<String, FsError> {
let mut normalized = path.replace('\\', "/");
if !normalized.starts_with('/') {
normalized = format!("/{}", normalized);
}
let mut segments = Vec::new();
for segment in normalized.split('/') {
match segment {
"" | "." => continue,
".." => {
return Err(FsError::InvalidPath(
"parent traversal '..' is not allowed".to_string(),
));
}
_ => segments.push(segment),
}
}
if segments.is_empty() {
Ok("/".to_string())
} else {
Ok(format!("/{}", segments.join("/")))
}
}
pub fn list_dir(&self, path: &str) -> Result<Vec<FsEntry>, FsError> {
let normalized = Self::normalize_path(path)?;
let backend = self.backend.as_ref().ok_or(FsError::NotMounted)?;
backend.list_dir(&normalized)
}
pub fn read_file(&self, path: &str) -> Result<Vec<u8>, FsError> {
let normalized = Self::normalize_path(path)?;
let backend = self.backend.as_ref().ok_or(FsError::NotMounted)?;
backend.read_file(&normalized)
}
pub fn write_file(&mut self, path: &str, data: &[u8]) -> Result<(), FsError> {
let normalized = Self::normalize_path(path)?;
let backend = self.backend.as_mut().ok_or(FsError::NotMounted)?;
backend.write_file(&normalized, data)
}
pub fn delete(&mut self, path: &str) -> Result<(), FsError> {
let normalized = Self::normalize_path(path)?;
if normalized == "/" {
return Err(FsError::PermissionDenied);
}
let backend = self.backend.as_mut().ok_or(FsError::NotMounted)?;
backend.delete(&normalized)
}
pub fn exists(&self, path: &str) -> bool {
let Ok(normalized) = Self::normalize_path(path) else {
return false;
};
self.backend.as_ref().map(|b| b.exists(&normalized)).unwrap_or(false)
}
pub fn is_healthy(&self) -> bool {
self.backend.as_ref().map(|b| b.is_healthy()).unwrap_or(false)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashMap;
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
#[derive(Default)]
struct CallCounters {
list_dir: AtomicUsize,
read_file: AtomicUsize,
write_file: AtomicUsize,
delete: AtomicUsize,
exists: AtomicUsize,
}
struct MockBackend {
files: HashMap<String, Vec<u8>>,
healthy: bool,
calls: Arc<CallCounters>,
}
impl MockBackend {
fn new() -> Self {
Self::with_calls(Arc::new(CallCounters::default()))
}
fn with_calls(calls: Arc<CallCounters>) -> Self {
Self { files: HashMap::new(), healthy: true, calls }
}
}
impl FsBackend for MockBackend {
fn mount(&mut self) -> Result<(), FsError> {
Ok(())
}
fn unmount(&mut self) {}
fn list_dir(&self, _path: &str) -> Result<Vec<FsEntry>, FsError> {
self.calls.list_dir.fetch_add(1, Ordering::Relaxed);
Ok(self
.files
.keys()
.map(|name| FsEntry { name: name.clone(), is_dir: false, size: 0 })
.collect())
}
fn read_file(&self, path: &str) -> Result<Vec<u8>, FsError> {
self.calls.read_file.fetch_add(1, Ordering::Relaxed);
self.files.get(path).cloned().ok_or(FsError::NotFound)
}
fn write_file(&mut self, path: &str, data: &[u8]) -> Result<(), FsError> {
self.calls.write_file.fetch_add(1, Ordering::Relaxed);
self.files.insert(path.to_string(), data.to_vec());
Ok(())
}
fn delete(&mut self, path: &str) -> Result<(), FsError> {
self.calls.delete.fetch_add(1, Ordering::Relaxed);
self.files.remove(path);
Ok(())
}
fn exists(&self, path: &str) -> bool {
self.calls.exists.fetch_add(1, Ordering::Relaxed);
self.files.contains_key(path)
}
fn is_healthy(&self) -> bool {
self.healthy
}
}
#[test]
fn test_virtual_fs_operations() {
let mut vfs = VirtualFS::new();
let backend = MockBackend::new();
vfs.mount(Box::new(backend)).unwrap();
let test_file = "/user/test.txt";
let content = b"hello world";
vfs.write_file(test_file, content).unwrap();
assert!(vfs.exists(test_file));
let read_content = vfs.read_file(test_file).unwrap();
assert_eq!(read_content, content);
vfs.delete(test_file).unwrap();
assert!(!vfs.exists(test_file));
}
#[test]
fn test_virtual_fs_health() {
let mut vfs = VirtualFS::new();
let mut backend = MockBackend::new();
backend.healthy = false;
vfs.mount(Box::new(backend)).unwrap();
assert!(!vfs.is_healthy());
}
#[test]
fn test_normalize_path_rejects_parent_traversal() {
for path in ["../x", "/../x", "/user/../../x", "\\user\\..\\..\\x"] {
let error = VirtualFS::normalize_path(path).unwrap_err();
assert!(matches!(error, FsError::InvalidPath(_)));
}
}
#[test]
fn test_invalid_paths_never_reach_backend() {
let calls = Arc::new(CallCounters::default());
let mut vfs = VirtualFS::new();
vfs.mount(Box::new(MockBackend::with_calls(calls.clone()))).unwrap();
assert!(matches!(vfs.read_file("../secret.txt"), Err(FsError::InvalidPath(_))));
assert!(matches!(
vfs.write_file("/user/../../secret.txt", b"nope"),
Err(FsError::InvalidPath(_))
));
assert!(matches!(vfs.delete("\\user\\..\\secret.txt"), Err(FsError::InvalidPath(_))));
assert!(matches!(vfs.list_dir("/../"), Err(FsError::InvalidPath(_))));
assert!(!vfs.exists("../x"));
assert_eq!(calls.read_file.load(Ordering::Relaxed), 0);
assert_eq!(calls.write_file.load(Ordering::Relaxed), 0);
assert_eq!(calls.delete.load(Ordering::Relaxed), 0);
assert_eq!(calls.list_dir.load(Ordering::Relaxed), 0);
assert_eq!(calls.exists.load(Ordering::Relaxed), 0);
}
#[test]
fn test_delete_root_is_rejected_before_backend() {
let calls = Arc::new(CallCounters::default());
let mut vfs = VirtualFS::new();
vfs.mount(Box::new(MockBackend::with_calls(calls.clone()))).unwrap();
assert!(matches!(vfs.delete("/"), Err(FsError::PermissionDenied)));
assert_eq!(calls.delete.load(Ordering::Relaxed), 0);
}
}

View File

@ -1,431 +0,0 @@
use crate::fs::{FsError, VirtualFS};
use std::collections::HashMap;
pub const MEMCARD_SLOT_COUNT: usize = 32;
pub const MEMCARD_SLOT_CAPACITY_BYTES: usize = 32 * 1024;
const SLOT_FILE_MAGIC: &[u8; 4] = b"PMMS";
const SLOT_FILE_VERSION: u8 = 1;
const SLOT_HEADER_SIZE: usize = 4 + 1 + 16 + 8 + 4 + 4;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(i32)]
pub enum MemcardStatus {
Ok = 0,
Empty = 1,
NotFound = 2,
NoSpace = 3,
AccessDenied = 4,
Corrupt = 5,
Conflict = 6,
Unavailable = 7,
InvalidState = 8,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(i32)]
pub enum MemcardSlotState {
Empty = 0,
Staged = 1,
Committed = 2,
Corrupt = 3,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct MemcardSlotStat {
pub state: MemcardSlotState,
pub used_bytes: u32,
pub generation: u64,
pub checksum: u32,
}
pub struct MemcardReadResult {
pub status: MemcardStatus,
pub bytes: Vec<u8>,
pub bytes_read: u32,
}
pub struct MemcardWriteResult {
pub status: MemcardStatus,
pub bytes_written: u32,
}
#[derive(Debug, Clone)]
struct SlotImage {
payload: Vec<u8>,
generation: u64,
checksum: u32,
save_uuid: [u8; 16],
}
#[derive(Default)]
pub struct MemcardService {
staged: HashMap<(u32, u8), Vec<u8>>,
}
impl MemcardService {
pub fn new() -> Self {
Self::default()
}
pub fn clear_all_staging(&mut self) {
self.staged.clear();
}
pub fn slot_count(&self) -> usize {
MEMCARD_SLOT_COUNT
}
pub fn slot_stat(&self, fs: &VirtualFS, app_id: u32, slot: u8) -> MemcardSlotStat {
if let Some(staged_payload) = self.staged.get(&(app_id, slot)) {
let generation = match self.load_committed(fs, app_id, slot) {
Ok(Some(committed)) => committed.generation.saturating_add(1),
_ => 1,
};
return MemcardSlotStat {
state: MemcardSlotState::Staged,
used_bytes: staged_payload.len() as u32,
generation,
checksum: checksum32(staged_payload),
};
}
match self.load_committed(fs, app_id, slot) {
Ok(Some(committed)) => MemcardSlotStat {
state: MemcardSlotState::Committed,
used_bytes: committed.payload.len() as u32,
generation: committed.generation,
checksum: committed.checksum,
},
Ok(None) => MemcardSlotStat {
state: MemcardSlotState::Empty,
used_bytes: 0,
generation: 0,
checksum: 0,
},
Err(MemcardStatus::Corrupt) => MemcardSlotStat {
state: MemcardSlotState::Corrupt,
used_bytes: 0,
generation: 0,
checksum: 0,
},
Err(_) => MemcardSlotStat {
state: MemcardSlotState::Empty,
used_bytes: 0,
generation: 0,
checksum: 0,
},
}
}
pub fn slot_read(
&self,
fs: &VirtualFS,
app_id: u32,
slot: u8,
offset: usize,
max_bytes: usize,
) -> MemcardReadResult {
if let Some(staged_payload) = self.staged.get(&(app_id, slot)) {
return Self::slice_payload(staged_payload, offset, max_bytes);
}
match self.load_committed(fs, app_id, slot) {
Ok(Some(committed)) => Self::slice_payload(&committed.payload, offset, max_bytes),
Ok(None) => {
MemcardReadResult { status: MemcardStatus::Empty, bytes: Vec::new(), bytes_read: 0 }
}
Err(status) => MemcardReadResult { status, bytes: Vec::new(), bytes_read: 0 },
}
}
pub fn slot_write(
&mut self,
fs: &VirtualFS,
app_id: u32,
slot: u8,
offset: usize,
data: &[u8],
) -> MemcardWriteResult {
let end = match offset.checked_add(data.len()) {
Some(v) => v,
None => {
return MemcardWriteResult { status: MemcardStatus::NoSpace, bytes_written: 0 };
}
};
if end > MEMCARD_SLOT_CAPACITY_BYTES {
return MemcardWriteResult { status: MemcardStatus::NoSpace, bytes_written: 0 };
}
let mut payload = if let Some(staged_payload) = self.staged.get(&(app_id, slot)) {
staged_payload.clone()
} else {
match self.load_committed(fs, app_id, slot) {
Ok(Some(committed)) => committed.payload,
Ok(None) => Vec::new(),
Err(status) => {
return MemcardWriteResult { status, bytes_written: 0 };
}
}
};
if offset > payload.len() {
payload.resize(offset, 0);
}
if end > payload.len() {
payload.resize(end, 0);
}
payload[offset..end].copy_from_slice(data);
self.staged.insert((app_id, slot), payload);
MemcardWriteResult { status: MemcardStatus::Ok, bytes_written: data.len() as u32 }
}
pub fn slot_commit(&mut self, fs: &mut VirtualFS, app_id: u32, slot: u8) -> MemcardStatus {
let Some(staged_payload) = self.staged.get(&(app_id, slot)).cloned() else {
return MemcardStatus::InvalidState;
};
let (save_uuid, generation) = match self.load_committed(fs, app_id, slot) {
Ok(Some(committed)) => (committed.save_uuid, committed.generation.saturating_add(1)),
Ok(None) => (make_save_uuid(app_id, slot), 1),
Err(status) => return status,
};
let checksum = checksum32(&staged_payload);
let encoded = encode_slot_file(SlotImage {
payload: staged_payload,
generation,
checksum,
save_uuid,
});
let path = slot_path(app_id, slot);
match fs.write_file(&path, &encoded) {
Ok(()) => {
self.staged.remove(&(app_id, slot));
MemcardStatus::Ok
}
Err(err) => map_fs_error(err),
}
}
pub fn slot_clear(&mut self, fs: &mut VirtualFS, app_id: u32, slot: u8) -> MemcardStatus {
self.staged.remove(&(app_id, slot));
let path = slot_path(app_id, slot);
match fs.delete(&path) {
Ok(()) => MemcardStatus::Ok,
Err(FsError::NotFound) => MemcardStatus::Empty,
Err(err) => map_fs_error(err),
}
}
fn slice_payload(payload: &[u8], offset: usize, max_bytes: usize) -> MemcardReadResult {
if offset >= payload.len() || max_bytes == 0 {
return MemcardReadResult {
status: MemcardStatus::Ok,
bytes: Vec::new(),
bytes_read: 0,
};
}
let end = payload.len().min(offset.saturating_add(max_bytes));
let bytes = payload[offset..end].to_vec();
MemcardReadResult { status: MemcardStatus::Ok, bytes_read: bytes.len() as u32, bytes }
}
fn load_committed(
&self,
fs: &VirtualFS,
app_id: u32,
slot: u8,
) -> Result<Option<SlotImage>, MemcardStatus> {
let path = slot_path(app_id, slot);
match fs.read_file(&path) {
Ok(bytes) => decode_slot_file(&bytes).map(Some),
Err(FsError::NotFound) => Ok(None),
Err(err) => Err(map_fs_error(err)),
}
}
}
fn slot_path(app_id: u32, slot: u8) -> String {
format!("/user/games/{}/memcard/slot_{:02}.pmem", app_id, slot)
}
fn map_fs_error(err: FsError) -> MemcardStatus {
match err {
FsError::NotFound => MemcardStatus::NotFound,
FsError::AlreadyExists => MemcardStatus::Conflict,
FsError::PermissionDenied => MemcardStatus::AccessDenied,
FsError::NotMounted | FsError::IOError(_) | FsError::Other(_) | FsError::InvalidPath(_) => {
MemcardStatus::Unavailable
}
}
}
fn checksum32(data: &[u8]) -> u32 {
let mut a: u32 = 1;
let mut b: u32 = 0;
const MOD: u32 = 65521;
for &byte in data {
a = (a + byte as u32) % MOD;
b = (b + a) % MOD;
}
(b << 16) | a
}
fn make_save_uuid(app_id: u32, slot: u8) -> [u8; 16] {
let mut out = [0u8; 16];
out[0..4].copy_from_slice(&app_id.to_le_bytes());
out[4] = slot;
out[5..13].copy_from_slice(
&(std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos() as u64)
.unwrap_or(0))
.to_le_bytes(),
);
out[13] = 0x50;
out[14] = 0x4D;
out[15] = 0x31;
out
}
fn encode_slot_file(slot: SlotImage) -> Vec<u8> {
let mut out = Vec::with_capacity(SLOT_HEADER_SIZE + slot.payload.len());
out.extend_from_slice(SLOT_FILE_MAGIC);
out.push(SLOT_FILE_VERSION);
out.extend_from_slice(&slot.save_uuid);
out.extend_from_slice(&slot.generation.to_le_bytes());
out.extend_from_slice(&slot.checksum.to_le_bytes());
out.extend_from_slice(&(slot.payload.len() as u32).to_le_bytes());
out.extend_from_slice(&slot.payload);
out
}
fn decode_slot_file(bytes: &[u8]) -> Result<SlotImage, MemcardStatus> {
if bytes.len() < SLOT_HEADER_SIZE {
return Err(MemcardStatus::Corrupt);
}
if &bytes[0..4] != SLOT_FILE_MAGIC {
return Err(MemcardStatus::Corrupt);
}
if bytes[4] != SLOT_FILE_VERSION {
return Err(MemcardStatus::Corrupt);
}
let mut save_uuid = [0u8; 16];
save_uuid.copy_from_slice(&bytes[5..21]);
let generation =
u64::from_le_bytes(bytes[21..29].try_into().map_err(|_| MemcardStatus::Corrupt)?);
let checksum =
u32::from_le_bytes(bytes[29..33].try_into().map_err(|_| MemcardStatus::Corrupt)?);
let payload_size =
u32::from_le_bytes(bytes[33..37].try_into().map_err(|_| MemcardStatus::Corrupt)?) as usize;
if payload_size > MEMCARD_SLOT_CAPACITY_BYTES {
return Err(MemcardStatus::Corrupt);
}
if bytes.len() != SLOT_HEADER_SIZE + payload_size {
return Err(MemcardStatus::Corrupt);
}
let payload = bytes[SLOT_HEADER_SIZE..].to_vec();
if checksum32(&payload) != checksum {
return Err(MemcardStatus::Corrupt);
}
Ok(SlotImage { payload, generation, checksum, save_uuid })
}
#[cfg(test)]
mod tests {
use super::*;
use crate::fs::{FsBackend, FsEntry};
use std::collections::HashMap;
struct MockBackend {
files: HashMap<String, Vec<u8>>,
}
impl MockBackend {
fn new() -> Self {
Self { files: HashMap::new() }
}
}
impl FsBackend for MockBackend {
fn mount(&mut self) -> Result<(), FsError> {
Ok(())
}
fn unmount(&mut self) {}
fn list_dir(&self, _path: &str) -> Result<Vec<FsEntry>, FsError> {
Ok(Vec::new())
}
fn read_file(&self, path: &str) -> Result<Vec<u8>, FsError> {
self.files.get(path).cloned().ok_or(FsError::NotFound)
}
fn write_file(&mut self, path: &str, data: &[u8]) -> Result<(), FsError> {
self.files.insert(path.to_string(), data.to_vec());
Ok(())
}
fn delete(&mut self, path: &str) -> Result<(), FsError> {
self.files.remove(path).map(|_| ()).ok_or(FsError::NotFound)
}
fn exists(&self, path: &str) -> bool {
self.files.contains_key(path)
}
fn is_healthy(&self) -> bool {
true
}
}
#[test]
fn slot_roundtrip_commit_and_generation() {
let mut fs = VirtualFS::new();
fs.mount(Box::new(MockBackend::new())).expect("mount");
let mut mem = MemcardService::new();
let write = mem.slot_write(&fs, 10, 0, 0, b"hello");
assert_eq!(write.status, MemcardStatus::Ok);
assert_eq!(write.bytes_written, 5);
let staged = mem.slot_stat(&fs, 10, 0);
assert_eq!(staged.state, MemcardSlotState::Staged);
assert_eq!(staged.generation, 1);
assert_eq!(mem.slot_commit(&mut fs, 10, 0), MemcardStatus::Ok);
let committed = mem.slot_stat(&fs, 10, 0);
assert_eq!(committed.state, MemcardSlotState::Committed);
assert_eq!(committed.generation, 1);
let write2 = mem.slot_write(&fs, 10, 0, 5, b"!");
assert_eq!(write2.status, MemcardStatus::Ok);
assert_eq!(mem.slot_commit(&mut fs, 10, 0), MemcardStatus::Ok);
let committed2 = mem.slot_stat(&fs, 10, 0);
assert_eq!(committed2.generation, 2);
let read = mem.slot_read(&fs, 10, 0, 0, 10);
assert_eq!(read.status, MemcardStatus::Ok);
assert_eq!(read.bytes, b"hello!");
assert_eq!(read.bytes_read, 6);
}
#[test]
fn slot_clear_missing_is_empty() {
let mut fs = VirtualFS::new();
fs.mount(Box::new(MockBackend::new())).expect("mount");
let mut mem = MemcardService::new();
assert_eq!(mem.slot_clear(&mut fs, 1, 1), MemcardStatus::Empty);
}
#[test]
fn slot_stat_reports_corruption() {
let mut fs = VirtualFS::new();
let mut backend = MockBackend::new();
backend.files.insert(slot_path(7, 2), vec![0, 1, 2, 3, 4]);
fs.mount(Box::new(backend)).expect("mount");
let mem = MemcardService::new();
let stat = mem.slot_stat(&fs, 7, 2);
assert_eq!(stat.state, MemcardSlotState::Corrupt);
}
}

View File

@ -1,2 +0,0 @@
pub mod fs;
pub mod memcard;

View File

@ -1,50 +0,0 @@
mod dispatch;
mod lifecycle;
#[cfg(test)]
mod tests;
mod tick;
use crate::CrashReport;
use crate::fs::{FsState, VirtualFS};
use crate::services::memcard::MemcardService;
use prometeu_hal::cartridge::AppMode;
use prometeu_hal::log::LogService;
use prometeu_hal::telemetry::{AtomicTelemetry, CertificationConfig, Certifier};
use prometeu_vm::VirtualMachine;
use std::collections::HashMap;
use std::sync::Arc;
use std::time::Instant;
pub struct VirtualMachineRuntime {
pub tick_index: u64,
pub logical_frame_index: u64,
pub logical_frame_active: bool,
pub logical_frame_remaining_cycles: u64,
pub last_frame_cpu_time_us: u64,
pub fs: VirtualFS,
pub fs_state: FsState,
pub memcard: MemcardService,
pub open_files: HashMap<u32, String>,
pub next_handle: u32,
pub log_service: LogService,
pub current_app_id: u32,
pub current_cartridge_title: String,
pub current_cartridge_app_version: String,
pub current_cartridge_app_mode: AppMode,
pub logs_written_this_frame: HashMap<u32, u32>,
pub atomic_telemetry: Arc<AtomicTelemetry>,
pub last_crash_report: Option<CrashReport>,
pub certifier: Certifier,
pub paused: bool,
pub debug_step_request: bool,
pub inspection_active: bool,
pub(crate) needs_prepare_entry_call: bool,
pub(crate) boot_time: Instant,
}
impl VirtualMachineRuntime {
pub const CYCLES_PER_LOGICAL_FRAME: u64 = 1_500_000;
pub const SLICE_PER_TICK: u64 = 1_500_000;
pub const MAX_LOG_LEN: usize = 256;
pub const MAX_LOGS_PER_FRAME: u32 = 10;
}

View File

@ -1,658 +0,0 @@
use super::*;
use crate::services::memcard::{MemcardSlotState, MemcardStatus};
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, ComposerOpStatus, HostContext, HostReturn, NativeInterface, SyscallId,
expect_bool, expect_int,
};
use std::sync::atomic::Ordering;
impl VirtualMachineRuntime {
fn syscall_log_write(&mut self, level_val: i64, tag: u16, msg: String) -> Result<(), VmFault> {
let level = match level_val {
0 => LogLevel::Trace,
1 => LogLevel::Debug,
2 => LogLevel::Info,
3 => LogLevel::Warn,
4 => LogLevel::Error,
_ => return Err(VmFault::Trap(TRAP_TYPE, format!("Invalid log level: {}", level_val))),
};
let app_id = self.current_app_id;
let count = *self.logs_written_this_frame.get(&app_id).unwrap_or(&0);
if count >= Self::MAX_LOGS_PER_FRAME {
if count == Self::MAX_LOGS_PER_FRAME {
self.logs_written_this_frame.insert(app_id, count + 1);
self.log(
LogLevel::Warn,
LogSource::App { app_id },
0,
"App exceeded log limit per frame".to_string(),
);
}
return Ok(());
}
self.logs_written_this_frame.insert(app_id, count + 1);
let mut final_msg = msg;
if final_msg.len() > Self::MAX_LOG_LEN {
final_msg.truncate(Self::MAX_LOG_LEN);
}
self.log(level, LogSource::App { app_id }, tag, final_msg);
Ok(())
}
pub(crate) fn get_color(&self, value: i64) -> Color {
Color::from_raw(value as u16)
}
fn int_arg_to_usize_status(value: i64) -> Result<usize, ComposerOpStatus> {
usize::try_from(value).map_err(|_| ComposerOpStatus::ArgRangeInvalid)
}
fn int_arg_to_i32_trap(value: i64, name: &str) -> Result<i32, VmFault> {
i32::try_from(value)
.map_err(|_| VmFault::Trap(TRAP_OOB, format!("{name} value out of bounds")))
}
fn int_arg_to_u8_status(value: i64) -> Result<u8, ComposerOpStatus> {
u8::try_from(value).map_err(|_| ComposerOpStatus::ArgRangeInvalid)
}
fn int_arg_to_u16_status(value: i64) -> Result<u16, ComposerOpStatus> {
u16::try_from(value).map_err(|_| ComposerOpStatus::ArgRangeInvalid)
}
}
impl NativeInterface for VirtualMachineRuntime {
fn syscall(
&mut self,
id: SyscallId,
args: &[Value],
ret: &mut HostReturn,
ctx: &mut HostContext,
) -> Result<(), VmFault> {
self.atomic_telemetry.syscalls.fetch_add(1, Ordering::Relaxed);
let syscall = Syscall::from_u32(id).ok_or_else(|| {
VmFault::Trap(TRAP_INVALID_SYSCALL, format!("Unknown syscall: 0x{:08X}", id))
})?;
match syscall {
Syscall::SystemHasCart => {
ret.push_bool(true);
return Ok(());
}
Syscall::SystemRunCart => return Ok(()),
_ => {}
}
let hw = ctx.require_hw()?;
match syscall {
Syscall::SystemHasCart => unreachable!(),
Syscall::SystemRunCart => unreachable!(),
Syscall::GfxClear => {
let color = self.get_color(expect_int(args, 0)?);
hw.gfx_mut().clear(color);
Ok(())
}
Syscall::GfxFillRect => {
let x = expect_int(args, 0)? as i32;
let y = expect_int(args, 1)? as i32;
let w = expect_int(args, 2)? as i32;
let h = expect_int(args, 3)? as i32;
let color = self.get_color(expect_int(args, 4)?);
hw.gfx_mut().fill_rect(x, y, w, h, color);
Ok(())
}
Syscall::GfxDrawLine => {
let x1 = expect_int(args, 0)? as i32;
let y1 = expect_int(args, 1)? as i32;
let x2 = expect_int(args, 2)? as i32;
let y2 = expect_int(args, 3)? as i32;
let color = self.get_color(expect_int(args, 4)?);
hw.gfx_mut().draw_line(x1, y1, x2, y2, color);
Ok(())
}
Syscall::GfxDrawCircle => {
let x = expect_int(args, 0)? as i32;
let y = expect_int(args, 1)? as i32;
let r = expect_int(args, 2)? as i32;
let color = self.get_color(expect_int(args, 3)?);
hw.gfx_mut().draw_circle(x, y, r, color);
Ok(())
}
Syscall::GfxDrawDisc => {
let x = expect_int(args, 0)? as i32;
let y = expect_int(args, 1)? as i32;
let r = expect_int(args, 2)? as i32;
let border_color = self.get_color(expect_int(args, 3)?);
let fill_color = self.get_color(expect_int(args, 4)?);
hw.gfx_mut().draw_disc(x, y, r, border_color, fill_color);
Ok(())
}
Syscall::GfxDrawSquare => {
let x = expect_int(args, 0)? as i32;
let y = expect_int(args, 1)? as i32;
let w = expect_int(args, 2)? as i32;
let h = expect_int(args, 3)? as i32;
let border_color = self.get_color(expect_int(args, 4)?);
let fill_color = self.get_color(expect_int(args, 5)?);
hw.gfx_mut().draw_square(x, y, w, h, border_color, fill_color);
Ok(())
}
Syscall::GfxDrawText => {
let x = expect_int(args, 0)? as i32;
let y = expect_int(args, 1)? as i32;
let msg = expect_string(args, 2, "message")?;
let color = self.get_color(expect_int(args, 3)?);
hw.gfx_mut().draw_text(x, y, &msg, color);
Ok(())
}
Syscall::GfxClear565 => {
let color_val = expect_int(args, 0)? as u32;
if color_val > 0xFFFF {
return Err(VmFault::Trap(TRAP_OOB, "Color value out of bounds".into()));
}
hw.gfx_mut().clear(Color::from_raw(color_val as u16));
Ok(())
}
Syscall::ComposerBindScene => {
let scene_bank_id = match Self::int_arg_to_usize_status(expect_int(args, 0)?) {
Ok(id) => id,
Err(status) => {
ret.push_int(status as i64);
return Ok(());
}
};
let status = if hw.bind_scene(scene_bank_id) {
ComposerOpStatus::Ok
} else {
ComposerOpStatus::SceneUnavailable
};
ret.push_int(status as i64);
Ok(())
}
Syscall::ComposerUnbindScene => {
hw.unbind_scene();
ret.push_int(ComposerOpStatus::Ok as i64);
Ok(())
}
Syscall::ComposerSetCamera => {
let x = Self::int_arg_to_i32_trap(expect_int(args, 0)?, "camera x")?;
let y = Self::int_arg_to_i32_trap(expect_int(args, 1)?, "camera y")?;
hw.set_camera(x, y);
Ok(())
}
Syscall::ComposerEmitSprite => {
let glyph_id = match Self::int_arg_to_u16_status(expect_int(args, 0)?) {
Ok(value) => value,
Err(status) => {
ret.push_int(status as i64);
return Ok(());
}
};
let palette_id = match Self::int_arg_to_u8_status(expect_int(args, 1)?) {
Ok(value) if value < 64 => value,
_ => {
ret.push_int(ComposerOpStatus::ArgRangeInvalid as i64);
return Ok(());
}
};
let x = Self::int_arg_to_i32_trap(expect_int(args, 2)?, "sprite x")?;
let y = Self::int_arg_to_i32_trap(expect_int(args, 3)?, "sprite y")?;
let layer = match Self::int_arg_to_u8_status(expect_int(args, 4)?) {
Ok(value) if value < 4 => value,
Ok(_) => {
ret.push_int(ComposerOpStatus::LayerInvalid as i64);
return Ok(());
}
Err(status) => {
ret.push_int(status as i64);
return Ok(());
}
};
let bank_id = match Self::int_arg_to_u8_status(expect_int(args, 5)?) {
Ok(value) => value,
Err(status) => {
ret.push_int(status as i64);
return Ok(());
}
};
let flip_x = expect_bool(args, 6)?;
let flip_y = expect_bool(args, 7)?;
let priority = match Self::int_arg_to_u8_status(expect_int(args, 8)?) {
Ok(value) => value,
Err(status) => {
ret.push_int(status as i64);
return Ok(());
}
};
if !hw.has_glyph_bank(bank_id as usize) {
ret.push_int(ComposerOpStatus::BankInvalid as i64);
return Ok(());
}
let emitted = hw.emit_sprite(Sprite {
glyph: Glyph { glyph_id, palette_id },
x,
y,
layer,
bank_id,
active: false,
flip_x,
flip_y,
priority,
});
let status =
if emitted { ComposerOpStatus::Ok } else { ComposerOpStatus::SpriteOverflow };
ret.push_int(status as i64);
Ok(())
}
Syscall::AudioPlaySample => {
let sample_id_raw = expect_int(args, 0)?;
let voice_id_raw = expect_int(args, 1)?;
let volume_raw = expect_int(args, 2)?;
let pan_raw = expect_int(args, 3)?;
let pitch = expect_number(args, 4, "pitch")?;
if !(0..16).contains(&voice_id_raw) {
ret.push_int(AudioOpStatus::VoiceInvalid as i64);
return Ok(());
}
if sample_id_raw < 0
|| sample_id_raw > u16::MAX as i64
|| !(0..=255).contains(&volume_raw)
|| !(0..=255).contains(&pan_raw)
|| !pitch.is_finite()
|| pitch <= 0.0
{
ret.push_int(AudioOpStatus::ArgRangeInvalid as i64);
return Ok(());
}
let status = hw.audio_mut().play(
0,
sample_id_raw as u16,
voice_id_raw as usize,
volume_raw as u8,
pan_raw as u8,
pitch,
0,
prometeu_hal::LoopMode::Off,
);
ret.push_int(status as i64);
Ok(())
}
Syscall::AudioPlay => {
let bank_id = expect_int(args, 0)? as u8;
let sample_id_raw = expect_int(args, 1)?;
let voice_id_raw = expect_int(args, 2)?;
let volume_raw = expect_int(args, 3)?;
let pan_raw = expect_int(args, 4)?;
let pitch = expect_number(args, 5, "pitch")?;
let loop_mode = match expect_int(args, 6)? {
0 => prometeu_hal::LoopMode::Off,
_ => prometeu_hal::LoopMode::On,
};
if !(0..16).contains(&voice_id_raw) {
ret.push_int(AudioOpStatus::VoiceInvalid as i64);
return Ok(());
}
if hw.assets().slot_info(SlotRef::audio(bank_id as usize)).asset_id.is_none() {
ret.push_int(AudioOpStatus::BankInvalid as i64);
return Ok(());
}
if sample_id_raw < 0
|| sample_id_raw > u16::MAX as i64
|| !(0..=255).contains(&volume_raw)
|| !(0..=255).contains(&pan_raw)
|| !pitch.is_finite()
|| pitch <= 0.0
{
ret.push_int(AudioOpStatus::ArgRangeInvalid as i64);
return Ok(());
}
let status = hw.audio_mut().play(
bank_id,
sample_id_raw as u16,
voice_id_raw as usize,
volume_raw as u8,
pan_raw as u8,
pitch,
0,
loop_mode,
);
ret.push_int(status as i64);
Ok(())
}
Syscall::FsOpen => {
let path = expect_string(args, 0, "path")?;
if self.fs_state != FsState::Mounted {
ret.push_int(-1);
return Ok(());
}
let handle = self.next_handle;
self.open_files.insert(handle, path);
self.next_handle += 1;
ret.push_int(handle as i64);
Ok(())
}
Syscall::FsRead => {
let handle = expect_int(args, 0)? as u32;
let path = self
.open_files
.get(&handle)
.ok_or_else(|| VmFault::Panic("Invalid handle".into()))?;
match self.fs.read_file(path) {
Ok(data) => ret.push_string(String::from_utf8_lossy(&data).into_owned()),
Err(_) => ret.push_null(),
}
Ok(())
}
Syscall::FsWrite => {
let handle = expect_int(args, 0)? as u32;
let content = expect_string(args, 1, "content")?.into_bytes();
let path = self
.open_files
.get(&handle)
.ok_or_else(|| VmFault::Panic("Invalid handle".into()))?;
match self.fs.write_file(path, &content) {
Ok(_) => ret.push_bool(true),
Err(_) => ret.push_bool(false),
}
Ok(())
}
Syscall::FsClose => {
self.open_files.remove(&(expect_int(args, 0)? as u32));
Ok(())
}
Syscall::FsListDir => {
let path = expect_string(args, 0, "path")?;
match self.fs.list_dir(&path) {
Ok(entries) => {
let names: Vec<String> = entries.into_iter().map(|e| e.name).collect();
ret.push_string(names.join(";"));
}
Err(_) => ret.push_null(),
}
Ok(())
}
Syscall::FsExists => {
ret.push_bool(self.fs.exists(&expect_string(args, 0, "path")?));
Ok(())
}
Syscall::FsDelete => {
match self.fs.delete(&expect_string(args, 0, "path")?) {
Ok(_) => ret.push_bool(true),
Err(_) => ret.push_bool(false),
}
Ok(())
}
Syscall::MemSlotCount => {
if self.current_cartridge_app_mode != AppMode::Game {
ret.push_int(MemcardStatus::AccessDenied as i64);
ret.push_int(0);
return Ok(());
}
ret.push_int(MemcardStatus::Ok as i64);
ret.push_int(self.memcard.slot_count() as i64);
Ok(())
}
Syscall::MemSlotStat => {
let slot = expect_slot_index(args, 0)?;
if self.current_cartridge_app_mode != AppMode::Game {
ret.push_int(MemcardStatus::AccessDenied as i64);
ret.push_int(MemcardSlotState::Empty as i64);
ret.push_int(0);
ret.push_int(0);
ret.push_int(0);
return Ok(());
}
let stat = self.memcard.slot_stat(&self.fs, self.current_app_id, slot);
let status = if stat.state == MemcardSlotState::Corrupt {
MemcardStatus::Corrupt
} else {
MemcardStatus::Ok
};
ret.push_int(status as i64);
ret.push_int(stat.state as i64);
ret.push_int(stat.used_bytes as i64);
ret.push_int(stat.generation as i64);
ret.push_int(stat.checksum as i64);
Ok(())
}
Syscall::MemSlotRead => {
let slot = expect_slot_index(args, 0)?;
let offset = expect_non_negative_usize(args, 1, "offset")?;
let max_bytes = expect_non_negative_usize(args, 2, "max_bytes")?;
if self.current_cartridge_app_mode != AppMode::Game {
ret.push_int(MemcardStatus::AccessDenied as i64);
ret.push_string(String::new());
ret.push_int(0);
return Ok(());
}
let read =
self.memcard.slot_read(&self.fs, self.current_app_id, slot, offset, max_bytes);
ret.push_int(read.status as i64);
ret.push_string(hex_encode(&read.bytes));
ret.push_int(read.bytes_read as i64);
Ok(())
}
Syscall::MemSlotWrite => {
let slot = expect_slot_index(args, 0)?;
let offset = expect_non_negative_usize(args, 1, "offset")?;
let payload_hex = expect_string(args, 2, "payload_hex")?;
if self.current_cartridge_app_mode != AppMode::Game {
ret.push_int(MemcardStatus::AccessDenied as i64);
ret.push_int(0);
return Ok(());
}
let payload = hex_decode(&payload_hex)?;
let write =
self.memcard.slot_write(&self.fs, self.current_app_id, slot, offset, &payload);
ret.push_int(write.status as i64);
ret.push_int(write.bytes_written as i64);
Ok(())
}
Syscall::MemSlotCommit => {
let slot = expect_slot_index(args, 0)?;
if self.current_cartridge_app_mode != AppMode::Game {
ret.push_int(MemcardStatus::AccessDenied as i64);
return Ok(());
}
let status = {
let memcard = &mut self.memcard;
let fs = &mut self.fs;
memcard.slot_commit(fs, self.current_app_id, slot)
};
ret.push_int(status as i64);
Ok(())
}
Syscall::MemSlotClear => {
let slot = expect_slot_index(args, 0)?;
if self.current_cartridge_app_mode != AppMode::Game {
ret.push_int(MemcardStatus::AccessDenied as i64);
return Ok(());
}
let status = {
let memcard = &mut self.memcard;
let fs = &mut self.fs;
memcard.slot_clear(fs, self.current_app_id, slot)
};
ret.push_int(status as i64);
Ok(())
}
Syscall::LogWrite => {
self.syscall_log_write(
expect_int(args, 0)?,
0,
expect_string(args, 1, "message")?,
)?;
Ok(())
}
Syscall::LogWriteTag => {
self.syscall_log_write(
expect_int(args, 0)?,
expect_int(args, 1)? as u16,
expect_string(args, 2, "message")?,
)?;
Ok(())
}
Syscall::AssetLoad => {
let raw_asset_id = expect_int(args, 0)?;
let asset_id = AssetId::try_from(raw_asset_id).map_err(|_| {
VmFault::Trap(TRAP_TYPE, format!("asset_id out of i32 range: {}", raw_asset_id))
})?;
let slot_index = expect_int(args, 1)? as usize;
match hw.assets().load(asset_id, slot_index) {
Ok(handle) => {
ret.push_int(AssetOpStatus::Ok as i64);
ret.push_int(handle as i64);
Ok(())
}
Err(status) => {
ret.push_int(status as i64);
ret.push_int(0);
Ok(())
}
}
}
Syscall::AssetStatus => {
ret.push_int(hw.assets().status(expect_int(args, 0)? as u32) as i64);
Ok(())
}
Syscall::AssetCommit => {
let status = hw.assets().commit(expect_int(args, 0)? as u32);
ret.push_int(status as i64);
Ok(())
}
Syscall::AssetCancel => {
let status = hw.assets().cancel(expect_int(args, 0)? as u32);
ret.push_int(status as i64);
Ok(())
}
Syscall::BankInfo => {
let asset_type = match expect_int(args, 0)? as u32 {
0 => BankType::GLYPH,
1 => BankType::SOUNDS,
_ => return Err(VmFault::Trap(TRAP_TYPE, "Invalid asset type".to_string())),
};
let telemetry = hw
.assets()
.bank_telemetry()
.into_iter()
.find(|entry| entry.bank_type == asset_type)
.unwrap_or(prometeu_hal::asset::BankTelemetry {
bank_type: asset_type,
used_slots: 0,
total_slots: 0,
});
let json = serde_json::to_string(&telemetry).unwrap_or_default();
ret.push_string(json);
Ok(())
}
Syscall::BankSlotInfo => {
let asset_type = match expect_int(args, 0)? as u32 {
0 => BankType::GLYPH,
1 => BankType::SOUNDS,
_ => return Err(VmFault::Trap(TRAP_TYPE, "Invalid asset type".to_string())),
};
let slot = SlotRef { asset_type, index: expect_int(args, 1)? as usize };
let json = serde_json::to_string(&hw.assets().slot_info(slot)).unwrap_or_default();
ret.push_string(json);
Ok(())
}
}
}
}
fn expect_string(args: &[Value], index: usize, field: &str) -> Result<String, VmFault> {
match args.get(index).ok_or_else(|| VmFault::Trap(TRAP_TYPE, format!("Missing {}", field)))? {
Value::String(value) => Ok(value.clone()),
_ => Err(VmFault::Trap(TRAP_TYPE, format!("Expected string {}", field))),
}
}
fn expect_number(args: &[Value], index: usize, field: &str) -> Result<f64, VmFault> {
match args.get(index).ok_or_else(|| VmFault::Trap(TRAP_TYPE, format!("Missing {}", field)))? {
Value::Float(f) => Ok(*f),
Value::Int32(i) => Ok(*i as f64),
Value::Int64(i) => Ok(*i as f64),
_ => Err(VmFault::Trap(TRAP_TYPE, format!("Expected number for {}", field))),
}
}
fn expect_slot_index(args: &[Value], index: usize) -> Result<u8, VmFault> {
let slot = expect_int(args, index)?;
if !(0..32).contains(&slot) {
return Err(VmFault::Trap(TRAP_OOB, format!("slot index out of bounds: {}", slot)));
}
Ok(slot as u8)
}
fn expect_non_negative_usize(args: &[Value], index: usize, field: &str) -> Result<usize, VmFault> {
let val = expect_int(args, index)?;
if val < 0 {
return Err(VmFault::Trap(TRAP_OOB, format!("{} must be non-negative", field)));
}
Ok(val as usize)
}
fn hex_encode(bytes: &[u8]) -> String {
const HEX: &[u8; 16] = b"0123456789abcdef";
let mut out = String::with_capacity(bytes.len() * 2);
for &b in bytes {
out.push(HEX[(b >> 4) as usize] as char);
out.push(HEX[(b & 0x0f) as usize] as char);
}
out
}
fn hex_decode(s: &str) -> Result<Vec<u8>, VmFault> {
fn nibble(c: u8) -> Option<u8> {
match c {
b'0'..=b'9' => Some(c - b'0'),
b'a'..=b'f' => Some(10 + c - b'a'),
b'A'..=b'F' => Some(10 + c - b'A'),
_ => None,
}
}
let bytes = s.as_bytes();
if !bytes.len().is_multiple_of(2) {
return Err(VmFault::Trap(TRAP_TYPE, "payload_hex must have even length".to_string()));
}
let mut out = Vec::with_capacity(bytes.len() / 2);
let mut i = 0usize;
while i < bytes.len() {
let hi = nibble(bytes[i]).ok_or_else(|| {
VmFault::Trap(TRAP_TYPE, "payload_hex contains invalid hex".to_string())
})?;
let lo = nibble(bytes[i + 1]).ok_or_else(|| {
VmFault::Trap(TRAP_TYPE, "payload_hex contains invalid hex".to_string())
})?;
out.push((hi << 4) | lo);
i += 2;
}
Ok(out)
}

View File

@ -1,146 +0,0 @@
use super::*;
use crate::CrashReport;
use crate::fs::FsBackend;
use prometeu_hal::cartridge::Cartridge;
use prometeu_hal::log::{LogLevel, LogSource};
impl VirtualMachineRuntime {
pub fn new(cap_config: Option<CertificationConfig>) -> Self {
let boot_time = Instant::now();
let log_service = LogService::new(4096);
let atomic_telemetry = Arc::new(AtomicTelemetry::new(Arc::clone(&log_service.logs_count)));
let mut os = Self {
tick_index: 0,
logical_frame_index: 0,
logical_frame_active: false,
logical_frame_remaining_cycles: 0,
last_frame_cpu_time_us: 0,
fs: VirtualFS::new(),
fs_state: FsState::Unmounted,
memcard: MemcardService::new(),
open_files: HashMap::new(),
next_handle: 1,
log_service,
current_app_id: 0,
current_cartridge_title: String::new(),
current_cartridge_app_version: String::new(),
current_cartridge_app_mode: AppMode::Game,
logs_written_this_frame: HashMap::new(),
atomic_telemetry,
last_crash_report: None,
certifier: Certifier::new(cap_config.unwrap_or_default()),
paused: false,
debug_step_request: false,
inspection_active: false,
needs_prepare_entry_call: false,
boot_time,
};
os.log(LogLevel::Info, LogSource::Pos, 0, "PrometeuOS starting...".to_string());
os
}
pub fn log(&mut self, level: LogLevel, source: LogSource, tag: u16, msg: String) {
let ts_ms = self.boot_time.elapsed().as_millis() as u64;
let frame = self.logical_frame_index;
self.log_service.log(ts_ms, frame, level, source, tag, msg);
}
pub fn mount_fs(&mut self, backend: Box<dyn FsBackend>) {
self.log(LogLevel::Info, LogSource::Fs, 0, "Attempting to mount filesystem".to_string());
match self.fs.mount(backend) {
Ok(_) => {
self.fs_state = FsState::Mounted;
self.log(
LogLevel::Info,
LogSource::Fs,
0,
"Filesystem mounted successfully".to_string(),
);
}
Err(e) => {
let err_msg = format!("Failed to mount filesystem: {:?}", e);
self.log(LogLevel::Error, LogSource::Fs, 0, err_msg);
self.fs_state = FsState::Error(e);
}
}
}
pub fn unmount_fs(&mut self) {
self.fs.unmount();
self.fs_state = FsState::Unmounted;
}
pub(crate) fn update_fs(&mut self) {
if self.fs_state == FsState::Mounted && !self.fs.is_healthy() {
self.log(
LogLevel::Error,
LogSource::Fs,
0,
"Filesystem became unhealthy, unmounting".to_string(),
);
self.unmount_fs();
}
}
pub(crate) fn clear_cartridge_state(&mut self) {
self.logical_frame_index = 0;
self.logical_frame_active = false;
self.logical_frame_remaining_cycles = 0;
self.last_frame_cpu_time_us = 0;
self.atomic_telemetry.reset();
self.open_files.clear();
self.next_handle = 1;
self.memcard.clear_all_staging();
self.current_app_id = 0;
self.current_cartridge_title.clear();
self.current_cartridge_app_version.clear();
self.current_cartridge_app_mode = AppMode::Game;
self.logs_written_this_frame.clear();
self.last_crash_report = None;
self.paused = false;
self.debug_step_request = false;
self.inspection_active = false;
self.needs_prepare_entry_call = false;
}
pub fn reset(&mut self, vm: &mut VirtualMachine) {
*vm = VirtualMachine::default();
self.clear_cartridge_state();
}
pub fn initialize_vm(
&mut self,
vm: &mut VirtualMachine,
cartridge: &Cartridge,
) -> Result<(), CrashReport> {
self.clear_cartridge_state();
vm.set_capabilities(cartridge.capabilities);
match vm.initialize(cartridge.program.clone()) {
Ok(_) => {
self.current_app_id = cartridge.app_id;
self.current_cartridge_title = cartridge.title.clone();
self.current_cartridge_app_version = cartridge.app_version.clone();
self.current_cartridge_app_mode = cartridge.app_mode;
Ok(())
}
Err(e) => {
let report = CrashReport::VmInit { error: e };
self.last_crash_report = Some(report.clone());
self.log(
LogLevel::Error,
LogSource::Vm,
report.log_tag(),
format!("Failed to initialize VM: {}", report),
);
Err(report)
}
}
}
}

View File

@ -1,257 +0,0 @@
use super::*;
use crate::CrashReport;
use prometeu_hal::asset::{BankTelemetry, BankType};
use prometeu_hal::log::{LogLevel, LogSource};
use prometeu_hal::{HardwareBridge, HostContext, InputSignals};
use prometeu_vm::LogicalFrameEndingReason;
use std::sync::atomic::Ordering;
impl VirtualMachineRuntime {
fn bank_telemetry_summary(hw: &dyn HardwareBridge) -> (BankTelemetry, BankTelemetry) {
let telemetry = hw.assets().bank_telemetry();
let glyph =
telemetry.iter().find(|entry| entry.bank_type == BankType::GLYPH).cloned().unwrap_or(
BankTelemetry { bank_type: BankType::GLYPH, used_slots: 0, total_slots: 0 },
);
let sounds =
telemetry.iter().find(|entry| entry.bank_type == BankType::SOUNDS).cloned().unwrap_or(
BankTelemetry { bank_type: BankType::SOUNDS, used_slots: 0, total_slots: 0 },
);
(glyph, sounds)
}
pub fn debug_step_instruction(
&mut self,
vm: &mut VirtualMachine,
hw: &mut dyn HardwareBridge,
) -> Option<CrashReport> {
let mut ctx = HostContext::new(Some(hw));
match vm.step(self, &mut ctx) {
Ok(_) => None,
Err(e) => {
let report = match e {
LogicalFrameEndingReason::Trap(trap) => CrashReport::VmTrap { trap },
LogicalFrameEndingReason::Panic(message) => {
CrashReport::VmPanic { message, pc: Some(vm.pc() as u32) }
}
other => CrashReport::VmPanic {
message: format!("Unexpected fault during step: {:?}", other),
pc: Some(vm.pc() as u32),
},
};
self.log(
LogLevel::Error,
LogSource::Vm,
report.log_tag(),
format!("PVM Fault during Step: {}", report),
);
self.last_crash_report = Some(report.clone());
Some(report)
}
}
}
pub fn tick(
&mut self,
vm: &mut VirtualMachine,
signals: &InputSignals,
hw: &mut dyn HardwareBridge,
) -> Option<CrashReport> {
let start = Instant::now();
self.tick_index += 1;
if self.paused && !self.debug_step_request {
return None;
}
self.update_fs();
if !self.logical_frame_active {
self.logical_frame_active = true;
self.logical_frame_remaining_cycles = Self::CYCLES_PER_LOGICAL_FRAME;
self.begin_logical_frame(signals, hw);
if self.needs_prepare_entry_call || vm.call_stack_is_empty() {
vm.prepare_boot_call();
self.needs_prepare_entry_call = false;
}
self.atomic_telemetry.frame_index.store(self.logical_frame_index, Ordering::Relaxed);
self.atomic_telemetry.cycles_budget.store(
self.certifier
.config
.cycles_budget_per_frame
.unwrap_or(Self::CYCLES_PER_LOGICAL_FRAME),
Ordering::Relaxed,
);
self.atomic_telemetry.cycles_used.store(0, Ordering::Relaxed);
self.atomic_telemetry.syscalls.store(0, Ordering::Relaxed);
self.atomic_telemetry.vm_steps.store(0, Ordering::Relaxed);
}
let budget = std::cmp::min(Self::SLICE_PER_TICK, self.logical_frame_remaining_cycles);
if budget > 0 {
let run_result = {
let mut ctx = HostContext::new(Some(hw));
vm.run_budget(budget, self, &mut ctx)
};
match run_result {
Ok(run) => {
self.logical_frame_remaining_cycles =
self.logical_frame_remaining_cycles.saturating_sub(run.cycles_used);
self.atomic_telemetry.cycles_used.fetch_add(run.cycles_used, Ordering::Relaxed);
self.atomic_telemetry.vm_steps.fetch_add(run.steps_executed, Ordering::Relaxed);
if run.reason == LogicalFrameEndingReason::Breakpoint {
self.paused = true;
self.debug_step_request = false;
self.log(
LogLevel::Info,
LogSource::Vm,
0xDEB1,
format!("Breakpoint hit at PC 0x{:X}", vm.pc()),
);
}
if let LogicalFrameEndingReason::Panic(err) = run.reason {
let report =
CrashReport::VmPanic { message: err, pc: Some(vm.pc() as u32) };
self.log(
LogLevel::Error,
LogSource::Vm,
report.log_tag(),
report.summary(),
);
self.last_crash_report = Some(report.clone());
return Some(report);
}
if let LogicalFrameEndingReason::Trap(trap) = &run.reason {
let report = CrashReport::VmTrap { trap: trap.clone() };
self.log(
LogLevel::Error,
LogSource::Vm,
report.log_tag(),
report.summary(),
);
self.last_crash_report = Some(report.clone());
return Some(report);
}
if run.reason == LogicalFrameEndingReason::FrameSync
|| run.reason == LogicalFrameEndingReason::EndOfRom
{
hw.render_frame();
// 1. Snapshot full telemetry at logical frame end
let (glyph_bank, sound_bank) = Self::bank_telemetry_summary(hw);
self.atomic_telemetry
.glyph_slots_used
.store(glyph_bank.used_slots as u32, Ordering::Relaxed);
self.atomic_telemetry
.glyph_slots_total
.store(glyph_bank.total_slots as u32, Ordering::Relaxed);
self.atomic_telemetry
.sound_slots_used
.store(sound_bank.used_slots as u32, Ordering::Relaxed);
self.atomic_telemetry
.sound_slots_total
.store(sound_bank.total_slots as u32, Ordering::Relaxed);
self.atomic_telemetry
.heap_used_bytes
.store(vm.heap().used_bytes.load(Ordering::Relaxed), Ordering::Relaxed);
self.atomic_telemetry
.host_cpu_time_us
.store(start.elapsed().as_micros() as u64, Ordering::Relaxed);
let current_frame_logs =
self.atomic_telemetry.current_logs_count.load(Ordering::Relaxed);
self.atomic_telemetry
.logs_count
.store(current_frame_logs, Ordering::Relaxed);
let ts_ms = self.boot_time.elapsed().as_millis() as u64;
let telemetry_snapshot = self.atomic_telemetry.snapshot();
let violations = self.certifier.evaluate(
&telemetry_snapshot,
&mut self.log_service,
ts_ms,
) as u32;
self.atomic_telemetry.violations.store(violations, Ordering::Relaxed);
self.atomic_telemetry
.completed_logical_frames
.fetch_add(1, Ordering::Relaxed);
self.log_service.reset_count();
self.logical_frame_index += 1;
self.logical_frame_active = false;
self.logical_frame_remaining_cycles = 0;
if run.reason == LogicalFrameEndingReason::FrameSync {
self.needs_prepare_entry_call = true;
}
if self.debug_step_request {
self.paused = true;
self.debug_step_request = false;
}
}
}
Err(e) => {
let report = CrashReport::VmPanic { message: e, pc: Some(vm.pc() as u32) };
self.log(LogLevel::Error, LogSource::Vm, report.log_tag(), report.summary());
self.last_crash_report = Some(report.clone());
return Some(report);
}
}
}
self.last_frame_cpu_time_us = start.elapsed().as_micros() as u64;
// 2. High-frequency telemetry update (only if inspection is active)
if self.inspection_active {
let (glyph_bank, sound_bank) = Self::bank_telemetry_summary(hw);
self.atomic_telemetry
.glyph_slots_used
.store(glyph_bank.used_slots as u32, Ordering::Relaxed);
self.atomic_telemetry
.glyph_slots_total
.store(glyph_bank.total_slots as u32, Ordering::Relaxed);
self.atomic_telemetry
.sound_slots_used
.store(sound_bank.used_slots as u32, Ordering::Relaxed);
self.atomic_telemetry
.sound_slots_total
.store(sound_bank.total_slots as u32, Ordering::Relaxed);
self.atomic_telemetry
.heap_used_bytes
.store(vm.heap().used_bytes.load(Ordering::Relaxed), Ordering::Relaxed);
self.atomic_telemetry.frame_index.store(self.logical_frame_index, Ordering::Relaxed);
self.atomic_telemetry
.host_cpu_time_us
.store(start.elapsed().as_micros() as u64, Ordering::Relaxed);
}
None
}
pub(crate) fn begin_logical_frame(
&mut self,
_signals: &InputSignals,
hw: &mut dyn HardwareBridge,
) {
hw.begin_frame();
hw.audio_mut().clear_commands();
self.logs_written_this_frame.clear();
}
}

View File

@ -1,12 +0,0 @@
[package]
name = "prometeu-vm"
version = "0.1.0"
edition = "2024"
license.workspace = true
[dependencies]
prometeu-bytecode = { path = "../prometeu-bytecode" }
prometeu-hal = { path = "../prometeu-hal" }
[dev-dependencies]
prometeu-test-support = { path = "../../dev/prometeu-test-support" }

File diff suppressed because it is too large Load Diff

View File

@ -1,801 +0,0 @@
use crate::call_frame::CallFrame;
use crate::object::{ObjectHeader, ObjectKind};
use prometeu_bytecode::{HeapRef, Value};
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
/// Internal stored object: header plus opaque payload bytes.
#[derive(Debug, Clone)]
pub struct StoredObject {
pub header: ObjectHeader,
/// Raw payload bytes for byte-oriented kinds (e.g., String, Bytes).
pub payload: Vec<u8>,
/// Optional typed elements for `ObjectKind::Array`.
/// When present, `header.payload_len` must equal `array_elems.len() as u32`.
pub array_elems: Option<Vec<Value>>,
/// Optional captured environment for `ObjectKind::Closure`.
/// Invariants for closures:
/// - `header.payload_len == 8` and `payload` bytes are `[fn_id: u32][env_len: u32]` (LE).
/// - The actual `env_len` Value slots are stored here (not in `payload`) so
/// they stay directly GC-visible. The GC must traverse exactly `env_len`
/// entries from this slice, in order.
pub closure_env: Option<Vec<Value>>,
/// Optional coroutine data for `ObjectKind::Coroutine`.
pub coroutine: Option<CoroutineData>,
}
impl StoredObject {
/// Returns the approximate memory footprint of this object in bytes.
pub fn bytes(&self) -> usize {
let mut total = std::mem::size_of::<ObjectHeader>();
total += self.payload.capacity();
if let Some(elems) = &self.array_elems {
total += std::mem::size_of::<Vec<Value>>();
total += elems.capacity() * std::mem::size_of::<Value>();
}
if let Some(env) = &self.closure_env {
total += std::mem::size_of::<Vec<Value>>();
total += env.capacity() * std::mem::size_of::<Value>();
}
if let Some(coro) = &self.coroutine {
total += std::mem::size_of::<CoroutineData>();
total += coro.stack.capacity() * std::mem::size_of::<Value>();
total += coro.frames.capacity() * std::mem::size_of::<CallFrame>();
}
total
}
}
/// Execution state of a coroutine.
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub enum CoroutineState {
Ready,
Running,
Sleeping,
Finished,
// Faulted,
}
/// Stored payload for coroutine objects.
#[derive(Debug, Clone)]
pub struct CoroutineData {
pub pc: usize,
pub state: CoroutineState,
pub wake_tick: u64,
pub stack: Vec<Value>,
pub frames: Vec<CallFrame>,
}
/// Simple vector-backed heap. No GC or compaction.
#[derive(Debug, Default, Clone)]
pub struct Heap {
// Tombstone-aware store: Some(obj) = live allocation; None = freed slot.
objects: Vec<Option<StoredObject>>,
// Reclaimed slots available for deterministic reuse (LIFO).
free_list: Vec<usize>,
/// Total bytes currently used by all objects in the heap.
pub used_bytes: Arc<AtomicUsize>,
}
impl Heap {
pub fn new() -> Self {
Self {
objects: Vec::new(),
free_list: Vec::new(),
used_bytes: Arc::new(AtomicUsize::new(0)),
}
}
fn insert_object(&mut self, obj: StoredObject) -> HeapRef {
self.used_bytes.fetch_add(obj.bytes(), Ordering::Relaxed);
if let Some(idx) = self.free_list.pop() {
debug_assert!(self.objects.get(idx).is_some_and(|slot| slot.is_none()));
self.objects[idx] = Some(obj);
HeapRef(idx as u32)
} else {
let idx = self.objects.len();
self.objects.push(Some(obj));
HeapRef(idx as u32)
}
}
/// Allocate a new object with the given kind and raw payload bytes.
/// Returns an opaque `HeapRef` handle.
#[cfg(test)]
pub fn allocate_object(&mut self, kind: ObjectKind, payload: &[u8]) -> HeapRef {
let header = ObjectHeader::new(kind, payload.len() as u32);
let obj = StoredObject {
header,
payload: payload.to_vec(),
array_elems: None,
closure_env: None,
coroutine: None,
};
self.insert_object(obj)
}
/// Allocate a new `Array` object with the given `Value` elements.
/// `payload_len` stores the element count; raw `payload` bytes are empty.
#[cfg(test)]
pub fn allocate_array(&mut self, elements: Vec<Value>) -> HeapRef {
let header = ObjectHeader::new(ObjectKind::Array, elements.len() as u32);
let obj = StoredObject {
header,
payload: Vec::new(),
array_elems: Some(elements),
closure_env: None,
coroutine: None,
};
self.insert_object(obj)
}
/// Allocate a new `Closure` object with the given function id and captured environment.
/// Layout:
/// payload bytes: [fn_id: u32][env_len: u32]
/// env slots: stored out-of-line in `closure_env` for GC visibility
pub fn alloc_closure(&mut self, fn_id: u32, env_values: &[Value]) -> HeapRef {
let mut payload = Vec::with_capacity(8);
payload.extend_from_slice(&fn_id.to_le_bytes());
let env_len = env_values.len() as u32;
payload.extend_from_slice(&env_len.to_le_bytes());
let header = ObjectHeader::new(ObjectKind::Closure, payload.len() as u32);
let obj = StoredObject {
header,
payload,
array_elems: None,
closure_env: Some(env_values.to_vec()),
coroutine: None,
};
self.insert_object(obj)
}
/// Allocate a new `Coroutine` object with provided initial data.
/// `payload_len` is 0; stack and frames are stored out-of-line for GC visibility.
pub fn allocate_coroutine(
&mut self,
pc: usize,
state: CoroutineState,
wake_tick: u64,
stack: Vec<Value>,
frames: Vec<CallFrame>,
) -> HeapRef {
let header = ObjectHeader::new(ObjectKind::Coroutine, 0);
let obj = StoredObject {
header,
payload: Vec::new(),
array_elems: None,
closure_env: None,
coroutine: Some(CoroutineData { pc, state, wake_tick, stack, frames }),
};
self.insert_object(obj)
}
/// Returns true if this handle refers to an allocated object.
pub fn is_valid(&self, r: HeapRef) -> bool {
let idx = r.0 as usize;
if idx >= self.objects.len() {
return false;
}
self.objects[idx].is_some()
}
/// Returns a shared reference to the coroutine data for the given handle, if it is a Coroutine.
#[cfg(test)]
pub fn coroutine_data(&self, r: HeapRef) -> Option<&CoroutineData> {
let idx = r.0 as usize;
self.objects.get(idx).and_then(|slot| slot.as_ref()).and_then(|obj| obj.coroutine.as_ref())
}
/// Returns a mutable reference to the coroutine data for the given handle, if it is a Coroutine.
pub fn coroutine_data_mut(&mut self, r: HeapRef) -> Option<&mut CoroutineData> {
let idx = r.0 as usize;
self.objects
.get_mut(idx)
.and_then(|slot| slot.as_mut())
.and_then(|obj| obj.coroutine.as_mut())
}
/// Get immutable access to an object's header by handle.
pub fn header(&self, r: HeapRef) -> Option<&ObjectHeader> {
self.objects.get(r.0 as usize).and_then(|slot| slot.as_ref()).map(|o| &o.header)
}
/// Internal: get mutable access to an object's header by handle.
fn header_mut(&mut self, r: HeapRef) -> Option<&mut ObjectHeader> {
self.objects.get_mut(r.0 as usize).and_then(|slot| slot.as_mut()).map(|o| &mut o.header)
}
// Internal: list inner `HeapRef` children of an object without allocating.
// Note: GC mark no longer uses this helper; kept for potential diagnostics.
// fn children_of(&self, r: HeapRef) -> Box<dyn Iterator<Item = HeapRef> + '_> {
// let idx = r.0 as usize;
// if let Some(Some(o)) = self.objects.get(idx) {
// match o.header.kind {
// ObjectKind::Array => {
// let it = o
// .array_elems
// .as_deref()
// .into_iter()
// .flat_map(|slice| slice.iter())
// .filter_map(|val| if let Value::HeapRef(h) = val { Some(*h) } else { None });
// return Box::new(it);
// }
// ObjectKind::Closure => {
// // Read env_len from payload; traverse exactly that many entries.
// debug_assert_eq!(o.header.kind, ObjectKind::Closure);
// debug_assert_eq!(o.payload.len(), 8, "closure payload metadata must be 8 bytes");
// let mut nbytes = [0u8; 4];
// nbytes.copy_from_slice(&o.payload[4..8]);
// let env_len = u32::from_le_bytes(nbytes) as usize;
// let it = o
// .closure_env
// .as_deref()
// .map(|slice| {
// debug_assert_eq!(slice.len(), env_len, "closure env length must match encoded env_len");
// &slice[..env_len]
// })
// .into_iter()
// .flat_map(|slice| slice.iter())
// .filter_map(|val| if let Value::HeapRef(h) = val { Some(*h) } else { None });
// return Box::new(it);
// }
// ObjectKind::Coroutine => {
// if let Some(co) = o.coroutine.as_ref() {
// let it = co
// .stack
// .iter()
// .filter_map(|v| if let Value::HeapRef(h) = v { Some(*h) } else { None });
// return Box::new(it);
// }
// return Box::new(std::iter::empty());
// }
// _ => return Box::new(std::iter::empty()),
// }
// }
// Box::new(std::iter::empty())
// }
/// Read the `fn_id` stored in a closure object. Returns None if kind mismatch or invalid ref.
pub fn closure_fn_id(&self, r: HeapRef) -> Option<u32> {
let idx = r.0 as usize;
let slot = self.objects.get(idx)?.as_ref()?;
if slot.header.kind != ObjectKind::Closure {
return None;
}
if slot.payload.len() < 8 {
return None;
}
debug_assert_eq!(slot.header.payload_len, 8);
let mut bytes = [0u8; 4];
bytes.copy_from_slice(&slot.payload[0..4]);
Some(u32::from_le_bytes(bytes))
}
/// Get the captured environment slice of a closure. Returns None if kind mismatch or invalid ref.
#[cfg(test)]
pub fn closure_env_slice(&self, r: HeapRef) -> Option<&[Value]> {
let idx = r.0 as usize;
let slot = self.objects.get(idx)?.as_ref()?;
if slot.header.kind != ObjectKind::Closure {
return None;
}
if slot.payload.len() >= 8 {
let mut nbytes = [0u8; 4];
nbytes.copy_from_slice(&slot.payload[4..8]);
let env_len = u32::from_le_bytes(nbytes) as usize;
if let Some(env) = slot.closure_env.as_deref() {
debug_assert_eq!(env.len(), env_len);
}
}
slot.closure_env.as_deref()
}
/// Mark phase: starting from the given roots, traverse and set mark bits
/// on all reachable objects. Uses an explicit stack to avoid recursion.
pub fn mark_from_roots<I: IntoIterator<Item = HeapRef>>(&mut self, roots: I) {
let mut stack: Vec<HeapRef> = roots.into_iter().collect();
while let Some(r) = stack.pop() {
if !self.is_valid(r) {
continue;
}
// If already marked, skip.
let already_marked =
self.header(r).map(|h: &ObjectHeader| h.is_marked()).unwrap_or(false);
if already_marked {
continue;
}
// Set mark bit.
if let Some(h) = self.header_mut(r) {
h.set_marked(true);
}
// Push children by scanning payload directly (no intermediate Vec allocs).
let idx = r.0 as usize;
if let Some(Some(obj)) = self.objects.get(idx) {
match obj.header.kind {
ObjectKind::Array => {
if let Some(elems) = obj.array_elems.as_ref() {
for val in elems.iter() {
if let Value::HeapRef(child) = val
&& self.is_valid(*child)
{
let marked = self
.header(*child)
.map(|h: &ObjectHeader| h.is_marked())
.unwrap_or(false);
if !marked {
stack.push(*child);
}
}
}
}
}
ObjectKind::Closure => {
debug_assert_eq!(obj.payload.len(), 8, "closure payload must be 8 bytes");
let mut nbytes = [0u8; 4];
nbytes.copy_from_slice(&obj.payload[4..8]);
let env_len = u32::from_le_bytes(nbytes) as usize;
if let Some(env) = obj.closure_env.as_ref() {
debug_assert_eq!(
env.len(),
env_len,
"closure env len must match encoded env_len"
);
for val in env[..env_len].iter() {
if let Value::HeapRef(child) = val
&& self.is_valid(*child)
{
let marked = self
.header(*child)
.map(|h: &ObjectHeader| h.is_marked())
.unwrap_or(false);
if !marked {
stack.push(*child);
}
}
}
}
}
ObjectKind::Coroutine => {
if let Some(co) = obj.coroutine.as_ref() {
for val in co.stack.iter() {
if let Value::HeapRef(child) = val
&& self.is_valid(*child)
{
let marked = self
.header(*child)
.map(|h: &ObjectHeader| h.is_marked())
.unwrap_or(false);
if !marked {
stack.push(*child);
}
}
}
}
}
_ => {}
}
}
}
}
/// Sweep phase: reclaim unmarked objects by turning their slots into
/// tombstones (None), and clear the mark bit on the remaining live ones
/// to prepare for the next GC cycle. Does not move or compact objects.
pub fn sweep(&mut self) {
for (idx, slot) in self.objects.iter_mut().enumerate() {
if let Some(obj) = slot {
if obj.header.is_marked() {
// Live: clear mark for next cycle.
obj.header.set_marked(false);
} else {
// Unreachable: reclaim by dropping and turning into tombstone.
self.used_bytes.fetch_sub(obj.bytes(), Ordering::Relaxed);
*slot = None;
self.free_list.push(idx);
}
}
}
}
/// Current number of allocated (live) objects.
pub fn len(&self) -> usize {
self.objects.iter().filter(|s| s.is_some()).count()
}
/// Enumerate handles of coroutines that are currently suspended (i.e., not running):
/// Ready or Sleeping. These must be treated as GC roots by the runtime so their
/// stacks/frames are scanned during mark.
pub fn suspended_coroutine_handles(&self) -> Vec<HeapRef> {
let mut out = Vec::new();
for (idx, slot) in self.objects.iter().enumerate() {
if let Some(obj) = slot
&& obj.header.kind == ObjectKind::Coroutine
&& let Some(co) = &obj.coroutine
&& matches!(co.state, CoroutineState::Ready | CoroutineState::Sleeping)
{
out.push(HeapRef(idx as u32));
}
}
out
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn basic_allocation_returns_valid_refs() {
let mut heap = Heap::new();
let r1 = heap.allocate_object(ObjectKind::String, b"hello");
let r2 = heap.allocate_object(ObjectKind::Bytes, &[1, 2, 3, 4]);
let r3 = heap.allocate_array(vec![]);
assert!(heap.is_valid(r1));
assert!(heap.is_valid(r2));
assert!(heap.is_valid(r3));
assert_eq!(heap.len(), 3);
let h1 = heap.header(r1).unwrap();
assert_eq!(h1.kind, ObjectKind::String);
assert_eq!(h1.payload_len, 5);
let h2 = heap.header(r2).unwrap();
assert_eq!(h2.kind, ObjectKind::Bytes);
assert_eq!(h2.payload_len, 4);
let h3 = heap.header(r3).unwrap();
assert_eq!(h3.kind, ObjectKind::Array);
assert_eq!(h3.payload_len, 0);
}
#[test]
fn allocate_and_transition_coroutine() {
let mut heap = Heap::new();
// Create a coroutine with a small stack containing a HeapRef to verify GC traversal later.
let obj_ref = heap.allocate_object(ObjectKind::Bytes, &[4, 5, 6]);
let coro = heap.allocate_coroutine(
0,
CoroutineState::Ready,
0,
vec![Value::Int32(1), Value::HeapRef(obj_ref)],
vec![CallFrame { return_pc: 0, stack_base: 0, func_idx: 0 }],
);
let hdr = heap.header(coro).unwrap();
assert_eq!(hdr.kind, ObjectKind::Coroutine);
assert_eq!(hdr.payload_len, 0);
// Manually mutate state transitions via access to inner data.
{
let slot = heap.objects.get_mut(coro.0 as usize).and_then(|s| s.as_mut()).unwrap();
let co = slot.coroutine.as_mut().unwrap();
assert_eq!(co.state, CoroutineState::Ready);
co.state = CoroutineState::Running;
assert_eq!(co.state, CoroutineState::Running);
co.state = CoroutineState::Sleeping;
co.wake_tick = 42;
assert_eq!(co.wake_tick, 42);
co.state = CoroutineState::Finished;
assert_eq!(co.state, CoroutineState::Finished);
}
// GC should mark the object referenced from the coroutine stack when the coroutine is a root.
heap.mark_from_roots([coro]);
assert!(heap.header(obj_ref).unwrap().is_marked());
}
#[test]
fn mark_reachable_through_array() {
let mut heap = Heap::new();
// Target object B (unreferenced yet)
let b = heap.allocate_object(ObjectKind::Bytes, &[9, 9, 9]);
// Array A that contains a reference to B among other primitives
let a =
heap.allocate_array(vec![Value::Int32(1), Value::HeapRef(b), Value::Boolean(false)]);
// Mark starting from root A
heap.mark_from_roots([a]);
// Both A and B must be marked; random other objects are not allocated
assert!(heap.header(a).unwrap().is_marked());
assert!(heap.header(b).unwrap().is_marked());
}
#[test]
fn mark_does_not_mark_unreachable() {
let mut heap = Heap::new();
let unreachable = heap.allocate_object(ObjectKind::String, b"orphan");
let root = heap.allocate_object(ObjectKind::Bytes, &[1, 2, 3]);
heap.mark_from_roots([root]);
assert!(heap.header(root).unwrap().is_marked());
assert!(!heap.header(unreachable).unwrap().is_marked());
}
#[test]
fn mark_handles_cycles() {
let mut heap = Heap::new();
// Create two arrays that reference each other: A -> B, B -> A
// Allocate empty arrays first to get handles
let a = heap.allocate_array(vec![]);
let b = heap.allocate_array(vec![]);
// Now mutate their internal vectors via re-allocation pattern:
// replace with arrays containing cross-references. Since our simple
// heap doesn't support in-place element edits via API, simulate by
// directly editing stored objects.
if let Some(slot) = heap.objects.get_mut(a.0 as usize) {
if let Some(obj) = slot.as_mut() {
obj.array_elems = Some(vec![Value::HeapRef(b)]);
obj.header.payload_len = 1;
}
}
if let Some(slot) = heap.objects.get_mut(b.0 as usize) {
if let Some(obj) = slot.as_mut() {
obj.array_elems = Some(vec![Value::HeapRef(a)]);
obj.header.payload_len = 1;
}
}
// Mark from A; should terminate and mark both.
heap.mark_from_roots([a]);
assert!(heap.header(a).unwrap().is_marked());
assert!(heap.header(b).unwrap().is_marked());
}
#[test]
fn closure_allocation_with_empty_env() {
let mut heap = Heap::new();
let c = heap.alloc_closure(42, &[]);
assert!(heap.is_valid(c));
let h = heap.header(c).unwrap();
assert_eq!(h.kind, ObjectKind::Closure);
// payload has only metadata (8 bytes)
assert_eq!(h.payload_len, 8);
assert_eq!(heap.closure_fn_id(c), Some(42));
let env = heap.closure_env_slice(c).unwrap();
assert_eq!(env.len(), 0);
}
#[test]
fn closure_allocation_with_env_and_access() {
let mut heap = Heap::new();
let a = heap.allocate_object(ObjectKind::String, b"a");
let env_vals = vec![Value::Int32(7), Value::HeapRef(a), Value::Boolean(true)];
let c = heap.alloc_closure(7, &env_vals);
let h = heap.header(c).unwrap();
assert_eq!(h.kind, ObjectKind::Closure);
assert_eq!(h.payload_len, 8);
assert_eq!(heap.closure_fn_id(c), Some(7));
let env = heap.closure_env_slice(c).unwrap();
assert_eq!(env, &env_vals[..]);
// GC traversal should see the inner HeapRef in closure env when marking.
heap.mark_from_roots([c]);
assert!(heap.header(c).unwrap().is_marked());
assert!(heap.header(a).unwrap().is_marked());
}
#[test]
fn sweep_reclaims_unreachable_and_invalidates_handles() {
let mut heap = Heap::new();
// Allocate two objects; only one will be a root.
let unreachable = heap.allocate_object(ObjectKind::String, b"orphan");
let root = heap.allocate_object(ObjectKind::Bytes, &[1, 2, 3]);
// Mark from root and then sweep.
heap.mark_from_roots([root]);
// Precondition: root marked, unreachable not marked.
assert!(heap.header(root).unwrap().is_marked());
assert!(!heap.header(unreachable).unwrap().is_marked());
heap.sweep();
// Unreachable must be reclaimed: handle becomes invalid.
assert!(!heap.is_valid(unreachable));
assert!(heap.header(unreachable).is_none());
// Root must survive and have its mark bit cleared for next cycle.
assert!(heap.is_valid(root));
assert!(!heap.header(root).unwrap().is_marked());
}
#[test]
fn sweep_keeps_indices_stable_and_len_counts_live() {
let mut heap = Heap::new();
let a = heap.allocate_object(ObjectKind::String, b"a");
let b = heap.allocate_object(ObjectKind::String, b"b");
let c = heap.allocate_object(ObjectKind::String, b"c");
// Only keep A live.
heap.mark_from_roots([a]);
heap.sweep();
// B and C are now invalidated, A remains valid.
assert!(heap.is_valid(a));
assert!(!heap.is_valid(b));
assert!(!heap.is_valid(c));
// Len counts only live objects.
assert_eq!(heap.len(), 1);
// Indices are stable: A's index is still within the backing store bounds.
// We can't access internal vector here, but stability is implied by handle not changing.
assert_eq!(a.0, a.0); // placeholder sanity check
}
#[test]
fn sweep_reuses_freed_slot_on_next_allocation() {
let mut heap = Heap::new();
let dead = heap.allocate_object(ObjectKind::String, b"dead");
let live = heap.allocate_object(ObjectKind::String, b"live");
heap.mark_from_roots([live]);
heap.sweep();
assert!(!heap.is_valid(dead));
assert_eq!(heap.free_list, vec![dead.0 as usize]);
let reused = heap.allocate_object(ObjectKind::Bytes, &[1, 2, 3]);
assert_eq!(reused, dead);
assert!(heap.is_valid(reused));
assert!(heap.is_valid(live));
}
#[test]
fn live_handles_remain_stable_when_freelist_is_reused() {
let mut heap = Heap::new();
let live = heap.allocate_object(ObjectKind::String, b"live");
let dead = heap.allocate_object(ObjectKind::String, b"dead");
heap.mark_from_roots([live]);
heap.sweep();
let replacement = heap.allocate_object(ObjectKind::Bytes, &[9]);
assert_eq!(replacement, dead);
assert_eq!(heap.header(live).unwrap().kind, ObjectKind::String);
assert_eq!(heap.header(replacement).unwrap().kind, ObjectKind::Bytes);
assert_eq!(live.0, 0);
}
#[test]
fn freelist_reuse_is_deterministic_lifo() {
let mut heap = Heap::new();
let a = heap.allocate_object(ObjectKind::String, b"a");
let b = heap.allocate_object(ObjectKind::String, b"b");
let c = heap.allocate_object(ObjectKind::String, b"c");
heap.mark_from_roots([]);
heap.sweep();
assert_eq!(heap.free_list, vec![a.0 as usize, b.0 as usize, c.0 as usize]);
let r1 = heap.allocate_object(ObjectKind::Bytes, &[1]);
let r2 = heap.allocate_object(ObjectKind::Bytes, &[2]);
let r3 = heap.allocate_object(ObjectKind::Bytes, &[3]);
assert_eq!(r1, c);
assert_eq!(r2, b);
assert_eq!(r3, a);
}
#[test]
fn sweep_reclaims_unrooted_cycle() {
let mut heap = Heap::new();
// Build a 2-node cycle A <-> B using internal mutation (module-private access).
let a = heap.allocate_array(vec![]);
let b = heap.allocate_array(vec![]);
// Make A point to B and B point to A.
if let Some(slot) = heap.objects.get_mut(a.0 as usize) {
if let Some(obj) = slot.as_mut() {
obj.array_elems = Some(vec![Value::HeapRef(b)]);
obj.header.payload_len = 1;
}
}
if let Some(slot) = heap.objects.get_mut(b.0 as usize) {
if let Some(obj) = slot.as_mut() {
obj.array_elems = Some(vec![Value::HeapRef(a)]);
obj.header.payload_len = 1;
}
}
// No roots: perform sweep directly; both should be reclaimed.
heap.sweep();
assert!(!heap.is_valid(a));
assert!(!heap.is_valid(b));
assert_eq!(heap.len(), 0);
}
#[test]
fn gc_scans_closure_env_and_keeps_captured_heap_object() {
let mut heap = Heap::new();
// Captured heap object.
let obj = heap.allocate_object(ObjectKind::Bytes, &[4, 5, 6]);
// Closure capturing the heap object among other primitive values.
let env = [Value::Boolean(true), Value::HeapRef(obj), Value::Int32(123)];
let clo = heap.alloc_closure(1, &env);
// Mark from closure root: both closure and captured object must be marked.
heap.mark_from_roots([clo]);
assert!(heap.header(clo).unwrap().is_marked());
assert!(heap.header(obj).unwrap().is_marked());
// Sweep should keep both and clear their marks.
heap.sweep();
assert!(heap.is_valid(clo));
assert!(heap.is_valid(obj));
assert!(!heap.header(clo).unwrap().is_marked());
assert!(!heap.header(obj).unwrap().is_marked());
}
#[test]
fn gc_scans_nested_closures_and_keeps_inner_when_outer_is_rooted() {
let mut heap = Heap::new();
// Inner closure (no env).
let inner = heap.alloc_closure(2, &[]);
// Outer closure captures the inner closure as a Value::HeapRef.
let outer = heap.alloc_closure(3, &[Value::HeapRef(inner)]);
// Root only the outer closure.
heap.mark_from_roots([outer]);
// Both must be marked reachable.
assert!(heap.header(outer).unwrap().is_marked());
assert!(heap.header(inner).unwrap().is_marked());
// After sweep, both survive and have marks cleared.
heap.sweep();
assert!(heap.is_valid(outer));
assert!(heap.is_valid(inner));
assert!(!heap.header(outer).unwrap().is_marked());
assert!(!heap.header(inner).unwrap().is_marked());
}
#[test]
fn gc_collects_unreferenced_closure_and_captures() {
let mut heap = Heap::new();
// Captured heap object and a closure capturing it.
let captured = heap.allocate_object(ObjectKind::String, b"dead");
let clo = heap.alloc_closure(9, &[Value::HeapRef(captured)]);
// No roots are provided; sweeping should reclaim both.
heap.sweep();
assert!(!heap.is_valid(clo));
assert!(!heap.is_valid(captured));
assert_eq!(heap.len(), 0);
}
}

View File

@ -1,26 +0,0 @@
mod builtins;
mod call_frame;
mod local_addressing;
// Keep the verifier internal in production builds, but expose it for integration tests
// so the golden verifier suite can exercise it without widening the public API in releases.
mod heap;
mod object;
mod roots;
mod scheduler;
#[cfg(not(test))]
mod verifier;
#[cfg(test)]
mod verifier;
mod virtual_machine;
mod vm_init_error;
pub use builtins::{
AbiType, BuiltinConstKey, BuiltinConstMaterializer, BuiltinConstMeta, BuiltinConstSlotValue,
BuiltinFieldMeta, BuiltinLayoutType, BuiltinScalarType, BuiltinTypeKey, BuiltinTypeMeta,
BuiltinTypeShape, BuiltinValueError, IntrinsicExecutionError, IntrinsicImplementation,
IntrinsicKey, IntrinsicMeta, lookup_builtin_constant, lookup_builtin_type, lookup_intrinsic,
lookup_intrinsic_by_id, materialize_builtin_constant,
};
pub use prometeu_hal::{HostContext, HostReturn, NativeInterface, SyscallId};
pub use virtual_machine::{BudgetReport, LogicalFrameEndingReason, VirtualMachine};
pub use vm_init_error::{LoaderPatchError, VmInitError};

View File

@ -1,35 +0,0 @@
use crate::call_frame::CallFrame;
use prometeu_bytecode::FunctionMeta;
use prometeu_bytecode::{TRAP_INVALID_LOCAL, TrapInfo};
// /// Computes the absolute stack index for the start of the current frame's locals (including args).
// pub fn local_base(frame: &CallFrame) -> usize {
// frame.stack_base
// }
/// Computes the absolute stack index for a given local slot.
pub fn local_index(frame: &CallFrame, slot: u32) -> usize {
frame.stack_base + slot as usize
}
/// Validates that a local slot index is within the valid range for the function.
/// Range: 0 <= slot < (param_slots + local_slots)
pub fn check_local_slot(
meta: &FunctionMeta,
slot: u32,
opcode: u16,
pc: u32,
) -> Result<(), TrapInfo> {
let limit = meta.param_slots as u32 + meta.local_slots as u32;
if slot < limit {
Ok(())
} else {
Err(TrapInfo {
code: TRAP_INVALID_LOCAL,
opcode,
message: format!("Local slot {} out of bounds for function (limit {})", slot, limit),
pc,
span: None,
})
}
}

View File

@ -1,135 +0,0 @@
//! Canonical heap object header and kind tags.
//!
//! This module defines the minimal common header that prefixes every
//! heap-allocated object managed by the VM. The purpose of the header is to:
//! - allow the garbage collector (GC) to identify and classify objects,
//! - carry the object "kind" (type tag),
//! - optionally carry size/length metadata for variable-sized payloads.
//!
//! Scope of this file:
//! - No GC/traversal logic is implemented here.
//! - No allocation strategies are defined here.
//! - Only the data layout and documentation are provided.
//!
//! Layout and semantics
//! --------------------
//! The header has a fixed layout and uses `repr(C)` to keep a stable field order.
//!
//! Fields:
//! - `flags` (u8): bit flags used by the runtime/GC. Bit 0 is the GC "mark" bit.
//! Remaining bits are reserved for future use (e.g., color, pinning, etc.).
//! - `kind` (ObjectKind): object kind tag (stored as `u8`). It describes how the
//! object should be interpreted by higher layers (array, string, closure, ...).
//! - `payload_len` (u32): optional, object-specific length field. For fixed-size
//! objects this MAY be zero. For variable-size objects it typically stores the
//! element count (arrays) or byte length (strings). Exact interpretation is
//! defined by each object kind; the GC treats it as an opaque metadata field.
//!
//! Closure-specific note: for `ObjectKind::Closure`, `payload_len` is the
//! fixed size `8` and the payload layout is exactly two little-endian `u32`s:
//! `[fn_id][env_len]`. The captured environment values themselves are NOT in
//! the raw payload; they live in a separate GC-visible area managed by the
//! heap (see `Heap`), and the GC must traverse exactly `env_len` values from
//! that environment slice.
//!
//! Notes:
//! - The GC only relies on `flags` (mark bit) and `kind` to traverse/trace.
//! Actual traversal logic will be implemented elsewhere in future PRs.
//! - The header is intentionally compact (8 bytes on most targets) to minimize
//! per-object overhead.
//!
//! Safety & invariants:
//! - Every heap object MUST begin with an `ObjectHeader`.
//! - `kind` must contain a valid `ObjectKind` tag for the object's payload.
//! - `payload_len` must be consistent with the chosen `kind` (if applicable).
/// Object kind tags for heap objects.
///
/// This `repr(u8)` enum is stable across FFI boundaries and persisted images.
/// Do not reorder variants; append new ones at the end.
#[repr(u8)]
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
pub enum ObjectKind {
/// Reserved/unknown kind. Should not appear in valid allocations.
#[allow(dead_code)]
// Kept for stable tag layout and persisted images, even if not constructed in this crate yet
Unknown = 0,
/// UTF-8 string. `payload_len` is the number of bytes.
#[allow(dead_code)] // Public/stable tag retained; construction may live in higher layers
String = 1,
/// Homogeneous array of VM values/handles. `payload_len` is element count.
#[allow(dead_code)] // Public/stable tag retained; constructed via Heap helpers in tests
Array = 2,
/// Compiled closure/function value.
///
/// Invariants for `payload_len` and payload layout:
/// - `payload_len == 8` (fixed).
/// - payload bytes are `[fn_id: u32][env_len: u32]` (little-endian).
/// - The `env_len` captured values are stored out-of-line in the heap so
/// they remain directly visible to the GC during traversal.
Closure = 3,
/// Byte buffer / blob. `payload_len` is the number of bytes.
#[allow(dead_code)] // Public/stable tag retained for future/host APIs
Bytes = 4,
/// User-defined/native host object. Payload shape is host-defined.
#[allow(dead_code)] // Reserved for host/native integrations
UserData = 5,
/// Coroutine object: suspended execution context with its own stack/frames.
///
/// Notes:
/// - Stack/frames are stored in typed fields inside the heap storage
/// (not inside raw `payload` bytes) so the GC can traverse their
/// contained `HeapRef`s directly.
/// - `payload_len` is 0 for this fixed-layout object.
Coroutine = 6,
// Future kinds must be appended here to keep tag numbers stable.
}
/// Bit flags stored in `ObjectHeader.flags`.
pub mod object_flags {
/// GC mark bit (used during tracing). 1 = marked, 0 = not marked.
pub const MARKED: u8 = 0b0000_0001;
// Reserved bits for future use:
// pub const PINNED: u8 = 0b0000_0010; // example: prevent movement/collection
}
/// Common header that prefixes every heap-allocated object.
#[repr(C)]
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub struct ObjectHeader {
/// Runtime/GC flags. See `object_flags` for meanings.
pub flags: u8,
/// Object kind tag (compact `u8`). See `ObjectKind`.
pub kind: ObjectKind,
/// Optional length metadata for variable-sized payloads.
/// For fixed-size objects this may be zero.
pub payload_len: u32,
}
impl ObjectHeader {
/// Create a new header with given `kind` and `payload_len`, flags cleared.
pub const fn new(kind: ObjectKind, payload_len: u32) -> Self {
Self { flags: 0, kind, payload_len }
}
/// Returns true if the GC mark bit is set.
pub fn is_marked(&self) -> bool {
(self.flags & object_flags::MARKED) != 0
}
/// Sets or clears the GC mark bit. Note: actual GC logic lives elsewhere.
pub fn set_marked(&mut self, value: bool) {
if value {
self.flags |= object_flags::MARKED;
} else {
self.flags &= !object_flags::MARKED;
}
}
}

View File

@ -1,41 +0,0 @@
use prometeu_bytecode::{HeapRef, Value};
/// Visitor for GC roots. Implementors receive every `HeapRef` discovered
/// during root traversal. No marking/sweeping semantics here.
pub trait RootVisitor {
fn visit_heap_ref(&mut self, r: HeapRef);
}
/// Helper: if `val` is a `Value::HeapRef`, call the visitor.
pub fn visit_value_for_roots<V: RootVisitor + ?Sized>(val: &Value, visitor: &mut V) {
if let Value::HeapRef(r) = val {
visitor.visit_heap_ref(*r);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::VirtualMachine;
struct CollectVisitor {
pub seen: Vec<HeapRef>,
}
impl RootVisitor for CollectVisitor {
fn visit_heap_ref(&mut self, r: HeapRef) {
self.seen.push(r);
}
}
#[test]
fn visits_heapref_on_operand_stack() {
let mut vm = VirtualMachine::default();
// Place a HeapRef on the operand stack
vm.push_operand_for_test(Value::HeapRef(HeapRef(123)));
let mut v = CollectVisitor { seen: vec![] };
vm.visit_roots(&mut v);
assert_eq!(v.seen, vec![HeapRef(123)]);
}
}

Some files were not shown because too many files have changed in this diff Show More