Compare commits
No commits in common. "master" and "merge/master" have entirely different histories.
master
...
merge/mast
29
.github/workflows/ci.yml
vendored
29
.github/workflows/ci.yml
vendored
@ -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
7
.gitignore
vendored
@ -55,9 +55,4 @@ dist-staging
|
||||
dist-staging/**
|
||||
|
||||
temp
|
||||
temp/**
|
||||
|
||||
**/build/**
|
||||
**/node_modules/**
|
||||
|
||||
AGENTS.md
|
||||
temp/**
|
||||
591
Cargo.lock
generated
591
Cargo.lock
generated
@ -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",
|
||||
|
||||
20
Cargo.toml
20
Cargo.toml
@ -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"
|
||||
|
||||
|
||||
32
Makefile
32
Makefile
@ -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
|
||||
27
README.md
27
README.md
@ -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.
|
||||
|
||||
|
||||
@ -1 +1 @@
|
||||
|
||||
0.1.0
|
||||
|
||||
17
build/program.disasm.txt
Normal file
17
build/program.disasm.txt
Normal 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
BIN
build/program.pbc
Normal file
Binary file not shown.
74
build/symbols.json
Normal file
74
build/symbols.json
Normal 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
|
||||
}
|
||||
]
|
||||
@ -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>,
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
@ -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),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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"))
|
||||
}
|
||||
@ -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};
|
||||
@ -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;
|
||||
@ -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)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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};
|
||||
@ -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(¤t_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(§ion_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 §ions {
|
||||
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(§ion_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));
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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");
|
||||
}
|
||||
}
|
||||
@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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");
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
@ -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
@ -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());
|
||||
}
|
||||
}
|
||||
@ -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
@ -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)
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
@ -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" }
|
||||
@ -1,10 +0,0 @@
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
||||
pub enum BootTarget {
|
||||
#[default]
|
||||
Hub,
|
||||
Cartridge {
|
||||
path: String,
|
||||
debug: bool,
|
||||
debug_port: u16,
|
||||
},
|
||||
}
|
||||
@ -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),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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"));
|
||||
}
|
||||
}
|
||||
@ -1,3 +0,0 @@
|
||||
pub mod firmware;
|
||||
|
||||
pub use firmware::*;
|
||||
@ -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 }
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
@ -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>,
|
||||
}
|
||||
@ -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));
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
@ -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,
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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<'_>;
|
||||
}
|
||||
@ -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));
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
@ -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)))
|
||||
}
|
||||
@ -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>;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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 { .. }))
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
@ -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;
|
||||
@ -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),
|
||||
];
|
||||
@ -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),
|
||||
];
|
||||
@ -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),
|
||||
];
|
||||
@ -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),
|
||||
];
|
||||
@ -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),
|
||||
];
|
||||
@ -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),
|
||||
];
|
||||
@ -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),
|
||||
];
|
||||
@ -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())
|
||||
}
|
||||
@ -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),
|
||||
];
|
||||
@ -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",
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
@ -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(¤t));
|
||||
tel.logs_count.store(3, Ordering::Relaxed);
|
||||
|
||||
let snapshot = tel.snapshot();
|
||||
|
||||
assert_eq!(snapshot.logs_count, 3);
|
||||
assert_eq!(current.load(Ordering::Relaxed), 7);
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -1,6 +0,0 @@
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
pub enum VmFault {
|
||||
Trap(u32, String),
|
||||
Panic(String),
|
||||
Unavailable,
|
||||
}
|
||||
@ -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" }
|
||||
@ -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())
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
@ -1,3 +0,0 @@
|
||||
mod prometeu_hub;
|
||||
|
||||
pub use prometeu_hub::PrometeuHub;
|
||||
@ -1,5 +0,0 @@
|
||||
#[allow(clippy::module_inception)]
|
||||
mod prometeu_hub;
|
||||
mod window_manager;
|
||||
|
||||
pub use prometeu_hub::PrometeuHub;
|
||||
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -1,2 +0,0 @@
|
||||
pub mod fs;
|
||||
pub mod memcard;
|
||||
@ -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;
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@ -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();
|
||||
}
|
||||
}
|
||||
@ -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
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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};
|
||||
@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
Loading…
x
Reference in New Issue
Block a user