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
|
|
||||||
10
.gitignore
vendored
10
.gitignore
vendored
@ -1,5 +1,4 @@
|
|||||||
# ===== Rust / Cargo =====
|
# ===== Rust / Cargo =====
|
||||||
.cargo/config.toml
|
|
||||||
/target/
|
/target/
|
||||||
**/target/
|
**/target/
|
||||||
|
|
||||||
@ -11,7 +10,6 @@
|
|||||||
# RustRover / IntelliJ
|
# RustRover / IntelliJ
|
||||||
.idea/
|
.idea/
|
||||||
*.iml
|
*.iml
|
||||||
.junie
|
|
||||||
|
|
||||||
# VS Code
|
# VS Code
|
||||||
.vscode/
|
.vscode/
|
||||||
@ -58,11 +56,3 @@ dist-staging/**
|
|||||||
|
|
||||||
temp
|
temp
|
||||||
temp/**
|
temp/**
|
||||||
|
|
||||||
**/build/**
|
|
||||||
**/node_modules/**
|
|
||||||
|
|
||||||
AGENTS.md
|
|
||||||
|
|
||||||
.python-version
|
|
||||||
.venv
|
|
||||||
|
|||||||
601
Cargo.lock
generated
601
Cargo.lock
generated
@ -25,7 +25,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75"
|
checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"getrandom 0.3.4",
|
"getrandom",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"version_check",
|
"version_check",
|
||||||
"zerocopy",
|
"zerocopy",
|
||||||
@ -86,7 +86,7 @@ dependencies = [
|
|||||||
"ndk-context",
|
"ndk-context",
|
||||||
"ndk-sys 0.6.0+11769913",
|
"ndk-sys 0.6.0+11769913",
|
||||||
"num_enum",
|
"num_enum",
|
||||||
"thiserror",
|
"thiserror 1.0.69",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -156,9 +156,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "anyhow"
|
name = "anyhow"
|
||||||
version = "1.0.101"
|
version = "1.0.100"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea"
|
checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "arrayref"
|
name = "arrayref"
|
||||||
@ -288,7 +288,7 @@ dependencies = [
|
|||||||
"polling",
|
"polling",
|
||||||
"rustix 0.38.44",
|
"rustix 0.38.44",
|
||||||
"slab",
|
"slab",
|
||||||
"thiserror",
|
"thiserror 1.0.69",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -303,6 +303,15 @@ dependencies = [
|
|||||||
"wayland-client",
|
"wayland-client",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "castaway"
|
||||||
|
version = "0.2.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a"
|
||||||
|
dependencies = [
|
||||||
|
"rustversion",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cc"
|
name = "cc"
|
||||||
version = "1.2.52"
|
version = "1.2.52"
|
||||||
@ -406,7 +415,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e"
|
checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"termcolor",
|
"termcolor",
|
||||||
"unicode-width",
|
"unicode-width 0.1.14",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -456,6 +465,20 @@ dependencies = [
|
|||||||
"memchr",
|
"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]]
|
[[package]]
|
||||||
name = "concurrent-queue"
|
name = "concurrent-queue"
|
||||||
version = "2.5.0"
|
version = "2.5.0"
|
||||||
@ -525,6 +548,12 @@ dependencies = [
|
|||||||
"bindgen",
|
"bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cow-utils"
|
||||||
|
version = "0.1.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "417bef24afe1460300965a25ff4a24b8b45ad011948302ec221e8a0a81eb2c79"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cpal"
|
name = "cpal"
|
||||||
version = "0.15.3"
|
version = "0.15.3"
|
||||||
@ -604,6 +633,12 @@ version = "0.1.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76"
|
checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dragonbox_ecma"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6a5577f010d4e1bb3f3c4d6081e05718eb6992cf20119cab4d3abadff198b5ae"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "either"
|
name = "either"
|
||||||
version = "1.15.0"
|
version = "1.15.0"
|
||||||
@ -626,6 +661,12 @@ dependencies = [
|
|||||||
"windows-sys 0.61.2",
|
"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]]
|
[[package]]
|
||||||
name = "find-msvc-tools"
|
name = "find-msvc-tools"
|
||||||
version = "0.1.7"
|
version = "0.1.7"
|
||||||
@ -669,17 +710,6 @@ dependencies = [
|
|||||||
"windows-link",
|
"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]]
|
[[package]]
|
||||||
name = "getrandom"
|
name = "getrandom"
|
||||||
version = "0.3.4"
|
version = "0.3.4"
|
||||||
@ -757,7 +787,7 @@ checksum = "6f56f6318968d03c18e1bcf4857ff88c61157e9da8e47c5f29055d60e1228884"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"log",
|
"log",
|
||||||
"presser",
|
"presser",
|
||||||
"thiserror",
|
"thiserror 1.0.69",
|
||||||
"winapi",
|
"winapi",
|
||||||
"windows 0.52.0",
|
"windows 0.52.0",
|
||||||
]
|
]
|
||||||
@ -797,6 +827,9 @@ name = "hashbrown"
|
|||||||
version = "0.16.1"
|
version = "0.16.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
|
checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
|
||||||
|
dependencies = [
|
||||||
|
"allocator-api2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hassle-rs"
|
name = "hassle-rs"
|
||||||
@ -808,7 +841,7 @@ dependencies = [
|
|||||||
"com",
|
"com",
|
||||||
"libc",
|
"libc",
|
||||||
"libloading 0.8.9",
|
"libloading 0.8.9",
|
||||||
"thiserror",
|
"thiserror 1.0.69",
|
||||||
"widestring",
|
"widestring",
|
||||||
"winapi",
|
"winapi",
|
||||||
]
|
]
|
||||||
@ -873,7 +906,7 @@ dependencies = [
|
|||||||
"combine",
|
"combine",
|
||||||
"jni-sys",
|
"jni-sys",
|
||||||
"log",
|
"log",
|
||||||
"thiserror",
|
"thiserror 1.0.69",
|
||||||
"walkdir",
|
"walkdir",
|
||||||
"windows-sys 0.45.0",
|
"windows-sys 0.45.0",
|
||||||
]
|
]
|
||||||
@ -890,7 +923,7 @@ version = "0.1.34"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33"
|
checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"getrandom 0.3.4",
|
"getrandom",
|
||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -1055,7 +1088,7 @@ dependencies = [
|
|||||||
"rustc-hash 1.1.0",
|
"rustc-hash 1.1.0",
|
||||||
"spirv",
|
"spirv",
|
||||||
"termcolor",
|
"termcolor",
|
||||||
"thiserror",
|
"thiserror 1.0.69",
|
||||||
"unicode-xid",
|
"unicode-xid",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -1070,7 +1103,7 @@ dependencies = [
|
|||||||
"log",
|
"log",
|
||||||
"ndk-sys 0.5.0+25.2.9519653",
|
"ndk-sys 0.5.0+25.2.9519653",
|
||||||
"num_enum",
|
"num_enum",
|
||||||
"thiserror",
|
"thiserror 1.0.69",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -1085,7 +1118,7 @@ dependencies = [
|
|||||||
"ndk-sys 0.6.0+11769913",
|
"ndk-sys 0.6.0+11769913",
|
||||||
"num_enum",
|
"num_enum",
|
||||||
"raw-window-handle",
|
"raw-window-handle",
|
||||||
"thiserror",
|
"thiserror 1.0.69",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -1122,6 +1155,22 @@ dependencies = [
|
|||||||
"minimal-lexical",
|
"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]]
|
[[package]]
|
||||||
name = "num-derive"
|
name = "num-derive"
|
||||||
version = "0.4.2"
|
version = "0.4.2"
|
||||||
@ -1133,6 +1182,15 @@ dependencies = [
|
|||||||
"syn 2.0.114",
|
"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]]
|
[[package]]
|
||||||
name = "num-traits"
|
name = "num-traits"
|
||||||
version = "0.2.19"
|
version = "0.2.19"
|
||||||
@ -1440,6 +1498,212 @@ dependencies = [
|
|||||||
"ttf-parser",
|
"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]]
|
[[package]]
|
||||||
name = "parking_lot"
|
name = "parking_lot"
|
||||||
version = "0.12.5"
|
version = "0.12.5"
|
||||||
@ -1469,32 +1733,55 @@ version = "1.0.15"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
|
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "pbxgen-dummy-boy"
|
|
||||||
version = "0.1.0"
|
|
||||||
dependencies = [
|
|
||||||
"anyhow",
|
|
||||||
"prometeu-bytecode",
|
|
||||||
"prometeu-hal",
|
|
||||||
"serde_json",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "pbxgen-stress"
|
|
||||||
version = "0.1.0"
|
|
||||||
dependencies = [
|
|
||||||
"anyhow",
|
|
||||||
"prometeu-bytecode",
|
|
||||||
"prometeu-hal",
|
|
||||||
"serde_json",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "percent-encoding"
|
name = "percent-encoding"
|
||||||
version = "2.3.2"
|
version = "2.3.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
|
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]]
|
[[package]]
|
||||||
name = "pin-project"
|
name = "pin-project"
|
||||||
version = "1.1.10"
|
version = "1.1.10"
|
||||||
@ -1530,7 +1817,7 @@ dependencies = [
|
|||||||
"bytemuck",
|
"bytemuck",
|
||||||
"pollster",
|
"pollster",
|
||||||
"raw-window-handle",
|
"raw-window-handle",
|
||||||
"thiserror",
|
"thiserror 1.0.69",
|
||||||
"ultraviolet",
|
"ultraviolet",
|
||||||
"wgpu",
|
"wgpu",
|
||||||
]
|
]
|
||||||
@ -1576,15 +1863,6 @@ dependencies = [
|
|||||||
"portable-atomic",
|
"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]]
|
[[package]]
|
||||||
name = "presser"
|
name = "presser"
|
||||||
version = "0.3.1"
|
version = "0.3.1"
|
||||||
@ -1615,42 +1893,39 @@ version = "1.0.17"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3eb8486b569e12e2c32ad3e204dbaba5e4b5b216e9367044f25f1dba42341773"
|
checksum = "3eb8486b569e12e2c32ad3e204dbaba5e4b5b216e9367044f25f1dba42341773"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "prometeu"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"clap",
|
||||||
|
"prometeu-compiler",
|
||||||
|
"prometeu-runtime-desktop",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "prometeu-bytecode"
|
name = "prometeu-bytecode"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
|
||||||
"serde",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "prometeu-cli"
|
name = "prometeu-compiler"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
"clap",
|
"clap",
|
||||||
"prometeu-host-desktop-winit",
|
"oxc_allocator",
|
||||||
]
|
"oxc_ast",
|
||||||
|
"oxc_ast_visit",
|
||||||
[[package]]
|
"oxc_parser",
|
||||||
name = "prometeu-drivers"
|
"oxc_span",
|
||||||
version = "0.1.0"
|
"prometeu-bytecode",
|
||||||
dependencies = [
|
"prometeu-core",
|
||||||
"prometeu-hal",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "prometeu-firmware"
|
name = "prometeu-core"
|
||||||
version = "0.1.0"
|
|
||||||
dependencies = [
|
|
||||||
"prometeu-bytecode",
|
|
||||||
"prometeu-drivers",
|
|
||||||
"prometeu-hal",
|
|
||||||
"prometeu-system",
|
|
||||||
"prometeu-vm",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "prometeu-hal"
|
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"prometeu-bytecode",
|
"prometeu-bytecode",
|
||||||
@ -1659,62 +1934,17 @@ dependencies = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "prometeu-host-desktop-winit"
|
name = "prometeu-runtime-desktop"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"clap",
|
|
||||||
"cpal",
|
"cpal",
|
||||||
"pixels",
|
"pixels",
|
||||||
"prometeu-drivers",
|
"prometeu-core",
|
||||||
"prometeu-firmware",
|
|
||||||
"prometeu-hal",
|
|
||||||
"prometeu-system",
|
|
||||||
"ringbuf",
|
"ringbuf",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"winit",
|
"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]]
|
[[package]]
|
||||||
name = "quick-xml"
|
name = "quick-xml"
|
||||||
version = "0.38.4"
|
version = "0.38.4"
|
||||||
@ -1739,36 +1969,6 @@ version = "5.3.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
|
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]]
|
[[package]]
|
||||||
name = "range-alloc"
|
name = "range-alloc"
|
||||||
version = "0.1.4"
|
version = "0.1.4"
|
||||||
@ -1898,6 +2098,12 @@ version = "1.0.22"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
|
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ryu"
|
||||||
|
version = "1.0.22"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "safe_arch"
|
name = "safe_arch"
|
||||||
version = "0.7.4"
|
version = "0.7.4"
|
||||||
@ -1941,6 +2147,12 @@ dependencies = [
|
|||||||
"tiny-skia",
|
"tiny-skia",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "seq-macro"
|
||||||
|
version = "0.3.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1bc711410fbe7399f390ca1c3b60ad0f53f80e95c5eb935e52268a0e2cd49acc"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde"
|
name = "serde"
|
||||||
version = "1.0.228"
|
version = "1.0.228"
|
||||||
@ -1990,6 +2202,12 @@ version = "1.3.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "siphasher"
|
||||||
|
version = "1.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "slab"
|
name = "slab"
|
||||||
version = "0.4.11"
|
version = "0.4.11"
|
||||||
@ -2011,6 +2229,12 @@ version = "1.15.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
|
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "smawk"
|
||||||
|
version = "0.3.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "smithay-client-toolkit"
|
name = "smithay-client-toolkit"
|
||||||
version = "0.19.2"
|
version = "0.19.2"
|
||||||
@ -2025,7 +2249,7 @@ dependencies = [
|
|||||||
"log",
|
"log",
|
||||||
"memmap2",
|
"memmap2",
|
||||||
"rustix 0.38.44",
|
"rustix 0.38.44",
|
||||||
"thiserror",
|
"thiserror 1.0.69",
|
||||||
"wayland-backend",
|
"wayland-backend",
|
||||||
"wayland-client",
|
"wayland-client",
|
||||||
"wayland-csd-frame",
|
"wayland-csd-frame",
|
||||||
@ -2103,13 +2327,33 @@ dependencies = [
|
|||||||
"winapi-util",
|
"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]]
|
[[package]]
|
||||||
name = "thiserror"
|
name = "thiserror"
|
||||||
version = "1.0.69"
|
version = "1.0.69"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
|
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
|
||||||
dependencies = [
|
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]]
|
[[package]]
|
||||||
@ -2123,6 +2367,17 @@ dependencies = [
|
|||||||
"syn 2.0.114",
|
"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]]
|
[[package]]
|
||||||
name = "tiny-skia"
|
name = "tiny-skia"
|
||||||
version = "0.11.4"
|
version = "0.11.4"
|
||||||
@ -2171,9 +2426,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toml_parser"
|
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"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0742ff5ff03ea7e67c8ae6c93cac239e0d9784833362da3f9a9c1da8dfefcbdc"
|
checksum = "a3198b4b0a8e11f09dd03e133c0280504d0801269e9afa46362ffde1cbeebf44"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"winnow",
|
"winnow",
|
||||||
]
|
]
|
||||||
@ -2209,12 +2464,24 @@ dependencies = [
|
|||||||
"wide",
|
"wide",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-id-start"
|
||||||
|
version = "1.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "81b79ad29b5e19de4260020f8919b443b2ef0277d242ce532ec7b7a2cc8b6007"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-ident"
|
name = "unicode-ident"
|
||||||
version = "1.0.22"
|
version = "1.0.22"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
|
checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-linebreak"
|
||||||
|
version = "0.1.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-segmentation"
|
name = "unicode-segmentation"
|
||||||
version = "1.12.0"
|
version = "1.12.0"
|
||||||
@ -2227,6 +2494,12 @@ version = "0.1.14"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af"
|
checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-width"
|
||||||
|
version = "0.2.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-xid"
|
name = "unicode-xid"
|
||||||
version = "0.2.6"
|
version = "0.2.6"
|
||||||
@ -2255,12 +2528,6 @@ dependencies = [
|
|||||||
"winapi-util",
|
"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]]
|
[[package]]
|
||||||
name = "wasip2"
|
name = "wasip2"
|
||||||
version = "1.0.1+wasi-0.2.4"
|
version = "1.0.1+wasi-0.2.4"
|
||||||
@ -2501,7 +2768,7 @@ dependencies = [
|
|||||||
"raw-window-handle",
|
"raw-window-handle",
|
||||||
"rustc-hash 1.1.0",
|
"rustc-hash 1.1.0",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
"thiserror",
|
"thiserror 1.0.69",
|
||||||
"web-sys",
|
"web-sys",
|
||||||
"wgpu-hal",
|
"wgpu-hal",
|
||||||
"wgpu-types",
|
"wgpu-types",
|
||||||
@ -2545,7 +2812,7 @@ dependencies = [
|
|||||||
"renderdoc-sys",
|
"renderdoc-sys",
|
||||||
"rustc-hash 1.1.0",
|
"rustc-hash 1.1.0",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
"thiserror",
|
"thiserror 1.0.69",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
"web-sys",
|
"web-sys",
|
||||||
"wgpu-types",
|
"wgpu-types",
|
||||||
|
|||||||
18
Cargo.toml
18
Cargo.toml
@ -1,18 +1,10 @@
|
|||||||
[workspace]
|
[workspace]
|
||||||
members = [
|
members = [
|
||||||
"crates/console/prometeu-bytecode",
|
"crates/prometeu-core",
|
||||||
"crates/console/prometeu-drivers",
|
"crates/prometeu-runtime-desktop",
|
||||||
"crates/console/prometeu-firmware",
|
"crates/prometeu",
|
||||||
"crates/console/prometeu-hal",
|
"crates/prometeu-bytecode",
|
||||||
"crates/console/prometeu-system",
|
"crates/prometeu-compiler",
|
||||||
"crates/console/prometeu-vm",
|
|
||||||
"crates/host/prometeu-host-desktop-winit",
|
|
||||||
"crates/tools/prometeu-cli",
|
|
||||||
"crates/tools/pbxgen-dummy-boy",
|
|
||||||
"crates/tools/pbxgen-stress",
|
|
||||||
"crates/dev/prometeu-test-support",
|
|
||||||
"crates/dev/prometeu-layer-tests",
|
|
||||||
"crates/dev/prometeu-quality-checks",
|
|
||||||
]
|
]
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
|
|
||||||
|
|||||||
49
Makefile
49
Makefile
@ -1,49 +0,0 @@
|
|||||||
.PHONY: fmt fmt-check clippy test-local test-debugger-socket clean coverage coverage-xml coverage-json coverage-report-json coverage-domain-list coverage-domain-evidence coverage-domain-gate test ci ci-domains 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-report-json:
|
|
||||||
cargo llvm-cov report --json --output-path target/llvm-cov/report.json
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
coverage-domain-list:
|
|
||||||
./scripts/coverage-domain-evidence.sh --list
|
|
||||||
|
|
||||||
coverage-domain-evidence:
|
|
||||||
@if [ -z "$(DOMAIN)" ]; then \
|
|
||||||
echo "usage: make coverage-domain-evidence DOMAIN=<domain>"; \
|
|
||||||
exit 1; \
|
|
||||||
fi
|
|
||||||
./scripts/coverage-domain-evidence.sh "$(DOMAIN)"
|
|
||||||
|
|
||||||
coverage-domain-gate:
|
|
||||||
./scripts/coverage-domain-evidence.sh --check-all
|
|
||||||
|
|
||||||
test: fmt-check clippy test-local test-debugger-socket
|
|
||||||
ci: clean fmt-check clippy coverage coverage-report-json
|
|
||||||
ci-domains: clean fmt-check clippy coverage coverage-report-json coverage-xml coverage-json coverage-domain-gate
|
|
||||||
cobertura: coverage-xml coverage-json
|
|
||||||
196
README.md
196
README.md
@ -1,188 +1,84 @@
|
|||||||
# PROMETEU Runtime
|
# PROMETEU
|
||||||
|
|
||||||
PROMETEU is an educational and experimental fantasy handheld / fantasy console.
|
PROMETEU is an **educational and experimental ecosystem** inspired by classic consoles, focusing on **teaching programming, system architecture, and hardware concepts through software**.
|
||||||
This repository contains the Rust runtime workspace for the machine: VM-facing components, virtual peripherals, firmware/system services, host integration, CLI entrypoints, development utilities, specs, and discussion artifacts.
|
|
||||||
|
|
||||||
The VM is only one subsystem of the machine. The canonical docs in this repository intentionally distinguish:
|
> PROMETEU is a simple, explicit, and educational virtual machine.
|
||||||
|
|
||||||
- machine-level specs;
|
---
|
||||||
- VM/runtime internal architecture;
|
|
||||||
- implementation crates;
|
|
||||||
- discussion workflow and lessons learned.
|
|
||||||
|
|
||||||
## What This Repository Contains
|
## 🎯 Project Goals
|
||||||
|
|
||||||
- `crates/console/`: core machine/runtime crates such as `prometeu-vm`, `prometeu-system`, `prometeu-hal`, `prometeu-drivers`, and `prometeu-firmware`
|
- **Simulate simple "logical hardware"**: Create a low entry barrier for understanding how computers work.
|
||||||
- `crates/host/`: host-side execution surfaces, currently including `prometeu-host-desktop-winit`
|
- **Deterministic Loop**: Ensure the same code produces the same result on any platform.
|
||||||
- `crates/tools/`: user-facing and support binaries such as `prometeu` and `pbxgen-stress`
|
- **Total Portability**: The core does not depend on an operating system, allowing it to run from modern computers to dedicated hardware.
|
||||||
- `crates/dev/`: test support, layer tests, and quality-check utilities
|
- **First-Class Tools**: Offer deep debugging and inspection as a central part of the experience.
|
||||||
- `docs/specs/runtime/`: canonical PROMETEU machine specs
|
|
||||||
- `docs/vm-arch/`: canonical VM/runtime architecture and ISA references
|
|
||||||
- `devtools/`: debugger protocol material
|
|
||||||
- `discussion/`: agendas, decisions, plans, and lessons for architectural work
|
|
||||||
- `test-cartridges/`: cartridge fixtures used for validation and manual runs
|
|
||||||
|
|
||||||
## Canonical Documentation
|
---
|
||||||
|
|
||||||
Use these entrypoints instead of inferring the model from isolated source files:
|
## 🧠 Design Philosophy
|
||||||
|
|
||||||
- [Machine specs](docs/specs/runtime/README.md): authoritative contract for the PROMETEU machine, peripherals, firmware, cartridge format, timing, and host ABI
|
- **No magic**: everything is explicit.
|
||||||
- [VM architecture](docs/vm-arch/ARCHITECTURE.md): authoritative internal architecture for VM/runtime invariants
|
- **No implicit heuristics**: the system doesn't "guess intentions".
|
||||||
- [ISA reference](docs/vm-arch/ISA_CORE.md): bytecode-level instruction set authority
|
- **Deterministic**: same input → same result.
|
||||||
- [Discussion workflow](discussion/index.ndjson): architectural agenda, decision, and execution traceability
|
- **Hardware-first**: APIs model peripherals, not modern frameworks.
|
||||||
|
- **Portable by definition**: if it doesn't work on all platforms, it doesn't exist.
|
||||||
|
|
||||||
## Workspace Layout
|
---
|
||||||
|
|
||||||
This repository is a Rust workspace rooted at [Cargo.toml](Cargo.toml) with members in:
|
## 📦 Monorepo Structure
|
||||||
|
|
||||||
- `crates/console`
|
This repository is organized as a Rust workspace and contains several components:
|
||||||
- `crates/host`
|
|
||||||
- `crates/tools`
|
|
||||||
- `crates/dev`
|
|
||||||
|
|
||||||
The main user-facing dispatcher binary is `prometeu`, built from `crates/tools/prometeu-cli`.
|
- **[crates/](./crates)**: Software implementation in Rust.
|
||||||
The same package also builds `prometeu-runtime`, which embeds the current desktop host runtime.
|
- **[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.
|
||||||
|
|
||||||
Current CLI surface:
|
---
|
||||||
|
|
||||||
- `prometeu run <cart>`
|
## 🛠️ Requirements
|
||||||
- `prometeu debug <cart> --port <port>`
|
|
||||||
|
|
||||||
The dispatcher forwards runtime execution to `prometeu-runtime`.
|
- **Rust**: Version defined in `rust-toolchain.toml`.
|
||||||
For lower-level local runs, `prometeu-runtime` also accepts:
|
- **Installation**: Use [rustup](https://rustup.rs/) to install the required toolchain.
|
||||||
|
|
||||||
- `--run <cart>`
|
---
|
||||||
- `--debug <cart> --port <port>`
|
|
||||||
- `--fs-root <path>`
|
|
||||||
- `--cap <path>`
|
|
||||||
|
|
||||||
Other ecosystem commands, such as build, pack, and verification tooling, are not part of the current dispatcher surface in this workspace.
|
## ▶️ Quick Start
|
||||||
|
|
||||||
## Requirements
|
To compile the full project:
|
||||||
|
|
||||||
- Rust toolchain from [rust-toolchain.toml](rust-toolchain.toml)
|
|
||||||
- `rustup` for toolchain installation and management
|
|
||||||
- `cargo-llvm-cov` for coverage targets such as `make coverage` and `make ci-domains`
|
|
||||||
- a local graphical environment for desktop runtime commands
|
|
||||||
|
|
||||||
## Quick Start
|
|
||||||
|
|
||||||
Build the workspace:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cargo build
|
cargo build
|
||||||
```
|
```
|
||||||
|
|
||||||
Inspect the CLI:
|
To run an example cartridge:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cargo run -q -p prometeu-cli --bin prometeu -- --help
|
./target/debug/prometeu run test-cartridges/color-square
|
||||||
```
|
```
|
||||||
|
|
||||||
Run the current stress cartridge fixture:
|
For more details on how to use the CLI, see the **[prometeu](./crates/prometeu)** README.
|
||||||
|
|
||||||
```bash
|
---
|
||||||
cargo run -q -p prometeu-cli --bin prometeu -- run test-cartridges/stress-console
|
|
||||||
```
|
|
||||||
|
|
||||||
Run the embedded runtime binary directly:
|
## 🚧 Project Status
|
||||||
|
|
||||||
```bash
|
⚠️ **Early stage (bootstrap)**
|
||||||
cargo run -q -p prometeu-cli --bin prometeu-runtime -- --run test-cartridges/stress-console
|
|
||||||
```
|
|
||||||
|
|
||||||
Regenerate and run the stress cartridge fixture:
|
Currently, the focus is on stabilizing the core architecture and debugging protocol. Nothing here should be considered a stable API yet.
|
||||||
|
|
||||||
```bash
|
---
|
||||||
./scripts/run-stress.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
The desktop runtime opens a native window through the host layer, so runtime commands are intended for a local graphical environment.
|
## 📜 License
|
||||||
|
|
||||||
## Validation
|
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
||||||
|
|
||||||
Format, lint, and test the workspace:
|
---
|
||||||
|
|
||||||
```bash
|
## ✨ Final Note
|
||||||
make test
|
|
||||||
```
|
|
||||||
|
|
||||||
Run the CI-oriented non-domain gate:
|
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.
|
||||||
|
|
||||||
```bash
|
|
||||||
make ci
|
|
||||||
```
|
|
||||||
|
|
||||||
Run the CI-oriented domain coverage gate:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
make ci-domains
|
|
||||||
```
|
|
||||||
|
|
||||||
The debugger socket tests bind localhost sockets and are marked ignored in Rust test metadata; `make test` includes them through the dedicated `test-debugger-socket` target.
|
|
||||||
|
|
||||||
## Coverage Governance
|
|
||||||
|
|
||||||
The repository uses a two-layer runtime-edge coverage model:
|
|
||||||
|
|
||||||
- a mandatory CI gate based on canonical runtime-edge domains;
|
|
||||||
- domain-oriented review evidence for the canonical runtime-edge domains.
|
|
||||||
|
|
||||||
Current canonical domains:
|
|
||||||
|
|
||||||
- `system/runtime`
|
|
||||||
- `fs`
|
|
||||||
- `asset/bank`
|
|
||||||
- `firmware`
|
|
||||||
- `host-dependent`
|
|
||||||
|
|
||||||
Global coverage artifacts:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
make ci
|
|
||||||
make cobertura
|
|
||||||
make ci-domains
|
|
||||||
```
|
|
||||||
|
|
||||||
This produces:
|
|
||||||
|
|
||||||
- `target/llvm-cov/html`
|
|
||||||
- `target/llvm-cov/report.json`
|
|
||||||
- `target/llvm-cov/summary.json`
|
|
||||||
- `target/llvm-cov/cobertura.xml`
|
|
||||||
|
|
||||||
Domain-oriented evidence helpers:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
make coverage-domain-list
|
|
||||||
make coverage-domain-evidence DOMAIN=firmware
|
|
||||||
make coverage-domain-evidence DOMAIN=host-dependent
|
|
||||||
```
|
|
||||||
|
|
||||||
Per-domain baselines currently start around `60`.
|
|
||||||
That does not waive test obligations. It means the project is adopting explicit domain evidence immediately while tightening quantitative expectations incrementally over time.
|
|
||||||
|
|
||||||
`make ci-domains` is the CI entrypoint used by Jenkins.
|
|
||||||
It runs the standard coverage pipeline, produces the HTML/XML/JSON artifacts, and fails when any canonical domain falls below its configured `baseline_percent`.
|
|
||||||
The CI gate currently compares domain `lines` coverage against each domain baseline.
|
|
||||||
|
|
||||||
When a PR changes the observable contract of a canonical domain, it is expected to:
|
|
||||||
|
|
||||||
- add or adjust tests for that domain; or
|
|
||||||
- justify explicitly why the observable contract did not change.
|
|
||||||
|
|
||||||
Improving the global percentage alone is not sufficient review evidence when the changed behavior belongs to a canonical domain.
|
|
||||||
|
|
||||||
## Current State
|
|
||||||
|
|
||||||
The project is still in active architectural and implementation convergence.
|
|
||||||
|
|
||||||
- the machine contract is being clarified through the specs and discussion workflow;
|
|
||||||
- the workspace already contains concrete runtime and host code;
|
|
||||||
- the CLI dispatcher currently exposes the runtime paths that are backed by binaries in this workspace.
|
|
||||||
|
|
||||||
Treat APIs, file formats, and execution flows as evolving unless the relevant spec explicitly defines them as stable.
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
This project is licensed under the [MIT License](LICENSE).
|
|
||||||
|
|||||||
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::{string_materialization_count, 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: "gfx2d".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.to_string()),
|
|
||||||
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 gfx2d.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());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -1,436 +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, InputPlatform, NoopTelemetryPlatform, PadBridge, RenderBackend,
|
|
||||||
RuntimePlatform, TelemetryPlatform, TouchBridge,
|
|
||||||
};
|
|
||||||
use prometeu_hal::{
|
|
||||||
Game2DFrameComposer, Game2DFramePacket, RenderSubmission, RenderSubmissionPacket,
|
|
||||||
};
|
|
||||||
use prometeu_hal::{RenderSubmissionSink, RenderSubmitError};
|
|
||||||
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**: 480x270 (16:9 Aspect Ratio).
|
|
||||||
/// - **Color Format**: RGBA8888.
|
|
||||||
/// - **Audio**: Stereo, Command-based mixing.
|
|
||||||
/// - **Input**: 12-button Digital Gamepad + Absolute Touch/Mouse.
|
|
||||||
pub struct Hardware {
|
|
||||||
/// Shared physical memory bank ownership used by peripherals and worker backends.
|
|
||||||
pub memory_banks: Arc<MemoryBanks>,
|
|
||||||
/// 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 Hardware {
|
|
||||||
fn consume_render_submission(&mut self, submission: &RenderSubmission) {
|
|
||||||
match &submission.packet {
|
|
||||||
RenderSubmissionPacket::Game2D(packet) => {
|
|
||||||
if packet.composer.bound_scene.is_none() {
|
|
||||||
self.gfx.render_game2d_frame_packet(packet);
|
|
||||||
} else {
|
|
||||||
self.frame_composer.render_game2d_frame_packet(packet, &mut self.gfx);
|
|
||||||
self.gfx.apply_gfx2d_commands(&packet.gfx2d);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
RenderSubmissionPacket::ShellUi(packet) => {
|
|
||||||
self.gfx.render_shell_ui_frame_packet(packet);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
self.gfx.present();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RenderSubmissionSink for Hardware {
|
|
||||||
fn submit_render_submission(
|
|
||||||
&mut self,
|
|
||||||
submission: RenderSubmission,
|
|
||||||
) -> Result<(), RenderSubmitError> {
|
|
||||||
self.consume_render_submission(&submission);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RenderBackend for Hardware {
|
|
||||||
fn render_frame(&mut self) {
|
|
||||||
if self.frame_composer.active_scene_id().is_none() {
|
|
||||||
let packet = self.frame_composer.close_game2d_packet();
|
|
||||||
self.gfx.render_game2d_frame_packet(&packet);
|
|
||||||
} else {
|
|
||||||
self.frame_composer.render_frame(&mut self.gfx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Game2DFrameComposer for Hardware {
|
|
||||||
fn begin_frame(&mut self) {
|
|
||||||
self.frame_composer.begin_frame();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn bind_scene(&mut self, scene_bank_id: usize) -> prometeu_hal::ComposerOpStatus {
|
|
||||||
if self.frame_composer.bind_scene(scene_bank_id) {
|
|
||||||
prometeu_hal::ComposerOpStatus::Ok
|
|
||||||
} else {
|
|
||||||
prometeu_hal::ComposerOpStatus::SceneUnavailable
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn unbind_scene(&mut self) {
|
|
||||||
self.frame_composer.unbind_scene();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_camera(&mut self, x: i32, y: i32) -> prometeu_hal::ComposerOpStatus {
|
|
||||||
self.frame_composer.set_camera(x, y);
|
|
||||||
prometeu_hal::ComposerOpStatus::Ok
|
|
||||||
}
|
|
||||||
|
|
||||||
fn emit_sprite(&mut self, sprite: Sprite) -> prometeu_hal::ComposerOpStatus {
|
|
||||||
if self.gfx.glyph_banks.glyph_bank_slot(sprite.bank_id as usize).is_none() {
|
|
||||||
return prometeu_hal::ComposerOpStatus::BankInvalid;
|
|
||||||
}
|
|
||||||
|
|
||||||
if self.frame_composer.emit_sprite(sprite) {
|
|
||||||
prometeu_hal::ComposerOpStatus::Ok
|
|
||||||
} else {
|
|
||||||
prometeu_hal::ComposerOpStatus::SpriteOverflow
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn close_game2d_packet(&self) -> Game2DFramePacket {
|
|
||||||
self.frame_composer.close_game2d_packet()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl InputPlatform for Hardware {
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static NOOP_TELEMETRY: NoopTelemetryPlatform = NoopTelemetryPlatform;
|
|
||||||
|
|
||||||
impl RuntimePlatform for Hardware {
|
|
||||||
fn render_submission_sink(&mut self) -> &mut dyn RenderSubmissionSink {
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_backend(&mut self) -> &mut dyn RenderBackend {
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
fn game2d_frame_composer(&mut self) -> &mut dyn Game2DFrameComposer {
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
fn audio(&self) -> &dyn AudioBridge {
|
|
||||||
&self.audio
|
|
||||||
}
|
|
||||||
|
|
||||||
fn audio_mut(&mut self) -> &mut dyn AudioBridge {
|
|
||||||
&mut self.audio
|
|
||||||
}
|
|
||||||
|
|
||||||
fn input(&self) -> &dyn InputPlatform {
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
fn input_mut(&mut self) -> &mut dyn InputPlatform {
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
fn assets(&self) -> &dyn AssetBridge {
|
|
||||||
&self.assets
|
|
||||||
}
|
|
||||||
|
|
||||||
fn assets_mut(&mut self) -> &mut dyn AssetBridge {
|
|
||||||
&mut self.assets
|
|
||||||
}
|
|
||||||
|
|
||||||
fn telemetry(&self) -> &dyn TelemetryPlatform {
|
|
||||||
&NOOP_TELEMETRY
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Hardware {
|
|
||||||
/// Internal hardware width in pixels.
|
|
||||||
pub const W: usize = 480;
|
|
||||||
/// Internal hardware height in pixels.
|
|
||||||
pub const H: usize = 270;
|
|
||||||
|
|
||||||
/// 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 {
|
|
||||||
let 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>,
|
|
||||||
);
|
|
||||||
let glyph_slot_index = assets.glyph_asset_slot_index();
|
|
||||||
Self {
|
|
||||||
memory_banks: Arc::clone(&memory_banks),
|
|
||||||
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>,
|
|
||||||
glyph_slot_index,
|
|
||||||
),
|
|
||||||
audio: Audio::new(Arc::clone(&memory_banks) as Arc<dyn SoundBankPoolAccess>),
|
|
||||||
pad: Pad::default(),
|
|
||||||
touch: Touch::default(),
|
|
||||||
assets,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[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::primitives::Rect;
|
|
||||||
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;
|
|
||||||
use prometeu_hal::{
|
|
||||||
BoundScenePacket, Camera2D, ComposerFramePacket, FrameId, GameSpritePacket, Gfx2dCommand,
|
|
||||||
GfxUiCommand, ShellUiFramePacket,
|
|
||||||
};
|
|
||||||
|
|
||||||
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 {
|
|
||||||
make_scene_with_glyph_asset_id(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn make_scene_with_glyph_asset_id(glyph_asset_id: i32) -> SceneBank {
|
|
||||||
let layer = SceneLayer {
|
|
||||||
active: true,
|
|
||||||
glyph_asset_id,
|
|
||||||
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_with_glyph_asset_id(0)));
|
|
||||||
|
|
||||||
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.render_scene_from_cache(&cache, &update, &[0, 0, 0, 0]);
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn publish_game2d_submission_applies_gfx2d_overlay_after_active_scene() {
|
|
||||||
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_with_glyph_asset_id(0)));
|
|
||||||
|
|
||||||
let mut hardware = Hardware::new_with_memory_banks(banks);
|
|
||||||
let mut glyph_slots = [None; 16];
|
|
||||||
glyph_slots[0] = Some(0);
|
|
||||||
hardware.assets.glyph_asset_slot_index().rebuild_from_slots(&glyph_slots);
|
|
||||||
assert_eq!(
|
|
||||||
Game2DFrameComposer::bind_scene(&mut hardware, 0),
|
|
||||||
prometeu_hal::ComposerOpStatus::Ok
|
|
||||||
);
|
|
||||||
|
|
||||||
let mut packet = Game2DFrameComposer::close_game2d_packet(&hardware);
|
|
||||||
packet.gfx2d = vec![Gfx2dCommand::FillRect {
|
|
||||||
rect: Rect { x: 0, y: 0, w: 1, h: 1 },
|
|
||||||
color: Color::BLUE,
|
|
||||||
}];
|
|
||||||
|
|
||||||
RenderSubmissionSink::submit_render_submission(
|
|
||||||
&mut hardware,
|
|
||||||
RenderSubmission::game2d(FrameId::ZERO, packet),
|
|
||||||
)
|
|
||||||
.expect("local sink should publish Game2D packet");
|
|
||||||
|
|
||||||
assert_eq!(hardware.gfx.front_buffer()[0], Color::BLUE.raw());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn publish_game2d_submission_uses_packet_scene_and_sprites_not_live_frame_state() {
|
|
||||||
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_with_glyph_asset_id(0)));
|
|
||||||
|
|
||||||
let mut hardware = Hardware::new_with_memory_banks(banks);
|
|
||||||
let mut glyph_slots = [None; 16];
|
|
||||||
glyph_slots[0] = Some(0);
|
|
||||||
hardware.assets.glyph_asset_slot_index().rebuild_from_slots(&glyph_slots);
|
|
||||||
assert_eq!(
|
|
||||||
Game2DFrameComposer::bind_scene(&mut hardware, 0),
|
|
||||||
prometeu_hal::ComposerOpStatus::Ok
|
|
||||||
);
|
|
||||||
|
|
||||||
let packet = Game2DFramePacket::new(
|
|
||||||
ComposerFramePacket {
|
|
||||||
bound_scene: Some(BoundScenePacket { bank_id: 0 }),
|
|
||||||
camera: Camera2D::default(),
|
|
||||||
sprites: vec![GameSpritePacket {
|
|
||||||
glyph_id: 0,
|
|
||||||
palette_id: 0,
|
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
layer: 0,
|
|
||||||
bank_id: 0,
|
|
||||||
flip_x: false,
|
|
||||||
flip_y: false,
|
|
||||||
priority: 0,
|
|
||||||
}],
|
|
||||||
hud: Default::default(),
|
|
||||||
},
|
|
||||||
Vec::new(),
|
|
||||||
);
|
|
||||||
|
|
||||||
Game2DFrameComposer::begin_frame(&mut hardware);
|
|
||||||
RenderSubmissionSink::submit_render_submission(
|
|
||||||
&mut hardware,
|
|
||||||
RenderSubmission::game2d(FrameId::ZERO, packet),
|
|
||||||
)
|
|
||||||
.expect("local sink should publish Game2D packet");
|
|
||||||
|
|
||||||
assert_eq!(hardware.gfx.front_buffer()[0], Color::RED.raw());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn render_submission_sink_accepts_owned_game2d_submission() {
|
|
||||||
let mut hardware = Hardware::new();
|
|
||||||
let packet = Game2DFramePacket::new(
|
|
||||||
ComposerFramePacket::default(),
|
|
||||||
vec![Gfx2dCommand::Clear { color: Color::BLUE }],
|
|
||||||
);
|
|
||||||
let submission = RenderSubmission::game2d(FrameId::ZERO, packet);
|
|
||||||
|
|
||||||
RenderSubmissionSink::submit_render_submission(&mut hardware, submission)
|
|
||||||
.expect("local sink should accept owned Game2D submissions");
|
|
||||||
|
|
||||||
assert_eq!(hardware.gfx.front_buffer()[0], Color::BLUE.raw());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn render_submission_sink_accepts_owned_shell_ui_submission() {
|
|
||||||
let mut hardware = Hardware::new();
|
|
||||||
let packet = ShellUiFramePacket::new(vec![GfxUiCommand::Clear { color: Color::RED }]);
|
|
||||||
let submission = RenderSubmission::shell_ui(FrameId::ZERO, packet);
|
|
||||||
|
|
||||||
RenderSubmissionSink::submit_render_submission(&mut hardware, submission)
|
|
||||||
.expect("local sink should accept owned ShellUi submissions");
|
|
||||||
|
|
||||||
assert_eq!(hardware.gfx.front_buffer()[0], Color::RED.raw());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,22 +0,0 @@
|
|||||||
mod asset;
|
|
||||||
mod audio;
|
|
||||||
mod frame_composer;
|
|
||||||
mod gfx;
|
|
||||||
pub mod hardware;
|
|
||||||
mod local_render_backend;
|
|
||||||
mod memory_banks;
|
|
||||||
mod pad;
|
|
||||||
mod test_platform;
|
|
||||||
mod touch;
|
|
||||||
|
|
||||||
pub use crate::asset::AssetManager;
|
|
||||||
pub use crate::audio::{Audio, AudioCommand, Channel, MAX_CHANNELS, OUTPUT_SAMPLE_RATE};
|
|
||||||
pub use crate::frame_composer::{ComposerBuffer, FrameComposer, SceneStatus, SpriteController};
|
|
||||||
pub use crate::gfx::Gfx;
|
|
||||||
pub use crate::local_render_backend::LocalFramebufferRenderBackend;
|
|
||||||
pub use crate::memory_banks::{
|
|
||||||
GlyphBankPoolAccess, GlyphBankPoolInstaller, MemoryBanks, SceneBankPoolAccess,
|
|
||||||
SceneBankPoolInstaller, SoundBankPoolAccess, SoundBankPoolInstaller,
|
|
||||||
};
|
|
||||||
pub use crate::pad::Pad;
|
|
||||||
pub use crate::test_platform::TestPlatform;
|
|
||||||
@ -1,164 +0,0 @@
|
|||||||
use crate::asset::GlyphAssetSlotIndex;
|
|
||||||
use crate::frame_composer::FrameComposer;
|
|
||||||
use crate::gfx::Gfx;
|
|
||||||
use crate::memory_banks::{GlyphBankPoolAccess, MemoryBanks, SceneBankPoolAccess};
|
|
||||||
use prometeu_hal::app_mode::AppMode;
|
|
||||||
use prometeu_hal::{
|
|
||||||
OwnedRgba8888Frame, RenderSubmission, RenderSubmissionPacket, RenderWorkerBackend,
|
|
||||||
RenderWorkerError,
|
|
||||||
};
|
|
||||||
use std::sync::{Arc, Mutex};
|
|
||||||
|
|
||||||
pub struct LocalFramebufferRenderBackend {
|
|
||||||
gfx: Mutex<Gfx>,
|
|
||||||
frame_composer: Mutex<FrameComposer>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LocalFramebufferRenderBackend {
|
|
||||||
pub fn new(width: usize, height: usize, glyph_banks: Arc<dyn GlyphBankPoolAccess>) -> Self {
|
|
||||||
Self::new_with_parts(
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
glyph_banks,
|
|
||||||
Arc::new(MemoryBanks::new()),
|
|
||||||
GlyphAssetSlotIndex::new(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn new_with_memory_banks(
|
|
||||||
width: usize,
|
|
||||||
height: usize,
|
|
||||||
memory_banks: Arc<MemoryBanks>,
|
|
||||||
glyph_slot_index: GlyphAssetSlotIndex,
|
|
||||||
) -> Self {
|
|
||||||
Self::new_with_parts(
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
Arc::clone(&memory_banks) as Arc<dyn GlyphBankPoolAccess>,
|
|
||||||
memory_banks,
|
|
||||||
glyph_slot_index,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn new_with_parts(
|
|
||||||
width: usize,
|
|
||||||
height: usize,
|
|
||||||
glyph_banks: Arc<dyn GlyphBankPoolAccess>,
|
|
||||||
scene_banks: Arc<dyn SceneBankPoolAccess>,
|
|
||||||
glyph_slot_index: GlyphAssetSlotIndex,
|
|
||||||
) -> Self {
|
|
||||||
Self {
|
|
||||||
gfx: Mutex::new(Gfx::new(width, height, glyph_banks)),
|
|
||||||
frame_composer: Mutex::new(FrameComposer::new(
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
scene_banks,
|
|
||||||
glyph_slot_index,
|
|
||||||
)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn render_submission(
|
|
||||||
&self,
|
|
||||||
submission: &RenderSubmission,
|
|
||||||
) -> Result<OwnedRgba8888Frame, RenderWorkerError> {
|
|
||||||
let mut gfx = self.gfx.lock().map_err(|_| RenderWorkerError::InternalFailure)?;
|
|
||||||
match &submission.packet {
|
|
||||||
RenderSubmissionPacket::Game2D(packet) if submission.app_mode == AppMode::Game => {
|
|
||||||
if packet.composer.bound_scene.is_none() {
|
|
||||||
gfx.render_game2d_frame_packet(packet);
|
|
||||||
} else {
|
|
||||||
let mut frame_composer = self
|
|
||||||
.frame_composer
|
|
||||||
.lock()
|
|
||||||
.map_err(|_| RenderWorkerError::InternalFailure)?;
|
|
||||||
frame_composer.render_game2d_frame_packet(packet, &mut *gfx);
|
|
||||||
gfx.apply_gfx2d_commands(&packet.gfx2d);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
RenderSubmissionPacket::ShellUi(packet) if submission.app_mode == AppMode::Shell => {
|
|
||||||
gfx.render_shell_ui_frame_packet(packet);
|
|
||||||
}
|
|
||||||
_ => return Err(RenderWorkerError::RenderFailed),
|
|
||||||
}
|
|
||||||
|
|
||||||
gfx.present();
|
|
||||||
let (width, height) = gfx.size();
|
|
||||||
OwnedRgba8888Frame::packed(
|
|
||||||
submission.frame_id,
|
|
||||||
submission.ownership,
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
gfx.front_buffer().to_vec(),
|
|
||||||
)
|
|
||||||
.map_err(|_| RenderWorkerError::InternalFailure)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RenderWorkerBackend for LocalFramebufferRenderBackend {
|
|
||||||
fn render(
|
|
||||||
&self,
|
|
||||||
submission: &RenderSubmission,
|
|
||||||
) -> Result<OwnedRgba8888Frame, RenderWorkerError> {
|
|
||||||
self.render_submission(submission)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use crate::memory_banks::MemoryBanks;
|
|
||||||
use prometeu_hal::app_mode::AppMode;
|
|
||||||
use prometeu_hal::color::Color;
|
|
||||||
use prometeu_hal::primitives::Rect;
|
|
||||||
use prometeu_hal::{
|
|
||||||
FrameId, Game2DFramePacket, Gfx2dCommand, GfxUiCommand, RenderOwnership, ShellUiFramePacket,
|
|
||||||
};
|
|
||||||
|
|
||||||
fn backend() -> LocalFramebufferRenderBackend {
|
|
||||||
let banks = Arc::new(MemoryBanks::new());
|
|
||||||
LocalFramebufferRenderBackend::new(4, 4, banks)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn local_backend_renders_game2d_submission_to_owned_frame() {
|
|
||||||
let backend = backend();
|
|
||||||
let packet = Game2DFramePacket {
|
|
||||||
gfx2d: vec![Gfx2dCommand::FillRect {
|
|
||||||
rect: Rect { x: 1, y: 1, w: 2, h: 2 },
|
|
||||||
color: Color::RED,
|
|
||||||
}],
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
let ownership = RenderOwnership::new(3, AppMode::Game, 9);
|
|
||||||
let submission =
|
|
||||||
RenderSubmission::game2d(FrameId::new(5), packet).with_ownership(ownership);
|
|
||||||
|
|
||||||
let frame = backend.render(&submission).expect("game frame should render");
|
|
||||||
|
|
||||||
assert_eq!(frame.frame_id, FrameId::new(5));
|
|
||||||
assert_eq!(frame.ownership, ownership);
|
|
||||||
assert_eq!(frame.width, 4);
|
|
||||||
assert_eq!(frame.height, 4);
|
|
||||||
assert_eq!(frame.pixels[1 + 4], Color::RED.raw());
|
|
||||||
assert_eq!(frame.pixels[2 + 2 * 4], Color::RED.raw());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn local_backend_renders_shell_ui_submission_to_owned_frame() {
|
|
||||||
let backend = backend();
|
|
||||||
let packet = ShellUiFramePacket::new(vec![GfxUiCommand::FillRect {
|
|
||||||
rect: Rect { x: 0, y: 0, w: 1, h: 1 },
|
|
||||||
color: Color::GREEN,
|
|
||||||
}]);
|
|
||||||
let ownership = RenderOwnership::new(4, AppMode::Shell, 10);
|
|
||||||
let submission =
|
|
||||||
RenderSubmission::shell_ui(FrameId::new(6), packet).with_ownership(ownership);
|
|
||||||
|
|
||||||
let frame = backend.render(&submission).expect("shell frame should render");
|
|
||||||
|
|
||||||
assert_eq!(frame.frame_id, FrameId::new(6));
|
|
||||||
assert_eq!(frame.ownership, ownership);
|
|
||||||
assert_eq!(frame.pixels[0], Color::GREEN.raw());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,248 +0,0 @@
|
|||||||
//! Memory bank access boundary.
|
|
||||||
//!
|
|
||||||
//! Render consumers must cross this boundary through small, read-only traits.
|
|
||||||
//! Frame packets carry resource IDs; large resident resources stay in banks and
|
|
||||||
//! are read in place through a narrow HAL callback contract. Installation and
|
|
||||||
//! slot replacement remain separate owner-side operations and are not part of
|
|
||||||
//! the render read surface.
|
|
||||||
|
|
||||||
use prometeu_hal::glyph_bank::GlyphBank;
|
|
||||||
use prometeu_hal::render_resources::RenderResourceAccess as HalRenderResourceAccess;
|
|
||||||
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>);
|
|
||||||
/// Clears all resident GlyphBank slots.
|
|
||||||
fn clear_glyph_banks(&self);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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>);
|
|
||||||
/// Clears all resident SoundBank slots.
|
|
||||||
fn clear_sound_banks(&self);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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>);
|
|
||||||
/// Clears all resident SceneBank slots.
|
|
||||||
fn clear_scene_banks(&self);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn clear_glyph_banks(&self) {
|
|
||||||
self.glyph_bank_pool.write().unwrap().fill(None);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn clear_sound_banks(&self) {
|
|
||||||
self.sound_bank_pool.write().unwrap().fill(None);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn clear_scene_banks(&self) {
|
|
||||||
self.scene_bank_pool.write().unwrap().fill(None);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl HalRenderResourceAccess for MemoryBanks {
|
|
||||||
fn with_glyph_bank<R>(&self, bank_id: usize, read: impl FnOnce(&GlyphBank) -> R) -> Option<R> {
|
|
||||||
let pool = self.glyph_bank_pool.read().unwrap();
|
|
||||||
let bank = pool.get(bank_id)?.as_deref()?;
|
|
||||||
Some(read(bank))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn glyph_bank_count(&self) -> usize {
|
|
||||||
16
|
|
||||||
}
|
|
||||||
|
|
||||||
fn with_scene_bank<R>(&self, bank_id: usize, read: impl FnOnce(&SceneBank) -> R) -> Option<R> {
|
|
||||||
let pool = self.scene_bank_pool.read().unwrap();
|
|
||||||
let bank = pool.get(bank_id)?.as_deref()?;
|
|
||||||
Some(read(bank))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn scene_bank_count(&self) -> usize {
|
|
||||||
16
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use prometeu_hal::color::Color;
|
|
||||||
use prometeu_hal::glyph::Glyph;
|
|
||||||
use prometeu_hal::glyph_bank::TileSize;
|
|
||||||
use prometeu_hal::scene_layer::{ParallaxFactor, SceneLayer};
|
|
||||||
use prometeu_hal::tile::Tile;
|
|
||||||
use prometeu_hal::tilemap::TileMap;
|
|
||||||
|
|
||||||
fn assert_send_sync<T: Send + Sync>() {}
|
|
||||||
|
|
||||||
fn make_glyph_bank() -> GlyphBank {
|
|
||||||
let mut bank = GlyphBank::new(TileSize::Size8, 8, 8);
|
|
||||||
bank.palettes[0][1] = Color::WHITE;
|
|
||||||
bank
|
|
||||||
}
|
|
||||||
|
|
||||||
fn make_scene_bank() -> SceneBank {
|
|
||||||
let layer = SceneLayer {
|
|
||||||
active: true,
|
|
||||||
glyph_asset_id: 0,
|
|
||||||
tile_size: TileSize::Size8,
|
|
||||||
parallax_factor: ParallaxFactor { x: 1.0, y: 1.0 },
|
|
||||||
tilemap: TileMap {
|
|
||||||
width: 1,
|
|
||||||
height: 1,
|
|
||||||
tiles: vec![Tile {
|
|
||||||
active: true,
|
|
||||||
glyph: Glyph { glyph_id: 0, palette_id: 0 },
|
|
||||||
flip_x: false,
|
|
||||||
flip_y: false,
|
|
||||||
}],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
SceneBank { layers: std::array::from_fn(|_| layer.clone()) }
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn render_resource_access_traits_are_thread_safe() {
|
|
||||||
assert_send_sync::<MemoryBanks>();
|
|
||||||
assert_send_sync::<Arc<dyn GlyphBankPoolAccess>>();
|
|
||||||
assert_send_sync::<Arc<dyn SceneBankPoolAccess>>();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn render_resource_access_exposes_read_only_glyph_and_scene_banks_without_arc_contract() {
|
|
||||||
let banks = MemoryBanks::new();
|
|
||||||
banks.install_glyph_bank(0, Arc::new(make_glyph_bank()));
|
|
||||||
banks.install_scene_bank(1, Arc::new(make_scene_bank()));
|
|
||||||
|
|
||||||
let glyph_tile_size =
|
|
||||||
HalRenderResourceAccess::with_glyph_bank(&banks, 0, |bank| bank.tile_size);
|
|
||||||
let scene_tile_size =
|
|
||||||
HalRenderResourceAccess::with_scene_bank(&banks, 1, |bank| bank.layers[0].tile_size);
|
|
||||||
|
|
||||||
assert_eq!(glyph_tile_size, Some(TileSize::Size8));
|
|
||||||
assert_eq!(scene_tile_size, Some(TileSize::Size8));
|
|
||||||
assert_eq!(HalRenderResourceAccess::with_glyph_bank(&banks, 15, |_| ()), None);
|
|
||||||
assert_eq!(HalRenderResourceAccess::with_scene_bank(&banks, 15, |_| ()), None);
|
|
||||||
assert_eq!(HalRenderResourceAccess::glyph_bank_count(&banks), 16);
|
|
||||||
assert_eq!(HalRenderResourceAccess::scene_bank_count(&banks), 16);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,100 +0,0 @@
|
|||||||
use crate::hardware::Hardware;
|
|
||||||
use crate::memory_banks::MemoryBanks;
|
|
||||||
use prometeu_hal::{
|
|
||||||
AssetBridge, AudioBridge, Game2DFrameComposer, InputPlatform, RenderBackend,
|
|
||||||
RenderSubmissionSink, RuntimePlatform, TelemetryPlatform,
|
|
||||||
};
|
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
pub struct TestPlatform {
|
|
||||||
hardware: Hardware,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for TestPlatform {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::new()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TestPlatform {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self { hardware: Hardware::new() }
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn new_with_memory_banks(memory_banks: Arc<MemoryBanks>) -> Self {
|
|
||||||
Self { hardware: Hardware::new_with_memory_banks(memory_banks) }
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn local_hardware(&self) -> &Hardware {
|
|
||||||
&self.hardware
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn local_hardware_mut(&mut self) -> &mut Hardware {
|
|
||||||
&mut self.hardware
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RuntimePlatform for TestPlatform {
|
|
||||||
fn render_submission_sink(&mut self) -> &mut dyn RenderSubmissionSink {
|
|
||||||
self.hardware.render_submission_sink()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_backend(&mut self) -> &mut dyn RenderBackend {
|
|
||||||
self.hardware.render_backend()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn game2d_frame_composer(&mut self) -> &mut dyn Game2DFrameComposer {
|
|
||||||
self.hardware.game2d_frame_composer()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn audio(&self) -> &dyn AudioBridge {
|
|
||||||
self.hardware.audio()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn audio_mut(&mut self) -> &mut dyn AudioBridge {
|
|
||||||
self.hardware.audio_mut()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn input(&self) -> &dyn InputPlatform {
|
|
||||||
self.hardware.input()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn input_mut(&mut self) -> &mut dyn InputPlatform {
|
|
||||||
self.hardware.input_mut()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn assets(&self) -> &dyn AssetBridge {
|
|
||||||
self.hardware.assets()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn assets_mut(&mut self) -> &mut dyn AssetBridge {
|
|
||||||
self.hardware.assets_mut()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn telemetry(&self) -> &dyn TelemetryPlatform {
|
|
||||||
self.hardware.telemetry()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use prometeu_hal::{FrameId, Game2DFramePacket, RenderSubmission};
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_platform_initializes_required_facades() {
|
|
||||||
let mut platform = TestPlatform::new();
|
|
||||||
|
|
||||||
platform.game2d_frame_composer().begin_frame();
|
|
||||||
platform
|
|
||||||
.render_submission_sink()
|
|
||||||
.submit_render_submission(RenderSubmission::game2d(
|
|
||||||
FrameId::ZERO,
|
|
||||||
Game2DFramePacket::default(),
|
|
||||||
))
|
|
||||||
.expect("test platform render sink should accept local submission");
|
|
||||||
assert!(!platform.audio().is_playing(0));
|
|
||||||
assert!(!platform.input().pad().any());
|
|
||||||
assert!(platform.telemetry().telemetry_snapshot().is_none());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@ -1,68 +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_hal::{FrameId, GfxUiCommand, RenderSubmission, ShellUiFramePacket};
|
|
||||||
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.platform.input_mut().pad_mut().begin_frame(ctx.signals);
|
|
||||||
|
|
||||||
let packet = ShellUiFramePacket::new(vec![GfxUiCommand::Clear { color: Color::RED }]);
|
|
||||||
ctx.platform
|
|
||||||
.render_submission_sink()
|
|
||||||
.submit_render_submission(RenderSubmission::shell_ui(FrameId::ZERO, packet))
|
|
||||||
.expect("firmware crash render submission should publish");
|
|
||||||
|
|
||||||
// If START is pressed, return to the Hub
|
|
||||||
if ctx.platform.input().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,73 +0,0 @@
|
|||||||
use crate::firmware::firmware_state::{AppCrashesStep, FirmwareState, HubHomeStep};
|
|
||||||
use crate::firmware::prometeu_context::PrometeuContext;
|
|
||||||
use prometeu_hal::app_mode::AppMode;
|
|
||||||
use prometeu_hal::log::{LogLevel, LogSource};
|
|
||||||
use prometeu_system::task::{TaskId, TaskState};
|
|
||||||
use prometeu_system::{CrashReport, ResidentGameState};
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct GameRunningStep {
|
|
||||||
pub task_id: TaskId,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl GameRunningStep {
|
|
||||||
pub fn new(task_id: TaskId) -> Self {
|
|
||||||
Self { task_id }
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn on_enter(&mut self, ctx: &mut PrometeuContext) {
|
|
||||||
ctx.os.log(LogLevel::Info, LogSource::Pos, 0, "Entering GameRunning".to_string());
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn on_update(&mut self, ctx: &mut PrometeuContext) -> Option<FirmwareState> {
|
|
||||||
let Some(task_state) = ctx.os.lifecycle().task_state(self.task_id) else {
|
|
||||||
// TODO: is it panic?
|
|
||||||
let report =
|
|
||||||
CrashReport::VmPanic { message: "task is gone, panic!".to_string(), pc: None };
|
|
||||||
return Some(FirmwareState::AppCrashes(AppCrashesStep { report }));
|
|
||||||
};
|
|
||||||
|
|
||||||
if task_state != TaskState::Foreground {
|
|
||||||
// TODO: is it panic?
|
|
||||||
let report = CrashReport::VmPanic {
|
|
||||||
message: "should be running as foreground, panic!".to_string(),
|
|
||||||
pc: None,
|
|
||||||
};
|
|
||||||
let _ = ctx.os.lifecycle().crash_task(self.task_id, Some(&report));
|
|
||||||
return Some(FirmwareState::AppCrashes(AppCrashesStep { report }));
|
|
||||||
}
|
|
||||||
|
|
||||||
let pause_requested = matches!(
|
|
||||||
ctx.os.lifecycle().resident_game().map(|game| game.state),
|
|
||||||
Some(ResidentGameState::PauseRequested { .. })
|
|
||||||
);
|
|
||||||
let result = ctx.os.vm().tick_session(self.task_id, ctx.signals, ctx.platform);
|
|
||||||
|
|
||||||
if let Some(report) = result {
|
|
||||||
let _ = ctx.os.lifecycle().crash_task(self.task_id, Some(&report));
|
|
||||||
return Some(FirmwareState::AppCrashes(AppCrashesStep { report }));
|
|
||||||
}
|
|
||||||
|
|
||||||
if pause_requested {
|
|
||||||
match ctx.os.lifecycle().advance_game_pause_budget(self.task_id) {
|
|
||||||
Ok(true) => {
|
|
||||||
ctx.os.vm().transition_render_owner(AppMode::Shell, 0);
|
|
||||||
return Some(FirmwareState::HubHome(HubHomeStep));
|
|
||||||
}
|
|
||||||
Ok(false) => {}
|
|
||||||
Err(_) => {
|
|
||||||
let report = CrashReport::VmPanic {
|
|
||||||
message: "failed to advance game pause budget".to_string(),
|
|
||||||
pc: None,
|
|
||||||
};
|
|
||||||
let _ = ctx.os.lifecycle().crash_task(self.task_id, Some(&report));
|
|
||||||
return Some(FirmwareState::AppCrashes(AppCrashesStep { report }));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn on_exit(&mut self, _ctx: &mut PrometeuContext) {}
|
|
||||||
}
|
|
||||||
@ -1,128 +0,0 @@
|
|||||||
use crate::firmware::firmware_state::{
|
|
||||||
AppCrashesStep, FirmwareState, LoadCartridgeStep, ShellRunningStep,
|
|
||||||
};
|
|
||||||
use crate::firmware::prometeu_context::PrometeuContext;
|
|
||||||
use crate::firmware::resident_game_termination;
|
|
||||||
use prometeu_hal::app_mode::AppMode;
|
|
||||||
use prometeu_hal::cartridge_loader::CartridgeLoader;
|
|
||||||
use prometeu_hal::log::{LogLevel, LogSource};
|
|
||||||
use prometeu_hal::primitives::Rect;
|
|
||||||
use prometeu_system::SystemProfileAction;
|
|
||||||
use prometeu_system::windows::WindowOwner;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct HubHomeStep;
|
|
||||||
|
|
||||||
impl HubHomeStep {
|
|
||||||
pub fn on_enter(&mut self, ctx: &mut PrometeuContext) {
|
|
||||||
ctx.os.log(LogLevel::Info, LogSource::Hub, 0, "Entering HubHome".to_string());
|
|
||||||
ctx.os.windows().remove_all_windows();
|
|
||||||
ctx.hub.init();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn on_update(&mut self, ctx: &mut PrometeuContext) -> Option<FirmwareState> {
|
|
||||||
let outcome = ctx.hub.update_shell_profile(ctx.os, ctx.platform);
|
|
||||||
if let Some(report) = outcome.crash {
|
|
||||||
return Some(FirmwareState::AppCrashes(AppCrashesStep { report }));
|
|
||||||
}
|
|
||||||
|
|
||||||
match outcome.action {
|
|
||||||
Some(SystemProfileAction::LaunchNativeShell(app)) => {
|
|
||||||
let task_id = match ctx
|
|
||||||
.os
|
|
||||||
.sessions()
|
|
||||||
.try_create_native_shell_task(app.app_id(), app.title())
|
|
||||||
{
|
|
||||||
Ok(task_id) => task_id,
|
|
||||||
Err(error) => {
|
|
||||||
ctx.os.log(
|
|
||||||
LogLevel::Warn,
|
|
||||||
LogSource::Hub,
|
|
||||||
0,
|
|
||||||
format!("Failed to launch native shell {}: {:?}", app.title(), error),
|
|
||||||
);
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let id = ctx.os.windows().add_window(
|
|
||||||
app.title().to_string(),
|
|
||||||
WindowOwner::Task(task_id),
|
|
||||||
Rect { x: 40, y: 20, w: 400, h: 220 },
|
|
||||||
app.color(),
|
|
||||||
);
|
|
||||||
ctx.os.windows().set_focus(id);
|
|
||||||
return Some(FirmwareState::ShellRunning(ShellRunningStep::new(task_id)));
|
|
||||||
}
|
|
||||||
Some(SystemProfileAction::LaunchGame { target }) => {
|
|
||||||
match CartridgeLoader::load(&target.path) {
|
|
||||||
Ok(cartridge) => {
|
|
||||||
if cartridge.app_mode == AppMode::Game
|
|
||||||
&& let Some(resident_task_id) = ctx.os.lifecycle().resident_game_task()
|
|
||||||
{
|
|
||||||
let resident_app_id = ctx
|
|
||||||
.os
|
|
||||||
.sessions()
|
|
||||||
.vm_session_for_task(resident_task_id)
|
|
||||||
.map(|session| session.app_id)
|
|
||||||
.unwrap_or(0);
|
|
||||||
|
|
||||||
if resident_app_id == cartridge.app_id {
|
|
||||||
return Some(FirmwareState::ResumeResidentGame);
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Err(error) = resident_game_termination::terminate_resident_game(
|
|
||||||
ctx.os,
|
|
||||||
ctx.platform,
|
|
||||||
false,
|
|
||||||
) {
|
|
||||||
ctx.os.log(
|
|
||||||
LogLevel::Error,
|
|
||||||
LogSource::Hub,
|
|
||||||
0,
|
|
||||||
format!(
|
|
||||||
"Failed to terminate resident game app_id {} before launching {}: {:?}",
|
|
||||||
resident_app_id, cartridge.title, error
|
|
||||||
),
|
|
||||||
);
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return Some(FirmwareState::LoadCartridge(
|
|
||||||
LoadCartridgeStep::new_game_switch(cartridge),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
Err(error) => {
|
|
||||||
ctx.os.log(
|
|
||||||
LogLevel::Error,
|
|
||||||
LogSource::Hub,
|
|
||||||
0,
|
|
||||||
format!(
|
|
||||||
"Failed to launch Home game {}: {:?}",
|
|
||||||
target.path.display(),
|
|
||||||
error
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some(SystemProfileAction::KillResidentGame) => {
|
|
||||||
if let Err(error) =
|
|
||||||
resident_game_termination::terminate_resident_game(ctx.os, ctx.platform, true)
|
|
||||||
{
|
|
||||||
ctx.os.log(
|
|
||||||
LogLevel::Error,
|
|
||||||
LogSource::Hub,
|
|
||||||
0,
|
|
||||||
format!("Failed to kill resident game from Hub: {error:?}"),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some(SystemProfileAction::CloseShell) | None => {}
|
|
||||||
}
|
|
||||||
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn on_exit(&mut self, _ctx: &mut PrometeuContext) {}
|
|
||||||
}
|
|
||||||
@ -1,135 +0,0 @@
|
|||||||
use crate::firmware::firmware_state::{
|
|
||||||
AppCrashesStep, FirmwareState, GameRunningStep, HubHomeStep, ShellRunningStep,
|
|
||||||
};
|
|
||||||
use crate::firmware::prometeu_context::PrometeuContext;
|
|
||||||
use crate::firmware::resident_game_termination;
|
|
||||||
use prometeu_hal::app_mode::AppMode;
|
|
||||||
use prometeu_hal::cartridge::Cartridge;
|
|
||||||
use prometeu_hal::color::Color;
|
|
||||||
use prometeu_hal::log::{LogLevel, LogSource};
|
|
||||||
use prometeu_hal::primitives::Rect;
|
|
||||||
use prometeu_system::CrashReport;
|
|
||||||
use prometeu_system::windows::WindowOwner;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct LoadCartridgeStep {
|
|
||||||
pub cartridge: Cartridge,
|
|
||||||
init_error: Option<CrashReport>,
|
|
||||||
loaded_task_id: Option<prometeu_system::task::TaskId>,
|
|
||||||
loaded_app_mode: Option<AppMode>,
|
|
||||||
failure_recovery: LoadCartridgeFailureRecovery,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
||||||
enum LoadCartridgeFailureRecovery {
|
|
||||||
AppCrash,
|
|
||||||
ReturnToHub,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LoadCartridgeStep {
|
|
||||||
pub fn new(cartridge: Cartridge) -> Self {
|
|
||||||
Self {
|
|
||||||
cartridge,
|
|
||||||
init_error: None,
|
|
||||||
loaded_task_id: None,
|
|
||||||
loaded_app_mode: None,
|
|
||||||
failure_recovery: LoadCartridgeFailureRecovery::AppCrash,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn new_game_switch(cartridge: Cartridge) -> Self {
|
|
||||||
Self { failure_recovery: LoadCartridgeFailureRecovery::ReturnToHub, ..Self::new(cartridge) }
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn on_enter(&mut self, ctx: &mut PrometeuContext) {
|
|
||||||
ctx.os.log(
|
|
||||||
LogLevel::Info,
|
|
||||||
LogSource::Pos,
|
|
||||||
0,
|
|
||||||
format!("Loading cartridge: {}", self.cartridge.title),
|
|
||||||
);
|
|
||||||
|
|
||||||
ctx.platform.game2d_frame_composer().unbind_scene();
|
|
||||||
|
|
||||||
// Initialize Asset Manager
|
|
||||||
ctx.platform.assets_mut().initialize_for_cartridge(
|
|
||||||
self.cartridge.asset_table.clone(),
|
|
||||||
self.cartridge.preload.clone(),
|
|
||||||
self.cartridge.assets.clone(),
|
|
||||||
);
|
|
||||||
|
|
||||||
match ctx.os.sessions().load_vm_cartridge(&self.cartridge) {
|
|
||||||
Ok(loaded) => {
|
|
||||||
self.loaded_task_id = Some(loaded.task_id);
|
|
||||||
self.loaded_app_mode = Some(loaded.app_mode);
|
|
||||||
}
|
|
||||||
Err(report) => {
|
|
||||||
self.init_error = Some(report.into_crash_report("load VM cartridge into session"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn on_update(&mut self, ctx: &mut PrometeuContext) -> Option<FirmwareState> {
|
|
||||||
if let Some(report) = self.init_error.take() {
|
|
||||||
if self.failure_recovery == LoadCartridgeFailureRecovery::ReturnToHub {
|
|
||||||
if self.cartridge.app_mode == AppMode::Game
|
|
||||||
&& let Some(task_id) = ctx.os.lifecycle().resident_game_task()
|
|
||||||
{
|
|
||||||
let is_partial_target = ctx
|
|
||||||
.os
|
|
||||||
.sessions()
|
|
||||||
.vm_session_for_task(task_id)
|
|
||||||
.map(|session| session.app_id == self.cartridge.app_id)
|
|
||||||
.unwrap_or(false);
|
|
||||||
|
|
||||||
if is_partial_target {
|
|
||||||
let _ = resident_game_termination::terminate_resident_game(
|
|
||||||
ctx.os,
|
|
||||||
ctx.platform,
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.os.log(
|
|
||||||
LogLevel::Error,
|
|
||||||
LogSource::Hub,
|
|
||||||
0,
|
|
||||||
format!(
|
|
||||||
"Failed to switch to game {} app_id {}; returning to Home: {:?}",
|
|
||||||
self.cartridge.title, self.cartridge.app_id, report
|
|
||||||
),
|
|
||||||
);
|
|
||||||
return Some(FirmwareState::HubHome(HubHomeStep));
|
|
||||||
}
|
|
||||||
|
|
||||||
return Some(FirmwareState::AppCrashes(AppCrashesStep { report }));
|
|
||||||
}
|
|
||||||
|
|
||||||
let Some(task_id) = self.loaded_task_id.take() else {
|
|
||||||
let report = CrashReport::VmPanic {
|
|
||||||
message: format!(
|
|
||||||
"cartridge {} was not loaded into a VM session",
|
|
||||||
self.cartridge.title
|
|
||||||
),
|
|
||||||
pc: None,
|
|
||||||
};
|
|
||||||
return Some(FirmwareState::AppCrashes(AppCrashesStep { report }));
|
|
||||||
};
|
|
||||||
|
|
||||||
if self.loaded_app_mode == Some(AppMode::Shell) {
|
|
||||||
let id = ctx.os.windows().add_window(
|
|
||||||
self.cartridge.title.clone(),
|
|
||||||
WindowOwner::Task(task_id),
|
|
||||||
Rect { x: 40, y: 20, w: 240, h: 140 },
|
|
||||||
Color::WHITE,
|
|
||||||
);
|
|
||||||
ctx.os.windows().set_focus(id);
|
|
||||||
return Some(FirmwareState::ShellRunning(ShellRunningStep::new(task_id)));
|
|
||||||
}
|
|
||||||
|
|
||||||
Some(FirmwareState::GameRunning(GameRunningStep::new(task_id)))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn on_exit(&mut self, _ctx: &mut PrometeuContext) {}
|
|
||||||
}
|
|
||||||
@ -1,131 +0,0 @@
|
|||||||
use crate::firmware::firmware_state::{AppCrashesStep, FirmwareState, HubHomeStep};
|
|
||||||
use crate::firmware::prometeu_context::PrometeuContext;
|
|
||||||
use prometeu_hal::log::LogSource;
|
|
||||||
use prometeu_system::process::ProcessKind;
|
|
||||||
use prometeu_system::task::{TaskId, TaskState};
|
|
||||||
use prometeu_system::{CrashReport, SystemProfileAction};
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct ShellRunningStep {
|
|
||||||
pub task_id: TaskId,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ShellRunningStep {
|
|
||||||
pub fn new(task_id: TaskId) -> Self {
|
|
||||||
Self { task_id }
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn on_enter(&mut self, ctx: &mut PrometeuContext) {
|
|
||||||
ctx.os.info(LogSource::Hub, "Entering ShellRunning".to_string());
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn on_update(&mut self, ctx: &mut PrometeuContext) -> Option<FirmwareState> {
|
|
||||||
let Some(task_state) = ctx.os.lifecycle().task_state(self.task_id) else {
|
|
||||||
let report = CrashReport::VmPanic {
|
|
||||||
message: format!("ShellRunningStep has no task for {:?}", self.task_id),
|
|
||||||
pc: None,
|
|
||||||
};
|
|
||||||
|
|
||||||
return Some(FirmwareState::AppCrashes(AppCrashesStep { report }));
|
|
||||||
};
|
|
||||||
|
|
||||||
match task_state {
|
|
||||||
TaskState::Foreground => {
|
|
||||||
// Continue below.
|
|
||||||
}
|
|
||||||
|
|
||||||
TaskState::Closed => {
|
|
||||||
return Some(FirmwareState::HubHome(HubHomeStep));
|
|
||||||
}
|
|
||||||
|
|
||||||
TaskState::Crashed => {
|
|
||||||
let report = CrashReport::VmPanic {
|
|
||||||
message: format!("Shell task {:?} is already crashed", self.task_id),
|
|
||||||
pc: None,
|
|
||||||
};
|
|
||||||
|
|
||||||
return Some(FirmwareState::AppCrashes(AppCrashesStep { report }));
|
|
||||||
}
|
|
||||||
|
|
||||||
TaskState::Suspended | TaskState::Background => {
|
|
||||||
let report = CrashReport::VmPanic {
|
|
||||||
message: format!(
|
|
||||||
"ShellRunningStep expected foreground task {:?}, got {:?}",
|
|
||||||
self.task_id, task_state,
|
|
||||||
),
|
|
||||||
pc: None,
|
|
||||||
};
|
|
||||||
|
|
||||||
return Some(FirmwareState::AppCrashes(AppCrashesStep { report }));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let process_kind = match ctx.os.lifecycle().process_kind_for_task(self.task_id) {
|
|
||||||
Ok(process_kind) => process_kind,
|
|
||||||
Err(error) => {
|
|
||||||
let report = CrashReport::VmPanic {
|
|
||||||
message: format!(
|
|
||||||
"ShellRunningStep cannot resolve process for {:?}: {:?}",
|
|
||||||
self.task_id, error,
|
|
||||||
),
|
|
||||||
pc: None,
|
|
||||||
};
|
|
||||||
return Some(FirmwareState::AppCrashes(AppCrashesStep { report }));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let outcome = match process_kind {
|
|
||||||
ProcessKind::VmShell => {
|
|
||||||
ctx.hub.update_vm_shell_profile(ctx.os, self.task_id, ctx.signals, ctx.platform)
|
|
||||||
}
|
|
||||||
ProcessKind::NativeShell => ctx.hub.update_native_shell_profile(ctx.os, ctx.platform),
|
|
||||||
ProcessKind::VmGame => {
|
|
||||||
let report = CrashReport::VmPanic {
|
|
||||||
message: format!(
|
|
||||||
"ShellRunningStep expected shell process for {:?}, got VmGame",
|
|
||||||
self.task_id,
|
|
||||||
),
|
|
||||||
pc: None,
|
|
||||||
};
|
|
||||||
return Some(FirmwareState::AppCrashes(AppCrashesStep { report }));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Some(report) = outcome.crash {
|
|
||||||
let _ = ctx.os.lifecycle().crash_task(self.task_id, Some(&report));
|
|
||||||
return Some(FirmwareState::AppCrashes(AppCrashesStep { report }));
|
|
||||||
}
|
|
||||||
|
|
||||||
if outcome.action == Some(SystemProfileAction::CloseShell) {
|
|
||||||
self.close_current_shell(ctx);
|
|
||||||
return Some(FirmwareState::HubHome(HubHomeStep));
|
|
||||||
}
|
|
||||||
|
|
||||||
if !ctx.os.windows().focused_window_belongs_to_task(self.task_id) {
|
|
||||||
ctx.os.info(
|
|
||||||
LogSource::Hub,
|
|
||||||
format!(
|
|
||||||
"Closing shell task {:?}: focused window does not belong to task",
|
|
||||||
self.task_id,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
self.close_current_shell(ctx);
|
|
||||||
return Some(FirmwareState::HubHome(HubHomeStep));
|
|
||||||
}
|
|
||||||
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
fn close_current_shell(&mut self, ctx: &mut PrometeuContext) {
|
|
||||||
if let Err(error) = ctx.os.lifecycle().close_task(self.task_id) {
|
|
||||||
ctx.os.error(
|
|
||||||
LogSource::Hub,
|
|
||||||
format!("Failed to close shell task {:?}: {:?}", self.task_id, error),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn on_exit(&mut self, ctx: &mut PrometeuContext) {
|
|
||||||
ctx.os.info(LogSource::Hub, "Exiting ShellRunning".to_string());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,11 +0,0 @@
|
|||||||
use crate::firmware::boot_target::BootTarget;
|
|
||||||
use prometeu_hal::{InputSignals, RuntimePlatform};
|
|
||||||
use prometeu_system::{PrometeuHub, SystemOS};
|
|
||||||
|
|
||||||
pub struct PrometeuContext<'a> {
|
|
||||||
pub os: &'a mut SystemOS,
|
|
||||||
pub hub: &'a mut PrometeuHub,
|
|
||||||
pub boot_target: &'a BootTarget,
|
|
||||||
pub signals: &'a InputSignals,
|
|
||||||
pub platform: &'a mut dyn RuntimePlatform,
|
|
||||||
}
|
|
||||||
@ -1,42 +0,0 @@
|
|||||||
use prometeu_hal::RuntimePlatform;
|
|
||||||
use prometeu_hal::app_mode::AppMode;
|
|
||||||
use prometeu_hal::log::LogSource;
|
|
||||||
use prometeu_system::task::TaskId;
|
|
||||||
use prometeu_system::{LifecycleError, SystemOS};
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
||||||
pub(crate) enum ResidentGameTerminationOutcome {
|
|
||||||
NoResidentGame,
|
|
||||||
Terminated { task_id: TaskId, title: String },
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn terminate_resident_game(
|
|
||||||
os: &mut SystemOS,
|
|
||||||
platform: &mut dyn RuntimePlatform,
|
|
||||||
log_manual_hub_kill: bool,
|
|
||||||
) -> Result<ResidentGameTerminationOutcome, LifecycleError> {
|
|
||||||
let Some(task_id) = os.lifecycle().resident_game_task() else {
|
|
||||||
return Ok(ResidentGameTerminationOutcome::NoResidentGame);
|
|
||||||
};
|
|
||||||
|
|
||||||
let title = os
|
|
||||||
.sessions()
|
|
||||||
.vm_session_for_task(task_id)
|
|
||||||
.map(|session| session.title.clone())
|
|
||||||
.unwrap_or_else(|| "<unknown>".to_string());
|
|
||||||
|
|
||||||
let terminated = os.lifecycle().terminate_resident_game()?;
|
|
||||||
let Some(task_id) = terminated else {
|
|
||||||
return Ok(ResidentGameTerminationOutcome::NoResidentGame);
|
|
||||||
};
|
|
||||||
|
|
||||||
os.vm().transition_render_owner(AppMode::Shell, 0);
|
|
||||||
platform.game2d_frame_composer().unbind_scene();
|
|
||||||
platform.assets_mut().shutdown();
|
|
||||||
|
|
||||||
if log_manual_hub_kill {
|
|
||||||
os.info(LogSource::Hub, format!("Resident game killed from Hub: {title}"));
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(ResidentGameTerminationOutcome::Terminated { task_id, title })
|
|
||||||
}
|
|
||||||
@ -1,3 +0,0 @@
|
|||||||
pub mod firmware;
|
|
||||||
|
|
||||||
pub use firmware::*;
|
|
||||||
@ -1,7 +0,0 @@
|
|||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
|
|
||||||
pub enum AppMode {
|
|
||||||
Game,
|
|
||||||
Shell,
|
|
||||||
}
|
|
||||||
@ -1,203 +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;
|
|
||||||
// Per-layer header:
|
|
||||||
// flags:u8 + glyph_asset_id:i32 + tile_size:u8 + reserved:u16
|
|
||||||
// + parallax_x:f32 + parallax_y:f32 + width:u32 + height:u32
|
|
||||||
// + tile_count:u32 + reserved:u32
|
|
||||||
pub const SCENE_LAYER_HEADER_BYTES_V1: usize = 32;
|
|
||||||
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,
|
|
||||||
QUEUED = 7,
|
|
||||||
ACTIVE = 8,
|
|
||||||
SUPERSEDED = 9,
|
|
||||||
EMPTY = 10,
|
|
||||||
INVALID = 11,
|
|
||||||
BackendUnavailable = 12,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[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,
|
|
||||||
Superseded = 3,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
||||||
pub struct AssetBacklogInfo {
|
|
||||||
pub status: AssetOpStatus,
|
|
||||||
pub pending_count: u32,
|
|
||||||
pub active_handle: HandleId,
|
|
||||||
pub active_asset_id: Option<AssetId>,
|
|
||||||
pub active_bank_type: Option<BankType>,
|
|
||||||
pub active_slot: Option<usize>,
|
|
||||||
pub active_progress: u16,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
||||||
pub struct AssetBacklogPosition {
|
|
||||||
pub status: AssetOpStatus,
|
|
||||||
pub state: LoadStatus,
|
|
||||||
pub position: u32,
|
|
||||||
pub progress: u16,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
||||||
pub struct AssetTargetStatus {
|
|
||||||
pub status: AssetOpStatus,
|
|
||||||
pub asset_id: Option<AssetId>,
|
|
||||||
pub handle: HandleId,
|
|
||||||
pub state: LoadStatus,
|
|
||||||
pub position: u32,
|
|
||||||
pub progress: u16,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[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,29 +0,0 @@
|
|||||||
use crate::asset::{
|
|
||||||
AssetBacklogInfo, AssetBacklogPosition, AssetEntry, AssetId, AssetLoadError, AssetOpStatus,
|
|
||||||
AssetTargetStatus, BankTelemetry, BankType, 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 backlog_info(&self) -> AssetBacklogInfo;
|
|
||||||
fn backlog_position(&self, handle: HandleId) -> AssetBacklogPosition;
|
|
||||||
fn backlog_move(&self, handle: HandleId, new_position: usize) -> AssetOpStatus;
|
|
||||||
fn backlog_promote(&self, handle: HandleId) -> AssetOpStatus;
|
|
||||||
fn backlog_demote(&self, handle: HandleId) -> AssetOpStatus;
|
|
||||||
fn target_status(&self, bank_type: BankType, slot: usize) -> AssetTargetStatus;
|
|
||||||
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,310 +0,0 @@
|
|||||||
use crate::app_mode::AppMode;
|
|
||||||
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)]
|
|
||||||
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,
|
|
||||||
Composer,
|
|
||||||
Gfx2d,
|
|
||||||
Gfxui,
|
|
||||||
Audio,
|
|
||||||
Fs,
|
|
||||||
Log,
|
|
||||||
Asset,
|
|
||||||
Bank,
|
|
||||||
All,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, 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,589 +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::Composer => caps::COMPOSER,
|
|
||||||
Capability::Gfx2d => caps::GFX2D,
|
|
||||||
Capability::Gfxui => caps::GFXUI,
|
|
||||||
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 * 4),
|
|
||||||
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 * 4),
|
|
||||||
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,67 +0,0 @@
|
|||||||
/// Represents a canonical RGBA8888 color in RGBA channel order.
|
|
||||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
|
||||||
pub struct Color(pub u32);
|
|
||||||
|
|
||||||
impl Color {
|
|
||||||
pub const BLACK: Self = Self::rgb(0, 0, 0);
|
|
||||||
pub const WHITE: Self = Self::rgb(255, 255, 255);
|
|
||||||
pub const RED: Self = Self::rgb(255, 0, 0);
|
|
||||||
pub const GREEN: Self = Self::rgb(0, 255, 0);
|
|
||||||
pub const BLUE: Self = Self::rgb(0, 0, 255);
|
|
||||||
pub const YELLOW: Self = Self::rgb(255, 255, 0);
|
|
||||||
pub const ORANGE: Self = Self::rgb(255, 128, 0);
|
|
||||||
pub const INDIGO: Self = Self::rgb(75, 0, 130);
|
|
||||||
pub const GRAY: Self = Self::rgb(128, 128, 128);
|
|
||||||
pub const CYAN: Self = Self::rgb(0, 255, 255);
|
|
||||||
pub const MAGENTA: Self = Self::rgb(255, 0, 255);
|
|
||||||
pub const TRANSPARENT: Self = Self::rgba(0, 0, 0, 0);
|
|
||||||
#[deprecated(note = "Use RGBA alpha transparency; COLOR_KEY is temporary migration residue.")]
|
|
||||||
pub const COLOR_KEY: Self = Self::TRANSPARENT;
|
|
||||||
|
|
||||||
/// Extracts RGBA8888 channels from canonical RGBA raw order.
|
|
||||||
#[inline(always)]
|
|
||||||
pub const fn unpack_to_native(px: u32) -> (u8, u8, u8, u8) {
|
|
||||||
let r = ((px >> 24) & 0xFF) as u8;
|
|
||||||
let g = ((px >> 16) & 0xFF) as u8;
|
|
||||||
let b = ((px >> 8) & 0xFF) as u8;
|
|
||||||
let a = (px & 0xFF) as u8;
|
|
||||||
(r, g, b, a)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Packs RGBA8888 channels into canonical RGBA raw order.
|
|
||||||
#[inline(always)]
|
|
||||||
pub const fn pack_from_native(r: u8, g: u8, b: u8, a: u8) -> u32 {
|
|
||||||
((r as u32) << 24) | ((g as u32) << 16) | ((b as u32) << 8) | a as u32
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Creates an opaque RGBA8888 color from 8-bit RGB components.
|
|
||||||
pub const fn rgb(r: u8, g: u8, b: u8) -> Self {
|
|
||||||
Self::rgba(r, g, b, 255)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub const fn rgba(r: u8, g: u8, b: u8, a: u8) -> Self {
|
|
||||||
Self(Self::pack_from_native(r, g, b, a))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub const fn gray_scale(c: u8) -> Self {
|
|
||||||
Self::rgb(c, c, c)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub const fn from_raw(raw: u32) -> Self {
|
|
||||||
Self(raw)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub const fn raw(self) -> u32 {
|
|
||||||
self.0
|
|
||||||
}
|
|
||||||
|
|
||||||
pub const fn hex(self) -> i32 {
|
|
||||||
let (r, g, b, _) = Self::unpack_to_native(self.0);
|
|
||||||
let hex = (r as u32) << 16 | (g as u32) << 8 | b as u32;
|
|
||||||
hex as i32
|
|
||||||
}
|
|
||||||
|
|
||||||
pub const fn alpha(self) -> u8 {
|
|
||||||
(self.0 & 0xFF) as u8
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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,147 +0,0 @@
|
|||||||
use crate::app_mode::AppMode;
|
|
||||||
use prometeu_bytecode::Value;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
pub const DEVTOOLS_PROTOCOL_VERSION: u32 = 1;
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
|
||||||
#[serde(tag = "type")]
|
|
||||||
pub enum DebugCommand {
|
|
||||||
#[serde(rename = "ok")]
|
|
||||||
Ok,
|
|
||||||
#[serde(rename = "start")]
|
|
||||||
Start,
|
|
||||||
#[serde(rename = "pause")]
|
|
||||||
Pause,
|
|
||||||
#[serde(rename = "resume")]
|
|
||||||
Resume,
|
|
||||||
#[serde(rename = "step")]
|
|
||||||
Step,
|
|
||||||
#[serde(rename = "stepFrame")]
|
|
||||||
StepFrame,
|
|
||||||
#[serde(rename = "getState")]
|
|
||||||
GetState,
|
|
||||||
#[serde(rename = "setBreakpoint")]
|
|
||||||
SetBreakpoint { pc: usize },
|
|
||||||
#[serde(rename = "listBreakpoints")]
|
|
||||||
ListBreakpoints,
|
|
||||||
#[serde(rename = "clearBreakpoint")]
|
|
||||||
ClearBreakpoint { pc: usize },
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
|
||||||
#[serde(tag = "type")]
|
|
||||||
pub enum DebugResponse {
|
|
||||||
#[serde(rename = "handshake")]
|
|
||||||
Handshake { protocol_version: u32, runtime_version: String, cartridge: HandshakeCartridge },
|
|
||||||
#[serde(rename = "getState")]
|
|
||||||
GetState { pc: usize, stack_top: Vec<Value>, frame_index: u64, app_id: u32 },
|
|
||||||
#[serde(rename = "breakpoints")]
|
|
||||||
Breakpoints { pcs: Vec<usize> },
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub struct HandshakeCartridge {
|
|
||||||
pub app_id: u32,
|
|
||||||
pub title: String,
|
|
||||||
pub app_version: String,
|
|
||||||
pub app_mode: AppMode,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
|
||||||
#[serde(tag = "event")]
|
|
||||||
pub enum DebugEvent {
|
|
||||||
#[serde(rename = "breakpointHit")]
|
|
||||||
BreakpointHit { pc: usize, frame_index: u64 },
|
|
||||||
#[serde(rename = "log")]
|
|
||||||
Log { level: String, source: String, msg: String },
|
|
||||||
#[serde(rename = "telemetry")]
|
|
||||||
Telemetry {
|
|
||||||
frame_index: u64,
|
|
||||||
vm_steps: u32,
|
|
||||||
syscalls: u32,
|
|
||||||
cycles: u64,
|
|
||||||
cycles_budget: u64,
|
|
||||||
host_cpu_time_us: u64,
|
|
||||||
violations: u32,
|
|
||||||
glyph_slots_used: u32,
|
|
||||||
glyph_slots_total: u32,
|
|
||||||
sound_slots_used: u32,
|
|
||||||
sound_slots_total: u32,
|
|
||||||
scene_slots_used: u32,
|
|
||||||
scene_slots_total: u32,
|
|
||||||
},
|
|
||||||
#[serde(rename = "fault")]
|
|
||||||
Fault {
|
|
||||||
kind: String,
|
|
||||||
summary: String,
|
|
||||||
pc: Option<u32>,
|
|
||||||
trap_code: Option<u32>,
|
|
||||||
opcode: Option<u16>,
|
|
||||||
},
|
|
||||||
#[serde(rename = "cert")]
|
|
||||||
Cert { rule: String, used: u64, limit: u64, frame_index: u64 },
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_telemetry_event_serialization() {
|
|
||||||
let event = DebugEvent::Telemetry {
|
|
||||||
frame_index: 10,
|
|
||||||
vm_steps: 100,
|
|
||||||
syscalls: 5,
|
|
||||||
cycles: 5000,
|
|
||||||
cycles_budget: 10000,
|
|
||||||
host_cpu_time_us: 1200,
|
|
||||||
violations: 0,
|
|
||||||
glyph_slots_used: 1,
|
|
||||||
glyph_slots_total: 16,
|
|
||||||
sound_slots_used: 2,
|
|
||||||
sound_slots_total: 16,
|
|
||||||
scene_slots_used: 3,
|
|
||||||
scene_slots_total: 16,
|
|
||||||
};
|
|
||||||
|
|
||||||
let json = serde_json::to_string(&event).unwrap();
|
|
||||||
assert!(json.contains("\"event\":\"telemetry\""));
|
|
||||||
assert!(json.contains("\"cycles\":5000"));
|
|
||||||
assert!(json.contains("\"cycles_budget\":10000"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_get_state_serialization() {
|
|
||||||
let resp = DebugResponse::GetState {
|
|
||||||
pc: 42,
|
|
||||||
stack_top: vec![Value::Int64(10), Value::String("test".into()), Value::Boolean(true)],
|
|
||||||
frame_index: 5,
|
|
||||||
app_id: 1,
|
|
||||||
};
|
|
||||||
|
|
||||||
let json = serde_json::to_string(&resp).unwrap();
|
|
||||||
assert!(json.contains("\"type\":\"getState\""));
|
|
||||||
assert!(json.contains("\"pc\":42"));
|
|
||||||
assert!(json.contains("\"stack_top\":[10,\"test\",true]"));
|
|
||||||
assert!(json.contains("\"frame_index\":5"));
|
|
||||||
assert!(json.contains("\"app_id\":1"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_fault_event_serialization() {
|
|
||||||
let event = DebugEvent::Fault {
|
|
||||||
kind: "vm_trap".into(),
|
|
||||||
summary: "PVM Trap 0x00000004".into(),
|
|
||||||
pc: Some(12),
|
|
||||||
trap_code: Some(4),
|
|
||||||
opcode: Some(0x70),
|
|
||||||
};
|
|
||||||
|
|
||||||
let json = serde_json::to_string(&event).unwrap();
|
|
||||||
assert!(json.contains("\"event\":\"fault\""));
|
|
||||||
assert!(json.contains("\"kind\":\"vm_trap\""));
|
|
||||||
assert!(json.contains("\"trap_code\":4"));
|
|
||||||
assert!(json.contains("\"opcode\":112"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,76 +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) -> &[u32];
|
|
||||||
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,
|
|
||||||
resolved_glyph_slots: &[usize; 4],
|
|
||||||
);
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
@ -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,81 +0,0 @@
|
|||||||
use crate::color::Color;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
pub const GLYPH_BANK_PALETTE_COUNT_V1: usize = 64;
|
|
||||||
pub const GLYPH_BANK_COLORS_PER_PALETTE: usize = 16;
|
|
||||||
|
|
||||||
/// Standard sizes for square tiles.
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize, Serialize)]
|
|
||||||
pub enum TileSize {
|
|
||||||
/// 8x8 pixels.
|
|
||||||
Size8 = 8,
|
|
||||||
/// 16x16 pixels.
|
|
||||||
Size16 = 16,
|
|
||||||
/// 32x32 pixels.
|
|
||||||
Size32 = 32,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A container for graphical assets.
|
|
||||||
///
|
|
||||||
/// A GlyphBank stores the decoded runtime representation of a glyph-bank asset.
|
|
||||||
///
|
|
||||||
/// Serialized `assets.pa` payloads keep pixel indices packed as `4bpp` nibbles.
|
|
||||||
/// After decode, the runtime expands them to one `u8` palette index per pixel
|
|
||||||
/// while preserving the same `0..15` logical range.
|
|
||||||
pub struct GlyphBank {
|
|
||||||
/// Dimension of each individual tile in the bank.
|
|
||||||
pub tile_size: TileSize,
|
|
||||||
/// Width of the full bank sheet in pixels.
|
|
||||||
pub width: usize,
|
|
||||||
/// Height of the full bank sheet in pixels.
|
|
||||||
pub height: usize,
|
|
||||||
|
|
||||||
/// Decoded pixel data stored as one palette index per pixel.
|
|
||||||
/// Serialized payloads are packed; runtime memory is expanded.
|
|
||||||
/// Palette indices are ordinary indices; transparency is resolved through
|
|
||||||
/// the RGBA alpha channel of the palette entry.
|
|
||||||
pub pixel_indices: Vec<u8>,
|
|
||||||
/// Runtime-facing v1 palette table: 64 palettes of 16 RGBA8888 colors each.
|
|
||||||
pub palettes: [[Color; GLYPH_BANK_COLORS_PER_PALETTE]; GLYPH_BANK_PALETTE_COUNT_V1],
|
|
||||||
}
|
|
||||||
|
|
||||||
impl GlyphBank {
|
|
||||||
/// Creates an empty glyph bank with the specified dimensions.
|
|
||||||
pub fn new(tile_size: TileSize, width: usize, height: usize) -> Self {
|
|
||||||
Self {
|
|
||||||
tile_size,
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
pixel_indices: vec![0; width * height],
|
|
||||||
palettes: [[Color::BLACK; GLYPH_BANK_COLORS_PER_PALETTE]; GLYPH_BANK_PALETTE_COUNT_V1],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Resolves a global tile ID and local pixel coordinates to a palette index.
|
|
||||||
/// tile_id: the tile index in the bank
|
|
||||||
/// local_x, local_y: the pixel position inside the tile (0 to tile_size-1)
|
|
||||||
pub fn get_pixel_index(&self, tile_id: u16, local_x: usize, local_y: usize) -> u8 {
|
|
||||||
let size = self.tile_size as usize;
|
|
||||||
let tiles_per_row = self.width / size;
|
|
||||||
let tile_x = (tile_id as usize % tiles_per_row) * size;
|
|
||||||
let tile_y = (tile_id as usize / tiles_per_row) * size;
|
|
||||||
|
|
||||||
let pixel_x = tile_x + local_x;
|
|
||||||
let pixel_y = tile_y + local_y;
|
|
||||||
|
|
||||||
if pixel_x < self.width && pixel_y < self.height {
|
|
||||||
self.pixel_indices[pixel_y * self.width + pixel_x]
|
|
||||||
} else {
|
|
||||||
0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Maps a 4-bit index to a real RGBA8888 Color using the specified palette.
|
|
||||||
pub fn resolve_color(&self, palette_id: u8, pixel_index: u8) -> Color {
|
|
||||||
self.palettes
|
|
||||||
.get(palette_id as usize)
|
|
||||||
.and_then(|palette| palette.get(pixel_index as usize))
|
|
||||||
.copied()
|
|
||||||
.unwrap_or(Color::TRANSPARENT)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,28 +0,0 @@
|
|||||||
use crate::platform::RuntimePlatform;
|
|
||||||
use crate::vm_fault::VmFault;
|
|
||||||
|
|
||||||
pub struct HostContext<'a> {
|
|
||||||
pub platform: Option<&'a mut dyn RuntimePlatform>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> HostContext<'a> {
|
|
||||||
pub fn new(platform: Option<&'a mut dyn RuntimePlatform>) -> Self {
|
|
||||||
Self { platform }
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn new_platform(platform: Option<&'a mut dyn RuntimePlatform>) -> Self {
|
|
||||||
Self::new(platform)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub fn require_platform(&mut self) -> Result<&mut dyn RuntimePlatform, VmFault> {
|
|
||||||
match &mut self.platform {
|
|
||||||
Some(platform) => Ok(*platform),
|
|
||||||
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,66 +0,0 @@
|
|||||||
pub mod app_mode;
|
|
||||||
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 host_context;
|
|
||||||
pub mod host_return;
|
|
||||||
pub mod input_signals;
|
|
||||||
pub mod log;
|
|
||||||
pub mod native_helpers;
|
|
||||||
pub mod native_interface;
|
|
||||||
pub mod owned_frame;
|
|
||||||
pub mod pad_bridge;
|
|
||||||
pub mod platform;
|
|
||||||
pub mod primitives;
|
|
||||||
pub mod render_resources;
|
|
||||||
pub mod render_submission;
|
|
||||||
pub mod render_worker;
|
|
||||||
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 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 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 owned_frame::{OwnedRgba8888Frame, OwnedRgba8888FrameError};
|
|
||||||
pub use pad_bridge::PadBridge;
|
|
||||||
pub use platform::{
|
|
||||||
AssetStoragePlatform, AudioPlatform, ClockPacingPlatform, Game2DFrameComposer, InputPlatform,
|
|
||||||
NoopTelemetryPlatform, RenderBackend, RenderSubmissionSink, RenderSubmitError, RuntimePlatform,
|
|
||||||
TelemetryPlatform, TestPlatform,
|
|
||||||
};
|
|
||||||
pub use render_resources::RenderResourceAccess;
|
|
||||||
pub use render_submission::{
|
|
||||||
BoundScenePacket, Camera2D, ComposerFramePacket, FrameId, Game2DFramePacket, GameSpritePacket,
|
|
||||||
Gfx2dCommand, GfxUiCommand, HudCommand, HudPacket, RenderOwnership, RenderSubmission,
|
|
||||||
RenderSubmissionPacket, ShellUiFramePacket,
|
|
||||||
};
|
|
||||||
pub use render_worker::{
|
|
||||||
RenderWorkerBackend, RenderWorkerError, RenderWorkerFrameSink, RenderWorkerTelemetry,
|
|
||||||
};
|
|
||||||
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,127 +0,0 @@
|
|||||||
use crate::render_submission::{FrameId, RenderOwnership};
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
||||||
pub struct OwnedRgba8888Frame {
|
|
||||||
pub frame_id: FrameId,
|
|
||||||
pub ownership: RenderOwnership,
|
|
||||||
pub width: usize,
|
|
||||||
pub height: usize,
|
|
||||||
pub stride_pixels: usize,
|
|
||||||
pub pixels: Vec<u32>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
||||||
pub enum OwnedRgba8888FrameError {
|
|
||||||
EmptyDimensions,
|
|
||||||
StrideTooSmall,
|
|
||||||
PixelBufferTooSmall,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl OwnedRgba8888Frame {
|
|
||||||
pub fn new(
|
|
||||||
frame_id: FrameId,
|
|
||||||
ownership: RenderOwnership,
|
|
||||||
width: usize,
|
|
||||||
height: usize,
|
|
||||||
stride_pixels: usize,
|
|
||||||
pixels: Vec<u32>,
|
|
||||||
) -> Result<Self, OwnedRgba8888FrameError> {
|
|
||||||
if width == 0 || height == 0 {
|
|
||||||
return Err(OwnedRgba8888FrameError::EmptyDimensions);
|
|
||||||
}
|
|
||||||
if stride_pixels < width {
|
|
||||||
return Err(OwnedRgba8888FrameError::StrideTooSmall);
|
|
||||||
}
|
|
||||||
let required_len = stride_pixels
|
|
||||||
.checked_mul(height)
|
|
||||||
.ok_or(OwnedRgba8888FrameError::PixelBufferTooSmall)?;
|
|
||||||
if pixels.len() < required_len {
|
|
||||||
return Err(OwnedRgba8888FrameError::PixelBufferTooSmall);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(Self { frame_id, ownership, width, height, stride_pixels, pixels })
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn packed(
|
|
||||||
frame_id: FrameId,
|
|
||||||
ownership: RenderOwnership,
|
|
||||||
width: usize,
|
|
||||||
height: usize,
|
|
||||||
pixels: Vec<u32>,
|
|
||||||
) -> Result<Self, OwnedRgba8888FrameError> {
|
|
||||||
Self::new(frame_id, ownership, width, height, width, pixels)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn required_pixel_len(&self) -> usize {
|
|
||||||
self.stride_pixels * self.height
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn row(&self, y: usize) -> Option<&[u32]> {
|
|
||||||
if y >= self.height {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
let start = y * self.stride_pixels;
|
|
||||||
Some(&self.pixels[start..start + self.width])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use crate::app_mode::AppMode;
|
|
||||||
|
|
||||||
fn ownership() -> RenderOwnership {
|
|
||||||
RenderOwnership::new(11, AppMode::Game, 42)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn owned_rgba8888_frame_preserves_metadata_and_pixels() {
|
|
||||||
let pixels = vec![0x11223344, 0x55667788, 0x99AABBCC, 0xDDEEFF00];
|
|
||||||
let frame = OwnedRgba8888Frame::packed(FrameId::new(7), ownership(), 2, 2, pixels.clone())
|
|
||||||
.expect("valid packed RGBA8888 frame");
|
|
||||||
|
|
||||||
assert_eq!(frame.frame_id, FrameId::new(7));
|
|
||||||
assert_eq!(frame.ownership, ownership());
|
|
||||||
assert_eq!(frame.width, 2);
|
|
||||||
assert_eq!(frame.height, 2);
|
|
||||||
assert_eq!(frame.stride_pixels, 2);
|
|
||||||
assert_eq!(frame.pixels, pixels);
|
|
||||||
assert_eq!(frame.required_pixel_len(), 4);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn checked_constructor_rejects_empty_dimensions() {
|
|
||||||
let err = OwnedRgba8888Frame::new(FrameId::ZERO, ownership(), 0, 1, 1, vec![0])
|
|
||||||
.expect_err("zero width should be rejected");
|
|
||||||
|
|
||||||
assert_eq!(err, OwnedRgba8888FrameError::EmptyDimensions);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn checked_constructor_rejects_stride_smaller_than_width() {
|
|
||||||
let err = OwnedRgba8888Frame::new(FrameId::ZERO, ownership(), 3, 1, 2, vec![0, 0, 0])
|
|
||||||
.expect_err("stride smaller than width should be rejected");
|
|
||||||
|
|
||||||
assert_eq!(err, OwnedRgba8888FrameError::StrideTooSmall);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn checked_constructor_rejects_short_pixel_buffer() {
|
|
||||||
let err = OwnedRgba8888Frame::new(FrameId::ZERO, ownership(), 2, 2, 3, vec![0; 5])
|
|
||||||
.expect_err("buffer shorter than stride * height should be rejected");
|
|
||||||
|
|
||||||
assert_eq!(err, OwnedRgba8888FrameError::PixelBufferTooSmall);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn row_returns_visible_width_without_stride_padding() {
|
|
||||||
let frame =
|
|
||||||
OwnedRgba8888Frame::new(FrameId::ZERO, ownership(), 2, 2, 3, vec![1, 2, 99, 3, 4, 88])
|
|
||||||
.expect("valid padded frame");
|
|
||||||
|
|
||||||
assert_eq!(frame.row(0), Some(&[1, 2][..]));
|
|
||||||
assert_eq!(frame.row(1), Some(&[3, 4][..]));
|
|
||||||
assert_eq!(frame.row(2), None);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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,140 +0,0 @@
|
|||||||
use crate::asset_bridge::AssetBridge;
|
|
||||||
use crate::audio_bridge::AudioBridge;
|
|
||||||
use crate::composer_status::ComposerOpStatus;
|
|
||||||
use crate::pad_bridge::PadBridge;
|
|
||||||
use crate::render_submission::{Game2DFramePacket, RenderSubmission};
|
|
||||||
use crate::sprite::Sprite;
|
|
||||||
use crate::telemetry::TelemetryFrame;
|
|
||||||
use crate::touch_bridge::TouchBridge;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
||||||
pub enum RenderSubmitError {
|
|
||||||
BackendUnavailable,
|
|
||||||
SubmissionRejected,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait RenderSubmissionSink {
|
|
||||||
fn submit_render_submission(
|
|
||||||
&mut self,
|
|
||||||
submission: RenderSubmission,
|
|
||||||
) -> Result<(), RenderSubmitError>;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait RenderBackend {
|
|
||||||
fn render_frame(&mut self);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait Game2DFrameComposer {
|
|
||||||
fn begin_frame(&mut self);
|
|
||||||
fn bind_scene(&mut self, scene_bank_id: usize) -> ComposerOpStatus;
|
|
||||||
fn unbind_scene(&mut self);
|
|
||||||
fn set_camera(&mut self, x: i32, y: i32) -> ComposerOpStatus;
|
|
||||||
fn emit_sprite(&mut self, sprite: Sprite) -> ComposerOpStatus;
|
|
||||||
fn close_game2d_packet(&self) -> Game2DFramePacket;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait AudioPlatform: AudioBridge {}
|
|
||||||
impl<T: AudioBridge + ?Sized> AudioPlatform for T {}
|
|
||||||
|
|
||||||
pub trait InputPlatform {
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait AssetStoragePlatform: AssetBridge {}
|
|
||||||
impl<T: AssetBridge + ?Sized> AssetStoragePlatform for T {}
|
|
||||||
|
|
||||||
pub trait ClockPacingPlatform {}
|
|
||||||
|
|
||||||
pub trait TelemetryPlatform {
|
|
||||||
fn telemetry_snapshot(&self) -> Option<TelemetryFrame> {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Default)]
|
|
||||||
pub struct NoopTelemetryPlatform;
|
|
||||||
|
|
||||||
impl TelemetryPlatform for NoopTelemetryPlatform {}
|
|
||||||
|
|
||||||
pub trait RuntimePlatform {
|
|
||||||
fn display_size(&self) -> (usize, usize) {
|
|
||||||
(480, 270)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_submission_sink(&mut self) -> &mut dyn RenderSubmissionSink;
|
|
||||||
fn render_backend(&mut self) -> &mut dyn RenderBackend;
|
|
||||||
fn game2d_frame_composer(&mut self) -> &mut dyn Game2DFrameComposer;
|
|
||||||
fn audio(&self) -> &dyn AudioBridge;
|
|
||||||
fn audio_mut(&mut self) -> &mut dyn AudioBridge;
|
|
||||||
fn input(&self) -> &dyn InputPlatform;
|
|
||||||
fn input_mut(&mut self) -> &mut dyn InputPlatform;
|
|
||||||
fn assets(&self) -> &dyn AssetBridge;
|
|
||||||
fn assets_mut(&mut self) -> &mut dyn AssetBridge;
|
|
||||||
fn telemetry(&self) -> &dyn TelemetryPlatform;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait TestPlatform: RuntimePlatform {}
|
|
||||||
impl<T: RuntimePlatform + ?Sized> TestPlatform for T {}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
fn assert_render_sink_object(_: &mut dyn RenderSubmissionSink) {}
|
|
||||||
fn assert_backend_object(_: &mut dyn RenderBackend) {}
|
|
||||||
fn assert_composer_object(_: &mut dyn Game2DFrameComposer) {}
|
|
||||||
|
|
||||||
struct CompileOnly;
|
|
||||||
|
|
||||||
impl RenderSubmissionSink for CompileOnly {
|
|
||||||
fn submit_render_submission(
|
|
||||||
&mut self,
|
|
||||||
_submission: RenderSubmission,
|
|
||||||
) -> Result<(), RenderSubmitError> {
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RenderBackend for CompileOnly {
|
|
||||||
fn render_frame(&mut self) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Game2DFrameComposer for CompileOnly {
|
|
||||||
fn begin_frame(&mut self) {}
|
|
||||||
|
|
||||||
fn bind_scene(&mut self, _scene_bank_id: usize) -> ComposerOpStatus {
|
|
||||||
ComposerOpStatus::Ok
|
|
||||||
}
|
|
||||||
|
|
||||||
fn unbind_scene(&mut self) {}
|
|
||||||
|
|
||||||
fn set_camera(&mut self, _x: i32, _y: i32) -> ComposerOpStatus {
|
|
||||||
ComposerOpStatus::Ok
|
|
||||||
}
|
|
||||||
|
|
||||||
fn emit_sprite(&mut self, _sprite: Sprite) -> ComposerOpStatus {
|
|
||||||
ComposerOpStatus::Ok
|
|
||||||
}
|
|
||||||
|
|
||||||
fn close_game2d_packet(&self) -> Game2DFramePacket {
|
|
||||||
Game2DFramePacket::default()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn core_platform_facades_are_object_safe() {
|
|
||||||
let mut compile_only = CompileOnly;
|
|
||||||
|
|
||||||
assert_render_sink_object(&mut compile_only);
|
|
||||||
assert_backend_object(&mut compile_only);
|
|
||||||
assert_composer_object(&mut compile_only);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn render_submit_error_is_typed_and_debuggable() {
|
|
||||||
assert_eq!(format!("{:?}", RenderSubmitError::BackendUnavailable), "BackendUnavailable");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,7 +0,0 @@
|
|||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
||||||
pub struct Rect {
|
|
||||||
pub x: i32,
|
|
||||||
pub y: i32,
|
|
||||||
pub w: i32,
|
|
||||||
pub h: i32,
|
|
||||||
}
|
|
||||||
@ -1,109 +0,0 @@
|
|||||||
use crate::glyph_bank::GlyphBank;
|
|
||||||
use crate::scene_bank::SceneBank;
|
|
||||||
|
|
||||||
/// Read-only render resource lookup used by render backends.
|
|
||||||
///
|
|
||||||
/// Resource ids are stable bank slots carried by render submissions. The
|
|
||||||
/// callback shape keeps bank ownership inside the provider while exposing only
|
|
||||||
/// borrowed read access to the caller.
|
|
||||||
pub trait RenderResourceAccess {
|
|
||||||
/// Reads a resident glyph bank by id.
|
|
||||||
fn with_glyph_bank<R>(&self, bank_id: usize, read: impl FnOnce(&GlyphBank) -> R) -> Option<R>;
|
|
||||||
|
|
||||||
/// Returns the number of glyph bank ids accepted by this provider.
|
|
||||||
fn glyph_bank_count(&self) -> usize;
|
|
||||||
|
|
||||||
/// Reads a resident scene bank by id.
|
|
||||||
fn with_scene_bank<R>(&self, bank_id: usize, read: impl FnOnce(&SceneBank) -> R) -> Option<R>;
|
|
||||||
|
|
||||||
/// Returns the number of scene bank ids accepted by this provider.
|
|
||||||
fn scene_bank_count(&self) -> usize;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[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;
|
|
||||||
|
|
||||||
struct TestRenderResources {
|
|
||||||
glyph_banks: Vec<Option<GlyphBank>>,
|
|
||||||
scene_banks: Vec<Option<SceneBank>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RenderResourceAccess for TestRenderResources {
|
|
||||||
fn with_glyph_bank<R>(
|
|
||||||
&self,
|
|
||||||
bank_id: usize,
|
|
||||||
read: impl FnOnce(&GlyphBank) -> R,
|
|
||||||
) -> Option<R> {
|
|
||||||
self.glyph_banks.get(bank_id)?.as_ref().map(read)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn glyph_bank_count(&self) -> usize {
|
|
||||||
self.glyph_banks.len()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn with_scene_bank<R>(
|
|
||||||
&self,
|
|
||||||
bank_id: usize,
|
|
||||||
read: impl FnOnce(&SceneBank) -> R,
|
|
||||||
) -> Option<R> {
|
|
||||||
self.scene_banks.get(bank_id)?.as_ref().map(read)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn scene_bank_count(&self) -> usize {
|
|
||||||
self.scene_banks.len()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn scene_bank(tile_size: TileSize) -> SceneBank {
|
|
||||||
let layer = SceneLayer {
|
|
||||||
active: true,
|
|
||||||
glyph_asset_id: 7,
|
|
||||||
tile_size,
|
|
||||||
parallax_factor: ParallaxFactor { x: 1.0, y: 1.0 },
|
|
||||||
tilemap: TileMap {
|
|
||||||
width: 1,
|
|
||||||
height: 1,
|
|
||||||
tiles: vec![Tile {
|
|
||||||
active: true,
|
|
||||||
glyph: Glyph { glyph_id: 3, palette_id: 0 },
|
|
||||||
flip_x: false,
|
|
||||||
flip_y: false,
|
|
||||||
}],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
SceneBank { layers: std::array::from_fn(|_| layer.clone()) }
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn reads_resident_resources_by_id() {
|
|
||||||
let resources = TestRenderResources {
|
|
||||||
glyph_banks: vec![None, Some(GlyphBank::new(TileSize::Size16, 16, 16)), None],
|
|
||||||
scene_banks: vec![Some(scene_bank(TileSize::Size8)), None],
|
|
||||||
};
|
|
||||||
|
|
||||||
assert_eq!(resources.glyph_bank_count(), 3);
|
|
||||||
assert_eq!(resources.scene_bank_count(), 2);
|
|
||||||
assert_eq!(resources.with_glyph_bank(1, |bank| bank.tile_size), Some(TileSize::Size16));
|
|
||||||
assert_eq!(
|
|
||||||
resources.with_scene_bank(0, |bank| bank.layers[0].tile_size),
|
|
||||||
Some(TileSize::Size8)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn missing_resources_return_none() {
|
|
||||||
let resources = TestRenderResources { glyph_banks: vec![None], scene_banks: vec![None] };
|
|
||||||
|
|
||||||
assert_eq!(resources.with_glyph_bank(0, |bank| bank.width), None);
|
|
||||||
assert_eq!(resources.with_glyph_bank(9, |bank| bank.width), None);
|
|
||||||
assert_eq!(resources.with_scene_bank(0, |bank| bank.layers[0].active), None);
|
|
||||||
assert_eq!(resources.with_scene_bank(9, |bank| bank.layers[0].active), None);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,241 +0,0 @@
|
|||||||
use crate::app_mode::AppMode;
|
|
||||||
use crate::color::Color;
|
|
||||||
use crate::primitives::Rect;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
|
|
||||||
pub struct FrameId(pub u64);
|
|
||||||
|
|
||||||
impl FrameId {
|
|
||||||
pub const ZERO: Self = Self(0);
|
|
||||||
|
|
||||||
pub const fn new(value: u64) -> Self {
|
|
||||||
Self(value)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub const fn get(self) -> u64 {
|
|
||||||
self.0
|
|
||||||
}
|
|
||||||
|
|
||||||
pub const fn next(self) -> Self {
|
|
||||||
Self(self.0.wrapping_add(1))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
||||||
pub struct RenderSubmission {
|
|
||||||
pub frame_id: FrameId,
|
|
||||||
pub app_mode: AppMode,
|
|
||||||
pub ownership: RenderOwnership,
|
|
||||||
pub packet: RenderSubmissionPacket,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RenderSubmission {
|
|
||||||
pub const fn game2d(frame_id: FrameId, packet: Game2DFramePacket) -> Self {
|
|
||||||
Self {
|
|
||||||
frame_id,
|
|
||||||
app_mode: AppMode::Game,
|
|
||||||
ownership: RenderOwnership::new(0, AppMode::Game, 0),
|
|
||||||
packet: RenderSubmissionPacket::Game2D(packet),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub const fn shell_ui(frame_id: FrameId, packet: ShellUiFramePacket) -> Self {
|
|
||||||
Self {
|
|
||||||
frame_id,
|
|
||||||
app_mode: AppMode::Shell,
|
|
||||||
ownership: RenderOwnership::new(0, AppMode::Shell, 0),
|
|
||||||
packet: RenderSubmissionPacket::ShellUi(packet),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub const fn with_ownership(mut self, ownership: RenderOwnership) -> Self {
|
|
||||||
self.ownership = ownership;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub const fn is_coherent(&self) -> bool {
|
|
||||||
matches!(
|
|
||||||
(self.app_mode, &self.packet),
|
|
||||||
(AppMode::Game, RenderSubmissionPacket::Game2D(_))
|
|
||||||
| (AppMode::Shell, RenderSubmissionPacket::ShellUi(_))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
||||||
pub struct RenderOwnership {
|
|
||||||
pub epoch: u64,
|
|
||||||
pub app_mode: AppMode,
|
|
||||||
pub app_id: u32,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RenderOwnership {
|
|
||||||
pub const fn new(epoch: u64, app_mode: AppMode, app_id: u32) -> Self {
|
|
||||||
Self { epoch, app_mode, app_id }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
||||||
pub enum RenderSubmissionPacket {
|
|
||||||
Game2D(Game2DFramePacket),
|
|
||||||
ShellUi(ShellUiFramePacket),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
|
||||||
pub struct Game2DFramePacket {
|
|
||||||
pub composer: ComposerFramePacket,
|
|
||||||
pub gfx2d: Vec<Gfx2dCommand>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Game2DFramePacket {
|
|
||||||
pub fn new(composer: ComposerFramePacket, gfx2d: Vec<Gfx2dCommand>) -> Self {
|
|
||||||
Self { composer, gfx2d }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
||||||
pub struct ShellUiFramePacket {
|
|
||||||
pub commands: Vec<GfxUiCommand>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ShellUiFramePacket {
|
|
||||||
pub fn new(commands: Vec<GfxUiCommand>) -> Self {
|
|
||||||
Self { commands }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
|
||||||
pub struct ComposerFramePacket {
|
|
||||||
pub bound_scene: Option<BoundScenePacket>,
|
|
||||||
pub camera: Camera2D,
|
|
||||||
pub sprites: Vec<GameSpritePacket>,
|
|
||||||
pub hud: HudPacket,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
||||||
pub struct BoundScenePacket {
|
|
||||||
pub bank_id: i32,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
|
||||||
pub struct Camera2D {
|
|
||||||
pub x: i32,
|
|
||||||
pub y: i32,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
||||||
pub struct GameSpritePacket {
|
|
||||||
pub glyph_id: i32,
|
|
||||||
pub palette_id: i32,
|
|
||||||
pub x: i32,
|
|
||||||
pub y: i32,
|
|
||||||
pub layer: i32,
|
|
||||||
pub bank_id: i32,
|
|
||||||
pub flip_x: bool,
|
|
||||||
pub flip_y: bool,
|
|
||||||
pub priority: i32,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
|
||||||
pub struct HudPacket {
|
|
||||||
pub commands: Vec<HudCommand>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
||||||
pub enum HudCommand {
|
|
||||||
Clear { color: Color },
|
|
||||||
FillRect { rect: Rect, color: Color },
|
|
||||||
DrawText { x: i32, y: i32, text: String, color: Color },
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
||||||
pub enum Gfx2dCommand {
|
|
||||||
Clear { color: Color },
|
|
||||||
FillRect { rect: Rect, color: Color },
|
|
||||||
DrawLine { x0: i32, y0: i32, x1: i32, y1: i32, color: Color },
|
|
||||||
DrawCircle { x: i32, y: i32, radius: i32, color: Color },
|
|
||||||
DrawDisc { x: i32, y: i32, radius: i32, border_color: Color, fill_color: Color },
|
|
||||||
DrawSquare { rect: Rect, border_color: Color, fill_color: Color },
|
|
||||||
DrawText { x: i32, y: i32, text: String, color: Color },
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
||||||
pub enum GfxUiCommand {
|
|
||||||
Clear { color: Color },
|
|
||||||
FillRect { rect: Rect, color: Color },
|
|
||||||
DrawLine { x0: i32, y0: i32, x1: i32, y1: i32, color: Color },
|
|
||||||
DrawCircle { x: i32, y: i32, radius: i32, color: Color },
|
|
||||||
DrawDisc { x: i32, y: i32, radius: i32, border_color: Color, fill_color: Color },
|
|
||||||
DrawSquare { rect: Rect, border_color: Color, fill_color: Color },
|
|
||||||
DrawText { x: i32, y: i32, text: String, color: Color },
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn constructors_pin_app_mode_to_packet_kind() {
|
|
||||||
let game = RenderSubmission::game2d(FrameId::new(7), Game2DFramePacket::default());
|
|
||||||
assert_eq!(game.app_mode, AppMode::Game);
|
|
||||||
assert!(matches!(game.packet, RenderSubmissionPacket::Game2D(_)));
|
|
||||||
assert!(game.is_coherent());
|
|
||||||
|
|
||||||
let shell =
|
|
||||||
RenderSubmission::shell_ui(FrameId::new(8), ShellUiFramePacket::new(Vec::new()));
|
|
||||||
assert_eq!(shell.app_mode, AppMode::Shell);
|
|
||||||
assert!(matches!(shell.packet, RenderSubmissionPacket::ShellUi(_)));
|
|
||||||
assert!(shell.is_coherent());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn coherence_check_rejects_mismatched_envelope() {
|
|
||||||
let submission = RenderSubmission {
|
|
||||||
frame_id: FrameId::new(1),
|
|
||||||
app_mode: AppMode::Game,
|
|
||||||
ownership: RenderOwnership::new(0, AppMode::Game, 0),
|
|
||||||
packet: RenderSubmissionPacket::ShellUi(ShellUiFramePacket::new(Vec::new())),
|
|
||||||
};
|
|
||||||
|
|
||||||
assert!(!submission.is_coherent());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn ownership_can_be_stamped_by_runtime() {
|
|
||||||
let submission = RenderSubmission::game2d(FrameId::new(3), Game2DFramePacket::default())
|
|
||||||
.with_ownership(RenderOwnership::new(9, AppMode::Game, 42));
|
|
||||||
|
|
||||||
assert_eq!(submission.ownership, RenderOwnership::new(9, AppMode::Game, 42));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn closed_submission_owns_command_payload() {
|
|
||||||
let mut commands = vec![Gfx2dCommand::DrawText {
|
|
||||||
x: 1,
|
|
||||||
y: 2,
|
|
||||||
text: "before".to_string(),
|
|
||||||
color: Color::WHITE,
|
|
||||||
}];
|
|
||||||
let submission = RenderSubmission::game2d(
|
|
||||||
FrameId::new(2),
|
|
||||||
Game2DFramePacket::new(Default::default(), commands.clone()),
|
|
||||||
);
|
|
||||||
|
|
||||||
commands.push(Gfx2dCommand::Clear { color: Color::BLACK });
|
|
||||||
|
|
||||||
let RenderSubmissionPacket::Game2D(packet) = submission.packet else {
|
|
||||||
panic!("expected game packet");
|
|
||||||
};
|
|
||||||
assert_eq!(packet.gfx2d.len(), 1);
|
|
||||||
assert_eq!(
|
|
||||||
packet.gfx2d[0],
|
|
||||||
Gfx2dCommand::DrawText { x: 1, y: 2, text: "before".to_string(), color: Color::WHITE }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn frame_id_advances_monotonically_with_wrapping_arithmetic() {
|
|
||||||
assert_eq!(FrameId::new(41).next(), FrameId::new(42));
|
|
||||||
assert_eq!(FrameId::new(u64::MAX).next(), FrameId::ZERO);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,203 +0,0 @@
|
|||||||
use std::fmt;
|
|
||||||
|
|
||||||
use crate::{OwnedRgba8888Frame, RenderSubmission};
|
|
||||||
|
|
||||||
/// Typed failure surface for the real render worker and its backend.
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
||||||
pub enum RenderWorkerError {
|
|
||||||
BackendUnavailable,
|
|
||||||
RenderFailed,
|
|
||||||
PublishFailed,
|
|
||||||
PresentFailed,
|
|
||||||
ShutdownTimeout,
|
|
||||||
StaleSubmissionDiscarded,
|
|
||||||
WorkerPanic,
|
|
||||||
InternalFailure,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Display for RenderWorkerError {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
match self {
|
|
||||||
Self::BackendUnavailable => f.write_str("render backend unavailable"),
|
|
||||||
Self::RenderFailed => f.write_str("render failed"),
|
|
||||||
Self::PublishFailed => f.write_str("render publish failed"),
|
|
||||||
Self::PresentFailed => f.write_str("render present failed"),
|
|
||||||
Self::ShutdownTimeout => f.write_str("render worker shutdown timed out"),
|
|
||||||
Self::StaleSubmissionDiscarded => f.write_str("stale render submission discarded"),
|
|
||||||
Self::WorkerPanic => f.write_str("render worker panicked"),
|
|
||||||
Self::InternalFailure => f.write_str("internal render worker failure"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::error::Error for RenderWorkerError {}
|
|
||||||
|
|
||||||
pub trait RenderWorkerBackend: Send + 'static {
|
|
||||||
fn render(
|
|
||||||
&self,
|
|
||||||
submission: &RenderSubmission,
|
|
||||||
) -> Result<OwnedRgba8888Frame, RenderWorkerError>;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait RenderWorkerFrameSink: Send + 'static {
|
|
||||||
fn publish(&self, frame: OwnedRgba8888Frame) -> Result<(), RenderWorkerError>;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Snapshot of render worker handoff, render, publish, and shutdown counters.
|
|
||||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
|
|
||||||
pub struct RenderWorkerTelemetry {
|
|
||||||
pub produced_submissions: u64,
|
|
||||||
pub replaced_before_consume: u64,
|
|
||||||
pub consumed_submissions: u64,
|
|
||||||
pub published_frames: u64,
|
|
||||||
pub presented_frames: u64,
|
|
||||||
pub repeated_frames: u64,
|
|
||||||
pub stale_submission_discards: u64,
|
|
||||||
pub render_failures: u64,
|
|
||||||
pub publish_failures: u64,
|
|
||||||
pub present_failures: u64,
|
|
||||||
pub shutdown_timeouts: u64,
|
|
||||||
pub worker_panics: u64,
|
|
||||||
pub last_produced_frame_id: u64,
|
|
||||||
pub last_consumed_frame_id: u64,
|
|
||||||
pub last_published_frame_id: u64,
|
|
||||||
pub last_presented_frame_id: u64,
|
|
||||||
pub last_dropped_frame_id: u64,
|
|
||||||
pub last_error_frame_id: u64,
|
|
||||||
pub active_render_epoch: u64,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RenderWorkerTelemetry {
|
|
||||||
pub fn record_produced(&mut self, frame_id: u64) {
|
|
||||||
self.produced_submissions += 1;
|
|
||||||
self.last_produced_frame_id = frame_id;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn record_replaced_before_consume(&mut self, frame_id: u64) {
|
|
||||||
self.replaced_before_consume += 1;
|
|
||||||
self.last_dropped_frame_id = frame_id;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn record_consumed(&mut self, frame_id: u64) {
|
|
||||||
self.consumed_submissions += 1;
|
|
||||||
self.last_consumed_frame_id = frame_id;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn record_published(&mut self, frame_id: u64) {
|
|
||||||
self.published_frames += 1;
|
|
||||||
self.last_published_frame_id = frame_id;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn record_presented(&mut self, frame_id: u64) {
|
|
||||||
self.presented_frames += 1;
|
|
||||||
self.last_presented_frame_id = frame_id;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn record_repeated_frame(&mut self, frame_id: u64) {
|
|
||||||
self.repeated_frames += 1;
|
|
||||||
self.last_presented_frame_id = frame_id;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn record_stale_submission_discarded(&mut self, frame_id: u64) {
|
|
||||||
self.stale_submission_discards += 1;
|
|
||||||
self.last_dropped_frame_id = frame_id;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn record_error(&mut self, error: RenderWorkerError, frame_id: u64) {
|
|
||||||
match error {
|
|
||||||
RenderWorkerError::BackendUnavailable | RenderWorkerError::RenderFailed => {
|
|
||||||
self.render_failures += 1;
|
|
||||||
}
|
|
||||||
RenderWorkerError::PublishFailed => {
|
|
||||||
self.publish_failures += 1;
|
|
||||||
}
|
|
||||||
RenderWorkerError::PresentFailed => {
|
|
||||||
self.present_failures += 1;
|
|
||||||
}
|
|
||||||
RenderWorkerError::ShutdownTimeout => {
|
|
||||||
self.shutdown_timeouts += 1;
|
|
||||||
}
|
|
||||||
RenderWorkerError::WorkerPanic => {
|
|
||||||
self.worker_panics += 1;
|
|
||||||
}
|
|
||||||
RenderWorkerError::StaleSubmissionDiscarded => {
|
|
||||||
self.stale_submission_discards += 1;
|
|
||||||
self.last_dropped_frame_id = frame_id;
|
|
||||||
}
|
|
||||||
RenderWorkerError::InternalFailure => {
|
|
||||||
self.render_failures += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
self.last_error_frame_id = frame_id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn render_worker_error_is_copy_equatable_and_debuggable() {
|
|
||||||
let error = RenderWorkerError::ShutdownTimeout;
|
|
||||||
let copied = error;
|
|
||||||
|
|
||||||
assert_eq!(copied, RenderWorkerError::ShutdownTimeout);
|
|
||||||
assert_eq!(format!("{:?}", error), "ShutdownTimeout");
|
|
||||||
assert_eq!(error.to_string(), "render worker shutdown timed out");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn render_worker_telemetry_defaults_to_zero() {
|
|
||||||
assert_eq!(
|
|
||||||
RenderWorkerTelemetry::default(),
|
|
||||||
RenderWorkerTelemetry {
|
|
||||||
produced_submissions: 0,
|
|
||||||
replaced_before_consume: 0,
|
|
||||||
consumed_submissions: 0,
|
|
||||||
published_frames: 0,
|
|
||||||
presented_frames: 0,
|
|
||||||
repeated_frames: 0,
|
|
||||||
stale_submission_discards: 0,
|
|
||||||
render_failures: 0,
|
|
||||||
publish_failures: 0,
|
|
||||||
present_failures: 0,
|
|
||||||
shutdown_timeouts: 0,
|
|
||||||
worker_panics: 0,
|
|
||||||
last_produced_frame_id: 0,
|
|
||||||
last_consumed_frame_id: 0,
|
|
||||||
last_published_frame_id: 0,
|
|
||||||
last_presented_frame_id: 0,
|
|
||||||
last_dropped_frame_id: 0,
|
|
||||||
last_error_frame_id: 0,
|
|
||||||
active_render_epoch: 0,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn render_worker_telemetry_records_counters_and_frame_ids() {
|
|
||||||
let mut telemetry = RenderWorkerTelemetry::default();
|
|
||||||
|
|
||||||
telemetry.record_produced(10);
|
|
||||||
telemetry.record_replaced_before_consume(10);
|
|
||||||
telemetry.record_consumed(11);
|
|
||||||
telemetry.record_published(11);
|
|
||||||
telemetry.record_presented(11);
|
|
||||||
telemetry.record_repeated_frame(11);
|
|
||||||
telemetry.record_error(RenderWorkerError::PublishFailed, 12);
|
|
||||||
telemetry.record_error(RenderWorkerError::WorkerPanic, 13);
|
|
||||||
telemetry.record_stale_submission_discarded(14);
|
|
||||||
|
|
||||||
assert_eq!(telemetry.produced_submissions, 1);
|
|
||||||
assert_eq!(telemetry.replaced_before_consume, 1);
|
|
||||||
assert_eq!(telemetry.consumed_submissions, 1);
|
|
||||||
assert_eq!(telemetry.published_frames, 1);
|
|
||||||
assert_eq!(telemetry.presented_frames, 1);
|
|
||||||
assert_eq!(telemetry.repeated_frames, 1);
|
|
||||||
assert_eq!(telemetry.publish_failures, 1);
|
|
||||||
assert_eq!(telemetry.worker_panics, 1);
|
|
||||||
assert_eq!(telemetry.stale_submission_discards, 1);
|
|
||||||
assert_eq!(telemetry.last_error_frame_id, 13);
|
|
||||||
assert_eq!(telemetry.last_dropped_frame_id, 14);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,56 +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::asset::AssetId;
|
|
||||||
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_asset_id: AssetId,
|
|
||||||
parallax_x: f32,
|
|
||||||
parallax_y: f32,
|
|
||||||
glyph_id: u16,
|
|
||||||
) -> SceneLayer {
|
|
||||||
SceneLayer {
|
|
||||||
active: true,
|
|
||||||
glyph_asset_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: 1 },
|
|
||||||
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,60 +0,0 @@
|
|||||||
use crate::asset::AssetId;
|
|
||||||
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_asset_id: AssetId,
|
|
||||||
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_asset_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_asset_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,562 +0,0 @@
|
|||||||
use crate::asset::AssetId;
|
|
||||||
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_asset_id: AssetId,
|
|
||||||
}
|
|
||||||
|
|
||||||
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_asset_id: layer.glyph_asset_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_asset_id: AssetId,
|
|
||||||
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_asset_id: layer.glyph_asset_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_asset_id = layer.glyph_asset_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::asset::AssetId;
|
|
||||||
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_asset_id: AssetId, 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, 1, x % 2 == 0, y % 2 == 1));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
SceneLayer {
|
|
||||||
active: true,
|
|
||||||
glyph_asset_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_asset_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,537 +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::asset::AssetId;
|
|
||||||
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_asset_id: 1 as AssetId,
|
|
||||||
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(480, 270, 35, 21, 12, 20);
|
|
||||||
|
|
||||||
let update = resolver.update(&scene, 0, 0);
|
|
||||||
|
|
||||||
assert_eq!(update.master_anchor, TileAnchor { x: 17, y: 10 });
|
|
||||||
assert_eq!(update.layer_anchors[0], TileAnchor { x: 17, y: 10 });
|
|
||||||
assert_eq!(update.layer_anchors[1], TileAnchor { x: 17, y: 10 });
|
|
||||||
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(480, 270, 35, 21, 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(480, 270, 35, 21, 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: 17, y: 10 });
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn repeated_high_speed_movement_advances_in_tile_steps() {
|
|
||||||
let scene = make_scene();
|
|
||||||
let mut resolver = SceneViewportResolver::new(480, 270, 35, 21, 12, 20);
|
|
||||||
|
|
||||||
let _ = resolver.update(&scene, 0, 0);
|
|
||||||
let update = resolver.update(&scene, 64, 0);
|
|
||||||
|
|
||||||
assert!(update.master_anchor.x > 17);
|
|
||||||
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(480, 270, 35, 21, 12, 20);
|
|
||||||
|
|
||||||
let update = resolver.update(&scene, 10_000, 10_000);
|
|
||||||
|
|
||||||
assert_eq!(update.master_anchor, TileAnchor { x: 46, y: 53 });
|
|
||||||
assert_eq!(update.layer_anchors[0], TileAnchor { x: 46, y: 53 });
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn corner_trigger_converts_to_non_overlapping_region_requests() {
|
|
||||||
let scene = make_scene();
|
|
||||||
let mut resolver = SceneViewportResolver::new(480, 270, 35, 21, 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, 35);
|
|
||||||
assert_eq!(regions[0].height, 21);
|
|
||||||
assert_eq!(regions[1].width, 34);
|
|
||||||
assert_eq!(regions[1].height, 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn reset_scene_requests_full_invalidation_for_all_layers() {
|
|
||||||
let mut resolver = SceneViewportResolver::new(480, 270, 35, 21, 12, 20);
|
|
||||||
|
|
||||||
let requests = resolver.reset_scene();
|
|
||||||
|
|
||||||
assert_eq!(requests.len(), 4);
|
|
||||||
assert!(
|
|
||||||
requests
|
|
||||||
.iter()
|
|
||||||
.all(|request| matches!(request, CacheRefreshRequest::InvalidateLayer { .. }))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,171 +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**: Game 2D primitives (`gfx2d`) and Shell UI primitives (`gfxui`)
|
|
||||||
/// - **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,
|
|
||||||
GfxClear = 0x1001,
|
|
||||||
GfxFillRect = 0x1002,
|
|
||||||
GfxDrawLine = 0x1003,
|
|
||||||
GfxDrawCircle = 0x1004,
|
|
||||||
GfxDrawDisc = 0x1005,
|
|
||||||
GfxDrawSquare = 0x1006,
|
|
||||||
GfxDrawText = 0x1008,
|
|
||||||
GfxUiClear = 0x1201,
|
|
||||||
GfxUiFillRect = 0x1202,
|
|
||||||
GfxUiDrawLine = 0x1203,
|
|
||||||
GfxUiDrawCircle = 0x1204,
|
|
||||||
GfxUiDrawDisc = 0x1205,
|
|
||||||
GfxUiDrawSquare = 0x1206,
|
|
||||||
GfxUiDrawText = 0x1208,
|
|
||||||
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,
|
|
||||||
AssetBacklogInfo = 0x6005,
|
|
||||||
AssetBacklogPosition = 0x6006,
|
|
||||||
AssetBacklogMove = 0x6007,
|
|
||||||
AssetBacklogPromote = 0x6008,
|
|
||||||
AssetBacklogDemote = 0x6009,
|
|
||||||
AssetTargetStatus = 0x600A,
|
|
||||||
BankInfo = 0x6101,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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,14 +0,0 @@
|
|||||||
use super::CapFlags;
|
|
||||||
|
|
||||||
pub const NONE: CapFlags = 0;
|
|
||||||
pub const SYSTEM: CapFlags = 1 << 0;
|
|
||||||
pub const COMPOSER: 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 GFX2D: CapFlags = 1 << 7;
|
|
||||||
pub const GFXUI: CapFlags = 1 << 8;
|
|
||||||
pub const GFX: CapFlags = COMPOSER | GFX2D;
|
|
||||||
pub const ALL: CapFlags = SYSTEM | COMPOSER | AUDIO | FS | LOG | ASSET | BANK | GFX2D | GFXUI;
|
|
||||||
@ -1,63 +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),
|
|
||||||
SyscallRegistryEntry::builder(Syscall::AssetBacklogInfo, "asset", "backlog_info")
|
|
||||||
.args(0)
|
|
||||||
.rets(7)
|
|
||||||
.caps(caps::ASSET)
|
|
||||||
.non_deterministic()
|
|
||||||
.cost(5),
|
|
||||||
SyscallRegistryEntry::builder(Syscall::AssetBacklogPosition, "asset", "backlog_position")
|
|
||||||
.args(1)
|
|
||||||
.rets(4)
|
|
||||||
.caps(caps::ASSET)
|
|
||||||
.non_deterministic()
|
|
||||||
.cost(5),
|
|
||||||
SyscallRegistryEntry::builder(Syscall::AssetBacklogMove, "asset", "backlog_move")
|
|
||||||
.args(2)
|
|
||||||
.rets(1)
|
|
||||||
.caps(caps::ASSET)
|
|
||||||
.non_deterministic()
|
|
||||||
.cost(10),
|
|
||||||
SyscallRegistryEntry::builder(Syscall::AssetBacklogPromote, "asset", "backlog_promote")
|
|
||||||
.args(1)
|
|
||||||
.rets(1)
|
|
||||||
.caps(caps::ASSET)
|
|
||||||
.non_deterministic()
|
|
||||||
.cost(10),
|
|
||||||
SyscallRegistryEntry::builder(Syscall::AssetBacklogDemote, "asset", "backlog_demote")
|
|
||||||
.args(1)
|
|
||||||
.rets(1)
|
|
||||||
.caps(caps::ASSET)
|
|
||||||
.non_deterministic()
|
|
||||||
.cost(10),
|
|
||||||
SyscallRegistryEntry::builder(Syscall::AssetTargetStatus, "asset", "target_status")
|
|
||||||
.args(2)
|
|
||||||
.rets(6)
|
|
||||||
.caps(caps::ASSET)
|
|
||||||
.non_deterministic()
|
|
||||||
.cost(5),
|
|
||||||
];
|
|
||||||
@ -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,7 +0,0 @@
|
|||||||
use crate::syscalls::{Syscall, SyscallRegistryEntry, caps};
|
|
||||||
|
|
||||||
pub(crate) const ENTRIES: &[SyscallRegistryEntry] =
|
|
||||||
&[SyscallRegistryEntry::builder(Syscall::BankInfo, "bank", "info")
|
|
||||||
.args(1)
|
|
||||||
.rets(2)
|
|
||||||
.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::COMPOSER)
|
|
||||||
.cost(5),
|
|
||||||
SyscallRegistryEntry::builder(Syscall::ComposerUnbindScene, "composer", "unbind_scene")
|
|
||||||
.rets(1)
|
|
||||||
.caps(caps::COMPOSER)
|
|
||||||
.cost(2),
|
|
||||||
SyscallRegistryEntry::builder(Syscall::ComposerSetCamera, "composer", "set_camera")
|
|
||||||
.args(2)
|
|
||||||
.caps(caps::COMPOSER)
|
|
||||||
.cost(2),
|
|
||||||
SyscallRegistryEntry::builder(Syscall::ComposerEmitSprite, "composer", "emit_sprite")
|
|
||||||
.args(9)
|
|
||||||
.rets(1)
|
|
||||||
.caps(caps::COMPOSER)
|
|
||||||
.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,32 +0,0 @@
|
|||||||
use crate::syscalls::{Syscall, SyscallRegistryEntry, caps};
|
|
||||||
|
|
||||||
pub(crate) const ENTRIES: &[SyscallRegistryEntry] = &[
|
|
||||||
SyscallRegistryEntry::builder(Syscall::GfxClear, "gfx2d", "clear")
|
|
||||||
.args(1)
|
|
||||||
.caps(caps::GFX2D)
|
|
||||||
.cost(20),
|
|
||||||
SyscallRegistryEntry::builder(Syscall::GfxFillRect, "gfx2d", "fill_rect")
|
|
||||||
.args(5)
|
|
||||||
.caps(caps::GFX2D)
|
|
||||||
.cost(20),
|
|
||||||
SyscallRegistryEntry::builder(Syscall::GfxDrawLine, "gfx2d", "draw_line")
|
|
||||||
.args(5)
|
|
||||||
.caps(caps::GFX2D)
|
|
||||||
.cost(5),
|
|
||||||
SyscallRegistryEntry::builder(Syscall::GfxDrawCircle, "gfx2d", "draw_circle")
|
|
||||||
.args(4)
|
|
||||||
.caps(caps::GFX2D)
|
|
||||||
.cost(5),
|
|
||||||
SyscallRegistryEntry::builder(Syscall::GfxDrawDisc, "gfx2d", "draw_disc")
|
|
||||||
.args(5)
|
|
||||||
.caps(caps::GFX2D)
|
|
||||||
.cost(5),
|
|
||||||
SyscallRegistryEntry::builder(Syscall::GfxDrawSquare, "gfx2d", "draw_square")
|
|
||||||
.args(6)
|
|
||||||
.caps(caps::GFX2D)
|
|
||||||
.cost(5),
|
|
||||||
SyscallRegistryEntry::builder(Syscall::GfxDrawText, "gfx2d", "draw_text")
|
|
||||||
.args(4)
|
|
||||||
.caps(caps::GFX2D)
|
|
||||||
.cost(20),
|
|
||||||
];
|
|
||||||
@ -1,32 +0,0 @@
|
|||||||
use crate::syscalls::{Syscall, SyscallRegistryEntry, caps};
|
|
||||||
|
|
||||||
pub(crate) const ENTRIES: &[SyscallRegistryEntry] = &[
|
|
||||||
SyscallRegistryEntry::builder(Syscall::GfxUiClear, "gfxui", "clear")
|
|
||||||
.args(1)
|
|
||||||
.caps(caps::GFXUI)
|
|
||||||
.cost(20),
|
|
||||||
SyscallRegistryEntry::builder(Syscall::GfxUiFillRect, "gfxui", "fill_rect")
|
|
||||||
.args(5)
|
|
||||||
.caps(caps::GFXUI)
|
|
||||||
.cost(20),
|
|
||||||
SyscallRegistryEntry::builder(Syscall::GfxUiDrawLine, "gfxui", "draw_line")
|
|
||||||
.args(5)
|
|
||||||
.caps(caps::GFXUI)
|
|
||||||
.cost(5),
|
|
||||||
SyscallRegistryEntry::builder(Syscall::GfxUiDrawCircle, "gfxui", "draw_circle")
|
|
||||||
.args(4)
|
|
||||||
.caps(caps::GFXUI)
|
|
||||||
.cost(5),
|
|
||||||
SyscallRegistryEntry::builder(Syscall::GfxUiDrawDisc, "gfxui", "draw_disc")
|
|
||||||
.args(5)
|
|
||||||
.caps(caps::GFXUI)
|
|
||||||
.cost(5),
|
|
||||||
SyscallRegistryEntry::builder(Syscall::GfxUiDrawSquare, "gfxui", "draw_square")
|
|
||||||
.args(6)
|
|
||||||
.caps(caps::GFXUI)
|
|
||||||
.cost(5),
|
|
||||||
SyscallRegistryEntry::builder(Syscall::GfxUiDrawText, "gfxui", "draw_text")
|
|
||||||
.args(4)
|
|
||||||
.caps(caps::GFXUI)
|
|
||||||
.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,24 +0,0 @@
|
|||||||
mod asset;
|
|
||||||
mod audio;
|
|
||||||
mod bank;
|
|
||||||
mod composer;
|
|
||||||
mod fs;
|
|
||||||
mod gfx;
|
|
||||||
mod gfxui;
|
|
||||||
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(gfxui::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,6 +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)];
|
|
||||||
@ -1,125 +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),
|
|
||||||
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),
|
|
||||||
0x1201 => Some(Self::GfxUiClear),
|
|
||||||
0x1202 => Some(Self::GfxUiFillRect),
|
|
||||||
0x1203 => Some(Self::GfxUiDrawLine),
|
|
||||||
0x1204 => Some(Self::GfxUiDrawCircle),
|
|
||||||
0x1205 => Some(Self::GfxUiDrawDisc),
|
|
||||||
0x1206 => Some(Self::GfxUiDrawSquare),
|
|
||||||
0x1208 => Some(Self::GfxUiDrawText),
|
|
||||||
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),
|
|
||||||
0x6005 => Some(Self::AssetBacklogInfo),
|
|
||||||
0x6006 => Some(Self::AssetBacklogPosition),
|
|
||||||
0x6007 => Some(Self::AssetBacklogMove),
|
|
||||||
0x6008 => Some(Self::AssetBacklogPromote),
|
|
||||||
0x6009 => Some(Self::AssetBacklogDemote),
|
|
||||||
0x600A => Some(Self::AssetTargetStatus),
|
|
||||||
0x6101 => Some(Self::BankInfo),
|
|
||||||
_ => 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::GfxClear => "GfxClear",
|
|
||||||
Self::GfxFillRect => "GfxFillRect",
|
|
||||||
Self::GfxDrawLine => "GfxDrawLine",
|
|
||||||
Self::GfxDrawCircle => "GfxDrawCircle",
|
|
||||||
Self::GfxDrawDisc => "GfxDrawDisc",
|
|
||||||
Self::GfxDrawSquare => "GfxDrawSquare",
|
|
||||||
Self::GfxDrawText => "GfxDrawText",
|
|
||||||
Self::GfxUiClear => "GfxUiClear",
|
|
||||||
Self::GfxUiFillRect => "GfxUiFillRect",
|
|
||||||
Self::GfxUiDrawLine => "GfxUiDrawLine",
|
|
||||||
Self::GfxUiDrawCircle => "GfxUiDrawCircle",
|
|
||||||
Self::GfxUiDrawDisc => "GfxUiDrawDisc",
|
|
||||||
Self::GfxUiDrawSquare => "GfxUiDrawSquare",
|
|
||||||
Self::GfxUiDrawText => "GfxUiDrawText",
|
|
||||||
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::AssetBacklogInfo => "AssetBacklogInfo",
|
|
||||||
Self::AssetBacklogPosition => "AssetBacklogPosition",
|
|
||||||
Self::AssetBacklogMove => "AssetBacklogMove",
|
|
||||||
Self::AssetBacklogPromote => "AssetBacklogPromote",
|
|
||||||
Self::AssetBacklogDemote => "AssetBacklogDemote",
|
|
||||||
Self::AssetTargetStatus => "AssetTargetStatus",
|
|
||||||
Self::BankInfo => "BankInfo",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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,488 +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("gfx2d", "clear", 1).expect("known identity must resolve");
|
|
||||||
assert_eq!(id.id, 0x1001);
|
|
||||||
assert_eq!(id.meta.module, "gfx2d");
|
|
||||||
assert_eq!(id.meta.name, "clear");
|
|
||||||
assert_eq!(id.meta.version, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn resolver_rejects_unknown_identity() {
|
|
||||||
let res = resolve_syscall("gfx2d", "nonexistent", 1);
|
|
||||||
assert!(res.is_none());
|
|
||||||
|
|
||||||
let requested = [SyscallIdentity { module: "gfx2d", name: "nonexistent", version: 1 }];
|
|
||||||
let err = resolve_program_syscalls(&requested, 0).unwrap_err();
|
|
||||||
match err {
|
|
||||||
LoadError::UnknownSyscall { module, name, version } => {
|
|
||||||
assert_eq!(module, "gfx2d");
|
|
||||||
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("gfx2d", "set_sprite", 1).is_none());
|
|
||||||
|
|
||||||
let requested = [SyscallIdentity { module: "gfx2d", name: "set_sprite", version: 1 }];
|
|
||||||
let err = resolve_program_syscalls(&requested, caps::ALL).unwrap_err();
|
|
||||||
assert_eq!(err, LoadError::UnknownSyscall { module: "gfx2d", name: "set_sprite", version: 1 });
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn resolver_rejects_userland_system_run_cart_identity() {
|
|
||||||
assert!(Syscall::from_u32(0x0002).is_none());
|
|
||||||
assert!(resolve_syscall("system", "run_cart", 1).is_none());
|
|
||||||
|
|
||||||
let requested = [SyscallIdentity { module: "system", name: "run_cart", version: 1 }];
|
|
||||||
let err = resolve_program_syscalls(&requested, caps::ALL).unwrap_err();
|
|
||||||
assert_eq!(err, LoadError::UnknownSyscall { module: "system", name: "run_cart", version: 1 });
|
|
||||||
|
|
||||||
let declared = [prometeu_bytecode::SyscallDecl {
|
|
||||||
module: "system".into(),
|
|
||||||
name: "run_cart".into(),
|
|
||||||
version: 1,
|
|
||||||
arg_slots: 0,
|
|
||||||
ret_slots: 0,
|
|
||||||
}];
|
|
||||||
let err = resolve_declared_program_syscalls(&declared, caps::ALL).unwrap_err();
|
|
||||||
assert_eq!(
|
|
||||||
err,
|
|
||||||
DeclaredLoadError::UnknownSyscall {
|
|
||||||
module: "system".into(),
|
|
||||||
name: "run_cart".into(),
|
|
||||||
version: 1,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn resolver_enforces_capabilities() {
|
|
||||||
let requested = [SyscallIdentity { module: "gfx2d", 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, "gfx2d");
|
|
||||||
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::GFX2D).expect("must resolve with caps");
|
|
||||||
assert_eq!(ok.len(), 1);
|
|
||||||
assert_eq!(ok[0].id, 0x1001);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn resolver_exposes_split_render_capabilities() {
|
|
||||||
let composer = resolve_syscall("composer", "bind_scene", 1).expect("composer resolves");
|
|
||||||
assert_eq!(composer.meta.caps, caps::COMPOSER);
|
|
||||||
|
|
||||||
let gfx2d = resolve_syscall("gfx2d", "clear", 1).expect("gfx2d resolves");
|
|
||||||
assert_eq!(gfx2d.meta.caps, caps::GFX2D);
|
|
||||||
|
|
||||||
let gfxui = resolve_syscall("gfxui", "clear", 1).expect("gfxui resolves");
|
|
||||||
assert_eq!(gfxui.id, 0x1201);
|
|
||||||
assert_eq!(gfxui.meta.caps, caps::GFXUI);
|
|
||||||
|
|
||||||
assert!(resolve_syscall("gfx", "clear", 1).is_none());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn declared_resolver_returns_expected_id_for_known_identity() {
|
|
||||||
let declared = [prometeu_bytecode::SyscallDecl {
|
|
||||||
module: "gfx2d".into(),
|
|
||||||
name: "clear".into(),
|
|
||||||
version: 1,
|
|
||||||
arg_slots: 1,
|
|
||||||
ret_slots: 0,
|
|
||||||
}];
|
|
||||||
|
|
||||||
let ok =
|
|
||||||
resolve_declared_program_syscalls(&declared, caps::GFX2D).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: "gfx2d".into(),
|
|
||||||
name: "nonexistent".into(),
|
|
||||||
version: 1,
|
|
||||||
arg_slots: 1,
|
|
||||||
ret_slots: 0,
|
|
||||||
}];
|
|
||||||
|
|
||||||
let err = resolve_declared_program_syscalls(&declared, caps::GFX2D).unwrap_err();
|
|
||||||
assert_eq!(
|
|
||||||
err,
|
|
||||||
DeclaredLoadError::UnknownSyscall {
|
|
||||||
module: "gfx2d".into(),
|
|
||||||
name: "nonexistent".into(),
|
|
||||||
version: 1,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn declared_resolver_rejects_missing_capability() {
|
|
||||||
let declared = [prometeu_bytecode::SyscallDecl {
|
|
||||||
module: "gfx2d".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::GFX2D,
|
|
||||||
provided: caps::NONE,
|
|
||||||
module: "gfx2d".into(),
|
|
||||||
name: "clear".into(),
|
|
||||||
version: 1,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn declared_resolver_rejects_abi_mismatch() {
|
|
||||||
let declared = [prometeu_bytecode::SyscallDecl {
|
|
||||||
module: "gfx2d".into(),
|
|
||||||
name: "draw_line".into(),
|
|
||||||
version: 1,
|
|
||||||
arg_slots: 4,
|
|
||||||
ret_slots: 0,
|
|
||||||
}];
|
|
||||||
|
|
||||||
let err = resolve_declared_program_syscalls(&declared, caps::GFX2D).unwrap_err();
|
|
||||||
assert_eq!(
|
|
||||||
err,
|
|
||||||
DeclaredLoadError::AbiMismatch {
|
|
||||||
module: "gfx2d".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 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);
|
|
||||||
|
|
||||||
let asset_backlog_info = meta_for(Syscall::AssetBacklogInfo);
|
|
||||||
assert_eq!(asset_backlog_info.arg_slots, 0);
|
|
||||||
assert_eq!(asset_backlog_info.ret_slots, 7);
|
|
||||||
|
|
||||||
let asset_backlog_position = meta_for(Syscall::AssetBacklogPosition);
|
|
||||||
assert_eq!(asset_backlog_position.arg_slots, 1);
|
|
||||||
assert_eq!(asset_backlog_position.ret_slots, 4);
|
|
||||||
|
|
||||||
let asset_backlog_move = meta_for(Syscall::AssetBacklogMove);
|
|
||||||
assert_eq!(asset_backlog_move.arg_slots, 2);
|
|
||||||
assert_eq!(asset_backlog_move.ret_slots, 1);
|
|
||||||
|
|
||||||
let asset_backlog_promote = meta_for(Syscall::AssetBacklogPromote);
|
|
||||||
assert_eq!(asset_backlog_promote.arg_slots, 1);
|
|
||||||
assert_eq!(asset_backlog_promote.ret_slots, 1);
|
|
||||||
|
|
||||||
let asset_backlog_demote = meta_for(Syscall::AssetBacklogDemote);
|
|
||||||
assert_eq!(asset_backlog_demote.arg_slots, 1);
|
|
||||||
assert_eq!(asset_backlog_demote.ret_slots, 1);
|
|
||||||
|
|
||||||
let asset_target_status = meta_for(Syscall::AssetTargetStatus);
|
|
||||||
assert_eq!(asset_target_status.arg_slots, 2);
|
|
||||||
assert_eq!(asset_target_status.ret_slots, 6);
|
|
||||||
|
|
||||||
let bank_info = meta_for(Syscall::BankInfo);
|
|
||||||
assert_eq!(bank_info.arg_slots, 1);
|
|
||||||
assert_eq!(bank_info.ret_slots, 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn resolver_rejects_removed_bank_slot_info_identity() {
|
|
||||||
assert!(resolve_syscall("bank", "slot_info", 1).is_none());
|
|
||||||
|
|
||||||
let requested = [SyscallIdentity { module: "bank", name: "slot_info", version: 1 }];
|
|
||||||
let err = resolve_program_syscalls(&requested, caps::ALL).unwrap_err();
|
|
||||||
assert_eq!(err, LoadError::UnknownSyscall { module: "bank", name: "slot_info", version: 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::COMPOSER | 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,497 +0,0 @@
|
|||||||
use crate::log::{LogLevel, LogService, LogSource};
|
|
||||||
use crate::render_worker::RenderWorkerTelemetry;
|
|
||||||
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,
|
|
||||||
pub scene_slots_used: u32,
|
|
||||||
pub scene_slots_total: u32,
|
|
||||||
|
|
||||||
// RAM (Heap)
|
|
||||||
pub heap_used_bytes: usize,
|
|
||||||
pub heap_max_bytes: usize,
|
|
||||||
pub vm_heap_allocations: u64,
|
|
||||||
pub vm_string_materializations: u64,
|
|
||||||
|
|
||||||
// Log Pressure from the last completed logical frame
|
|
||||||
pub logs_count: u32,
|
|
||||||
|
|
||||||
// Render handoff/presentation telemetry
|
|
||||||
pub produced_submissions: u64,
|
|
||||||
pub replaced_before_consume: u64,
|
|
||||||
pub consumed_submissions: u64,
|
|
||||||
pub published_frames: u64,
|
|
||||||
pub presented_frames: u64,
|
|
||||||
pub repeated_presents: u64,
|
|
||||||
pub render_errors: u64,
|
|
||||||
pub publish_errors: u64,
|
|
||||||
pub present_errors: u64,
|
|
||||||
pub stale_epoch_discards: u64,
|
|
||||||
pub shutdown_discards: u64,
|
|
||||||
pub shutdown_timeouts: u64,
|
|
||||||
pub worker_panics: u64,
|
|
||||||
pub last_produced_frame_id: u64,
|
|
||||||
pub last_consumed_frame_id: u64,
|
|
||||||
pub last_published_frame_id: u64,
|
|
||||||
pub last_presented_frame_id: u64,
|
|
||||||
pub last_dropped_frame_id: u64,
|
|
||||||
pub last_error_frame_id: u64,
|
|
||||||
pub active_render_epoch: u64,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TelemetryFrame {
|
|
||||||
pub fn render_worker_telemetry(&self) -> RenderWorkerTelemetry {
|
|
||||||
RenderWorkerTelemetry {
|
|
||||||
produced_submissions: self.produced_submissions,
|
|
||||||
replaced_before_consume: self.replaced_before_consume,
|
|
||||||
consumed_submissions: self.consumed_submissions,
|
|
||||||
published_frames: self.published_frames,
|
|
||||||
presented_frames: self.presented_frames,
|
|
||||||
repeated_frames: self.repeated_presents,
|
|
||||||
stale_submission_discards: self.stale_epoch_discards,
|
|
||||||
render_failures: self.render_errors,
|
|
||||||
publish_failures: self.publish_errors,
|
|
||||||
present_failures: self.present_errors,
|
|
||||||
shutdown_timeouts: self.shutdown_timeouts,
|
|
||||||
worker_panics: self.worker_panics,
|
|
||||||
last_produced_frame_id: self.last_produced_frame_id,
|
|
||||||
last_consumed_frame_id: self.last_consumed_frame_id,
|
|
||||||
last_published_frame_id: self.last_published_frame_id,
|
|
||||||
last_presented_frame_id: self.last_presented_frame_id,
|
|
||||||
last_dropped_frame_id: self.last_dropped_frame_id,
|
|
||||||
last_error_frame_id: self.last_error_frame_id,
|
|
||||||
active_render_epoch: self.active_render_epoch,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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,
|
|
||||||
pub scene_slots_used: AtomicU32,
|
|
||||||
pub scene_slots_total: AtomicU32,
|
|
||||||
|
|
||||||
// RAM (Heap)
|
|
||||||
pub heap_used_bytes: AtomicUsize,
|
|
||||||
pub heap_max_bytes: AtomicUsize,
|
|
||||||
pub vm_heap_allocations: AtomicU64,
|
|
||||||
pub vm_string_materializations: AtomicU64,
|
|
||||||
|
|
||||||
// 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,
|
|
||||||
|
|
||||||
// Render handoff/presentation telemetry
|
|
||||||
pub produced_submissions: AtomicU64,
|
|
||||||
pub replaced_before_consume: AtomicU64,
|
|
||||||
pub consumed_submissions: AtomicU64,
|
|
||||||
pub published_frames: AtomicU64,
|
|
||||||
pub presented_frames: AtomicU64,
|
|
||||||
pub repeated_presents: AtomicU64,
|
|
||||||
pub render_errors: AtomicU64,
|
|
||||||
pub publish_errors: AtomicU64,
|
|
||||||
pub present_errors: AtomicU64,
|
|
||||||
pub stale_epoch_discards: AtomicU64,
|
|
||||||
pub shutdown_discards: AtomicU64,
|
|
||||||
pub shutdown_timeouts: AtomicU64,
|
|
||||||
pub worker_panics: AtomicU64,
|
|
||||||
pub last_produced_frame_id: AtomicU64,
|
|
||||||
pub last_consumed_frame_id: AtomicU64,
|
|
||||||
pub last_published_frame_id: AtomicU64,
|
|
||||||
pub last_presented_frame_id: AtomicU64,
|
|
||||||
pub last_dropped_frame_id: AtomicU64,
|
|
||||||
pub last_error_frame_id: AtomicU64,
|
|
||||||
pub active_render_epoch: AtomicU64,
|
|
||||||
}
|
|
||||||
|
|
||||||
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),
|
|
||||||
scene_slots_used: self.scene_slots_used.load(Ordering::Relaxed),
|
|
||||||
scene_slots_total: self.scene_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),
|
|
||||||
vm_heap_allocations: self.vm_heap_allocations.load(Ordering::Relaxed),
|
|
||||||
vm_string_materializations: self.vm_string_materializations.load(Ordering::Relaxed),
|
|
||||||
logs_count: self.logs_count.load(Ordering::Relaxed),
|
|
||||||
vm_steps: self.vm_steps.load(Ordering::Relaxed),
|
|
||||||
produced_submissions: self.produced_submissions.load(Ordering::Relaxed),
|
|
||||||
replaced_before_consume: self.replaced_before_consume.load(Ordering::Relaxed),
|
|
||||||
consumed_submissions: self.consumed_submissions.load(Ordering::Relaxed),
|
|
||||||
published_frames: self.published_frames.load(Ordering::Relaxed),
|
|
||||||
presented_frames: self.presented_frames.load(Ordering::Relaxed),
|
|
||||||
repeated_presents: self.repeated_presents.load(Ordering::Relaxed),
|
|
||||||
render_errors: self.render_errors.load(Ordering::Relaxed),
|
|
||||||
publish_errors: self.publish_errors.load(Ordering::Relaxed),
|
|
||||||
present_errors: self.present_errors.load(Ordering::Relaxed),
|
|
||||||
stale_epoch_discards: self.stale_epoch_discards.load(Ordering::Relaxed),
|
|
||||||
shutdown_discards: self.shutdown_discards.load(Ordering::Relaxed),
|
|
||||||
shutdown_timeouts: self.shutdown_timeouts.load(Ordering::Relaxed),
|
|
||||||
worker_panics: self.worker_panics.load(Ordering::Relaxed),
|
|
||||||
last_produced_frame_id: self.last_produced_frame_id.load(Ordering::Relaxed),
|
|
||||||
last_consumed_frame_id: self.last_consumed_frame_id.load(Ordering::Relaxed),
|
|
||||||
last_published_frame_id: self.last_published_frame_id.load(Ordering::Relaxed),
|
|
||||||
last_presented_frame_id: self.last_presented_frame_id.load(Ordering::Relaxed),
|
|
||||||
last_dropped_frame_id: self.last_dropped_frame_id.load(Ordering::Relaxed),
|
|
||||||
last_error_frame_id: self.last_error_frame_id.load(Ordering::Relaxed),
|
|
||||||
active_render_epoch: self.active_render_epoch.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.scene_slots_used.store(0, Ordering::Relaxed);
|
|
||||||
self.scene_slots_total.store(0, Ordering::Relaxed);
|
|
||||||
self.heap_used_bytes.store(0, Ordering::Relaxed);
|
|
||||||
self.vm_heap_allocations.store(0, Ordering::Relaxed);
|
|
||||||
self.vm_string_materializations.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);
|
|
||||||
self.produced_submissions.store(0, Ordering::Relaxed);
|
|
||||||
self.replaced_before_consume.store(0, Ordering::Relaxed);
|
|
||||||
self.consumed_submissions.store(0, Ordering::Relaxed);
|
|
||||||
self.published_frames.store(0, Ordering::Relaxed);
|
|
||||||
self.presented_frames.store(0, Ordering::Relaxed);
|
|
||||||
self.repeated_presents.store(0, Ordering::Relaxed);
|
|
||||||
self.render_errors.store(0, Ordering::Relaxed);
|
|
||||||
self.publish_errors.store(0, Ordering::Relaxed);
|
|
||||||
self.present_errors.store(0, Ordering::Relaxed);
|
|
||||||
self.stale_epoch_discards.store(0, Ordering::Relaxed);
|
|
||||||
self.shutdown_discards.store(0, Ordering::Relaxed);
|
|
||||||
self.shutdown_timeouts.store(0, Ordering::Relaxed);
|
|
||||||
self.worker_panics.store(0, Ordering::Relaxed);
|
|
||||||
self.last_produced_frame_id.store(0, Ordering::Relaxed);
|
|
||||||
self.last_consumed_frame_id.store(0, Ordering::Relaxed);
|
|
||||||
self.last_published_frame_id.store(0, Ordering::Relaxed);
|
|
||||||
self.last_presented_frame_id.store(0, Ordering::Relaxed);
|
|
||||||
self.last_dropped_frame_id.store(0, Ordering::Relaxed);
|
|
||||||
self.last_error_frame_id.store(0, Ordering::Relaxed);
|
|
||||||
self.active_render_epoch.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 tel = TelemetryFrame {
|
|
||||||
cycles_used: 150,
|
|
||||||
syscalls: 10,
|
|
||||||
host_cpu_time_us: 500,
|
|
||||||
glyph_slots_used: 2,
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn snapshot_includes_internal_allocation_evidence() {
|
|
||||||
let current = Arc::new(AtomicU32::new(0));
|
|
||||||
let tel = AtomicTelemetry::new(current);
|
|
||||||
tel.vm_heap_allocations.store(2, Ordering::Relaxed);
|
|
||||||
tel.vm_string_materializations.store(5, Ordering::Relaxed);
|
|
||||||
|
|
||||||
let snapshot = tel.snapshot();
|
|
||||||
|
|
||||||
assert_eq!(snapshot.vm_heap_allocations, 2);
|
|
||||||
assert_eq!(snapshot.vm_string_materializations, 5);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn snapshot_maps_render_worker_telemetry() {
|
|
||||||
let current = Arc::new(AtomicU32::new(0));
|
|
||||||
let tel = AtomicTelemetry::new(current);
|
|
||||||
tel.produced_submissions.store(2, Ordering::Relaxed);
|
|
||||||
tel.replaced_before_consume.store(1, Ordering::Relaxed);
|
|
||||||
tel.consumed_submissions.store(1, Ordering::Relaxed);
|
|
||||||
tel.published_frames.store(1, Ordering::Relaxed);
|
|
||||||
tel.presented_frames.store(1, Ordering::Relaxed);
|
|
||||||
tel.repeated_presents.store(3, Ordering::Relaxed);
|
|
||||||
tel.render_errors.store(4, Ordering::Relaxed);
|
|
||||||
tel.publish_errors.store(5, Ordering::Relaxed);
|
|
||||||
tel.present_errors.store(6, Ordering::Relaxed);
|
|
||||||
tel.stale_epoch_discards.store(7, Ordering::Relaxed);
|
|
||||||
tel.shutdown_timeouts.store(8, Ordering::Relaxed);
|
|
||||||
tel.worker_panics.store(9, Ordering::Relaxed);
|
|
||||||
tel.last_produced_frame_id.store(10, Ordering::Relaxed);
|
|
||||||
tel.last_consumed_frame_id.store(11, Ordering::Relaxed);
|
|
||||||
tel.last_published_frame_id.store(12, Ordering::Relaxed);
|
|
||||||
tel.last_presented_frame_id.store(13, Ordering::Relaxed);
|
|
||||||
tel.last_dropped_frame_id.store(14, Ordering::Relaxed);
|
|
||||||
tel.last_error_frame_id.store(15, Ordering::Relaxed);
|
|
||||||
tel.active_render_epoch.store(16, Ordering::Relaxed);
|
|
||||||
|
|
||||||
let worker = tel.snapshot().render_worker_telemetry();
|
|
||||||
|
|
||||||
assert_eq!(worker.produced_submissions, 2);
|
|
||||||
assert_eq!(worker.replaced_before_consume, 1);
|
|
||||||
assert_eq!(worker.consumed_submissions, 1);
|
|
||||||
assert_eq!(worker.published_frames, 1);
|
|
||||||
assert_eq!(worker.presented_frames, 1);
|
|
||||||
assert_eq!(worker.repeated_frames, 3);
|
|
||||||
assert_eq!(worker.render_failures, 4);
|
|
||||||
assert_eq!(worker.publish_failures, 5);
|
|
||||||
assert_eq!(worker.present_failures, 6);
|
|
||||||
assert_eq!(worker.stale_submission_discards, 7);
|
|
||||||
assert_eq!(worker.shutdown_timeouts, 8);
|
|
||||||
assert_eq!(worker.worker_panics, 9);
|
|
||||||
assert_eq!(worker.last_produced_frame_id, 10);
|
|
||||||
assert_eq!(worker.last_consumed_frame_id, 11);
|
|
||||||
assert_eq!(worker.last_published_frame_id, 12);
|
|
||||||
assert_eq!(worker.last_presented_frame_id, 13);
|
|
||||||
assert_eq!(worker.last_dropped_frame_id, 14);
|
|
||||||
assert_eq!(worker.last_error_frame_id, 15);
|
|
||||||
assert_eq!(worker.active_render_epoch, 16);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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,36 +0,0 @@
|
|||||||
mod crash_report;
|
|
||||||
mod os;
|
|
||||||
mod programs;
|
|
||||||
mod services;
|
|
||||||
|
|
||||||
pub use crash_report::CrashReport;
|
|
||||||
pub use os::{
|
|
||||||
DEFAULT_GAME_PAUSE_BUDGET_TICKS, GameLifecycleEvent, GameLifecycleEventKind, LifecycleError,
|
|
||||||
LifecycleOperation, SessionError, SystemOS,
|
|
||||||
};
|
|
||||||
pub use programs::{
|
|
||||||
HUB_SUSPENDED_GAME_KILL_CONTROL, NativeShellApp, PrometeuHub, SystemProfileAction,
|
|
||||||
SystemProfileUpdate,
|
|
||||||
};
|
|
||||||
pub use prometeu_hal::{RenderWorkerBackend, RenderWorkerFrameSink};
|
|
||||||
pub use services::async_work::{
|
|
||||||
AsyncWorkActiveJob, AsyncWorkCancelToken, AsyncWorkJobContext, AsyncWorkJobId,
|
|
||||||
AsyncWorkJobKind, AsyncWorkJobOutcome, AsyncWorkLane, AsyncWorkLaneConfig,
|
|
||||||
AsyncWorkLaneController, AsyncWorkLaneError, AsyncWorkLaneTelemetry, AsyncWorkPriority,
|
|
||||||
};
|
|
||||||
pub use services::foreground::{
|
|
||||||
ForegroundOwner, ForegroundStack, ForegroundStackError, ResidentGame, ResidentGameState,
|
|
||||||
};
|
|
||||||
pub use services::fs;
|
|
||||||
pub use services::game_library::{
|
|
||||||
GameLibrary, GameLibraryDiagnostic, GameLibraryEntry, GameSwitchTarget, discover_games_root,
|
|
||||||
};
|
|
||||||
pub use services::memcard::MemcardAsyncLaneOperation;
|
|
||||||
pub use services::process;
|
|
||||||
pub use services::task;
|
|
||||||
pub use services::vm_runtime::{
|
|
||||||
LatestRenderFrameStore, RenderWorkerConfig, RenderWorkerController, RenderWorkerHandoff,
|
|
||||||
RenderWorkerHandoffWait, RenderWorkerOwnership, VirtualMachineRuntime,
|
|
||||||
};
|
|
||||||
pub use services::vm_session;
|
|
||||||
pub use services::windows;
|
|
||||||
@ -1,26 +0,0 @@
|
|||||||
use crate::fs::{FsBackend, FsState};
|
|
||||||
use crate::os::SystemOS;
|
|
||||||
|
|
||||||
pub struct FsFacade<'a> {
|
|
||||||
pub(in crate::os) os: &'a mut SystemOS,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> FsFacade<'a> {
|
|
||||||
pub fn needs_mount(&self) -> bool {
|
|
||||||
matches!(self.os.fs_state, FsState::Unmounted | FsState::Error(_))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn mount(&mut self, backend: Box<dyn FsBackend>) {
|
|
||||||
self.os.vm_runtime.mount_fs(
|
|
||||||
&mut self.os.log_service,
|
|
||||||
&mut self.os.fs,
|
|
||||||
&mut self.os.fs_state,
|
|
||||||
backend,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn unmount(&mut self) {
|
|
||||||
self.os.fs.unmount();
|
|
||||||
self.os.fs_state = FsState::Unmounted;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,299 +0,0 @@
|
|||||||
use crate::CrashReport;
|
|
||||||
use crate::os::SystemOS;
|
|
||||||
use crate::os::{
|
|
||||||
DEFAULT_GAME_PAUSE_BUDGET_TICKS, GameLifecycleEvent, GameLifecycleEventKind, LifecycleError,
|
|
||||||
LifecycleOperation,
|
|
||||||
};
|
|
||||||
use crate::process::{Process, ProcessId, ProcessKind, ProcessState};
|
|
||||||
use crate::services::foreground::{
|
|
||||||
ForegroundOwner, ForegroundStackError, ResidentGame, ResidentGameState,
|
|
||||||
};
|
|
||||||
use crate::task::{TaskId, TaskKind, TaskState};
|
|
||||||
|
|
||||||
pub struct LifecycleFacade<'a> {
|
|
||||||
pub(in crate::os) os: &'a mut SystemOS,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> LifecycleFacade<'a> {
|
|
||||||
pub fn task_state(&self, task_id: TaskId) -> Option<TaskState> {
|
|
||||||
self.os.task_manager.get(task_id).map(|task| task.state)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn process_state_for_task(&self, task_id: TaskId) -> Result<ProcessState, LifecycleError> {
|
|
||||||
process_for_task(self.os, task_id).map(|process| process.state)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn process_kind_for_task(&self, task_id: TaskId) -> Result<ProcessKind, LifecycleError> {
|
|
||||||
process_for_task(self.os, task_id).map(|process| process.kind)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_foreground_task(&mut self, task_id: TaskId) -> Result<(), LifecycleError> {
|
|
||||||
match task_kind_for_task(self.os, task_id)? {
|
|
||||||
TaskKind::Game => self.set_game_foreground_task(task_id),
|
|
||||||
TaskKind::Shell => self.set_shell_foreground_task(task_id),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_game_foreground_task(&mut self, task_id: TaskId) -> Result<(), LifecycleError> {
|
|
||||||
let process_id = process_id_for_task(self.os, task_id)?;
|
|
||||||
|
|
||||||
if task_kind_for_task(self.os, task_id)? != TaskKind::Game {
|
|
||||||
return Err(LifecycleError::InvalidTransition {
|
|
||||||
task_id,
|
|
||||||
from: self.task_state(task_id).unwrap_or(TaskState::Closed),
|
|
||||||
operation: LifecycleOperation::SetForeground,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
self.os.foreground_stack.set_game_foreground(task_id).map_err(map_foreground_error)?;
|
|
||||||
self.os.task_manager.set_foreground(task_id);
|
|
||||||
self.os.process_manager.mark_running(process_id);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_shell_foreground_task(&mut self, task_id: TaskId) -> Result<(), LifecycleError> {
|
|
||||||
let process_id = process_id_for_task(self.os, task_id)?;
|
|
||||||
|
|
||||||
if task_kind_for_task(self.os, task_id)? != TaskKind::Shell {
|
|
||||||
return Err(LifecycleError::InvalidTransition {
|
|
||||||
task_id,
|
|
||||||
from: self.task_state(task_id).unwrap_or(TaskState::Closed),
|
|
||||||
operation: LifecycleOperation::SetForeground,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(game_task_id) = self.os.foreground_stack.resident_game_task()
|
|
||||||
&& game_task_id != task_id
|
|
||||||
&& self.task_state(game_task_id) != Some(TaskState::Suspended)
|
|
||||||
{
|
|
||||||
let game_process_id = process_id_for_task(self.os, game_task_id)?;
|
|
||||||
self.os.task_manager.mark_suspended(game_task_id);
|
|
||||||
self.os.process_manager.mark_suspended(game_process_id);
|
|
||||||
}
|
|
||||||
|
|
||||||
self.os.foreground_stack.set_shell_foreground(task_id).map_err(map_foreground_error)?;
|
|
||||||
self.os.task_manager.set_foreground(task_id);
|
|
||||||
self.os.process_manager.mark_running(process_id);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn foreground_owner(&self) -> ForegroundOwner {
|
|
||||||
self.os.foreground_stack.owner()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn resident_game(&self) -> Option<ResidentGame> {
|
|
||||||
self.os.foreground_stack.resident_game()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn resident_game_task(&self) -> Option<TaskId> {
|
|
||||||
self.os.foreground_stack.resident_game_task()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn request_home_from_game(&mut self, task_id: TaskId) -> Result<(), LifecycleError> {
|
|
||||||
let _process_id = process_id_for_task(self.os, task_id)?;
|
|
||||||
let previous = self.os.foreground_stack.resident_game();
|
|
||||||
|
|
||||||
self.os
|
|
||||||
.foreground_stack
|
|
||||||
.request_home_from_game(task_id, DEFAULT_GAME_PAUSE_BUDGET_TICKS)
|
|
||||||
.map_err(map_foreground_error)?;
|
|
||||||
|
|
||||||
if !matches!(
|
|
||||||
previous.map(|game| game.state),
|
|
||||||
Some(ResidentGameState::PauseRequested { .. } | ResidentGameState::PausedSuspended)
|
|
||||||
) {
|
|
||||||
self.os
|
|
||||||
.game_lifecycle_events
|
|
||||||
.push(GameLifecycleEvent { task_id, kind: GameLifecycleEventKind::Pause });
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn advance_game_pause_budget(&mut self, task_id: TaskId) -> Result<bool, LifecycleError> {
|
|
||||||
let expired =
|
|
||||||
self.os.foreground_stack.advance_pause_budget(task_id).map_err(map_foreground_error)?;
|
|
||||||
|
|
||||||
if expired {
|
|
||||||
let process_id = process_id_for_task(self.os, task_id)?;
|
|
||||||
self.os.task_manager.mark_suspended(task_id);
|
|
||||||
self.os.process_manager.mark_suspended(process_id);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(expired)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn pending_game_lifecycle_events(&self) -> &[GameLifecycleEvent] {
|
|
||||||
&self.os.game_lifecycle_events
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn take_game_lifecycle_events(&mut self) -> Vec<GameLifecycleEvent> {
|
|
||||||
std::mem::take(&mut self.os.game_lifecycle_events)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn return_to_hub(&mut self) {
|
|
||||||
self.os.foreground_stack.return_to_hub();
|
|
||||||
self.os.task_manager.clear_foreground();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn suspend_task(&mut self, task_id: TaskId) -> Result<(), LifecycleError> {
|
|
||||||
let process_id = process_id_for_task(self.os, task_id)?;
|
|
||||||
|
|
||||||
self.os.task_manager.mark_suspended(task_id);
|
|
||||||
self.os.process_manager.mark_suspended(process_id);
|
|
||||||
match self.os.foreground_stack.owner() {
|
|
||||||
ForegroundOwner::Game(owner) if owner == task_id => {
|
|
||||||
self.os
|
|
||||||
.foreground_stack
|
|
||||||
.request_home_from_game(task_id, DEFAULT_GAME_PAUSE_BUDGET_TICKS)
|
|
||||||
.map_err(map_foreground_error)?;
|
|
||||||
self.os
|
|
||||||
.foreground_stack
|
|
||||||
.advance_pause_budget(task_id)
|
|
||||||
.map_err(map_foreground_error)?;
|
|
||||||
}
|
|
||||||
ForegroundOwner::Shell(owner) if owner == task_id => {
|
|
||||||
self.os.foreground_stack.return_to_hub();
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn resume_task(&mut self, task_id: TaskId) -> Result<(), LifecycleError> {
|
|
||||||
let task =
|
|
||||||
self.os.task_manager.get(task_id).ok_or(LifecycleError::TaskNotFound(task_id))?;
|
|
||||||
|
|
||||||
if task.state != TaskState::Suspended {
|
|
||||||
return Err(LifecycleError::InvalidTransition {
|
|
||||||
task_id,
|
|
||||||
from: task.state,
|
|
||||||
operation: LifecycleOperation::Resume,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
let process_id = process_id_for_task(self.os, task_id)?;
|
|
||||||
|
|
||||||
match task_kind_for_task(self.os, task_id)? {
|
|
||||||
TaskKind::Game => {
|
|
||||||
self.os
|
|
||||||
.foreground_stack
|
|
||||||
.request_resume_game(task_id)
|
|
||||||
.map_err(map_foreground_error)?;
|
|
||||||
self.os.task_manager.set_foreground(task_id);
|
|
||||||
self.os.process_manager.mark_running(process_id);
|
|
||||||
self.os.game_lifecycle_events.push(GameLifecycleEvent {
|
|
||||||
task_id,
|
|
||||||
kind: GameLifecycleEventKind::ResumeForeground,
|
|
||||||
});
|
|
||||||
self.os
|
|
||||||
.foreground_stack
|
|
||||||
.complete_resume_game(task_id)
|
|
||||||
.map_err(map_foreground_error)?;
|
|
||||||
}
|
|
||||||
TaskKind::Shell => {
|
|
||||||
self.os
|
|
||||||
.foreground_stack
|
|
||||||
.set_shell_foreground(task_id)
|
|
||||||
.map_err(map_foreground_error)?;
|
|
||||||
self.os.task_manager.set_foreground(task_id);
|
|
||||||
self.os.process_manager.mark_running(process_id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn close_task(&mut self, task_id: TaskId) -> Result<(), LifecycleError> {
|
|
||||||
let process_id = process_id_for_task(self.os, task_id)?;
|
|
||||||
|
|
||||||
self.os.task_manager.close_task(task_id);
|
|
||||||
self.os.process_manager.mark_stopped(process_id);
|
|
||||||
if self.os.foreground_stack.resident_game_task() == Some(task_id) {
|
|
||||||
self.os.foreground_stack.clear_resident_game(task_id);
|
|
||||||
} else if matches!(self.os.foreground_stack.owner(), ForegroundOwner::Shell(owner) if owner == task_id)
|
|
||||||
{
|
|
||||||
self.os.foreground_stack.return_to_hub();
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn terminate_resident_game(&mut self) -> Result<Option<TaskId>, LifecycleError> {
|
|
||||||
let Some(task_id) = self.os.foreground_stack.resident_game_task() else {
|
|
||||||
return Ok(None);
|
|
||||||
};
|
|
||||||
|
|
||||||
let process_id = process_id_for_task(self.os, task_id)?;
|
|
||||||
|
|
||||||
self.os.task_manager.close_task(task_id);
|
|
||||||
self.os.process_manager.mark_stopped(process_id);
|
|
||||||
self.os.foreground_stack.clear_resident_game(task_id);
|
|
||||||
self.os.vm_sessions.remove_for_task(task_id);
|
|
||||||
self.os.game_lifecycle_events.retain(|event| event.task_id != task_id);
|
|
||||||
|
|
||||||
if matches!(self.os.foreground_stack.owner(), ForegroundOwner::Game(owner) if owner == task_id)
|
|
||||||
{
|
|
||||||
self.os.foreground_stack.return_to_hub();
|
|
||||||
self.os.task_manager.clear_foreground();
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(Some(task_id))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn crash_task(
|
|
||||||
&mut self,
|
|
||||||
task_id: TaskId,
|
|
||||||
_report: Option<&CrashReport>,
|
|
||||||
) -> Result<(), LifecycleError> {
|
|
||||||
let process_id = process_id_for_task(self.os, task_id)?;
|
|
||||||
|
|
||||||
self.os.task_manager.mark_crashed(task_id);
|
|
||||||
self.os.process_manager.mark_crashed(process_id);
|
|
||||||
if self.os.foreground_stack.resident_game_task() == Some(task_id) {
|
|
||||||
self.os.foreground_stack.clear_resident_game(task_id);
|
|
||||||
} else if matches!(self.os.foreground_stack.owner(), ForegroundOwner::Shell(owner) if owner == task_id)
|
|
||||||
{
|
|
||||||
self.os.foreground_stack.return_to_hub();
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn task_kind_for_task(os: &SystemOS, task_id: TaskId) -> Result<TaskKind, LifecycleError> {
|
|
||||||
let task = os.task_manager.get(task_id).ok_or(LifecycleError::TaskNotFound(task_id))?;
|
|
||||||
Ok(task.kind)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn process_id_for_task(os: &SystemOS, task_id: TaskId) -> Result<ProcessId, LifecycleError> {
|
|
||||||
let task = os.task_manager.get(task_id).ok_or(LifecycleError::TaskNotFound(task_id))?;
|
|
||||||
|
|
||||||
if !os.process_manager.contains(task.process_id) {
|
|
||||||
return Err(LifecycleError::ProcessNotFound(task.process_id));
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(task.process_id)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn process_for_task(os: &SystemOS, task_id: TaskId) -> Result<&Process, LifecycleError> {
|
|
||||||
let process_id = process_id_for_task(os, task_id)?;
|
|
||||||
os.process_manager.get(process_id).ok_or(LifecycleError::ProcessNotFound(process_id))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn map_foreground_error(error: ForegroundStackError) -> LifecycleError {
|
|
||||||
match error {
|
|
||||||
ForegroundStackError::ResidentGameAlreadyExists { existing, requested } => {
|
|
||||||
LifecycleError::ResidentGameAlreadyExists { existing, requested }
|
|
||||||
}
|
|
||||||
ForegroundStackError::ForegroundShellAlreadyExists { existing, requested } => {
|
|
||||||
LifecycleError::ForegroundShellAlreadyExists { existing, requested }
|
|
||||||
}
|
|
||||||
ForegroundStackError::TaskIsNotResidentGame(task_id) => {
|
|
||||||
LifecycleError::TaskIsNotResidentGame(task_id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,11 +0,0 @@
|
|||||||
mod fs;
|
|
||||||
mod lifecycle;
|
|
||||||
mod sessions;
|
|
||||||
mod vm;
|
|
||||||
mod window;
|
|
||||||
|
|
||||||
pub use fs::FsFacade;
|
|
||||||
pub use lifecycle::LifecycleFacade;
|
|
||||||
pub use sessions::{SessionError, SessionsFacade};
|
|
||||||
pub use vm::VmFacade;
|
|
||||||
pub use window::WindowFacade;
|
|
||||||
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