Compare commits
No commits in common. "merge/master" and "master" have entirely different histories.
merge/mast
...
master
29
.github/workflows/ci.yml
vendored
Normal file
29
.github/workflows/ci.yml
vendored
Normal file
@ -0,0 +1,29 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main, master ]
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
components: rustfmt, clippy
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
|
||||
- name: Format check
|
||||
run: cargo fmt -- --check
|
||||
|
||||
- name: Clippy
|
||||
run: cargo clippy --workspace --all-features
|
||||
|
||||
- name: Test
|
||||
run: cargo test --workspace --all-targets --all-features --no-fail-fast
|
||||
7
.gitignore
vendored
7
.gitignore
vendored
@ -55,4 +55,9 @@ dist-staging
|
||||
dist-staging/**
|
||||
|
||||
temp
|
||||
temp/**
|
||||
temp/**
|
||||
|
||||
**/build/**
|
||||
**/node_modules/**
|
||||
|
||||
AGENTS.md
|
||||
591
Cargo.lock
generated
591
Cargo.lock
generated
@ -25,7 +25,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"getrandom",
|
||||
"getrandom 0.3.4",
|
||||
"once_cell",
|
||||
"version_check",
|
||||
"zerocopy",
|
||||
@ -86,7 +86,7 @@ dependencies = [
|
||||
"ndk-context",
|
||||
"ndk-sys 0.6.0+11769913",
|
||||
"num_enum",
|
||||
"thiserror 1.0.69",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -156,9 +156,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.100"
|
||||
version = "1.0.101"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
|
||||
checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea"
|
||||
|
||||
[[package]]
|
||||
name = "arrayref"
|
||||
@ -288,7 +288,7 @@ dependencies = [
|
||||
"polling",
|
||||
"rustix 0.38.44",
|
||||
"slab",
|
||||
"thiserror 1.0.69",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -303,15 +303,6 @@ dependencies = [
|
||||
"wayland-client",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "castaway"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a"
|
||||
dependencies = [
|
||||
"rustversion",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.2.52"
|
||||
@ -415,7 +406,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e"
|
||||
dependencies = [
|
||||
"termcolor",
|
||||
"unicode-width 0.1.14",
|
||||
"unicode-width",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -465,20 +456,6 @@ dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "compact_str"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3fdb1325a1cece981e8a296ab8f0f9b63ae357bd0784a9faaf548cc7b480707a"
|
||||
dependencies = [
|
||||
"castaway",
|
||||
"cfg-if",
|
||||
"itoa",
|
||||
"rustversion",
|
||||
"ryu",
|
||||
"static_assertions",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "concurrent-queue"
|
||||
version = "2.5.0"
|
||||
@ -548,12 +525,6 @@ dependencies = [
|
||||
"bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cow-utils"
|
||||
version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "417bef24afe1460300965a25ff4a24b8b45ad011948302ec221e8a0a81eb2c79"
|
||||
|
||||
[[package]]
|
||||
name = "cpal"
|
||||
version = "0.15.3"
|
||||
@ -633,12 +604,6 @@ version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76"
|
||||
|
||||
[[package]]
|
||||
name = "dragonbox_ecma"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6a5577f010d4e1bb3f3c4d6081e05718eb6992cf20119cab4d3abadff198b5ae"
|
||||
|
||||
[[package]]
|
||||
name = "either"
|
||||
version = "1.15.0"
|
||||
@ -661,12 +626,6 @@ dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fastrand"
|
||||
version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
|
||||
|
||||
[[package]]
|
||||
name = "find-msvc-tools"
|
||||
version = "0.1.7"
|
||||
@ -710,6 +669,17 @@ dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"wasi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.3.4"
|
||||
@ -787,7 +757,7 @@ checksum = "6f56f6318968d03c18e1bcf4857ff88c61157e9da8e47c5f29055d60e1228884"
|
||||
dependencies = [
|
||||
"log",
|
||||
"presser",
|
||||
"thiserror 1.0.69",
|
||||
"thiserror",
|
||||
"winapi",
|
||||
"windows 0.52.0",
|
||||
]
|
||||
@ -827,9 +797,6 @@ name = "hashbrown"
|
||||
version = "0.16.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
|
||||
dependencies = [
|
||||
"allocator-api2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hassle-rs"
|
||||
@ -841,7 +808,7 @@ dependencies = [
|
||||
"com",
|
||||
"libc",
|
||||
"libloading 0.8.9",
|
||||
"thiserror 1.0.69",
|
||||
"thiserror",
|
||||
"widestring",
|
||||
"winapi",
|
||||
]
|
||||
@ -906,7 +873,7 @@ dependencies = [
|
||||
"combine",
|
||||
"jni-sys",
|
||||
"log",
|
||||
"thiserror 1.0.69",
|
||||
"thiserror",
|
||||
"walkdir",
|
||||
"windows-sys 0.45.0",
|
||||
]
|
||||
@ -923,7 +890,7 @@ version = "0.1.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
"getrandom 0.3.4",
|
||||
"libc",
|
||||
]
|
||||
|
||||
@ -1088,7 +1055,7 @@ dependencies = [
|
||||
"rustc-hash 1.1.0",
|
||||
"spirv",
|
||||
"termcolor",
|
||||
"thiserror 1.0.69",
|
||||
"thiserror",
|
||||
"unicode-xid",
|
||||
]
|
||||
|
||||
@ -1103,7 +1070,7 @@ dependencies = [
|
||||
"log",
|
||||
"ndk-sys 0.5.0+25.2.9519653",
|
||||
"num_enum",
|
||||
"thiserror 1.0.69",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1118,7 +1085,7 @@ dependencies = [
|
||||
"ndk-sys 0.6.0+11769913",
|
||||
"num_enum",
|
||||
"raw-window-handle",
|
||||
"thiserror 1.0.69",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1155,22 +1122,6 @@ dependencies = [
|
||||
"minimal-lexical",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nonmax"
|
||||
version = "0.5.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "610a5acd306ec67f907abe5567859a3c693fb9886eb1f012ab8f2a47bef3db51"
|
||||
|
||||
[[package]]
|
||||
name = "num-bigint"
|
||||
version = "0.4.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9"
|
||||
dependencies = [
|
||||
"num-integer",
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-derive"
|
||||
version = "0.4.2"
|
||||
@ -1182,15 +1133,6 @@ dependencies = [
|
||||
"syn 2.0.114",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-integer"
|
||||
version = "0.1.46"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f"
|
||||
dependencies = [
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.2.19"
|
||||
@ -1498,212 +1440,6 @@ dependencies = [
|
||||
"ttf-parser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "owo-colors"
|
||||
version = "4.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c6901729fa79e91a0913333229e9ca5dc725089d1c363b2f4b4760709dc4a52"
|
||||
|
||||
[[package]]
|
||||
name = "oxc-miette"
|
||||
version = "2.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "60a7ba54c704edefead1f44e9ef09c43e5cfae666bdc33516b066011f0e6ebf7"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"owo-colors",
|
||||
"oxc-miette-derive",
|
||||
"textwrap",
|
||||
"thiserror 2.0.18",
|
||||
"unicode-segmentation",
|
||||
"unicode-width 0.2.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "oxc-miette-derive"
|
||||
version = "2.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d4faecb54d0971f948fbc1918df69b26007e6f279a204793669542e1e8b75eb3"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.114",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "oxc_allocator"
|
||||
version = "0.110.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2174c7c8f77137b1bd1c653d7a5a531ae41f3b8fec1dd0251c801689784e7a2e"
|
||||
dependencies = [
|
||||
"allocator-api2",
|
||||
"hashbrown 0.16.1",
|
||||
"oxc_data_structures",
|
||||
"rustc-hash 2.1.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "oxc_ast"
|
||||
version = "0.110.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "62f1902f97a5cac8767b76a1d8a1b3124e9db80c176ebbc98f75143dcc124a15"
|
||||
dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
"oxc_allocator",
|
||||
"oxc_ast_macros",
|
||||
"oxc_data_structures",
|
||||
"oxc_diagnostics",
|
||||
"oxc_estree",
|
||||
"oxc_regular_expression",
|
||||
"oxc_span",
|
||||
"oxc_syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "oxc_ast_macros"
|
||||
version = "0.110.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c5a31bd55516a98a35b2d99fa5813a3d3a5b798bad3262c819dfe7344bc6f390"
|
||||
dependencies = [
|
||||
"phf",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.114",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "oxc_ast_visit"
|
||||
version = "0.110.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e2c520a488c04ba5267223edd0bb245fb7f10e2358e8955802a5d962bb95b50a"
|
||||
dependencies = [
|
||||
"oxc_allocator",
|
||||
"oxc_ast",
|
||||
"oxc_span",
|
||||
"oxc_syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "oxc_data_structures"
|
||||
version = "0.110.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a42840ce8d83a08a92823dda6189e4d97359feca24a4fa732f3256c4614bb5a4"
|
||||
|
||||
[[package]]
|
||||
name = "oxc_diagnostics"
|
||||
version = "0.110.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b4f7b09c1563a67ede53af131f717b31ba89a992959ebad188b5158c21d4dc0a"
|
||||
dependencies = [
|
||||
"cow-utils",
|
||||
"oxc-miette",
|
||||
"percent-encoding",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "oxc_ecmascript"
|
||||
version = "0.110.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4813b352bd5b0b05badf0c9e6c5ec7ea58a6a7ab49bec8d18ead262624c6ef8d"
|
||||
dependencies = [
|
||||
"cow-utils",
|
||||
"num-bigint",
|
||||
"num-traits",
|
||||
"oxc_allocator",
|
||||
"oxc_ast",
|
||||
"oxc_regular_expression",
|
||||
"oxc_span",
|
||||
"oxc_syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "oxc_estree"
|
||||
version = "0.110.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e54fb3effe995e6538d68070bf0a450b5ffd11dd41b62f11a4d01efa1f40e278"
|
||||
|
||||
[[package]]
|
||||
name = "oxc_index"
|
||||
version = "4.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eb3e6120999627ec9703025eab7c9f410ebb7e95557632a8902ca48210416c2b"
|
||||
dependencies = [
|
||||
"nonmax",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "oxc_parser"
|
||||
version = "0.110.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5592bf8b64743944eb46528f9eabdde2b2435c8293cd502f5c183f9dff644e16"
|
||||
dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
"cow-utils",
|
||||
"memchr",
|
||||
"num-bigint",
|
||||
"num-traits",
|
||||
"oxc_allocator",
|
||||
"oxc_ast",
|
||||
"oxc_data_structures",
|
||||
"oxc_diagnostics",
|
||||
"oxc_ecmascript",
|
||||
"oxc_regular_expression",
|
||||
"oxc_span",
|
||||
"oxc_syntax",
|
||||
"rustc-hash 2.1.1",
|
||||
"seq-macro",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "oxc_regular_expression"
|
||||
version = "0.110.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09de7f7e0fb82f54750e3a95346a828fd354b9aeac00f131719008733e66a18d"
|
||||
dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
"oxc_allocator",
|
||||
"oxc_ast_macros",
|
||||
"oxc_diagnostics",
|
||||
"oxc_span",
|
||||
"phf",
|
||||
"rustc-hash 2.1.1",
|
||||
"unicode-id-start",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "oxc_span"
|
||||
version = "0.110.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2a42c0759b745eca0fe776890af46ce12e79e61796995e51a8eb9dcdf5516ab0"
|
||||
dependencies = [
|
||||
"compact_str",
|
||||
"oxc-miette",
|
||||
"oxc_allocator",
|
||||
"oxc_ast_macros",
|
||||
"oxc_estree",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "oxc_syntax"
|
||||
version = "0.110.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b63eac2e04a75a10c5714aeb753cdfa06b1abc66bbaa748b7994700f52c9b184"
|
||||
dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
"cow-utils",
|
||||
"dragonbox_ecma",
|
||||
"nonmax",
|
||||
"oxc_allocator",
|
||||
"oxc_ast_macros",
|
||||
"oxc_data_structures",
|
||||
"oxc_estree",
|
||||
"oxc_index",
|
||||
"oxc_span",
|
||||
"phf",
|
||||
"unicode-id-start",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot"
|
||||
version = "0.12.5"
|
||||
@ -1733,55 +1469,22 @@ version = "1.0.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
|
||||
|
||||
[[package]]
|
||||
name = "pbxgen-stress"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"prometeu-bytecode",
|
||||
"prometeu-hal",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "percent-encoding"
|
||||
version = "2.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
|
||||
|
||||
[[package]]
|
||||
name = "phf"
|
||||
version = "0.13.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf"
|
||||
dependencies = [
|
||||
"phf_macros",
|
||||
"phf_shared",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phf_generator"
|
||||
version = "0.13.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737"
|
||||
dependencies = [
|
||||
"fastrand",
|
||||
"phf_shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phf_macros"
|
||||
version = "0.13.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef"
|
||||
dependencies = [
|
||||
"phf_generator",
|
||||
"phf_shared",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.114",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phf_shared"
|
||||
version = "0.13.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266"
|
||||
dependencies = [
|
||||
"siphasher",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pin-project"
|
||||
version = "1.1.10"
|
||||
@ -1817,7 +1520,7 @@ dependencies = [
|
||||
"bytemuck",
|
||||
"pollster",
|
||||
"raw-window-handle",
|
||||
"thiserror 1.0.69",
|
||||
"thiserror",
|
||||
"ultraviolet",
|
||||
"wgpu",
|
||||
]
|
||||
@ -1863,6 +1566,15 @@ dependencies = [
|
||||
"portable-atomic",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ppv-lite86"
|
||||
version = "0.2.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
|
||||
dependencies = [
|
||||
"zerocopy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "presser"
|
||||
version = "0.3.1"
|
||||
@ -1893,39 +1605,42 @@ version = "1.0.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3eb8486b569e12e2c32ad3e204dbaba5e4b5b216e9367044f25f1dba42341773"
|
||||
|
||||
[[package]]
|
||||
name = "prometeu"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
"prometeu-compiler",
|
||||
"prometeu-runtime-desktop",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "prometeu-bytecode"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "prometeu-compiler"
|
||||
name = "prometeu-cli"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
"oxc_allocator",
|
||||
"oxc_ast",
|
||||
"oxc_ast_visit",
|
||||
"oxc_parser",
|
||||
"oxc_span",
|
||||
"prometeu-bytecode",
|
||||
"prometeu-core",
|
||||
"serde",
|
||||
"prometeu-host-desktop-winit",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "prometeu-drivers"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"prometeu-hal",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "prometeu-core"
|
||||
name = "prometeu-firmware"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"prometeu-bytecode",
|
||||
"prometeu-drivers",
|
||||
"prometeu-hal",
|
||||
"prometeu-system",
|
||||
"prometeu-vm",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "prometeu-hal"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"prometeu-bytecode",
|
||||
@ -1934,17 +1649,62 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "prometeu-runtime-desktop"
|
||||
name = "prometeu-host-desktop-winit"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"clap",
|
||||
"cpal",
|
||||
"pixels",
|
||||
"prometeu-core",
|
||||
"prometeu-drivers",
|
||||
"prometeu-firmware",
|
||||
"prometeu-hal",
|
||||
"prometeu-system",
|
||||
"ringbuf",
|
||||
"serde_json",
|
||||
"winit",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "prometeu-layer-tests"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"prometeu-bytecode",
|
||||
"prometeu-hal",
|
||||
"prometeu-test-support",
|
||||
"prometeu-vm",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "prometeu-quality-checks"
|
||||
version = "0.1.0"
|
||||
|
||||
[[package]]
|
||||
name = "prometeu-system"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"prometeu-bytecode",
|
||||
"prometeu-drivers",
|
||||
"prometeu-hal",
|
||||
"prometeu-vm",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "prometeu-test-support"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"rand",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "prometeu-vm"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"prometeu-bytecode",
|
||||
"prometeu-hal",
|
||||
"prometeu-test-support",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quick-xml"
|
||||
version = "0.38.4"
|
||||
@ -1969,6 +1729,36 @@ version = "5.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.8.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"rand_chacha",
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_chacha"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
|
||||
dependencies = [
|
||||
"ppv-lite86",
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.6.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
|
||||
dependencies = [
|
||||
"getrandom 0.2.17",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "range-alloc"
|
||||
version = "0.1.4"
|
||||
@ -2098,12 +1888,6 @@ version = "1.0.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
version = "1.0.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984"
|
||||
|
||||
[[package]]
|
||||
name = "safe_arch"
|
||||
version = "0.7.4"
|
||||
@ -2147,12 +1931,6 @@ dependencies = [
|
||||
"tiny-skia",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "seq-macro"
|
||||
version = "0.3.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1bc711410fbe7399f390ca1c3b60ad0f53f80e95c5eb935e52268a0e2cd49acc"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.228"
|
||||
@ -2202,12 +1980,6 @@ version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
||||
|
||||
[[package]]
|
||||
name = "siphasher"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d"
|
||||
|
||||
[[package]]
|
||||
name = "slab"
|
||||
version = "0.4.11"
|
||||
@ -2229,12 +2001,6 @@ version = "1.15.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
|
||||
|
||||
[[package]]
|
||||
name = "smawk"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c"
|
||||
|
||||
[[package]]
|
||||
name = "smithay-client-toolkit"
|
||||
version = "0.19.2"
|
||||
@ -2249,7 +2015,7 @@ dependencies = [
|
||||
"log",
|
||||
"memmap2",
|
||||
"rustix 0.38.44",
|
||||
"thiserror 1.0.69",
|
||||
"thiserror",
|
||||
"wayland-backend",
|
||||
"wayland-client",
|
||||
"wayland-csd-frame",
|
||||
@ -2327,33 +2093,13 @@ dependencies = [
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "textwrap"
|
||||
version = "0.16.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057"
|
||||
dependencies = [
|
||||
"smawk",
|
||||
"unicode-linebreak",
|
||||
"unicode-width 0.2.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.69"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
|
||||
dependencies = [
|
||||
"thiserror-impl 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",
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -2367,17 +2113,6 @@ dependencies = [
|
||||
"syn 2.0.114",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "2.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.114",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tiny-skia"
|
||||
version = "0.11.4"
|
||||
@ -2426,9 +2161,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "toml_parser"
|
||||
version = "1.0.6+spec-1.1.0"
|
||||
version = "1.0.8+spec-1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a3198b4b0a8e11f09dd03e133c0280504d0801269e9afa46362ffde1cbeebf44"
|
||||
checksum = "0742ff5ff03ea7e67c8ae6c93cac239e0d9784833362da3f9a9c1da8dfefcbdc"
|
||||
dependencies = [
|
||||
"winnow",
|
||||
]
|
||||
@ -2464,24 +2199,12 @@ dependencies = [
|
||||
"wide",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-id-start"
|
||||
version = "1.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "81b79ad29b5e19de4260020f8919b443b2ef0277d242ce532ec7b7a2cc8b6007"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-linebreak"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-segmentation"
|
||||
version = "1.12.0"
|
||||
@ -2494,12 +2217,6 @@ version = "0.1.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-width"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-xid"
|
||||
version = "0.2.6"
|
||||
@ -2528,6 +2245,12 @@ dependencies = [
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.11.1+wasi-snapshot-preview1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
|
||||
|
||||
[[package]]
|
||||
name = "wasip2"
|
||||
version = "1.0.1+wasi-0.2.4"
|
||||
@ -2768,7 +2491,7 @@ dependencies = [
|
||||
"raw-window-handle",
|
||||
"rustc-hash 1.1.0",
|
||||
"smallvec",
|
||||
"thiserror 1.0.69",
|
||||
"thiserror",
|
||||
"web-sys",
|
||||
"wgpu-hal",
|
||||
"wgpu-types",
|
||||
@ -2812,7 +2535,7 @@ dependencies = [
|
||||
"renderdoc-sys",
|
||||
"rustc-hash 1.1.0",
|
||||
"smallvec",
|
||||
"thiserror 1.0.69",
|
||||
"thiserror",
|
||||
"wasm-bindgen",
|
||||
"web-sys",
|
||||
"wgpu-types",
|
||||
|
||||
20
Cargo.toml
20
Cargo.toml
@ -1,10 +1,20 @@
|
||||
[workspace]
|
||||
members = [
|
||||
"crates/prometeu-core",
|
||||
"crates/prometeu-runtime-desktop",
|
||||
"crates/prometeu",
|
||||
"crates/prometeu-bytecode",
|
||||
"crates/prometeu-compiler",
|
||||
"crates/console/prometeu-bytecode",
|
||||
|
||||
"crates/console/prometeu-drivers",
|
||||
"crates/console/prometeu-firmware",
|
||||
"crates/console/prometeu-hal",
|
||||
"crates/console/prometeu-system",
|
||||
"crates/console/prometeu-vm",
|
||||
|
||||
"crates/host/prometeu-host-desktop-winit",
|
||||
|
||||
"crates/tools/prometeu-cli",
|
||||
"crates/tools/pbxgen-stress",
|
||||
"crates/dev/prometeu-test-support",
|
||||
"crates/dev/prometeu-layer-tests",
|
||||
"crates/dev/prometeu-quality-checks",
|
||||
]
|
||||
resolver = "2"
|
||||
|
||||
|
||||
32
Makefile
Normal file
32
Makefile
Normal file
@ -0,0 +1,32 @@
|
||||
.PHONY: fmt fmt-check clippy tes-local test-debugger-socket test ci cobertura
|
||||
|
||||
fmt:
|
||||
cargo fmt
|
||||
|
||||
fmt-check:
|
||||
cargo fmt -- --check
|
||||
|
||||
clippy:
|
||||
cargo clippy --workspace --all-features
|
||||
|
||||
test-local:
|
||||
cargo test --workspace --all-targets --all-features --no-fail-fast
|
||||
|
||||
test-debugger-socket:
|
||||
cargo test -p prometeu-host-desktop-winit --lib -- --ignored
|
||||
|
||||
clean:
|
||||
cargo llvm-cov clean --workspace
|
||||
|
||||
coverage:
|
||||
cargo llvm-cov --workspace --all-features --html --output-dir target/llvm-cov
|
||||
|
||||
coverage-xml:
|
||||
cargo llvm-cov report --cobertura --output-path target/llvm-cov/cobertura.xml
|
||||
|
||||
coverage-json:
|
||||
cargo llvm-cov report --json --summary-only --output-path target/llvm-cov/summary.json
|
||||
|
||||
test: fmt-check clippy test-local test-debugger-socket
|
||||
ci: clean fmt-check clippy coverage
|
||||
cobertura: coverage-xml coverage-json
|
||||
27
README.md
27
README.md
@ -1,8 +1,8 @@
|
||||
# PROMETEU
|
||||
|
||||
PROMETEU is an **educational and experimental ecosystem** inspired by classic consoles, focusing on **teaching programming, system architecture, and hardware concepts through software**.
|
||||
PROMETEU is an **educational and experimental fantasy handheld / fantasy console ecosystem** inspired by classic consoles, focusing on **teaching programming, system architecture, and hardware concepts through software**.
|
||||
|
||||
> PROMETEU is a simple, explicit, and educational virtual machine.
|
||||
> PROMETEU is a fantasy console with a simple, explicit, and educational VM/runtime inside it.
|
||||
|
||||
---
|
||||
|
||||
@ -12,6 +12,7 @@ PROMETEU is an **educational and experimental ecosystem** inspired by classic co
|
||||
- **Deterministic Loop**: Ensure the same code produces the same result on any platform.
|
||||
- **Total Portability**: The core does not depend on an operating system, allowing it to run from modern computers to dedicated hardware.
|
||||
- **First-Class Tools**: Offer deep debugging and inspection as a central part of the experience.
|
||||
- **DIY hardware affinity**: Keep the machine model close enough to handheld/console-era hardware that it can inform real embedded and homebrew-style implementations.
|
||||
|
||||
---
|
||||
|
||||
@ -22,6 +23,17 @@ PROMETEU is an **educational and experimental ecosystem** inspired by classic co
|
||||
- **Deterministic**: same input → same result.
|
||||
- **Hardware-first**: APIs model peripherals, not modern frameworks.
|
||||
- **Portable by definition**: if it doesn't work on all platforms, it doesn't exist.
|
||||
- **Console-era sensibility**: PROMETEU carries intentional influence from NES, SNES, Mega Drive, Game Boy, GBA, CPS-2, and adjacent DIY-friendly hardware thinking.
|
||||
|
||||
## 🧭 Canonical Architecture
|
||||
|
||||
PROMETEU is the machine. The VM/runtime is one subsystem inside that machine.
|
||||
|
||||
Authoritative documents:
|
||||
|
||||
- [`docs/runtime/virtual-machine/ARCHITECTURE.md`](docs/vm-arch/ARCHITECTURE.md) is normative for VM/runtime invariants.
|
||||
- [`docs/runtime/specs/README.md`](docs/specs/runtime/README.md) describes the broader PROMETEU machine, hardware model, and fantasy console context.
|
||||
- Supporting material under `docs/` may expand, explain, or propose changes, but it must not silently collapse the whole machine into the VM layer.
|
||||
|
||||
---
|
||||
|
||||
@ -30,9 +42,9 @@ PROMETEU is an **educational and experimental ecosystem** inspired by classic co
|
||||
This repository is organized as a Rust workspace and contains several components:
|
||||
|
||||
- **[crates/](./crates)**: Software implementation in Rust.
|
||||
- **[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.
|
||||
- **[prometeu](crates/tools/prometeu)**: Unified command-line interface (CLI).
|
||||
- **[prometeu-drivers](crates/console/prometeu-drivers)**: The virtual hardware (GPU, SPU, Input).
|
||||
- **[prometeu-host-desktop-winit](crates/host/prometeu-host-desktop-winit)**: Host for execution on Desktop systems.
|
||||
- **[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.
|
||||
@ -57,10 +69,10 @@ cargo build
|
||||
To run an example cartridge:
|
||||
|
||||
```bash
|
||||
./target/debug/prometeu run test-cartridges/color-square
|
||||
./target/debug/prometeu run test-cartridges/color-square-ts
|
||||
```
|
||||
|
||||
For more details on how to use the CLI, see the **[prometeu](./crates/prometeu)** README.
|
||||
For more details on how to use the CLI, see the **[prometeu](crates/tools/prometeu)** README.
|
||||
|
||||
---
|
||||
|
||||
@ -81,4 +93,3 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file
|
||||
## ✨ Final Note
|
||||
|
||||
PROMETEU is both a technical and pedagogical project. The idea is not to hide complexity, but to **expose the right complexity**, at the right level, so it can be understood, studied, and explored.
|
||||
|
||||
|
||||
@ -1 +1 @@
|
||||
0.1.0
|
||||
|
||||
|
||||
@ -1,17 +0,0 @@
|
||||
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
|
||||
Binary file not shown.
@ -1,74 +0,0 @@
|
||||
[
|
||||
{
|
||||
"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
|
||||
}
|
||||
]
|
||||
@ -6,4 +6,4 @@ license.workspace = true
|
||||
repository.workspace = true
|
||||
|
||||
[dependencies]
|
||||
# No dependencies for now
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
50
crates/console/prometeu-bytecode/src/abi.rs
Normal file
50
crates/console/prometeu-bytecode/src/abi.rs
Normal file
@ -0,0 +1,50 @@
|
||||
//! 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>,
|
||||
}
|
||||
367
crates/console/prometeu-bytecode/src/assembler.rs
Normal file
367
crates/console/prometeu-bytecode/src/assembler.rs
Normal file
@ -0,0 +1,367 @@
|
||||
//! 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)
|
||||
}
|
||||
133
crates/console/prometeu-bytecode/src/decoder.rs
Normal file
133
crates/console/prometeu-bytecode/src/decoder.rs
Normal file
@ -0,0 +1,133 @@
|
||||
//! 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),
|
||||
}
|
||||
}
|
||||
}
|
||||
120
crates/console/prometeu-bytecode/src/disassembler.rs
Normal file
120
crates/console/prometeu-bytecode/src/disassembler.rs
Normal file
@ -0,0 +1,120 @@
|
||||
//! 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"))
|
||||
}
|
||||
12
crates/console/prometeu-bytecode/src/isa/core.rs
Normal file
12
crates/console/prometeu-bytecode/src/isa/core.rs
Normal file
@ -0,0 +1,12 @@
|
||||
//! 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};
|
||||
6
crates/console/prometeu-bytecode/src/isa/mod.rs
Normal file
6
crates/console/prometeu-bytecode/src/isa/mod.rs
Normal file
@ -0,0 +1,6 @@
|
||||
//! 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;
|
||||
98
crates/console/prometeu-bytecode/src/layout.rs
Normal file
98
crates/console/prometeu-bytecode/src/layout.rs
Normal file
@ -0,0 +1,98 @@
|
||||
//! 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)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
24
crates/console/prometeu-bytecode/src/lib.rs
Normal file
24
crates/console/prometeu-bytecode/src/lib.rs
Normal file
@ -0,0 +1,24 @@
|
||||
mod abi;
|
||||
pub mod assembler;
|
||||
mod decoder;
|
||||
mod disassembler;
|
||||
pub mod isa; // canonical ISA boundary (core and future profiles)
|
||||
mod layout;
|
||||
pub mod model;
|
||||
mod opcode;
|
||||
mod opcode_spec;
|
||||
mod program_image;
|
||||
mod value;
|
||||
|
||||
pub use abi::{
|
||||
TrapInfo, TRAP_BAD_RET_SLOTS, TRAP_DIV_ZERO, TRAP_EXPLICIT, TRAP_ILLEGAL_INSTRUCTION,
|
||||
TRAP_INVALID_FUNC, TRAP_INVALID_INTRINSIC, TRAP_INVALID_LOCAL, TRAP_INVALID_SYSCALL, TRAP_OOB,
|
||||
TRAP_STACK_UNDERFLOW, TRAP_TYPE,
|
||||
};
|
||||
pub use assembler::{assemble, AsmError};
|
||||
pub use decoder::{decode_next, DecodeError};
|
||||
pub use disassembler::disassemble;
|
||||
pub use layout::{compute_function_layouts, FunctionLayout};
|
||||
pub use model::{BytecodeLoader, FunctionMeta, LoadError, SyscallDecl};
|
||||
pub use program_image::ProgramImage;
|
||||
pub use value::{HeapRef, Value};
|
||||
946
crates/console/prometeu-bytecode/src/model.rs
Normal file
946
crates/console/prometeu-bytecode/src/model.rs
Normal file
@ -0,0 +1,946 @@
|
||||
use crate::abi::SourceSpan;
|
||||
use crate::opcode::OpCode;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashSet;
|
||||
|
||||
/// An entry in the Constant Pool.
|
||||
///
|
||||
/// The Constant Pool is a table of unique values used by the program.
|
||||
/// Instead of embedding large data (like strings) directly in the instruction stream,
|
||||
/// the bytecode uses `PushConst <index>` to load these values onto the stack.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub enum ConstantPoolEntry {
|
||||
/// Reserved index (0). Represents a null/undefined value.
|
||||
Null,
|
||||
/// A 64-bit integer constant.
|
||||
Int64(i64),
|
||||
/// A 64-bit floating point constant.
|
||||
Float64(f64),
|
||||
/// A boolean constant.
|
||||
Boolean(bool),
|
||||
/// A UTF-8 string constant.
|
||||
String(String),
|
||||
/// A 32-bit integer constant.
|
||||
Int32(i32),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum LoadError {
|
||||
InvalidMagic,
|
||||
InvalidVersion,
|
||||
InvalidEndianness,
|
||||
OverlappingSections,
|
||||
SectionOutOfBounds,
|
||||
InvalidOpcode,
|
||||
InvalidConstIndex,
|
||||
InvalidFunctionIndex,
|
||||
MalformedHeader,
|
||||
MalformedSection,
|
||||
MissingSyscallSection,
|
||||
DuplicateSyscallIdentity,
|
||||
InvalidUtf8,
|
||||
UnexpectedEof,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
|
||||
pub struct FunctionMeta {
|
||||
pub code_offset: u32,
|
||||
pub code_len: u32,
|
||||
pub param_slots: u16,
|
||||
pub local_slots: u16,
|
||||
pub return_slots: u16,
|
||||
pub max_stack_slots: u16,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct DebugInfo {
|
||||
pub pc_to_span: Vec<(u32, SourceSpan)>, // Sorted by PC
|
||||
pub function_names: Vec<(u32, String)>, // (func_idx, name)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Export {
|
||||
pub symbol: String,
|
||||
pub func_idx: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct SyscallDecl {
|
||||
pub module: String,
|
||||
pub name: String,
|
||||
pub version: u16,
|
||||
pub arg_slots: u16,
|
||||
pub ret_slots: u16,
|
||||
}
|
||||
|
||||
const SECTION_KIND_CONST_POOL: u32 = 0;
|
||||
const SECTION_KIND_FUNCTIONS: u32 = 1;
|
||||
const SECTION_KIND_CODE: u32 = 2;
|
||||
const SECTION_KIND_DEBUG: u32 = 3;
|
||||
const SECTION_KIND_EXPORTS: u32 = 4;
|
||||
const SECTION_KIND_SYSCALLS: u32 = 5;
|
||||
|
||||
/// Represents the final serialized format of a PBS v0 module.
|
||||
///
|
||||
/// This structure is a pure data container for the PBS format. It does NOT
|
||||
/// contain any linker-like logic (symbol resolution, patching, etc.).
|
||||
/// All multi-module programs must be flattened and linked by the compiler
|
||||
/// before being serialized into this format.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct BytecodeModule {
|
||||
pub version: u16,
|
||||
pub const_pool: Vec<ConstantPoolEntry>,
|
||||
pub functions: Vec<FunctionMeta>,
|
||||
pub code: Vec<u8>,
|
||||
pub debug_info: Option<DebugInfo>,
|
||||
pub exports: Vec<Export>,
|
||||
pub syscalls: Vec<SyscallDecl>,
|
||||
}
|
||||
|
||||
impl BytecodeModule {
|
||||
pub fn serialize(&self) -> Vec<u8> {
|
||||
let cp_data = self.serialize_const_pool();
|
||||
let func_data = self.serialize_functions();
|
||||
let code_data = self.code.clone();
|
||||
let debug_data =
|
||||
self.debug_info.as_ref().map(|di| self.serialize_debug(di)).unwrap_or_default();
|
||||
let export_data = self.serialize_exports();
|
||||
let syscall_data = self.serialize_syscalls();
|
||||
|
||||
let mut final_sections = Vec::new();
|
||||
if !cp_data.is_empty() {
|
||||
final_sections.push((SECTION_KIND_CONST_POOL, cp_data));
|
||||
}
|
||||
if !func_data.is_empty() {
|
||||
final_sections.push((SECTION_KIND_FUNCTIONS, func_data));
|
||||
}
|
||||
if !code_data.is_empty() {
|
||||
final_sections.push((SECTION_KIND_CODE, code_data));
|
||||
}
|
||||
if !debug_data.is_empty() {
|
||||
final_sections.push((SECTION_KIND_DEBUG, debug_data));
|
||||
}
|
||||
if !export_data.is_empty() {
|
||||
final_sections.push((SECTION_KIND_EXPORTS, export_data));
|
||||
}
|
||||
final_sections.push((SECTION_KIND_SYSCALLS, syscall_data));
|
||||
|
||||
let mut out = Vec::new();
|
||||
// Magic "PBS\0"
|
||||
out.extend_from_slice(b"PBS\0");
|
||||
// Version 0
|
||||
out.extend_from_slice(&0u16.to_le_bytes());
|
||||
// Endianness 0 (Little Endian), Reserved
|
||||
out.extend_from_slice(&[0u8, 0u8]);
|
||||
// section_count
|
||||
out.extend_from_slice(&(final_sections.len() as u32).to_le_bytes());
|
||||
// padding to 32 bytes
|
||||
out.extend_from_slice(&[0u8; 20]);
|
||||
|
||||
let mut current_offset = 32 + (final_sections.len() as u32 * 12);
|
||||
|
||||
// Write section table
|
||||
for (kind, data) in &final_sections {
|
||||
let k: u32 = *kind;
|
||||
out.extend_from_slice(&k.to_le_bytes());
|
||||
out.extend_from_slice(¤t_offset.to_le_bytes());
|
||||
out.extend_from_slice(&(data.len() as u32).to_le_bytes());
|
||||
current_offset += data.len() as u32;
|
||||
}
|
||||
|
||||
// Write section data
|
||||
for (_, data) in final_sections {
|
||||
out.extend_from_slice(&data);
|
||||
}
|
||||
|
||||
out
|
||||
}
|
||||
|
||||
fn serialize_const_pool(&self) -> Vec<u8> {
|
||||
if self.const_pool.is_empty() {
|
||||
return Vec::new();
|
||||
}
|
||||
let mut data = Vec::new();
|
||||
data.extend_from_slice(&(self.const_pool.len() as u32).to_le_bytes());
|
||||
for entry in &self.const_pool {
|
||||
match entry {
|
||||
ConstantPoolEntry::Null => data.push(0),
|
||||
ConstantPoolEntry::Int64(v) => {
|
||||
data.push(1);
|
||||
data.extend_from_slice(&v.to_le_bytes());
|
||||
}
|
||||
ConstantPoolEntry::Float64(v) => {
|
||||
data.push(2);
|
||||
data.extend_from_slice(&v.to_le_bytes());
|
||||
}
|
||||
ConstantPoolEntry::Boolean(v) => {
|
||||
data.push(3);
|
||||
data.push(if *v { 1 } else { 0 });
|
||||
}
|
||||
ConstantPoolEntry::String(v) => {
|
||||
data.push(4);
|
||||
let s_bytes = v.as_bytes();
|
||||
data.extend_from_slice(&(s_bytes.len() as u32).to_le_bytes());
|
||||
data.extend_from_slice(s_bytes);
|
||||
}
|
||||
ConstantPoolEntry::Int32(v) => {
|
||||
data.push(5);
|
||||
data.extend_from_slice(&v.to_le_bytes());
|
||||
}
|
||||
}
|
||||
}
|
||||
data
|
||||
}
|
||||
|
||||
fn serialize_functions(&self) -> Vec<u8> {
|
||||
if self.functions.is_empty() {
|
||||
return Vec::new();
|
||||
}
|
||||
let mut data = Vec::new();
|
||||
data.extend_from_slice(&(self.functions.len() as u32).to_le_bytes());
|
||||
for f in &self.functions {
|
||||
data.extend_from_slice(&f.code_offset.to_le_bytes());
|
||||
data.extend_from_slice(&f.code_len.to_le_bytes());
|
||||
data.extend_from_slice(&f.param_slots.to_le_bytes());
|
||||
data.extend_from_slice(&f.local_slots.to_le_bytes());
|
||||
data.extend_from_slice(&f.return_slots.to_le_bytes());
|
||||
data.extend_from_slice(&f.max_stack_slots.to_le_bytes());
|
||||
}
|
||||
data
|
||||
}
|
||||
|
||||
fn serialize_debug(&self, di: &DebugInfo) -> Vec<u8> {
|
||||
let mut data = Vec::new();
|
||||
data.extend_from_slice(&(di.pc_to_span.len() as u32).to_le_bytes());
|
||||
for (pc, span) in &di.pc_to_span {
|
||||
data.extend_from_slice(&pc.to_le_bytes());
|
||||
data.extend_from_slice(&span.file_id.to_le_bytes());
|
||||
data.extend_from_slice(&span.start.to_le_bytes());
|
||||
data.extend_from_slice(&span.end.to_le_bytes());
|
||||
}
|
||||
data.extend_from_slice(&(di.function_names.len() as u32).to_le_bytes());
|
||||
for (idx, name) in &di.function_names {
|
||||
data.extend_from_slice(&idx.to_le_bytes());
|
||||
let n_bytes = name.as_bytes();
|
||||
data.extend_from_slice(&(n_bytes.len() as u32).to_le_bytes());
|
||||
data.extend_from_slice(n_bytes);
|
||||
}
|
||||
data
|
||||
}
|
||||
|
||||
fn serialize_exports(&self) -> Vec<u8> {
|
||||
if self.exports.is_empty() {
|
||||
return Vec::new();
|
||||
}
|
||||
let mut data = Vec::new();
|
||||
data.extend_from_slice(&(self.exports.len() as u32).to_le_bytes());
|
||||
for exp in &self.exports {
|
||||
data.extend_from_slice(&exp.func_idx.to_le_bytes());
|
||||
let s_bytes = exp.symbol.as_bytes();
|
||||
data.extend_from_slice(&(s_bytes.len() as u32).to_le_bytes());
|
||||
data.extend_from_slice(s_bytes);
|
||||
}
|
||||
data
|
||||
}
|
||||
|
||||
fn serialize_syscalls(&self) -> Vec<u8> {
|
||||
let mut data = Vec::new();
|
||||
data.extend_from_slice(&(self.syscalls.len() as u32).to_le_bytes());
|
||||
for syscall in &self.syscalls {
|
||||
let module = syscall.module.as_bytes();
|
||||
let name = syscall.name.as_bytes();
|
||||
assert!(u16::try_from(module.len()).is_ok(), "SYSC module name exceeds u16 length");
|
||||
assert!(u16::try_from(name.len()).is_ok(), "SYSC syscall name exceeds u16 length");
|
||||
|
||||
data.extend_from_slice(&(module.len() as u16).to_le_bytes());
|
||||
data.extend_from_slice(module);
|
||||
data.extend_from_slice(&(name.len() as u16).to_le_bytes());
|
||||
data.extend_from_slice(name);
|
||||
data.extend_from_slice(&syscall.version.to_le_bytes());
|
||||
data.extend_from_slice(&syscall.arg_slots.to_le_bytes());
|
||||
data.extend_from_slice(&syscall.ret_slots.to_le_bytes());
|
||||
}
|
||||
data
|
||||
}
|
||||
}
|
||||
|
||||
pub struct BytecodeLoader;
|
||||
|
||||
impl BytecodeLoader {
|
||||
pub fn load(bytes: &[u8]) -> Result<BytecodeModule, LoadError> {
|
||||
if bytes.len() < 32 {
|
||||
return Err(LoadError::UnexpectedEof);
|
||||
}
|
||||
|
||||
// Magic "PBS\0"
|
||||
if &bytes[0..4] != b"PBS\0" {
|
||||
return Err(LoadError::InvalidMagic);
|
||||
}
|
||||
|
||||
let version = u16::from_le_bytes([bytes[4], bytes[5]]);
|
||||
if version != 0 {
|
||||
return Err(LoadError::InvalidVersion);
|
||||
}
|
||||
|
||||
let endianness = bytes[6];
|
||||
if endianness != 0 {
|
||||
// 0 = Little Endian
|
||||
return Err(LoadError::InvalidEndianness);
|
||||
}
|
||||
|
||||
let section_count = u32::from_le_bytes([bytes[8], bytes[9], bytes[10], bytes[11]]);
|
||||
|
||||
let mut sections = Vec::new();
|
||||
let mut pos = 32;
|
||||
for _ in 0..section_count {
|
||||
if pos + 12 > bytes.len() {
|
||||
return Err(LoadError::UnexpectedEof);
|
||||
}
|
||||
let kind =
|
||||
u32::from_le_bytes([bytes[pos], bytes[pos + 1], bytes[pos + 2], bytes[pos + 3]]);
|
||||
let offset = u32::from_le_bytes([
|
||||
bytes[pos + 4],
|
||||
bytes[pos + 5],
|
||||
bytes[pos + 6],
|
||||
bytes[pos + 7],
|
||||
]);
|
||||
let length = u32::from_le_bytes([
|
||||
bytes[pos + 8],
|
||||
bytes[pos + 9],
|
||||
bytes[pos + 10],
|
||||
bytes[pos + 11],
|
||||
]);
|
||||
|
||||
// Basic bounds check
|
||||
if (offset as usize) + (length as usize) > bytes.len() {
|
||||
return Err(LoadError::SectionOutOfBounds);
|
||||
}
|
||||
|
||||
sections.push((kind, offset, length));
|
||||
pos += 12;
|
||||
}
|
||||
|
||||
// Check for overlapping sections
|
||||
for i in 0..sections.len() {
|
||||
for j in i + 1..sections.len() {
|
||||
let (_, o1, l1) = sections[i];
|
||||
let (_, o2, l2) = sections[j];
|
||||
|
||||
if (o1 < o2 + l2) && (o2 < o1 + l1) {
|
||||
return Err(LoadError::OverlappingSections);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut module = BytecodeModule {
|
||||
version,
|
||||
const_pool: Vec::new(),
|
||||
functions: Vec::new(),
|
||||
code: Vec::new(),
|
||||
debug_info: None,
|
||||
exports: Vec::new(),
|
||||
syscalls: Vec::new(),
|
||||
};
|
||||
let mut has_syscalls = false;
|
||||
|
||||
for (kind, offset, length) in sections {
|
||||
let section_data = &bytes[offset as usize..(offset + length) as usize];
|
||||
match kind {
|
||||
SECTION_KIND_CONST_POOL => {
|
||||
// Const Pool
|
||||
module.const_pool = parse_const_pool(section_data)?;
|
||||
}
|
||||
SECTION_KIND_FUNCTIONS => {
|
||||
// Functions
|
||||
module.functions = parse_functions(section_data)?;
|
||||
}
|
||||
SECTION_KIND_CODE => {
|
||||
// Code
|
||||
module.code = section_data.to_vec();
|
||||
}
|
||||
SECTION_KIND_DEBUG => {
|
||||
// Debug Info
|
||||
module.debug_info = Some(parse_debug_section(section_data)?);
|
||||
}
|
||||
SECTION_KIND_EXPORTS => {
|
||||
// Exports
|
||||
module.exports = parse_exports(section_data)?;
|
||||
}
|
||||
SECTION_KIND_SYSCALLS => {
|
||||
module.syscalls = parse_syscalls(section_data)?;
|
||||
has_syscalls = true;
|
||||
}
|
||||
_ => {} // Skip unknown or optional sections
|
||||
}
|
||||
}
|
||||
|
||||
if !has_syscalls {
|
||||
return Err(LoadError::MissingSyscallSection);
|
||||
}
|
||||
|
||||
// Additional validations
|
||||
validate_module(&module)?;
|
||||
|
||||
Ok(module)
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_const_pool(data: &[u8]) -> Result<Vec<ConstantPoolEntry>, LoadError> {
|
||||
if data.is_empty() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
if data.len() < 4 {
|
||||
return Err(LoadError::MalformedSection);
|
||||
}
|
||||
let count = u32::from_le_bytes([data[0], data[1], data[2], data[3]]) as usize;
|
||||
let mut cp = Vec::with_capacity(count);
|
||||
let mut pos = 4;
|
||||
|
||||
for _ in 0..count {
|
||||
if pos >= data.len() {
|
||||
return Err(LoadError::UnexpectedEof);
|
||||
}
|
||||
let tag = data[pos];
|
||||
pos += 1;
|
||||
match tag {
|
||||
0 => cp.push(ConstantPoolEntry::Null),
|
||||
1 => {
|
||||
// Int64
|
||||
if pos + 8 > data.len() {
|
||||
return Err(LoadError::UnexpectedEof);
|
||||
}
|
||||
let val = i64::from_le_bytes(data[pos..pos + 8].try_into().unwrap());
|
||||
cp.push(ConstantPoolEntry::Int64(val));
|
||||
pos += 8;
|
||||
}
|
||||
2 => {
|
||||
// Float64
|
||||
if pos + 8 > data.len() {
|
||||
return Err(LoadError::UnexpectedEof);
|
||||
}
|
||||
let val = f64::from_le_bytes(data[pos..pos + 8].try_into().unwrap());
|
||||
cp.push(ConstantPoolEntry::Float64(val));
|
||||
pos += 8;
|
||||
}
|
||||
3 => {
|
||||
// Boolean
|
||||
if pos >= data.len() {
|
||||
return Err(LoadError::UnexpectedEof);
|
||||
}
|
||||
cp.push(ConstantPoolEntry::Boolean(data[pos] != 0));
|
||||
pos += 1;
|
||||
}
|
||||
4 => {
|
||||
// String
|
||||
if pos + 4 > data.len() {
|
||||
return Err(LoadError::UnexpectedEof);
|
||||
}
|
||||
let len = u32::from_le_bytes(data[pos..pos + 4].try_into().unwrap()) as usize;
|
||||
pos += 4;
|
||||
if pos + len > data.len() {
|
||||
return Err(LoadError::UnexpectedEof);
|
||||
}
|
||||
let s = String::from_utf8_lossy(&data[pos..pos + len]).into_owned();
|
||||
cp.push(ConstantPoolEntry::String(s));
|
||||
pos += len;
|
||||
}
|
||||
5 => {
|
||||
// Int32
|
||||
if pos + 4 > data.len() {
|
||||
return Err(LoadError::UnexpectedEof);
|
||||
}
|
||||
let val = i32::from_le_bytes(data[pos..pos + 4].try_into().unwrap());
|
||||
cp.push(ConstantPoolEntry::Int32(val));
|
||||
pos += 4;
|
||||
}
|
||||
_ => return Err(LoadError::MalformedSection),
|
||||
}
|
||||
}
|
||||
Ok(cp)
|
||||
}
|
||||
|
||||
fn parse_functions(data: &[u8]) -> Result<Vec<FunctionMeta>, LoadError> {
|
||||
if data.is_empty() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
if data.len() < 4 {
|
||||
return Err(LoadError::MalformedSection);
|
||||
}
|
||||
let count = u32::from_le_bytes([data[0], data[1], data[2], data[3]]) as usize;
|
||||
let mut functions = Vec::with_capacity(count);
|
||||
let mut pos = 4;
|
||||
|
||||
for _ in 0..count {
|
||||
if pos + 16 > data.len() {
|
||||
return Err(LoadError::UnexpectedEof);
|
||||
}
|
||||
let code_offset = u32::from_le_bytes(data[pos..pos + 4].try_into().unwrap());
|
||||
let code_len = u32::from_le_bytes(data[pos + 4..pos + 8].try_into().unwrap());
|
||||
let param_slots = u16::from_le_bytes(data[pos + 8..pos + 10].try_into().unwrap());
|
||||
let local_slots = u16::from_le_bytes(data[pos + 10..pos + 12].try_into().unwrap());
|
||||
let return_slots = u16::from_le_bytes(data[pos + 12..pos + 14].try_into().unwrap());
|
||||
let max_stack_slots = u16::from_le_bytes(data[pos + 14..pos + 16].try_into().unwrap());
|
||||
|
||||
functions.push(FunctionMeta {
|
||||
code_offset,
|
||||
code_len,
|
||||
param_slots,
|
||||
local_slots,
|
||||
return_slots,
|
||||
max_stack_slots,
|
||||
});
|
||||
pos += 16;
|
||||
}
|
||||
Ok(functions)
|
||||
}
|
||||
|
||||
fn parse_debug_section(data: &[u8]) -> Result<DebugInfo, LoadError> {
|
||||
if data.is_empty() {
|
||||
return Ok(DebugInfo::default());
|
||||
}
|
||||
if data.len() < 8 {
|
||||
return Err(LoadError::MalformedSection);
|
||||
}
|
||||
|
||||
let mut pos = 0;
|
||||
|
||||
// PC to Span table
|
||||
let span_count = u32::from_le_bytes(data[pos..pos + 4].try_into().unwrap()) as usize;
|
||||
pos += 4;
|
||||
let mut pc_to_span = Vec::with_capacity(span_count);
|
||||
for _ in 0..span_count {
|
||||
if pos + 16 > data.len() {
|
||||
return Err(LoadError::UnexpectedEof);
|
||||
}
|
||||
let pc = u32::from_le_bytes(data[pos..pos + 4].try_into().unwrap());
|
||||
let file_id = u32::from_le_bytes(data[pos + 4..pos + 8].try_into().unwrap());
|
||||
let start = u32::from_le_bytes(data[pos + 8..pos + 12].try_into().unwrap());
|
||||
let end = u32::from_le_bytes(data[pos + 12..pos + 16].try_into().unwrap());
|
||||
pc_to_span.push((pc, SourceSpan { file_id, start, end }));
|
||||
pos += 16;
|
||||
}
|
||||
|
||||
// Function names table
|
||||
if pos + 4 > data.len() {
|
||||
return Err(LoadError::UnexpectedEof);
|
||||
}
|
||||
let func_name_count = u32::from_le_bytes(data[pos..pos + 4].try_into().unwrap()) as usize;
|
||||
pos += 4;
|
||||
let mut function_names = Vec::with_capacity(func_name_count);
|
||||
for _ in 0..func_name_count {
|
||||
if pos + 8 > data.len() {
|
||||
return Err(LoadError::UnexpectedEof);
|
||||
}
|
||||
let func_idx = u32::from_le_bytes(data[pos..pos + 4].try_into().unwrap());
|
||||
let name_len = u32::from_le_bytes(data[pos + 4..pos + 8].try_into().unwrap()) as usize;
|
||||
pos += 8;
|
||||
if pos + name_len > data.len() {
|
||||
return Err(LoadError::UnexpectedEof);
|
||||
}
|
||||
let name = String::from_utf8_lossy(&data[pos..pos + name_len]).into_owned();
|
||||
function_names.push((func_idx, name));
|
||||
pos += name_len;
|
||||
}
|
||||
|
||||
Ok(DebugInfo { pc_to_span, function_names })
|
||||
}
|
||||
|
||||
fn parse_exports(data: &[u8]) -> Result<Vec<Export>, LoadError> {
|
||||
if data.is_empty() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
if data.len() < 4 {
|
||||
return Err(LoadError::MalformedSection);
|
||||
}
|
||||
let count = u32::from_le_bytes([data[0], data[1], data[2], data[3]]) as usize;
|
||||
let mut exports = Vec::with_capacity(count);
|
||||
let mut pos = 4;
|
||||
|
||||
for _ in 0..count {
|
||||
if pos + 8 > data.len() {
|
||||
return Err(LoadError::UnexpectedEof);
|
||||
}
|
||||
let func_idx = u32::from_le_bytes(data[pos..pos + 4].try_into().unwrap());
|
||||
let name_len = u32::from_le_bytes(data[pos + 4..pos + 8].try_into().unwrap()) as usize;
|
||||
pos += 8;
|
||||
if pos + name_len > data.len() {
|
||||
return Err(LoadError::UnexpectedEof);
|
||||
}
|
||||
let symbol = String::from_utf8_lossy(&data[pos..pos + name_len]).into_owned();
|
||||
exports.push(Export { symbol, func_idx });
|
||||
pos += name_len;
|
||||
}
|
||||
Ok(exports)
|
||||
}
|
||||
|
||||
fn parse_syscalls(data: &[u8]) -> Result<Vec<SyscallDecl>, LoadError> {
|
||||
if data.len() < 4 {
|
||||
return Err(LoadError::MalformedSection);
|
||||
}
|
||||
|
||||
let count = u32::from_le_bytes([data[0], data[1], data[2], data[3]]) as usize;
|
||||
let mut syscalls = Vec::with_capacity(count);
|
||||
let mut pos = 4;
|
||||
|
||||
for _ in 0..count {
|
||||
if pos + 2 > data.len() {
|
||||
return Err(LoadError::UnexpectedEof);
|
||||
}
|
||||
let module_len = u16::from_le_bytes(data[pos..pos + 2].try_into().unwrap()) as usize;
|
||||
pos += 2;
|
||||
if pos + module_len > data.len() {
|
||||
return Err(LoadError::UnexpectedEof);
|
||||
}
|
||||
let module = std::str::from_utf8(&data[pos..pos + module_len])
|
||||
.map_err(|_| LoadError::InvalidUtf8)?;
|
||||
pos += module_len;
|
||||
|
||||
if pos + 2 > data.len() {
|
||||
return Err(LoadError::UnexpectedEof);
|
||||
}
|
||||
let name_len = u16::from_le_bytes(data[pos..pos + 2].try_into().unwrap()) as usize;
|
||||
pos += 2;
|
||||
if pos + name_len > data.len() {
|
||||
return Err(LoadError::UnexpectedEof);
|
||||
}
|
||||
let name =
|
||||
std::str::from_utf8(&data[pos..pos + name_len]).map_err(|_| LoadError::InvalidUtf8)?;
|
||||
pos += name_len;
|
||||
|
||||
if pos + 6 > data.len() {
|
||||
return Err(LoadError::UnexpectedEof);
|
||||
}
|
||||
let version = u16::from_le_bytes(data[pos..pos + 2].try_into().unwrap());
|
||||
let arg_slots = u16::from_le_bytes(data[pos + 2..pos + 4].try_into().unwrap());
|
||||
let ret_slots = u16::from_le_bytes(data[pos + 4..pos + 6].try_into().unwrap());
|
||||
pos += 6;
|
||||
|
||||
syscalls.push(SyscallDecl {
|
||||
module: module.to_owned(),
|
||||
name: name.to_owned(),
|
||||
version,
|
||||
arg_slots,
|
||||
ret_slots,
|
||||
});
|
||||
}
|
||||
|
||||
if pos != data.len() {
|
||||
return Err(LoadError::MalformedSection);
|
||||
}
|
||||
|
||||
Ok(syscalls)
|
||||
}
|
||||
|
||||
fn validate_module(module: &BytecodeModule) -> Result<(), LoadError> {
|
||||
let mut syscall_identities = HashSet::with_capacity(module.syscalls.len());
|
||||
for syscall in &module.syscalls {
|
||||
if !syscall_identities.insert((
|
||||
syscall.module.clone(),
|
||||
syscall.name.clone(),
|
||||
syscall.version,
|
||||
)) {
|
||||
return Err(LoadError::DuplicateSyscallIdentity);
|
||||
}
|
||||
}
|
||||
|
||||
for func in &module.functions {
|
||||
// Opcode stream bounds
|
||||
if (func.code_offset as usize) + (func.code_len as usize) > module.code.len() {
|
||||
return Err(LoadError::InvalidFunctionIndex);
|
||||
}
|
||||
}
|
||||
|
||||
// Basic opcode scan for const pool indices
|
||||
let mut pos = 0;
|
||||
while pos < module.code.len() {
|
||||
if pos + 2 > module.code.len() {
|
||||
break; // Unexpected EOF in middle of opcode, maybe should be error
|
||||
}
|
||||
let op_val = u16::from_le_bytes([module.code[pos], module.code[pos + 1]]);
|
||||
let opcode = OpCode::try_from(op_val).map_err(|_| LoadError::InvalidOpcode)?;
|
||||
pos += 2;
|
||||
|
||||
match opcode {
|
||||
OpCode::PushConst => {
|
||||
if pos + 4 > module.code.len() {
|
||||
return Err(LoadError::UnexpectedEof);
|
||||
}
|
||||
let idx =
|
||||
u32::from_le_bytes(module.code[pos..pos + 4].try_into().unwrap()) as usize;
|
||||
if idx >= module.const_pool.len() {
|
||||
return Err(LoadError::InvalidConstIndex);
|
||||
}
|
||||
pos += 4;
|
||||
}
|
||||
OpCode::PushI32
|
||||
| OpCode::Jmp
|
||||
| OpCode::JmpIfFalse
|
||||
| OpCode::JmpIfTrue
|
||||
| OpCode::GetGlobal
|
||||
| OpCode::SetGlobal
|
||||
| OpCode::GetLocal
|
||||
| OpCode::SetLocal
|
||||
| OpCode::PopN
|
||||
| OpCode::Hostcall
|
||||
| OpCode::Syscall
|
||||
| OpCode::Intrinsic => {
|
||||
pos += 4;
|
||||
}
|
||||
OpCode::PushI64 | OpCode::PushF64 => {
|
||||
pos += 8;
|
||||
}
|
||||
OpCode::PushBool => {
|
||||
pos += 1;
|
||||
}
|
||||
OpCode::Call => {
|
||||
pos += 4;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn create_header(section_count: u32) -> Vec<u8> {
|
||||
let mut h = vec![0u8; 32];
|
||||
h[0..4].copy_from_slice(b"PBS\0");
|
||||
h[4..6].copy_from_slice(&0u16.to_le_bytes()); // version
|
||||
h[6] = 0; // endianness
|
||||
h[8..12].copy_from_slice(§ion_count.to_le_bytes());
|
||||
h
|
||||
}
|
||||
|
||||
fn minimal_module() -> BytecodeModule {
|
||||
BytecodeModule {
|
||||
version: 0,
|
||||
const_pool: vec![],
|
||||
functions: vec![],
|
||||
code: vec![],
|
||||
debug_info: None,
|
||||
exports: vec![],
|
||||
syscalls: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
fn build_pbs_with_sections(sections: Vec<(u32, Vec<u8>)>) -> Vec<u8> {
|
||||
let mut data = create_header(sections.len() as u32);
|
||||
let mut offset = 32 + (sections.len() as u32 * 12);
|
||||
|
||||
for (kind, section_data) in §ions {
|
||||
data.extend_from_slice(&kind.to_le_bytes());
|
||||
data.extend_from_slice(&offset.to_le_bytes());
|
||||
data.extend_from_slice(&(section_data.len() as u32).to_le_bytes());
|
||||
offset += section_data.len() as u32;
|
||||
}
|
||||
|
||||
for (_, section_data) in sections {
|
||||
data.extend_from_slice(§ion_data);
|
||||
}
|
||||
|
||||
data
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_magic() {
|
||||
let mut data = create_header(0);
|
||||
data[0] = b'X';
|
||||
assert_eq!(BytecodeLoader::load(&data), Err(LoadError::InvalidMagic));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_version() {
|
||||
let mut data = create_header(0);
|
||||
data[4] = 1;
|
||||
assert_eq!(BytecodeLoader::load(&data), Err(LoadError::InvalidVersion));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_endianness() {
|
||||
let mut data = create_header(0);
|
||||
data[6] = 1;
|
||||
assert_eq!(BytecodeLoader::load(&data), Err(LoadError::InvalidEndianness));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_overlapping_sections() {
|
||||
let mut data = create_header(2);
|
||||
// Section 1: Kind 0, Offset 64, Length 32
|
||||
data.extend_from_slice(&0u32.to_le_bytes());
|
||||
data.extend_from_slice(&64u32.to_le_bytes());
|
||||
data.extend_from_slice(&32u32.to_le_bytes());
|
||||
// Section 2: Kind 1, Offset 80, Length 32 (Overlaps with Section 1)
|
||||
data.extend_from_slice(&1u32.to_le_bytes());
|
||||
data.extend_from_slice(&80u32.to_le_bytes());
|
||||
data.extend_from_slice(&32u32.to_le_bytes());
|
||||
|
||||
// Ensure data is long enough for the offsets
|
||||
data.resize(256, 0);
|
||||
|
||||
assert_eq!(BytecodeLoader::load(&data), Err(LoadError::OverlappingSections));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_section_out_of_bounds() {
|
||||
let mut data = create_header(1);
|
||||
// Section 1: Kind 0, Offset 64, Length 1000
|
||||
data.extend_from_slice(&0u32.to_le_bytes());
|
||||
data.extend_from_slice(&64u32.to_le_bytes());
|
||||
data.extend_from_slice(&1000u32.to_le_bytes());
|
||||
|
||||
data.resize(256, 0);
|
||||
|
||||
assert_eq!(BytecodeLoader::load(&data), Err(LoadError::SectionOutOfBounds));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_function_code_offset() {
|
||||
let mut data = create_header(3);
|
||||
// Section 1: Functions, Kind 1, Offset 80, Length 20 (Header 4 + 1 entry 16)
|
||||
data.extend_from_slice(&1u32.to_le_bytes());
|
||||
data.extend_from_slice(&80u32.to_le_bytes());
|
||||
data.extend_from_slice(&20u32.to_le_bytes());
|
||||
|
||||
// Section 2: Code, Kind 2, Offset 128, Length 10
|
||||
data.extend_from_slice(&2u32.to_le_bytes());
|
||||
data.extend_from_slice(&128u32.to_le_bytes());
|
||||
data.extend_from_slice(&10u32.to_le_bytes());
|
||||
|
||||
// Section 3: SYSC, Kind 5, Offset 160, Length 4 (empty)
|
||||
data.extend_from_slice(&5u32.to_le_bytes());
|
||||
data.extend_from_slice(&160u32.to_le_bytes());
|
||||
data.extend_from_slice(&4u32.to_le_bytes());
|
||||
|
||||
data.resize(256, 0);
|
||||
|
||||
// Setup functions section
|
||||
let func_data_start = 80;
|
||||
data[func_data_start..func_data_start + 4].copy_from_slice(&1u32.to_le_bytes()); // 1 function
|
||||
let entry_start = func_data_start + 4;
|
||||
data[entry_start..entry_start + 4].copy_from_slice(&5u32.to_le_bytes()); // code_offset = 5
|
||||
data[entry_start + 4..entry_start + 8].copy_from_slice(&10u32.to_le_bytes()); // code_len = 10
|
||||
// 5 + 10 = 15 > 10 (code section length)
|
||||
data[160..164].copy_from_slice(&0u32.to_le_bytes());
|
||||
|
||||
assert_eq!(BytecodeLoader::load(&data), Err(LoadError::InvalidFunctionIndex));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_const_index() {
|
||||
let mut data = create_header(3);
|
||||
// Section 1: Const Pool, Kind 0, Offset 80, Length 4 (Empty CP)
|
||||
data.extend_from_slice(&0u32.to_le_bytes());
|
||||
data.extend_from_slice(&80u32.to_le_bytes());
|
||||
data.extend_from_slice(&4u32.to_le_bytes());
|
||||
|
||||
// Section 2: Code, Kind 2, Offset 128, Length 6 (PushConst 0)
|
||||
data.extend_from_slice(&2u32.to_le_bytes());
|
||||
data.extend_from_slice(&128u32.to_le_bytes());
|
||||
data.extend_from_slice(&6u32.to_le_bytes());
|
||||
|
||||
// Section 3: SYSC, Kind 5, Offset 160, Length 4 (empty)
|
||||
data.extend_from_slice(&5u32.to_le_bytes());
|
||||
data.extend_from_slice(&160u32.to_le_bytes());
|
||||
data.extend_from_slice(&4u32.to_le_bytes());
|
||||
|
||||
data.resize(256, 0);
|
||||
|
||||
// Setup empty CP
|
||||
data[80..84].copy_from_slice(&0u32.to_le_bytes());
|
||||
|
||||
// Setup code with PushConst 0
|
||||
data[128..130].copy_from_slice(&(OpCode::PushConst as u16).to_le_bytes());
|
||||
data[130..134].copy_from_slice(&0u32.to_le_bytes());
|
||||
data[160..164].copy_from_slice(&0u32.to_le_bytes());
|
||||
|
||||
assert_eq!(BytecodeLoader::load(&data), Err(LoadError::InvalidConstIndex));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_missing_sysc_section_is_rejected() {
|
||||
let data = create_header(0);
|
||||
|
||||
assert_eq!(BytecodeLoader::load(&data), Err(LoadError::MissingSyscallSection));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_valid_minimal_load_with_empty_sysc() {
|
||||
let data = minimal_module().serialize();
|
||||
let module = BytecodeLoader::load(&data).unwrap();
|
||||
assert_eq!(module.version, 0);
|
||||
assert!(module.const_pool.is_empty());
|
||||
assert!(module.functions.is_empty());
|
||||
assert!(module.code.is_empty());
|
||||
assert!(module.syscalls.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_valid_sysc_roundtrip() {
|
||||
let mut module = minimal_module();
|
||||
module.syscalls = vec![SyscallDecl {
|
||||
module: "gfx".into(),
|
||||
name: "draw_line".into(),
|
||||
version: 1,
|
||||
arg_slots: 4,
|
||||
ret_slots: 0,
|
||||
}];
|
||||
|
||||
let data = module.serialize();
|
||||
let loaded = BytecodeLoader::load(&data).unwrap();
|
||||
|
||||
assert_eq!(loaded.syscalls, module.syscalls);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_malformed_sysc_section_is_rejected() {
|
||||
let data = build_pbs_with_sections(vec![(SECTION_KIND_SYSCALLS, vec![1, 0, 0])]);
|
||||
|
||||
assert_eq!(BytecodeLoader::load(&data), Err(LoadError::MalformedSection));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_utf8_in_sysc_section_is_rejected() {
|
||||
let mut sysc = Vec::new();
|
||||
sysc.extend_from_slice(&1u32.to_le_bytes());
|
||||
sysc.extend_from_slice(&1u16.to_le_bytes());
|
||||
sysc.push(0xFF);
|
||||
sysc.extend_from_slice(&1u16.to_le_bytes());
|
||||
sysc.push(b'x');
|
||||
sysc.extend_from_slice(&1u16.to_le_bytes());
|
||||
sysc.extend_from_slice(&0u16.to_le_bytes());
|
||||
sysc.extend_from_slice(&0u16.to_le_bytes());
|
||||
|
||||
let data = build_pbs_with_sections(vec![(SECTION_KIND_SYSCALLS, sysc)]);
|
||||
|
||||
assert_eq!(BytecodeLoader::load(&data), Err(LoadError::InvalidUtf8));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_duplicate_sysc_identity_is_rejected() {
|
||||
let mut module = minimal_module();
|
||||
module.syscalls = vec![
|
||||
SyscallDecl {
|
||||
module: "system".into(),
|
||||
name: "has_cart".into(),
|
||||
version: 1,
|
||||
arg_slots: 0,
|
||||
ret_slots: 1,
|
||||
},
|
||||
SyscallDecl {
|
||||
module: "system".into(),
|
||||
name: "has_cart".into(),
|
||||
version: 1,
|
||||
arg_slots: 0,
|
||||
ret_slots: 1,
|
||||
},
|
||||
];
|
||||
|
||||
let data = module.serialize();
|
||||
|
||||
assert_eq!(BytecodeLoader::load(&data), Err(LoadError::DuplicateSyscallIdentity));
|
||||
}
|
||||
}
|
||||
324
crates/console/prometeu-bytecode/src/opcode.rs
Normal file
324
crates/console/prometeu-bytecode/src/opcode.rs
Normal file
@ -0,0 +1,324 @@
|
||||
/// 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
567
crates/console/prometeu-bytecode/src/opcode_spec.rs
Normal file
567
crates/console/prometeu-bytecode/src/opcode_spec.rs
Normal file
@ -0,0 +1,567 @@
|
||||
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");
|
||||
}
|
||||
}
|
||||
124
crates/console/prometeu-bytecode/src/program_image.rs
Normal file
124
crates/console/prometeu-bytecode/src/program_image.rs
Normal file
@ -0,0 +1,124 @@
|
||||
use crate::abi::TrapInfo;
|
||||
use crate::model::{BytecodeModule, ConstantPoolEntry, DebugInfo, Export, FunctionMeta};
|
||||
use crate::value::Value;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Represents a fully linked, executable PBS program image.
|
||||
///
|
||||
/// Under the Prometeu architecture, the ProgramImage is a "closed-world" artifact
|
||||
/// produced by the compiler. All linking, relocation, and symbol resolution
|
||||
/// MUST be performed by the compiler before this image is created.
|
||||
///
|
||||
/// The runtime (VM) assumes this image is authoritative and performs no
|
||||
/// additional linking or fixups.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct ProgramImage {
|
||||
pub rom: Arc<[u8]>,
|
||||
pub constant_pool: Arc<[Value]>,
|
||||
pub functions: Arc<[FunctionMeta]>,
|
||||
pub debug_info: Option<DebugInfo>,
|
||||
pub exports: Arc<HashMap<String, u32>>,
|
||||
}
|
||||
|
||||
impl ProgramImage {
|
||||
pub fn new(
|
||||
rom: Vec<u8>,
|
||||
constant_pool: Vec<Value>,
|
||||
functions: Vec<FunctionMeta>,
|
||||
debug_info: Option<DebugInfo>,
|
||||
exports: HashMap<String, u32>,
|
||||
) -> Self {
|
||||
Self {
|
||||
rom: Arc::from(rom),
|
||||
constant_pool: Arc::from(constant_pool),
|
||||
functions: Arc::from(functions),
|
||||
debug_info,
|
||||
exports: Arc::new(exports),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create_trap(&self, code: u32, opcode: u16, mut message: String, pc: u32) -> TrapInfo {
|
||||
let span = self
|
||||
.debug_info
|
||||
.as_ref()
|
||||
.and_then(|di| di.pc_to_span.iter().find(|(p, _)| *p == pc).map(|(_, s)| s.clone()));
|
||||
|
||||
if let Some(func_idx) = self.find_function_index(pc) {
|
||||
if let Some(func_name) = self.get_function_name(func_idx) {
|
||||
message = format!("{} (in function {})", message, func_name);
|
||||
}
|
||||
}
|
||||
|
||||
TrapInfo { code, opcode, message, pc, span }
|
||||
}
|
||||
|
||||
pub fn find_function_index(&self, pc: u32) -> Option<usize> {
|
||||
self.functions.iter().position(|f| pc >= f.code_offset && pc < (f.code_offset + f.code_len))
|
||||
}
|
||||
|
||||
pub fn get_function_name(&self, func_idx: usize) -> Option<&str> {
|
||||
self.debug_info
|
||||
.as_ref()
|
||||
.and_then(|di| di.function_names.iter().find(|(idx, _)| *idx as usize == func_idx))
|
||||
.map(|(_, name)| name.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<BytecodeModule> for ProgramImage {
|
||||
fn from(module: BytecodeModule) -> Self {
|
||||
let constant_pool: Vec<Value> = module
|
||||
.const_pool
|
||||
.iter()
|
||||
.map(|entry| match entry {
|
||||
ConstantPoolEntry::Null => Value::Null,
|
||||
ConstantPoolEntry::Int64(v) => Value::Int64(*v),
|
||||
ConstantPoolEntry::Float64(v) => Value::Float(*v),
|
||||
ConstantPoolEntry::Boolean(v) => Value::Boolean(*v),
|
||||
ConstantPoolEntry::String(v) => Value::String(v.clone()),
|
||||
ConstantPoolEntry::Int32(v) => Value::Int32(*v),
|
||||
})
|
||||
.collect();
|
||||
|
||||
let mut exports = HashMap::new();
|
||||
for export in module.exports {
|
||||
exports.insert(export.symbol, export.func_idx);
|
||||
}
|
||||
|
||||
ProgramImage::new(module.code, constant_pool, module.functions, module.debug_info, exports)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ProgramImage> for BytecodeModule {
|
||||
fn from(program: ProgramImage) -> Self {
|
||||
let const_pool = program
|
||||
.constant_pool
|
||||
.iter()
|
||||
.map(|v| match v {
|
||||
Value::Null => ConstantPoolEntry::Null,
|
||||
Value::Int64(v) => ConstantPoolEntry::Int64(*v),
|
||||
Value::Float(v) => ConstantPoolEntry::Float64(*v),
|
||||
Value::Boolean(v) => ConstantPoolEntry::Boolean(*v),
|
||||
Value::String(v) => ConstantPoolEntry::String(v.clone()),
|
||||
Value::Int32(v) => ConstantPoolEntry::Int32(*v),
|
||||
Value::HeapRef(_) => ConstantPoolEntry::Null,
|
||||
})
|
||||
.collect();
|
||||
|
||||
let exports = program
|
||||
.exports
|
||||
.iter()
|
||||
.map(|(symbol, &func_idx)| Export { symbol: symbol.clone(), func_idx })
|
||||
.collect();
|
||||
|
||||
BytecodeModule {
|
||||
version: 0,
|
||||
const_pool,
|
||||
functions: program.functions.as_ref().to_vec(),
|
||||
code: program.rom.as_ref().to_vec(),
|
||||
debug_info: program.debug_info.clone(),
|
||||
exports,
|
||||
syscalls: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,15 +1,35 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::cmp::Ordering;
|
||||
|
||||
/// Opaque handle that references an object stored in the VM heap.
|
||||
///
|
||||
/// This is an index-based handle. It does not imply ownership and carries
|
||||
/// no lifetime information. GC/allocator integration will come in later PRs.
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)]
|
||||
pub struct HeapRef(pub u32);
|
||||
|
||||
/// Represents any piece of data that can be stored on the VM stack or in globals.
|
||||
///
|
||||
/// The PVM is "dynamically typed" at the bytecode level, meaning a single
|
||||
/// `Value` enum can hold different primitive types. The VM performs
|
||||
/// automatic type promotion (e.g., adding an Int32 to a Float64 results
|
||||
/// in a Float64) to ensure mathematical correctness.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum Value {
|
||||
/// 32-bit signed integer. Used for most loop counters and indices.
|
||||
Int32(i32),
|
||||
/// 64-bit signed integer. Used for large numbers and timestamps.
|
||||
Int64(i64),
|
||||
/// 64-bit double precision float. Used for physics and complex math.
|
||||
Float(f64),
|
||||
/// Boolean value (true/false).
|
||||
Boolean(bool),
|
||||
/// UTF-8 string. Strings are immutable and usually come from the Constant Pool.
|
||||
String(String),
|
||||
Ref(usize), // Heap reference
|
||||
/// A handle to an object on the heap (opaque reference).
|
||||
HeapRef(HeapRef),
|
||||
/// Represents the absence of a value (equivalent to `null` or `undefined`).
|
||||
Null,
|
||||
}
|
||||
|
||||
@ -27,7 +47,7 @@ impl PartialEq for Value {
|
||||
(Value::Float(a), Value::Int64(b)) => *a == *b as f64,
|
||||
(Value::Boolean(a), Value::Boolean(b)) => a == b,
|
||||
(Value::String(a), Value::String(b)) => a == b,
|
||||
(Value::Ref(a), Value::Ref(b)) => a == b,
|
||||
(Value::HeapRef(a), Value::HeapRef(b)) => a == b,
|
||||
(Value::Null, Value::Null) => true,
|
||||
_ => false,
|
||||
}
|
||||
@ -71,6 +91,19 @@ impl Value {
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::inherent_to_string)]
|
||||
pub fn to_string(&self) -> String {
|
||||
match self {
|
||||
Value::Int32(i) => i.to_string(),
|
||||
Value::Int64(i) => i.to_string(),
|
||||
Value::Float(f) => f.to_string(),
|
||||
Value::Boolean(b) => b.to_string(),
|
||||
Value::String(s) => s.clone(),
|
||||
Value::HeapRef(r) => format!("[HeapRef {}]", r.0),
|
||||
Value::Null => "null".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@ -116,4 +149,4 @@ mod tests {
|
||||
assert_eq!(v_bool.as_float(), None);
|
||||
assert_eq!(v_bool.as_integer(), None);
|
||||
}
|
||||
}
|
||||
}
|
||||
46
crates/console/prometeu-bytecode/tests/disasm_roundtrip.rs
Normal file
46
crates/console/prometeu-bytecode/tests/disasm_roundtrip.rs
Normal file
@ -0,0 +1,46 @@
|
||||
use prometeu_bytecode::isa::core::CoreOpCode;
|
||||
use prometeu_bytecode::{assemble, disassemble};
|
||||
|
||||
fn emit(op: CoreOpCode, imm: Option<&[u8]>, out: &mut Vec<u8>) {
|
||||
out.extend_from_slice(&(op as u16).to_le_bytes());
|
||||
if let Some(bytes) = imm {
|
||||
out.extend_from_slice(bytes);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn roundtrip_disasm_assemble_byte_equal_with_closures_and_coroutines() {
|
||||
// Program: PUSH_I32 7; MAKE_CLOSURE fn=1,captures=0; CALL_CLOSURE argc=1;
|
||||
// SPAWN fn=2,argc=1; YIELD; SLEEP 3; HOSTCALL 2; SYSCALL 0x1003; FRAME_SYNC; HALT
|
||||
let mut prog = Vec::new();
|
||||
|
||||
emit(CoreOpCode::PushI32, Some(&7i32.to_le_bytes()), &mut prog);
|
||||
// MAKE_CLOSURE (fn=1, captures=0)
|
||||
let mut mc = [0u8; 8];
|
||||
mc[0..4].copy_from_slice(&1u32.to_le_bytes());
|
||||
mc[4..8].copy_from_slice(&0u32.to_le_bytes());
|
||||
emit(CoreOpCode::MakeClosure, Some(&mc), &mut prog);
|
||||
// CALL_CLOSURE argc=1
|
||||
emit(CoreOpCode::CallClosure, Some(&1u32.to_le_bytes()), &mut prog);
|
||||
// SPAWN (fn=2, argc=1)
|
||||
let mut sp = [0u8; 8];
|
||||
sp[0..4].copy_from_slice(&2u32.to_le_bytes());
|
||||
sp[4..8].copy_from_slice(&1u32.to_le_bytes());
|
||||
emit(CoreOpCode::Spawn, Some(&sp), &mut prog);
|
||||
// YIELD
|
||||
emit(CoreOpCode::Yield, None, &mut prog);
|
||||
// SLEEP 3
|
||||
emit(CoreOpCode::Sleep, Some(&3u32.to_le_bytes()), &mut prog);
|
||||
// HOSTCALL sysc[2]
|
||||
emit(CoreOpCode::Hostcall, Some(&2u32.to_le_bytes()), &mut prog);
|
||||
// SYSCALL gfx.draw_line (0x1003)
|
||||
emit(CoreOpCode::Syscall, Some(&0x1003u32.to_le_bytes()), &mut prog);
|
||||
// FRAME_SYNC
|
||||
emit(CoreOpCode::FrameSync, None, &mut prog);
|
||||
// HALT
|
||||
emit(CoreOpCode::Halt, None, &mut prog);
|
||||
|
||||
let text = disassemble(&prog).expect("disasm ok");
|
||||
let rebuilt = assemble(&text).expect("assemble ok");
|
||||
assert_eq!(rebuilt, prog, "re-assembled bytes must match original");
|
||||
}
|
||||
43
crates/console/prometeu-bytecode/tests/disasm_snapshot.rs
Normal file
43
crates/console/prometeu-bytecode/tests/disasm_snapshot.rs
Normal file
@ -0,0 +1,43 @@
|
||||
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);
|
||||
}
|
||||
160
crates/console/prometeu-bytecode/tests/roundtrip.rs
Normal file
160
crates/console/prometeu-bytecode/tests/roundtrip.rs
Normal file
@ -0,0 +1,160 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
9
crates/console/prometeu-drivers/Cargo.toml
Normal file
9
crates/console/prometeu-drivers/Cargo.toml
Normal file
@ -0,0 +1,9 @@
|
||||
[package]
|
||||
name = "prometeu-drivers"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
license.workspace = true
|
||||
|
||||
[dependencies]
|
||||
serde_json = "1.0.149"
|
||||
prometeu-hal = { path = "../prometeu-hal" }
|
||||
1784
crates/console/prometeu-drivers/src/asset.rs
Normal file
1784
crates/console/prometeu-drivers/src/asset.rs
Normal file
File diff suppressed because it is too large
Load Diff
332
crates/console/prometeu-drivers/src/audio.rs
Normal file
332
crates/console/prometeu-drivers/src/audio.rs
Normal file
@ -0,0 +1,332 @@
|
||||
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());
|
||||
}
|
||||
}
|
||||
719
crates/console/prometeu-drivers/src/frame_composer.rs
Normal file
719
crates/console/prometeu-drivers/src/frame_composer.rs
Normal file
@ -0,0 +1,719 @@
|
||||
use crate::memory_banks::SceneBankPoolAccess;
|
||||
use prometeu_hal::GfxBridge;
|
||||
use prometeu_hal::glyph::Glyph;
|
||||
use prometeu_hal::scene_bank::SceneBank;
|
||||
use prometeu_hal::scene_viewport_cache::SceneViewportCache;
|
||||
use prometeu_hal::scene_viewport_resolver::{CacheRefreshRequest, SceneViewportResolver};
|
||||
use prometeu_hal::sprite::Sprite;
|
||||
use std::sync::Arc;
|
||||
|
||||
const EMPTY_SPRITE: Sprite = Sprite {
|
||||
glyph: Glyph { glyph_id: 0, palette_id: 0 },
|
||||
x: 0,
|
||||
y: 0,
|
||||
layer: 0,
|
||||
bank_id: 0,
|
||||
active: false,
|
||||
flip_x: false,
|
||||
flip_y: false,
|
||||
priority: 0,
|
||||
};
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
pub enum SceneStatus {
|
||||
#[default]
|
||||
Unbound,
|
||||
Available {
|
||||
scene_bank_id: usize,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct SpriteController {
|
||||
sprites: [Sprite; 512],
|
||||
sprite_count: usize,
|
||||
frame_counter: u64,
|
||||
dropped_sprites: usize,
|
||||
layer_buckets: [Vec<usize>; 4],
|
||||
}
|
||||
|
||||
impl Default for SpriteController {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl SpriteController {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
sprites: [EMPTY_SPRITE; 512],
|
||||
sprite_count: 0,
|
||||
frame_counter: 0,
|
||||
dropped_sprites: 0,
|
||||
layer_buckets: std::array::from_fn(|_| Vec::with_capacity(128)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn sprites(&self) -> &[Sprite; 512] {
|
||||
&self.sprites
|
||||
}
|
||||
|
||||
pub fn sprites_mut(&mut self) -> &mut [Sprite; 512] {
|
||||
&mut self.sprites
|
||||
}
|
||||
|
||||
pub fn begin_frame(&mut self) {
|
||||
self.frame_counter = self.frame_counter.wrapping_add(1);
|
||||
self.sprite_count = 0;
|
||||
self.dropped_sprites = 0;
|
||||
for bucket in &mut self.layer_buckets {
|
||||
bucket.clear();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn emit_sprite(&mut self, mut sprite: Sprite) -> bool {
|
||||
let Some(bucket) = self.layer_buckets.get_mut(sprite.layer as usize) else {
|
||||
self.dropped_sprites += 1;
|
||||
return false;
|
||||
};
|
||||
if self.sprite_count >= self.sprites.len() {
|
||||
self.dropped_sprites += 1;
|
||||
return false;
|
||||
}
|
||||
|
||||
sprite.active = true;
|
||||
let index = self.sprite_count;
|
||||
self.sprites[index] = sprite;
|
||||
self.sprite_count += 1;
|
||||
bucket.push(index);
|
||||
true
|
||||
}
|
||||
|
||||
pub fn sprite_count(&self) -> usize {
|
||||
self.sprite_count
|
||||
}
|
||||
|
||||
pub fn frame_counter(&self) -> u64 {
|
||||
self.frame_counter
|
||||
}
|
||||
|
||||
pub fn dropped_sprites(&self) -> usize {
|
||||
self.dropped_sprites
|
||||
}
|
||||
|
||||
pub fn ordered_sprites(&self) -> Vec<Sprite> {
|
||||
let mut ordered = Vec::with_capacity(self.sprite_count);
|
||||
for bucket in &self.layer_buckets {
|
||||
let mut indices = bucket.clone();
|
||||
indices.sort_by_key(|&index| self.sprites[index].priority);
|
||||
for index in indices {
|
||||
ordered.push(self.sprites[index]);
|
||||
}
|
||||
}
|
||||
ordered
|
||||
}
|
||||
}
|
||||
|
||||
pub struct FrameComposer {
|
||||
scene_bank_pool: Arc<dyn SceneBankPoolAccess>,
|
||||
viewport_width_px: usize,
|
||||
viewport_height_px: usize,
|
||||
active_scene_id: Option<usize>,
|
||||
active_scene: Option<Arc<SceneBank>>,
|
||||
scene_status: SceneStatus,
|
||||
camera_x_px: i32,
|
||||
camera_y_px: i32,
|
||||
cache: Option<SceneViewportCache>,
|
||||
resolver: Option<SceneViewportResolver>,
|
||||
sprite_controller: SpriteController,
|
||||
}
|
||||
|
||||
impl FrameComposer {
|
||||
pub fn new(
|
||||
viewport_width_px: usize,
|
||||
viewport_height_px: usize,
|
||||
scene_bank_pool: Arc<dyn SceneBankPoolAccess>,
|
||||
) -> Self {
|
||||
Self {
|
||||
scene_bank_pool,
|
||||
viewport_width_px,
|
||||
viewport_height_px,
|
||||
active_scene_id: None,
|
||||
active_scene: None,
|
||||
scene_status: SceneStatus::Unbound,
|
||||
camera_x_px: 0,
|
||||
camera_y_px: 0,
|
||||
cache: None,
|
||||
resolver: None,
|
||||
sprite_controller: SpriteController::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn viewport_size(&self) -> (usize, usize) {
|
||||
(self.viewport_width_px, self.viewport_height_px)
|
||||
}
|
||||
|
||||
pub fn scene_bank_pool(&self) -> &Arc<dyn SceneBankPoolAccess> {
|
||||
&self.scene_bank_pool
|
||||
}
|
||||
|
||||
pub fn scene_bank_slot(&self, slot: usize) -> Option<Arc<SceneBank>> {
|
||||
self.scene_bank_pool.scene_bank_slot(slot)
|
||||
}
|
||||
|
||||
pub fn scene_bank_slot_count(&self) -> usize {
|
||||
self.scene_bank_pool.scene_bank_slot_count()
|
||||
}
|
||||
|
||||
pub fn active_scene_id(&self) -> Option<usize> {
|
||||
self.active_scene_id
|
||||
}
|
||||
|
||||
pub fn active_scene(&self) -> Option<&Arc<SceneBank>> {
|
||||
self.active_scene.as_ref()
|
||||
}
|
||||
|
||||
pub fn scene_status(&self) -> SceneStatus {
|
||||
self.scene_status
|
||||
}
|
||||
|
||||
pub fn camera(&self) -> (i32, i32) {
|
||||
(self.camera_x_px, self.camera_y_px)
|
||||
}
|
||||
|
||||
pub fn bind_scene(&mut self, scene_bank_id: usize) -> bool {
|
||||
let Some(scene) = self.scene_bank_pool.scene_bank_slot(scene_bank_id) else {
|
||||
self.unbind_scene();
|
||||
return false;
|
||||
};
|
||||
|
||||
let (cache, resolver) =
|
||||
Self::build_scene_runtime(self.viewport_width_px, self.viewport_height_px, &scene);
|
||||
self.active_scene_id = Some(scene_bank_id);
|
||||
self.active_scene = Some(scene);
|
||||
self.scene_status = SceneStatus::Available { scene_bank_id };
|
||||
self.cache = Some(cache);
|
||||
self.resolver = Some(resolver);
|
||||
true
|
||||
}
|
||||
|
||||
pub fn unbind_scene(&mut self) {
|
||||
self.active_scene_id = None;
|
||||
self.active_scene = None;
|
||||
self.scene_status = SceneStatus::Unbound;
|
||||
self.cache = None;
|
||||
self.resolver = None;
|
||||
}
|
||||
|
||||
pub fn set_camera(&mut self, x: i32, y: i32) {
|
||||
self.camera_x_px = x;
|
||||
self.camera_y_px = y;
|
||||
}
|
||||
|
||||
pub fn cache(&self) -> Option<&SceneViewportCache> {
|
||||
self.cache.as_ref()
|
||||
}
|
||||
|
||||
pub fn resolver(&self) -> Option<&SceneViewportResolver> {
|
||||
self.resolver.as_ref()
|
||||
}
|
||||
|
||||
pub fn sprite_controller(&self) -> &SpriteController {
|
||||
&self.sprite_controller
|
||||
}
|
||||
|
||||
pub fn sprite_controller_mut(&mut self) -> &mut SpriteController {
|
||||
&mut self.sprite_controller
|
||||
}
|
||||
|
||||
pub fn begin_frame(&mut self) {
|
||||
self.sprite_controller.begin_frame();
|
||||
}
|
||||
|
||||
pub fn emit_sprite(&mut self, sprite: Sprite) -> bool {
|
||||
self.sprite_controller.emit_sprite(sprite)
|
||||
}
|
||||
|
||||
pub fn ordered_sprites(&self) -> Vec<Sprite> {
|
||||
self.sprite_controller.ordered_sprites()
|
||||
}
|
||||
|
||||
pub fn render_frame(&mut self, gfx: &mut dyn GfxBridge) {
|
||||
let ordered_sprites = self.ordered_sprites();
|
||||
gfx.load_frame_sprites(&ordered_sprites);
|
||||
|
||||
if let (Some(scene), Some(cache), Some(resolver)) =
|
||||
(self.active_scene.as_deref(), self.cache.as_mut(), self.resolver.as_mut())
|
||||
{
|
||||
let update = resolver.update(scene, self.camera_x_px, self.camera_y_px);
|
||||
Self::apply_refresh_requests(cache, scene, &update.refresh_requests);
|
||||
// `FrameComposer` owns only canonical game-frame composition.
|
||||
// Deferred `gfx.*` primitives are drained later by a separate
|
||||
// overlay/debug stage outside this service boundary.
|
||||
gfx.render_scene_from_cache(cache, &update);
|
||||
return;
|
||||
}
|
||||
|
||||
// No-scene frames still stop at canonical game composition. Final
|
||||
// overlay/debug work remains outside `FrameComposer`.
|
||||
gfx.render_no_scene_frame();
|
||||
}
|
||||
|
||||
fn build_scene_runtime(
|
||||
viewport_width_px: usize,
|
||||
viewport_height_px: usize,
|
||||
scene: &SceneBank,
|
||||
) -> (SceneViewportCache, SceneViewportResolver) {
|
||||
let min_tile_px =
|
||||
scene.layers.iter().map(|layer| layer.tile_size as usize).min().unwrap_or(8);
|
||||
let cache_width_tiles = viewport_width_px.div_ceil(min_tile_px) + 5;
|
||||
let cache_height_tiles = viewport_height_px.div_ceil(min_tile_px) + 4;
|
||||
let hysteresis_safe_px = min_tile_px.saturating_sub(4) as i32;
|
||||
let hysteresis_trigger_px = (min_tile_px + 4) as i32;
|
||||
|
||||
(
|
||||
SceneViewportCache::new(scene, cache_width_tiles, cache_height_tiles),
|
||||
SceneViewportResolver::new(
|
||||
viewport_width_px as i32,
|
||||
viewport_height_px as i32,
|
||||
cache_width_tiles,
|
||||
cache_height_tiles,
|
||||
hysteresis_safe_px,
|
||||
hysteresis_trigger_px,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fn apply_refresh_requests(
|
||||
cache: &mut SceneViewportCache,
|
||||
scene: &SceneBank,
|
||||
refresh_requests: &[CacheRefreshRequest],
|
||||
) {
|
||||
for request in refresh_requests {
|
||||
match *request {
|
||||
CacheRefreshRequest::InvalidateLayer { layer_index } => {
|
||||
cache.layers[layer_index].invalidate_all();
|
||||
}
|
||||
CacheRefreshRequest::RefreshLine { layer_index, cache_y } => {
|
||||
cache.refresh_layer_line(scene, layer_index, cache_y);
|
||||
}
|
||||
CacheRefreshRequest::RefreshColumn { layer_index, cache_x } => {
|
||||
cache.refresh_layer_column(scene, layer_index, cache_x);
|
||||
}
|
||||
CacheRefreshRequest::RefreshRegion { layer_index, region } => {
|
||||
cache.refresh_layer_region(scene, layer_index, region);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::gfx::Gfx;
|
||||
use crate::memory_banks::{
|
||||
GlyphBankPoolAccess, GlyphBankPoolInstaller, MemoryBanks, SceneBankPoolInstaller,
|
||||
};
|
||||
use prometeu_hal::color::Color;
|
||||
use prometeu_hal::glyph_bank::{GlyphBank, TileSize};
|
||||
use prometeu_hal::scene_layer::{ParallaxFactor, SceneLayer};
|
||||
use prometeu_hal::tile::Tile;
|
||||
use prometeu_hal::tilemap::TileMap;
|
||||
|
||||
fn make_scene() -> SceneBank {
|
||||
make_scene_with_palette(1, 1, TileSize::Size8)
|
||||
}
|
||||
|
||||
fn make_scene_with_palette(
|
||||
glyph_bank_id: u8,
|
||||
palette_id: u8,
|
||||
tile_size: TileSize,
|
||||
) -> SceneBank {
|
||||
let layer = SceneLayer {
|
||||
active: true,
|
||||
glyph_bank_id,
|
||||
tile_size,
|
||||
parallax_factor: ParallaxFactor { x: 1.0, y: 0.5 },
|
||||
tilemap: TileMap {
|
||||
width: 2,
|
||||
height: 2,
|
||||
tiles: vec![
|
||||
Tile {
|
||||
active: true,
|
||||
glyph: Glyph { glyph_id: 0, palette_id },
|
||||
flip_x: false,
|
||||
flip_y: false,
|
||||
};
|
||||
4
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
SceneBank { layers: std::array::from_fn(|_| layer.clone()) }
|
||||
}
|
||||
|
||||
fn make_glyph_bank(tile_size: TileSize, palette_id: u8, color: Color) -> GlyphBank {
|
||||
let size = tile_size as usize;
|
||||
let mut bank = GlyphBank::new(tile_size, size, size);
|
||||
bank.palettes[palette_id as usize][1] = color;
|
||||
for pixel in &mut bank.pixel_indices {
|
||||
*pixel = 1;
|
||||
}
|
||||
bank
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn frame_composer_starts_unbound_with_empty_owned_state() {
|
||||
let frame_composer = FrameComposer::new(320, 180, Arc::new(MemoryBanks::new()));
|
||||
|
||||
assert_eq!(frame_composer.viewport_size(), (320, 180));
|
||||
assert_eq!(frame_composer.active_scene_id(), None);
|
||||
assert!(frame_composer.active_scene().is_none());
|
||||
assert_eq!(frame_composer.scene_status(), SceneStatus::Unbound);
|
||||
assert_eq!(frame_composer.camera(), (0, 0));
|
||||
assert!(frame_composer.cache().is_none());
|
||||
assert!(frame_composer.resolver().is_none());
|
||||
assert_eq!(frame_composer.sprite_controller().sprites().len(), 512);
|
||||
assert_eq!(frame_composer.sprite_controller().sprite_count(), 0);
|
||||
assert_eq!(frame_composer.sprite_controller().dropped_sprites(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn frame_composer_exposes_shared_scene_bank_access() {
|
||||
let banks = Arc::new(MemoryBanks::new());
|
||||
banks.install_scene_bank(3, Arc::new(make_scene()));
|
||||
|
||||
let frame_composer = FrameComposer::new(320, 180, banks);
|
||||
let scene =
|
||||
frame_composer.scene_bank_slot(3).expect("scene bank slot 3 should be resident");
|
||||
|
||||
assert_eq!(frame_composer.scene_bank_slot_count(), 16);
|
||||
assert_eq!(scene.layers[0].tile_size, TileSize::Size8);
|
||||
assert_eq!(scene.layers[0].parallax_factor.y, 0.5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bind_scene_stores_scene_identity_and_shared_reference() {
|
||||
let banks = Arc::new(MemoryBanks::new());
|
||||
banks.install_scene_bank(3, Arc::new(make_scene()));
|
||||
|
||||
let expected_scene =
|
||||
banks.scene_bank_slot(3).expect("scene bank slot 3 should be resident");
|
||||
let mut frame_composer = FrameComposer::new(320, 180, banks);
|
||||
|
||||
assert!(frame_composer.bind_scene(3));
|
||||
|
||||
assert_eq!(frame_composer.active_scene_id(), Some(3));
|
||||
assert!(Arc::ptr_eq(
|
||||
frame_composer.active_scene().expect("active scene should exist"),
|
||||
&expected_scene,
|
||||
));
|
||||
assert_eq!(frame_composer.scene_status(), SceneStatus::Available { scene_bank_id: 3 });
|
||||
assert!(frame_composer.cache().is_some());
|
||||
assert!(frame_composer.resolver().is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unbind_scene_clears_scene_and_cache_state() {
|
||||
let banks = Arc::new(MemoryBanks::new());
|
||||
banks.install_scene_bank(1, Arc::new(make_scene()));
|
||||
|
||||
let mut frame_composer = FrameComposer::new(320, 180, banks);
|
||||
assert!(frame_composer.bind_scene(1));
|
||||
|
||||
frame_composer.unbind_scene();
|
||||
|
||||
assert_eq!(frame_composer.active_scene_id(), None);
|
||||
assert!(frame_composer.active_scene().is_none());
|
||||
assert_eq!(frame_composer.scene_status(), SceneStatus::Unbound);
|
||||
assert!(frame_composer.cache().is_none());
|
||||
assert!(frame_composer.resolver().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_camera_stores_top_left_pixel_coordinates() {
|
||||
let mut frame_composer = FrameComposer::new(320, 180, Arc::new(MemoryBanks::new()));
|
||||
|
||||
frame_composer.set_camera(-12, 48);
|
||||
|
||||
assert_eq!(frame_composer.camera(), (-12, 48));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bind_scene_derives_cache_and_resolver_from_eight_pixel_layers() {
|
||||
let banks = Arc::new(MemoryBanks::new());
|
||||
banks.install_scene_bank(0, Arc::new(make_scene()));
|
||||
|
||||
let mut frame_composer = FrameComposer::new(320, 180, banks);
|
||||
assert!(frame_composer.bind_scene(0));
|
||||
|
||||
let cache = frame_composer.cache().expect("cache should exist for bound scene");
|
||||
assert_eq!((cache.width(), cache.height()), (45, 27));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_scene_binding_falls_back_to_no_scene_state() {
|
||||
let mut frame_composer = FrameComposer::new(320, 180, Arc::new(MemoryBanks::new()));
|
||||
|
||||
assert!(!frame_composer.bind_scene(7));
|
||||
|
||||
assert_eq!(frame_composer.scene_status(), SceneStatus::Unbound);
|
||||
assert!(frame_composer.cache().is_none());
|
||||
assert!(frame_composer.resolver().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sprite_controller_begin_frame_resets_sprite_count_and_buckets() {
|
||||
let mut controller = SpriteController::new();
|
||||
let emitted = controller.emit_sprite(Sprite {
|
||||
glyph: Glyph { glyph_id: 1, palette_id: 2 },
|
||||
x: 4,
|
||||
y: 5,
|
||||
layer: 2,
|
||||
bank_id: 3,
|
||||
active: false,
|
||||
flip_x: false,
|
||||
flip_y: false,
|
||||
priority: 1,
|
||||
});
|
||||
assert!(emitted);
|
||||
|
||||
controller.begin_frame();
|
||||
|
||||
assert_eq!(controller.frame_counter(), 1);
|
||||
assert_eq!(controller.sprite_count(), 0);
|
||||
assert_eq!(controller.dropped_sprites(), 0);
|
||||
assert!(controller.ordered_sprites().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sprite_controller_orders_by_layer_then_priority_then_fifo() {
|
||||
let mut controller = SpriteController::new();
|
||||
controller.begin_frame();
|
||||
|
||||
assert!(controller.emit_sprite(Sprite {
|
||||
glyph: Glyph { glyph_id: 10, palette_id: 0 },
|
||||
x: 0,
|
||||
y: 0,
|
||||
layer: 1,
|
||||
bank_id: 0,
|
||||
active: false,
|
||||
flip_x: false,
|
||||
flip_y: false,
|
||||
priority: 2,
|
||||
}));
|
||||
assert!(controller.emit_sprite(Sprite {
|
||||
glyph: Glyph { glyph_id: 11, palette_id: 0 },
|
||||
x: 0,
|
||||
y: 0,
|
||||
layer: 0,
|
||||
bank_id: 0,
|
||||
active: false,
|
||||
flip_x: false,
|
||||
flip_y: false,
|
||||
priority: 3,
|
||||
}));
|
||||
assert!(controller.emit_sprite(Sprite {
|
||||
glyph: Glyph { glyph_id: 12, palette_id: 0 },
|
||||
x: 0,
|
||||
y: 0,
|
||||
layer: 1,
|
||||
bank_id: 0,
|
||||
active: false,
|
||||
flip_x: false,
|
||||
flip_y: false,
|
||||
priority: 1,
|
||||
}));
|
||||
assert!(controller.emit_sprite(Sprite {
|
||||
glyph: Glyph { glyph_id: 13, palette_id: 0 },
|
||||
x: 0,
|
||||
y: 0,
|
||||
layer: 1,
|
||||
bank_id: 0,
|
||||
active: false,
|
||||
flip_x: false,
|
||||
flip_y: false,
|
||||
priority: 2,
|
||||
}));
|
||||
|
||||
let ordered = controller.ordered_sprites();
|
||||
let ordered_ids: Vec<u16> = ordered.iter().map(|sprite| sprite.glyph.glyph_id).collect();
|
||||
|
||||
assert_eq!(ordered_ids, vec![11, 12, 10, 13]);
|
||||
assert!(ordered.iter().all(|sprite| sprite.active));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sprite_controller_drops_overflow_without_panicking() {
|
||||
let mut controller = SpriteController::new();
|
||||
controller.begin_frame();
|
||||
|
||||
for glyph_id in 0..512 {
|
||||
assert!(controller.emit_sprite(Sprite {
|
||||
glyph: Glyph { glyph_id, palette_id: 0 },
|
||||
x: 0,
|
||||
y: 0,
|
||||
layer: 0,
|
||||
bank_id: 0,
|
||||
active: false,
|
||||
flip_x: false,
|
||||
flip_y: false,
|
||||
priority: 0,
|
||||
}));
|
||||
}
|
||||
|
||||
let overflowed = controller.emit_sprite(Sprite {
|
||||
glyph: Glyph { glyph_id: 999, palette_id: 0 },
|
||||
x: 0,
|
||||
y: 0,
|
||||
layer: 0,
|
||||
bank_id: 0,
|
||||
active: false,
|
||||
flip_x: false,
|
||||
flip_y: false,
|
||||
priority: 0,
|
||||
});
|
||||
|
||||
assert!(!overflowed);
|
||||
assert_eq!(controller.sprite_count(), 512);
|
||||
assert_eq!(controller.dropped_sprites(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn frame_composer_emits_ordered_sprites_for_rendering() {
|
||||
let mut frame_composer = FrameComposer::new(320, 180, Arc::new(MemoryBanks::new()));
|
||||
frame_composer.begin_frame();
|
||||
|
||||
assert!(frame_composer.emit_sprite(Sprite {
|
||||
glyph: Glyph { glyph_id: 21, palette_id: 0 },
|
||||
x: 0,
|
||||
y: 0,
|
||||
layer: 2,
|
||||
bank_id: 1,
|
||||
active: false,
|
||||
flip_x: false,
|
||||
flip_y: false,
|
||||
priority: 1,
|
||||
}));
|
||||
assert!(frame_composer.emit_sprite(Sprite {
|
||||
glyph: Glyph { glyph_id: 20, palette_id: 0 },
|
||||
x: 0,
|
||||
y: 0,
|
||||
layer: 1,
|
||||
bank_id: 1,
|
||||
active: false,
|
||||
flip_x: false,
|
||||
flip_y: false,
|
||||
priority: 0,
|
||||
}));
|
||||
|
||||
let ordered = frame_composer.ordered_sprites();
|
||||
|
||||
assert_eq!(ordered.len(), 2);
|
||||
assert_eq!(ordered[0].glyph.glyph_id, 20);
|
||||
assert_eq!(ordered[1].glyph.glyph_id, 21);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_frame_without_scene_uses_sprite_only_path() {
|
||||
let banks = Arc::new(MemoryBanks::new());
|
||||
banks.install_glyph_bank(1, Arc::new(make_glyph_bank(TileSize::Size8, 3, Color::WHITE)));
|
||||
|
||||
let mut frame_composer =
|
||||
FrameComposer::new(16, 16, Arc::clone(&banks) as Arc<dyn SceneBankPoolAccess>);
|
||||
frame_composer.begin_frame();
|
||||
assert!(frame_composer.emit_sprite(Sprite {
|
||||
glyph: Glyph { glyph_id: 0, palette_id: 3 },
|
||||
x: 0,
|
||||
y: 0,
|
||||
layer: 0,
|
||||
bank_id: 1,
|
||||
active: false,
|
||||
flip_x: false,
|
||||
flip_y: false,
|
||||
priority: 0,
|
||||
}));
|
||||
|
||||
let mut gfx = Gfx::new(16, 16, Arc::clone(&banks) as Arc<dyn GlyphBankPoolAccess>);
|
||||
gfx.scene_fade_level = 31;
|
||||
gfx.hud_fade_level = 31;
|
||||
|
||||
frame_composer.render_frame(&mut gfx);
|
||||
gfx.present();
|
||||
|
||||
assert_eq!(gfx.front_buffer()[0], Color::WHITE.raw());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_frame_with_scene_applies_refreshes_before_composition() {
|
||||
let banks = Arc::new(MemoryBanks::new());
|
||||
banks.install_glyph_bank(0, Arc::new(make_glyph_bank(TileSize::Size8, 2, Color::BLUE)));
|
||||
banks.install_scene_bank(0, Arc::new(make_scene_with_palette(0, 2, TileSize::Size8)));
|
||||
|
||||
let mut frame_composer =
|
||||
FrameComposer::new(16, 16, Arc::clone(&banks) as Arc<dyn SceneBankPoolAccess>);
|
||||
assert!(frame_composer.bind_scene(0));
|
||||
|
||||
let mut gfx = Gfx::new(16, 16, Arc::clone(&banks) as Arc<dyn GlyphBankPoolAccess>);
|
||||
gfx.scene_fade_level = 31;
|
||||
gfx.hud_fade_level = 31;
|
||||
|
||||
frame_composer.render_frame(&mut gfx);
|
||||
gfx.present();
|
||||
|
||||
assert!(
|
||||
frame_composer
|
||||
.cache()
|
||||
.expect("cache should exist")
|
||||
.layers
|
||||
.iter()
|
||||
.all(|layer| layer.valid)
|
||||
);
|
||||
assert_eq!(gfx.front_buffer()[0], Color::BLUE.raw());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_frame_survives_scene_transition_through_unbind_and_rebind() {
|
||||
let banks = Arc::new(MemoryBanks::new());
|
||||
banks.install_glyph_bank(0, Arc::new(make_glyph_bank(TileSize::Size8, 1, Color::RED)));
|
||||
banks.install_glyph_bank(1, Arc::new(make_glyph_bank(TileSize::Size8, 2, Color::BLUE)));
|
||||
banks.install_glyph_bank(2, Arc::new(make_glyph_bank(TileSize::Size8, 3, Color::WHITE)));
|
||||
banks.install_scene_bank(0, Arc::new(make_scene_with_palette(0, 1, TileSize::Size8)));
|
||||
banks.install_scene_bank(1, Arc::new(make_scene_with_palette(1, 2, TileSize::Size8)));
|
||||
|
||||
let mut frame_composer =
|
||||
FrameComposer::new(16, 16, Arc::clone(&banks) as Arc<dyn SceneBankPoolAccess>);
|
||||
let mut gfx = Gfx::new(16, 16, Arc::clone(&banks) as Arc<dyn GlyphBankPoolAccess>);
|
||||
gfx.scene_fade_level = 31;
|
||||
gfx.hud_fade_level = 31;
|
||||
|
||||
assert!(frame_composer.bind_scene(0));
|
||||
frame_composer.render_frame(&mut gfx);
|
||||
gfx.present();
|
||||
assert_eq!(gfx.front_buffer()[0], Color::RED.raw());
|
||||
|
||||
frame_composer.unbind_scene();
|
||||
frame_composer.begin_frame();
|
||||
assert!(frame_composer.emit_sprite(Sprite {
|
||||
glyph: Glyph { glyph_id: 0, palette_id: 3 },
|
||||
x: 0,
|
||||
y: 0,
|
||||
layer: 0,
|
||||
bank_id: 2,
|
||||
active: false,
|
||||
flip_x: false,
|
||||
flip_y: false,
|
||||
priority: 0,
|
||||
}));
|
||||
frame_composer.render_frame(&mut gfx);
|
||||
gfx.present();
|
||||
assert_eq!(gfx.front_buffer()[0], Color::WHITE.raw());
|
||||
|
||||
frame_composer.begin_frame();
|
||||
assert!(frame_composer.bind_scene(1));
|
||||
frame_composer.render_frame(&mut gfx);
|
||||
gfx.present();
|
||||
assert_eq!(gfx.front_buffer()[0], Color::BLUE.raw());
|
||||
}
|
||||
}
|
||||
1241
crates/console/prometeu-drivers/src/gfx.rs
Normal file
1241
crates/console/prometeu-drivers/src/gfx.rs
Normal file
File diff suppressed because it is too large
Load Diff
39
crates/console/prometeu-drivers/src/gfx_overlay.rs
Normal file
39
crates/console/prometeu-drivers/src/gfx_overlay.rs
Normal file
@ -0,0 +1,39 @@
|
||||
use crate::gfx::BlendMode;
|
||||
use prometeu_hal::color::Color;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum OverlayCommand {
|
||||
FillRectBlend { x: i32, y: i32, w: i32, h: i32, color: Color, mode: BlendMode },
|
||||
DrawLine { x0: i32, y0: i32, x1: i32, y1: i32, color: Color },
|
||||
DrawCircle { x: i32, y: i32, r: i32, color: Color },
|
||||
DrawDisc { x: i32, y: i32, r: i32, border_color: Color, fill_color: Color },
|
||||
DrawSquare { x: i32, y: i32, w: i32, h: i32, border_color: Color, fill_color: Color },
|
||||
DrawText { x: i32, y: i32, text: String, color: Color },
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct DeferredGfxOverlay {
|
||||
commands: Vec<OverlayCommand>,
|
||||
}
|
||||
|
||||
impl DeferredGfxOverlay {
|
||||
pub fn begin_frame(&mut self) {
|
||||
self.commands.clear();
|
||||
}
|
||||
|
||||
pub fn push(&mut self, command: OverlayCommand) {
|
||||
self.commands.push(command);
|
||||
}
|
||||
|
||||
pub fn commands(&self) -> &[OverlayCommand] {
|
||||
&self.commands
|
||||
}
|
||||
|
||||
pub fn command_count(&self) -> usize {
|
||||
self.commands.len()
|
||||
}
|
||||
|
||||
pub fn take_commands(&mut self) -> Vec<OverlayCommand> {
|
||||
std::mem::take(&mut self.commands)
|
||||
}
|
||||
}
|
||||
240
crates/console/prometeu-drivers/src/hardware.rs
Normal file
240
crates/console/prometeu-drivers/src/hardware.rs
Normal file
@ -0,0 +1,240 @@
|
||||
use crate::asset::AssetManager;
|
||||
use crate::audio::Audio;
|
||||
use crate::frame_composer::FrameComposer;
|
||||
use crate::gfx::Gfx;
|
||||
use crate::memory_banks::{
|
||||
GlyphBankPoolAccess, GlyphBankPoolInstaller, MemoryBanks, SceneBankPoolAccess,
|
||||
SceneBankPoolInstaller, SoundBankPoolAccess, SoundBankPoolInstaller,
|
||||
};
|
||||
use crate::pad::Pad;
|
||||
use crate::touch::Touch;
|
||||
use prometeu_hal::cartridge::AssetsPayloadSource;
|
||||
use prometeu_hal::sprite::Sprite;
|
||||
use prometeu_hal::{AssetBridge, AudioBridge, GfxBridge, HardwareBridge, PadBridge, TouchBridge};
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Aggregate structure for all virtual hardware peripherals.
|
||||
///
|
||||
/// This struct represents the "Mainboard" of the PROMETEU console.
|
||||
/// It acts as a container for all hardware subsystems. In the Prometeu
|
||||
/// architecture, hardware is decoupled from the OS and VM, allowing
|
||||
/// for easier testing and different host implementations (Desktop, Web, etc.).
|
||||
///
|
||||
/// ### Console Specifications:
|
||||
/// - **Resolution**: 320x180 (16:9 Aspect Ratio).
|
||||
/// - **Color Depth**: RGB565 (16-bit).
|
||||
/// - **Audio**: Stereo, Command-based mixing.
|
||||
/// - **Input**: 12-button Digital Gamepad + Absolute Touch/Mouse.
|
||||
pub struct Hardware {
|
||||
/// The Graphics Processing Unit (GPU). Handles drawing primitives, sprites, and tilemaps.
|
||||
pub gfx: Gfx,
|
||||
/// Canonical frame orchestration owner for scene/camera/cache/resolver/sprites.
|
||||
pub frame_composer: FrameComposer,
|
||||
/// The Sound Processing Unit (SPU). Manages sample playback and volume control.
|
||||
pub audio: Audio,
|
||||
/// The standard digital gamepad. Provides state for D-Pad, face buttons, and triggers.
|
||||
pub pad: Pad,
|
||||
/// The absolute pointer input device (Mouse/Touchscreen).
|
||||
pub touch: Touch,
|
||||
/// The Asset Management system (DMA). Handles loading data into VRAM (GlyphBanks) and ARAM (SoundBanks).
|
||||
pub assets: AssetManager,
|
||||
}
|
||||
|
||||
impl Default for Hardware {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl HardwareBridge for Hardware {
|
||||
fn begin_frame(&mut self) {
|
||||
self.gfx.begin_overlay_frame();
|
||||
self.frame_composer.begin_frame();
|
||||
}
|
||||
|
||||
fn bind_scene(&mut self, scene_bank_id: usize) -> bool {
|
||||
self.frame_composer.bind_scene(scene_bank_id)
|
||||
}
|
||||
|
||||
fn unbind_scene(&mut self) {
|
||||
self.frame_composer.unbind_scene();
|
||||
}
|
||||
|
||||
fn set_camera(&mut self, x: i32, y: i32) {
|
||||
self.frame_composer.set_camera(x, y);
|
||||
}
|
||||
|
||||
fn emit_sprite(&mut self, sprite: Sprite) -> bool {
|
||||
self.frame_composer.emit_sprite(sprite)
|
||||
}
|
||||
|
||||
fn render_frame(&mut self) {
|
||||
self.frame_composer.render_frame(&mut self.gfx);
|
||||
self.gfx.drain_overlay_debug();
|
||||
}
|
||||
|
||||
fn has_glyph_bank(&self, bank_id: usize) -> bool {
|
||||
self.gfx.glyph_banks.glyph_bank_slot(bank_id).is_some()
|
||||
}
|
||||
|
||||
fn gfx(&self) -> &dyn GfxBridge {
|
||||
&self.gfx
|
||||
}
|
||||
fn gfx_mut(&mut self) -> &mut dyn GfxBridge {
|
||||
&mut self.gfx
|
||||
}
|
||||
|
||||
fn audio(&self) -> &dyn AudioBridge {
|
||||
&self.audio
|
||||
}
|
||||
fn audio_mut(&mut self) -> &mut dyn AudioBridge {
|
||||
&mut self.audio
|
||||
}
|
||||
|
||||
fn pad(&self) -> &dyn PadBridge {
|
||||
&self.pad
|
||||
}
|
||||
fn pad_mut(&mut self) -> &mut dyn PadBridge {
|
||||
&mut self.pad
|
||||
}
|
||||
|
||||
fn touch(&self) -> &dyn TouchBridge {
|
||||
&self.touch
|
||||
}
|
||||
fn touch_mut(&mut self) -> &mut dyn TouchBridge {
|
||||
&mut self.touch
|
||||
}
|
||||
|
||||
fn assets(&self) -> &dyn AssetBridge {
|
||||
&self.assets
|
||||
}
|
||||
fn assets_mut(&mut self) -> &mut dyn AssetBridge {
|
||||
&mut self.assets
|
||||
}
|
||||
}
|
||||
|
||||
impl Hardware {
|
||||
/// Internal hardware width in pixels.
|
||||
pub const W: usize = 320;
|
||||
/// Internal hardware height in pixels.
|
||||
pub const H: usize = 180;
|
||||
|
||||
/// Creates a fresh hardware instance with default settings.
|
||||
pub fn new() -> Self {
|
||||
Self::new_with_memory_banks(Arc::new(MemoryBanks::new()))
|
||||
}
|
||||
|
||||
/// Creates hardware with explicit shared bank ownership.
|
||||
pub fn new_with_memory_banks(memory_banks: Arc<MemoryBanks>) -> Self {
|
||||
Self {
|
||||
gfx: Gfx::new(
|
||||
Self::W,
|
||||
Self::H,
|
||||
Arc::clone(&memory_banks) as Arc<dyn GlyphBankPoolAccess>,
|
||||
),
|
||||
frame_composer: FrameComposer::new(
|
||||
Self::W,
|
||||
Self::H,
|
||||
Arc::clone(&memory_banks) as Arc<dyn SceneBankPoolAccess>,
|
||||
),
|
||||
audio: Audio::new(Arc::clone(&memory_banks) as Arc<dyn SoundBankPoolAccess>),
|
||||
pad: Pad::default(),
|
||||
touch: Touch::default(),
|
||||
assets: AssetManager::new(
|
||||
vec![],
|
||||
AssetsPayloadSource::empty(),
|
||||
Arc::clone(&memory_banks) as Arc<dyn GlyphBankPoolInstaller>,
|
||||
Arc::clone(&memory_banks) as Arc<dyn SoundBankPoolInstaller>,
|
||||
Arc::clone(&memory_banks) as Arc<dyn SceneBankPoolInstaller>,
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::memory_banks::{
|
||||
GlyphBankPoolInstaller, SceneBankPoolAccess, SceneBankPoolInstaller,
|
||||
};
|
||||
use prometeu_hal::color::Color;
|
||||
use prometeu_hal::glyph::Glyph;
|
||||
use prometeu_hal::glyph_bank::{GlyphBank, TileSize};
|
||||
use prometeu_hal::scene_bank::SceneBank;
|
||||
use prometeu_hal::scene_layer::{ParallaxFactor, SceneLayer};
|
||||
use prometeu_hal::scene_viewport_cache::SceneViewportCache;
|
||||
use prometeu_hal::scene_viewport_resolver::SceneViewportResolver;
|
||||
use prometeu_hal::tile::Tile;
|
||||
use prometeu_hal::tilemap::TileMap;
|
||||
|
||||
fn make_glyph_bank() -> GlyphBank {
|
||||
let mut bank = GlyphBank::new(TileSize::Size8, 8, 8);
|
||||
bank.palettes[0][1] = Color::RED;
|
||||
for pixel in &mut bank.pixel_indices {
|
||||
*pixel = 1;
|
||||
}
|
||||
bank
|
||||
}
|
||||
|
||||
fn make_scene() -> SceneBank {
|
||||
let layer = SceneLayer {
|
||||
active: true,
|
||||
glyph_bank_id: 0,
|
||||
tile_size: TileSize::Size8,
|
||||
parallax_factor: ParallaxFactor { x: 1.0, y: 1.0 },
|
||||
tilemap: TileMap {
|
||||
width: 4,
|
||||
height: 4,
|
||||
tiles: vec![
|
||||
Tile {
|
||||
active: true,
|
||||
glyph: Glyph { glyph_id: 0, palette_id: 0 },
|
||||
flip_x: false,
|
||||
flip_y: false,
|
||||
};
|
||||
16
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
SceneBank { layers: std::array::from_fn(|_| layer.clone()) }
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hardware_can_render_scene_from_shared_scene_bank_pipeline() {
|
||||
let banks = Arc::new(MemoryBanks::new());
|
||||
banks.install_glyph_bank(0, Arc::new(make_glyph_bank()));
|
||||
banks.install_scene_bank(0, Arc::new(make_scene()));
|
||||
|
||||
let mut hardware = Hardware::new_with_memory_banks(Arc::clone(&banks));
|
||||
let scene = banks.scene_bank_slot(0).expect("scene bank slot 0 should be resident");
|
||||
let mut cache = SceneViewportCache::new(&scene, 4, 4);
|
||||
cache.materialize_all_layers(&scene);
|
||||
|
||||
let mut resolver = SceneViewportResolver::new(16, 16, 4, 4, 12, 20);
|
||||
let update = resolver.update(&scene, 0, 0);
|
||||
|
||||
hardware.gfx.scene_fade_level = 31;
|
||||
hardware.gfx.hud_fade_level = 31;
|
||||
hardware.gfx.render_scene_from_cache(&cache, &update);
|
||||
hardware.gfx.present();
|
||||
|
||||
assert_eq!(hardware.gfx.front_buffer()[0], Color::RED.raw());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hardware_constructs_frame_composer_with_shared_scene_bank_access() {
|
||||
let banks = Arc::new(MemoryBanks::new());
|
||||
banks.install_scene_bank(2, Arc::new(make_scene()));
|
||||
|
||||
let hardware = Hardware::new_with_memory_banks(banks);
|
||||
let scene = hardware
|
||||
.frame_composer
|
||||
.scene_bank_slot(2)
|
||||
.expect("scene bank slot 2 should be resident");
|
||||
|
||||
assert_eq!(hardware.frame_composer.viewport_size(), (Hardware::W, Hardware::H));
|
||||
assert_eq!(hardware.frame_composer.scene_bank_slot_count(), 16);
|
||||
assert_eq!(scene.layers[0].tile_size, TileSize::Size8);
|
||||
}
|
||||
}
|
||||
20
crates/console/prometeu-drivers/src/lib.rs
Normal file
20
crates/console/prometeu-drivers/src/lib.rs
Normal file
@ -0,0 +1,20 @@
|
||||
mod asset;
|
||||
mod audio;
|
||||
mod frame_composer;
|
||||
mod gfx;
|
||||
mod gfx_overlay;
|
||||
pub mod hardware;
|
||||
mod memory_banks;
|
||||
mod pad;
|
||||
mod touch;
|
||||
|
||||
pub use crate::asset::AssetManager;
|
||||
pub use crate::audio::{Audio, AudioCommand, Channel, MAX_CHANNELS, OUTPUT_SAMPLE_RATE};
|
||||
pub use crate::frame_composer::{FrameComposer, SceneStatus, SpriteController};
|
||||
pub use crate::gfx::Gfx;
|
||||
pub use crate::gfx_overlay::{DeferredGfxOverlay, OverlayCommand};
|
||||
pub use crate::memory_banks::{
|
||||
GlyphBankPoolAccess, GlyphBankPoolInstaller, MemoryBanks, SceneBankPoolAccess,
|
||||
SceneBankPoolInstaller, SoundBankPoolAccess, SoundBankPoolInstaller,
|
||||
};
|
||||
pub use crate::pad::Pad;
|
||||
134
crates/console/prometeu-drivers/src/memory_banks.rs
Normal file
134
crates/console/prometeu-drivers/src/memory_banks.rs
Normal file
@ -0,0 +1,134 @@
|
||||
use prometeu_hal::glyph_bank::GlyphBank;
|
||||
use prometeu_hal::scene_bank::SceneBank;
|
||||
use prometeu_hal::sound_bank::SoundBank;
|
||||
use std::sync::{Arc, RwLock};
|
||||
|
||||
/// Non-generic interface for peripherals to access graphical glyph banks.
|
||||
pub trait GlyphBankPoolAccess: Send + Sync {
|
||||
/// Returns a reference to the resident GlyphBank in the specified slot, if any.
|
||||
fn glyph_bank_slot(&self, slot: usize) -> Option<Arc<GlyphBank>>;
|
||||
/// Returns the total number of slots available in this bank.
|
||||
fn glyph_bank_slot_count(&self) -> usize;
|
||||
}
|
||||
|
||||
/// Non-generic interface for the AssetManager to install graphical glyph banks.
|
||||
pub trait GlyphBankPoolInstaller: Send + Sync {
|
||||
/// Atomically swaps the resident GlyphBank in the specified slot.
|
||||
fn install_glyph_bank(&self, slot: usize, bank: Arc<GlyphBank>);
|
||||
}
|
||||
|
||||
/// Non-generic interface for peripherals to access sound banks.
|
||||
pub trait SoundBankPoolAccess: Send + Sync {
|
||||
/// Returns a reference to the resident SoundBank in the specified slot, if any.
|
||||
fn sound_bank_slot(&self, slot: usize) -> Option<Arc<SoundBank>>;
|
||||
/// Returns the total number of slots available in this bank.
|
||||
fn sound_bank_slot_count(&self) -> usize;
|
||||
}
|
||||
|
||||
/// Non-generic interface for the AssetManager to install sound banks.
|
||||
pub trait SoundBankPoolInstaller: Send + Sync {
|
||||
/// Atomically swaps the resident SoundBank in the specified slot.
|
||||
fn install_sound_bank(&self, slot: usize, bank: Arc<SoundBank>);
|
||||
}
|
||||
|
||||
/// Non-generic interface for peripherals to access scene banks.
|
||||
pub trait SceneBankPoolAccess: Send + Sync {
|
||||
/// Returns a reference to the resident SceneBank in the specified slot, if any.
|
||||
fn scene_bank_slot(&self, slot: usize) -> Option<Arc<SceneBank>>;
|
||||
/// Returns the total number of slots available in this bank.
|
||||
fn scene_bank_slot_count(&self) -> usize;
|
||||
}
|
||||
|
||||
/// Non-generic interface for the AssetManager to install scene banks.
|
||||
pub trait SceneBankPoolInstaller: Send + Sync {
|
||||
/// Atomically swaps the resident SceneBank in the specified slot.
|
||||
fn install_scene_bank(&self, slot: usize, bank: Arc<SceneBank>);
|
||||
}
|
||||
|
||||
/// Centralized container for all hardware memory banks.
|
||||
///
|
||||
/// MemoryBanks represent the actual hardware slot state.
|
||||
/// Peripherals consume this state via narrow, non-generic traits.
|
||||
/// AssetManager coordinates residency and installs assets into these slots.
|
||||
pub struct MemoryBanks {
|
||||
glyph_bank_pool: Arc<RwLock<[Option<Arc<GlyphBank>>; 16]>>,
|
||||
sound_bank_pool: Arc<RwLock<[Option<Arc<SoundBank>>; 16]>>,
|
||||
scene_bank_pool: Arc<RwLock<[Option<Arc<SceneBank>>; 16]>>,
|
||||
}
|
||||
|
||||
impl Default for MemoryBanks {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl MemoryBanks {
|
||||
/// Creates a new set of memory banks with empty slots.
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
glyph_bank_pool: Arc::new(RwLock::new(std::array::from_fn(|_| None))),
|
||||
sound_bank_pool: Arc::new(RwLock::new(std::array::from_fn(|_| None))),
|
||||
scene_bank_pool: Arc::new(RwLock::new(std::array::from_fn(|_| None))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl GlyphBankPoolAccess for MemoryBanks {
|
||||
fn glyph_bank_slot(&self, slot: usize) -> Option<Arc<GlyphBank>> {
|
||||
let pool = self.glyph_bank_pool.read().unwrap();
|
||||
pool.get(slot).and_then(|s| s.as_ref().map(Arc::clone))
|
||||
}
|
||||
|
||||
fn glyph_bank_slot_count(&self) -> usize {
|
||||
16
|
||||
}
|
||||
}
|
||||
|
||||
impl GlyphBankPoolInstaller for MemoryBanks {
|
||||
fn install_glyph_bank(&self, slot: usize, bank: Arc<GlyphBank>) {
|
||||
let mut pool = self.glyph_bank_pool.write().unwrap();
|
||||
if slot < 16 {
|
||||
pool[slot] = Some(bank);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SoundBankPoolAccess for MemoryBanks {
|
||||
fn sound_bank_slot(&self, slot: usize) -> Option<Arc<SoundBank>> {
|
||||
let pool = self.sound_bank_pool.read().unwrap();
|
||||
pool.get(slot).and_then(|s| s.as_ref().map(Arc::clone))
|
||||
}
|
||||
|
||||
fn sound_bank_slot_count(&self) -> usize {
|
||||
16
|
||||
}
|
||||
}
|
||||
|
||||
impl SoundBankPoolInstaller for MemoryBanks {
|
||||
fn install_sound_bank(&self, slot: usize, bank: Arc<SoundBank>) {
|
||||
let mut pool = self.sound_bank_pool.write().unwrap();
|
||||
if slot < 16 {
|
||||
pool[slot] = Some(bank);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SceneBankPoolAccess for MemoryBanks {
|
||||
fn scene_bank_slot(&self, slot: usize) -> Option<Arc<SceneBank>> {
|
||||
let pool = self.scene_bank_pool.read().unwrap();
|
||||
pool.get(slot).and_then(|s| s.as_ref().map(Arc::clone))
|
||||
}
|
||||
|
||||
fn scene_bank_slot_count(&self) -> usize {
|
||||
16
|
||||
}
|
||||
}
|
||||
|
||||
impl SceneBankPoolInstaller for MemoryBanks {
|
||||
fn install_scene_bank(&self, slot: usize, bank: Arc<SceneBank>) {
|
||||
let mut pool = self.scene_bank_pool.write().unwrap();
|
||||
if slot < 16 {
|
||||
pool[slot] = Some(bank);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,5 +1,5 @@
|
||||
use crate::model::Button;
|
||||
use crate::hardware::input_signal::InputSignals;
|
||||
use prometeu_hal::button::Button;
|
||||
use prometeu_hal::{InputSignals, PadBridge};
|
||||
|
||||
#[derive(Default, Clone, Copy, Debug)]
|
||||
pub struct Pad {
|
||||
@ -10,13 +10,59 @@ pub struct Pad {
|
||||
|
||||
pub a: Button, // ps: square
|
||||
pub b: Button, // ps: circle
|
||||
pub x: Button, // ps: triangle
|
||||
pub x: Button, // ps: triangle
|
||||
pub y: Button, // ps: cross
|
||||
pub l: Button, // ps: R
|
||||
pub r: Button, // ps: L
|
||||
|
||||
pub start: Button,
|
||||
pub select: Button,
|
||||
pub select: Button,
|
||||
}
|
||||
|
||||
impl PadBridge for Pad {
|
||||
fn begin_frame(&mut self, signals: &InputSignals) {
|
||||
self.begin_frame(signals)
|
||||
}
|
||||
fn any(&self) -> bool {
|
||||
self.any()
|
||||
}
|
||||
|
||||
fn up(&self) -> &Button {
|
||||
&self.up
|
||||
}
|
||||
fn down(&self) -> &Button {
|
||||
&self.down
|
||||
}
|
||||
fn left(&self) -> &Button {
|
||||
&self.left
|
||||
}
|
||||
fn right(&self) -> &Button {
|
||||
&self.right
|
||||
}
|
||||
fn a(&self) -> &Button {
|
||||
&self.a
|
||||
}
|
||||
fn b(&self) -> &Button {
|
||||
&self.b
|
||||
}
|
||||
fn x(&self) -> &Button {
|
||||
&self.x
|
||||
}
|
||||
fn y(&self) -> &Button {
|
||||
&self.y
|
||||
}
|
||||
fn l(&self) -> &Button {
|
||||
&self.l
|
||||
}
|
||||
fn r(&self) -> &Button {
|
||||
&self.r
|
||||
}
|
||||
fn start(&self) -> &Button {
|
||||
&self.start
|
||||
}
|
||||
fn select(&self) -> &Button {
|
||||
&self.select
|
||||
}
|
||||
}
|
||||
|
||||
impl Pad {
|
||||
@ -25,18 +71,18 @@ impl Pad {
|
||||
self.down.begin_frame(signals.down_signal);
|
||||
self.left.begin_frame(signals.left_signal);
|
||||
self.right.begin_frame(signals.right_signal);
|
||||
|
||||
|
||||
self.a.begin_frame(signals.a_signal);
|
||||
self.b.begin_frame(signals.b_signal);
|
||||
self.x.begin_frame(signals.x_signal);
|
||||
self.y.begin_frame(signals.y_signal);
|
||||
self.l.begin_frame(signals.l_signal);
|
||||
self.r.begin_frame(signals.r_signal);
|
||||
|
||||
|
||||
self.start.begin_frame(signals.start_signal);
|
||||
self.select.begin_frame(signals.select_signal);
|
||||
self.select.begin_frame(signals.select_signal);
|
||||
}
|
||||
|
||||
|
||||
pub fn any(&self) -> bool {
|
||||
self.a.down
|
||||
|| self.b.down
|
||||
@ -48,5 +94,3 @@ impl Pad {
|
||||
|| self.select.down
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
33
crates/console/prometeu-drivers/src/touch.rs
Normal file
33
crates/console/prometeu-drivers/src/touch.rs
Normal file
@ -0,0 +1,33 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
14
crates/console/prometeu-firmware/Cargo.toml
Normal file
14
crates/console/prometeu-firmware/Cargo.toml
Normal file
@ -0,0 +1,14 @@
|
||||
[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" }
|
||||
10
crates/console/prometeu-firmware/src/firmware/boot_target.rs
Normal file
10
crates/console/prometeu-firmware/src/firmware/boot_target.rs
Normal file
@ -0,0 +1,10 @@
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
||||
pub enum BootTarget {
|
||||
#[default]
|
||||
Hub,
|
||||
Cartridge {
|
||||
path: String,
|
||||
debug: bool,
|
||||
debug_port: u16,
|
||||
},
|
||||
}
|
||||
271
crates/console/prometeu-firmware/src/firmware/firmware.rs
Normal file
271
crates/console/prometeu-firmware/src/firmware/firmware.rs
Normal file
@ -0,0 +1,271 @@
|
||||
use crate::firmware::boot_target::BootTarget;
|
||||
use crate::firmware::firmware_state::{FirmwareState, LoadCartridgeStep, ResetStep};
|
||||
use crate::firmware::prometeu_context::PrometeuContext;
|
||||
use prometeu_hal::cartridge::Cartridge;
|
||||
use prometeu_hal::telemetry::CertificationConfig;
|
||||
use prometeu_hal::{HardwareBridge, InputSignals};
|
||||
use prometeu_system::{PrometeuHub, VirtualMachineRuntime};
|
||||
use prometeu_vm::VirtualMachine;
|
||||
|
||||
/// PROMETEU Firmware.
|
||||
///
|
||||
/// The central orchestrator of the console. The firmware acts as the
|
||||
/// "Control Unit", managing the high-level state machine of the system.
|
||||
///
|
||||
/// It is responsible for transitioning between different modes of operation,
|
||||
/// such as showing the splash screen, running the Hub (launcher), or
|
||||
/// executing a game/app.
|
||||
///
|
||||
/// ### Execution Loop:
|
||||
/// The firmware is designed to be ticked once per frame (60Hz). During each
|
||||
/// tick, it:
|
||||
/// 1. Updates peripherals with the latest input signals.
|
||||
/// 2. Delegates the logic update to the current active state.
|
||||
/// 3. Handles state transitions (e.g., from Loading to Playing).
|
||||
pub struct Firmware {
|
||||
/// The execution engine (PVM) for user applications.
|
||||
pub vm: VirtualMachine,
|
||||
/// The underlying OS services (Syscalls, Filesystem, Telemetry).
|
||||
pub os: VirtualMachineRuntime,
|
||||
/// The internal state of the system launcher (Hub).
|
||||
pub hub: PrometeuHub,
|
||||
/// The current operational state (e.g., Reset, SplashScreen, GameRunning).
|
||||
pub state: FirmwareState,
|
||||
/// The desired application to run after boot (Hub or specific Cartridge).
|
||||
pub boot_target: BootTarget,
|
||||
/// State-machine lifecycle tracker.
|
||||
state_initialized: bool,
|
||||
}
|
||||
|
||||
impl Firmware {
|
||||
/// Initializes the firmware in the `Reset` state.
|
||||
pub fn new(cap_config: Option<CertificationConfig>) -> Self {
|
||||
Self {
|
||||
vm: VirtualMachine::default(),
|
||||
os: VirtualMachineRuntime::new(cap_config),
|
||||
hub: PrometeuHub::new(),
|
||||
state: FirmwareState::Reset(ResetStep),
|
||||
boot_target: BootTarget::Hub,
|
||||
state_initialized: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// The main entry point for the Host to advance the system logic.
|
||||
///
|
||||
/// This method is called exactly once per Host frame (60Hz).
|
||||
/// It updates peripheral signals and delegates the logic to the current state.
|
||||
pub fn tick(&mut self, signals: &InputSignals, hw: &mut dyn HardwareBridge) {
|
||||
// 0. Process asset commits at the beginning of the frame boundary.
|
||||
hw.assets_mut().apply_commits();
|
||||
|
||||
// 1. Update the peripheral state using the latest signals from the Host.
|
||||
// This ensures input is consistent throughout the entire update.
|
||||
hw.pad_mut().begin_frame(signals);
|
||||
hw.touch_mut().begin_frame(signals);
|
||||
|
||||
// 2. State machine lifecycle management.
|
||||
if !self.state_initialized {
|
||||
self.on_enter(signals, hw);
|
||||
self.state_initialized = true;
|
||||
}
|
||||
|
||||
// 3. Update the current state and check for transitions.
|
||||
if let Some(next_state) = self.on_update(signals, hw) {
|
||||
self.change_state(next_state, signals, hw);
|
||||
}
|
||||
}
|
||||
|
||||
/// Transitions the system to a new state, handling lifecycle hooks.
|
||||
pub fn change_state(
|
||||
&mut self,
|
||||
new_state: FirmwareState,
|
||||
signals: &InputSignals,
|
||||
hw: &mut dyn HardwareBridge,
|
||||
) {
|
||||
self.on_exit(signals, hw);
|
||||
self.state = new_state;
|
||||
self.state_initialized = false;
|
||||
|
||||
// Enter the new state immediately to avoid "empty" frames during transitions.
|
||||
self.on_enter(signals, hw);
|
||||
self.state_initialized = true;
|
||||
}
|
||||
|
||||
/// Dispatches the `on_enter` event to the current state implementation.
|
||||
fn on_enter(&mut self, signals: &InputSignals, hw: &mut dyn HardwareBridge) {
|
||||
let mut req = PrometeuContext {
|
||||
vm: &mut self.vm,
|
||||
os: &mut self.os,
|
||||
hub: &mut self.hub,
|
||||
boot_target: &self.boot_target,
|
||||
signals,
|
||||
hw,
|
||||
};
|
||||
match &mut self.state {
|
||||
FirmwareState::Reset(s) => s.on_enter(&mut req),
|
||||
FirmwareState::SplashScreen(s) => s.on_enter(&mut req),
|
||||
FirmwareState::LaunchHub(s) => s.on_enter(&mut req),
|
||||
FirmwareState::HubHome(s) => s.on_enter(&mut req),
|
||||
FirmwareState::LoadCartridge(s) => s.on_enter(&mut req),
|
||||
FirmwareState::GameRunning(s) => s.on_enter(&mut req),
|
||||
FirmwareState::AppCrashes(s) => s.on_enter(&mut req),
|
||||
}
|
||||
}
|
||||
|
||||
/// Dispatches the `on_update` event to the current state implementation.
|
||||
/// Returns an optional `FirmwareState` if a transition is requested.
|
||||
fn on_update(
|
||||
&mut self,
|
||||
signals: &InputSignals,
|
||||
hw: &mut dyn HardwareBridge,
|
||||
) -> Option<FirmwareState> {
|
||||
let mut req = PrometeuContext {
|
||||
vm: &mut self.vm,
|
||||
os: &mut self.os,
|
||||
hub: &mut self.hub,
|
||||
boot_target: &self.boot_target,
|
||||
signals,
|
||||
hw,
|
||||
};
|
||||
match &mut self.state {
|
||||
FirmwareState::Reset(s) => s.on_update(&mut req),
|
||||
FirmwareState::SplashScreen(s) => s.on_update(&mut req),
|
||||
FirmwareState::LaunchHub(s) => s.on_update(&mut req),
|
||||
FirmwareState::HubHome(s) => s.on_update(&mut req),
|
||||
FirmwareState::LoadCartridge(s) => s.on_update(&mut req),
|
||||
FirmwareState::GameRunning(s) => s.on_update(&mut req),
|
||||
FirmwareState::AppCrashes(s) => s.on_update(&mut req),
|
||||
}
|
||||
}
|
||||
|
||||
/// Dispatches the `on_exit` event to the current state implementation.
|
||||
fn on_exit(&mut self, signals: &InputSignals, hw: &mut dyn HardwareBridge) {
|
||||
let mut req = PrometeuContext {
|
||||
vm: &mut self.vm,
|
||||
os: &mut self.os,
|
||||
hub: &mut self.hub,
|
||||
boot_target: &self.boot_target,
|
||||
signals,
|
||||
hw,
|
||||
};
|
||||
match &mut self.state {
|
||||
FirmwareState::Reset(s) => s.on_exit(&mut req),
|
||||
FirmwareState::SplashScreen(s) => s.on_exit(&mut req),
|
||||
FirmwareState::LaunchHub(s) => s.on_exit(&mut req),
|
||||
FirmwareState::HubHome(s) => s.on_exit(&mut req),
|
||||
FirmwareState::LoadCartridge(s) => s.on_exit(&mut req),
|
||||
FirmwareState::GameRunning(s) => s.on_exit(&mut req),
|
||||
FirmwareState::AppCrashes(s) => s.on_exit(&mut req),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn load_cartridge(&mut self, cartridge: Cartridge) {
|
||||
self.state = FirmwareState::LoadCartridge(LoadCartridgeStep::new(cartridge));
|
||||
self.state_initialized = false;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use prometeu_bytecode::assembler::assemble;
|
||||
use prometeu_bytecode::model::{BytecodeModule, FunctionMeta, SyscallDecl};
|
||||
use prometeu_drivers::hardware::Hardware;
|
||||
use prometeu_hal::cartridge::{AppMode, AssetsPayloadSource};
|
||||
use prometeu_hal::syscalls::caps;
|
||||
use prometeu_system::CrashReport;
|
||||
|
||||
fn invalid_game_cartridge() -> Cartridge {
|
||||
Cartridge {
|
||||
app_id: 7,
|
||||
title: "Broken Cart".into(),
|
||||
app_version: "1.0.0".into(),
|
||||
app_mode: AppMode::Game,
|
||||
capabilities: 0,
|
||||
program: vec![0, 0, 0, 0],
|
||||
assets: AssetsPayloadSource::empty(),
|
||||
asset_table: vec![],
|
||||
preload: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
fn trapping_game_cartridge() -> Cartridge {
|
||||
let code = assemble("PUSH_BOOL 1\nHOSTCALL 0\nHALT").expect("assemble");
|
||||
let program = BytecodeModule {
|
||||
version: 0,
|
||||
const_pool: vec![],
|
||||
functions: vec![FunctionMeta {
|
||||
code_offset: 0,
|
||||
code_len: code.len() as u32,
|
||||
..Default::default()
|
||||
}],
|
||||
code,
|
||||
debug_info: None,
|
||||
exports: vec![],
|
||||
syscalls: vec![SyscallDecl {
|
||||
module: "gfx".into(),
|
||||
name: "clear".into(),
|
||||
version: 1,
|
||||
arg_slots: 1,
|
||||
ret_slots: 0,
|
||||
}],
|
||||
}
|
||||
.serialize();
|
||||
|
||||
Cartridge {
|
||||
app_id: 8,
|
||||
title: "Trap Cart".into(),
|
||||
app_version: "1.0.0".into(),
|
||||
app_mode: AppMode::Game,
|
||||
capabilities: caps::GFX,
|
||||
program,
|
||||
assets: AssetsPayloadSource::empty(),
|
||||
asset_table: vec![],
|
||||
preload: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_cartridge_transitions_to_app_crashes_when_vm_init_fails() {
|
||||
let mut firmware = Firmware::new(None);
|
||||
let mut hardware = Hardware::new();
|
||||
let signals = InputSignals::default();
|
||||
|
||||
firmware.load_cartridge(invalid_game_cartridge());
|
||||
firmware.tick(&signals, &mut hardware);
|
||||
|
||||
match &firmware.state {
|
||||
FirmwareState::AppCrashes(step) => match &step.report {
|
||||
CrashReport::VmInit { error } => {
|
||||
assert!(matches!(error, prometeu_vm::VmInitError::InvalidFormat));
|
||||
}
|
||||
other => panic!("expected VmInit crash report, got {:?}", other),
|
||||
},
|
||||
other => panic!("expected AppCrashes state, got {:?}", other),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn game_running_transitions_to_app_crashes_when_runtime_surfaces_trap() {
|
||||
let mut firmware = Firmware::new(None);
|
||||
let mut hardware = Hardware::new();
|
||||
let signals = InputSignals::default();
|
||||
|
||||
firmware.load_cartridge(trapping_game_cartridge());
|
||||
firmware.tick(&signals, &mut hardware);
|
||||
|
||||
assert!(matches!(firmware.state, FirmwareState::GameRunning(_)));
|
||||
|
||||
firmware.tick(&signals, &mut hardware);
|
||||
|
||||
match &firmware.state {
|
||||
FirmwareState::AppCrashes(step) => match &step.report {
|
||||
CrashReport::VmTrap { trap } => {
|
||||
assert!(trap.message.contains("Expected integer at index 0"));
|
||||
}
|
||||
other => panic!("expected VmTrap crash report, got {:?}", other),
|
||||
},
|
||||
other => panic!("expected AppCrashes state, got {:?}", other),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,10 +1,10 @@
|
||||
pub use crate::firmware::firmware_step_crash_screen::AppCrashesStep;
|
||||
pub use crate::firmware::firmware_step_game_running::GameRunningStep;
|
||||
pub use crate::firmware::firmware_step_hub_home::HubHomeStep;
|
||||
pub use crate::firmware::firmware_step_launch_hub::LaunchHubStep;
|
||||
pub use crate::firmware::firmware_step_load_cartridge::LoadCartridgeStep;
|
||||
pub use crate::firmware::firmware_step_reset::ResetStep;
|
||||
pub use crate::firmware::firmware_step_splash_screen::SplashScreenStep;
|
||||
pub use crate::firmware::firmware_step_launch_hub::LaunchHubStep;
|
||||
pub use crate::firmware::firmware_step_hub_home::HubHomeStep;
|
||||
pub use crate::firmware::firmware_step_load_cartridge::LoadCartridgeStep;
|
||||
pub use crate::firmware::firmware_step_game_running::GameRunningStep;
|
||||
pub use crate::firmware::firmware_step_crash_screen::AppCrashesStep;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum FirmwareState {
|
||||
@ -15,4 +15,4 @@ pub enum FirmwareState {
|
||||
LoadCartridge(LoadCartridgeStep),
|
||||
GameRunning(GameRunningStep),
|
||||
AppCrashes(AppCrashesStep),
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,67 @@
|
||||
use crate::firmware::firmware_state::{FirmwareState, LaunchHubStep};
|
||||
use crate::firmware::prometeu_context::PrometeuContext;
|
||||
use prometeu_hal::color::Color;
|
||||
use prometeu_hal::log::{LogLevel, LogSource};
|
||||
use prometeu_system::CrashReport;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AppCrashesStep {
|
||||
pub report: CrashReport,
|
||||
}
|
||||
|
||||
impl AppCrashesStep {
|
||||
pub fn log_message(&self) -> String {
|
||||
format!("App Crashed: {}", self.report)
|
||||
}
|
||||
|
||||
pub fn on_enter(&mut self, ctx: &mut PrometeuContext) {
|
||||
ctx.os.log(LogLevel::Error, LogSource::Pos, self.report.log_tag(), self.log_message());
|
||||
}
|
||||
|
||||
pub fn on_update(&mut self, ctx: &mut PrometeuContext) -> Option<FirmwareState> {
|
||||
// Update peripherals for input on the crash screen
|
||||
ctx.hw.pad_mut().begin_frame(ctx.signals);
|
||||
|
||||
// Error screen: red background, white text
|
||||
ctx.hw.gfx_mut().clear(Color::RED);
|
||||
// For now we just log or show something simple
|
||||
// In the future, use draw_text
|
||||
ctx.hw.gfx_mut().present();
|
||||
|
||||
// If START is pressed, return to the Hub
|
||||
if ctx.hw.pad().start().down {
|
||||
return Some(FirmwareState::LaunchHub(LaunchHubStep));
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
pub fn on_exit(&mut self, _ctx: &mut PrometeuContext) {}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use prometeu_bytecode::TrapInfo;
|
||||
|
||||
#[test]
|
||||
fn crash_step_formats_trap_from_structured_report() {
|
||||
let step = AppCrashesStep {
|
||||
report: CrashReport::VmTrap {
|
||||
trap: TrapInfo {
|
||||
code: 0xAB,
|
||||
opcode: 0x22,
|
||||
message: "type mismatch".into(),
|
||||
pc: 0x44,
|
||||
span: None,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
let msg = step.log_message();
|
||||
assert!(msg.contains("PVM Trap 0x000000AB"));
|
||||
assert!(msg.contains("PC 0x44"));
|
||||
assert!(msg.contains("opcode 0x0022"));
|
||||
assert!(msg.contains("type mismatch"));
|
||||
}
|
||||
}
|
||||
@ -1,6 +1,6 @@
|
||||
use crate::firmware::firmware_state::{AppCrashesStep, FirmwareState};
|
||||
use crate::firmware::prometeu_context::PrometeuContext;
|
||||
use crate::log::{LogLevel, LogSource};
|
||||
use prometeu_hal::log::{LogLevel, LogSource};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct GameRunningStep;
|
||||
@ -12,12 +12,12 @@ impl GameRunningStep {
|
||||
|
||||
pub fn on_update(&mut self, ctx: &mut PrometeuContext) -> Option<FirmwareState> {
|
||||
let result = ctx.os.tick(ctx.vm, ctx.signals, ctx.hw);
|
||||
|
||||
|
||||
if !ctx.os.logical_frame_active {
|
||||
ctx.hw.gfx_mut().present();
|
||||
}
|
||||
|
||||
result.map(|err| FirmwareState::AppCrashes(AppCrashesStep { error: err }))
|
||||
result.map(|report| FirmwareState::AppCrashes(AppCrashesStep { report }))
|
||||
}
|
||||
|
||||
pub fn on_exit(&mut self, _ctx: &mut PrometeuContext) {}
|
||||
@ -1,6 +1,6 @@
|
||||
use crate::firmware::firmware_state::{AppCrashesStep, FirmwareState};
|
||||
use crate::firmware::prometeu_context::PrometeuContext;
|
||||
use crate::log::{LogLevel, LogSource};
|
||||
use prometeu_hal::log::{LogLevel, LogSource};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct HubHomeStep;
|
||||
@ -17,7 +17,7 @@ impl HubHomeStep {
|
||||
ctx.hub.gui_update(ctx.os, ctx.hw);
|
||||
|
||||
if let Some(focused_id) = ctx.hub.window_manager.focused {
|
||||
if ctx.hw.pad().start.down {
|
||||
if ctx.hw.pad().start().down {
|
||||
ctx.hub.window_manager.remove_window(focused_id);
|
||||
} else {
|
||||
// System App runs here, drawing over the Hub background
|
||||
@ -27,15 +27,15 @@ impl HubHomeStep {
|
||||
|
||||
// Renders the System App window borders
|
||||
ctx.hub.render(ctx.os, ctx.hw);
|
||||
|
||||
|
||||
ctx.hw.gfx_mut().present();
|
||||
|
||||
if let Some(err) = error {
|
||||
return Some(FirmwareState::AppCrashes(AppCrashesStep { error: err }));
|
||||
|
||||
if let Some(report) = error {
|
||||
return Some(FirmwareState::AppCrashes(AppCrashesStep { report }));
|
||||
}
|
||||
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
pub fn on_exit(&mut self, _ctx: &mut PrometeuContext) {}
|
||||
}
|
||||
}
|
||||
@ -1,8 +1,8 @@
|
||||
use crate::firmware::boot_target::BootTarget;
|
||||
use crate::firmware::firmware_state::{FirmwareState, HubHomeStep, LoadCartridgeStep};
|
||||
use crate::firmware::prometeu_context::PrometeuContext;
|
||||
use crate::model::CartridgeLoader;
|
||||
use crate::log::{LogLevel, LogSource};
|
||||
use prometeu_hal::cartridge_loader::CartridgeLoader;
|
||||
use prometeu_hal::log::{LogLevel, LogSource};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct LaunchHubStep;
|
||||
@ -19,10 +19,15 @@ impl LaunchHubStep {
|
||||
Ok(cartridge) => {
|
||||
// In the case of debug, we could pause here, but the requirement says
|
||||
// for the Runtime to open the socket and wait.
|
||||
return Some(FirmwareState::LoadCartridge(LoadCartridgeStep { cartridge }));
|
||||
return Some(FirmwareState::LoadCartridge(LoadCartridgeStep::new(cartridge)));
|
||||
}
|
||||
Err(e) => {
|
||||
ctx.os.log(LogLevel::Error, LogSource::Pos, 0, format!("Failed to auto-load cartridge: {:?}", e));
|
||||
ctx.os.log(
|
||||
LogLevel::Error,
|
||||
LogSource::Pos,
|
||||
0,
|
||||
format!("Failed to auto-load cartridge: {:?}", e),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -30,4 +35,4 @@ impl LaunchHubStep {
|
||||
}
|
||||
|
||||
pub fn on_exit(&mut self, _ctx: &mut PrometeuContext) {}
|
||||
}
|
||||
}
|
||||
@ -1,36 +1,55 @@
|
||||
use crate::firmware::firmware_state::{FirmwareState, GameRunningStep, HubHomeStep};
|
||||
use crate::firmware::firmware_state::{
|
||||
AppCrashesStep, FirmwareState, GameRunningStep, HubHomeStep,
|
||||
};
|
||||
use crate::firmware::prometeu_context::PrometeuContext;
|
||||
use crate::model::{AppMode, Cartridge, Color, Rect};
|
||||
use crate::log::{LogLevel, LogSource};
|
||||
use prometeu_hal::cartridge::{AppMode, Cartridge};
|
||||
use prometeu_hal::color::Color;
|
||||
use prometeu_hal::log::{LogLevel, LogSource};
|
||||
use prometeu_hal::window::Rect;
|
||||
use prometeu_system::CrashReport;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct LoadCartridgeStep {
|
||||
pub cartridge: Cartridge,
|
||||
init_error: Option<CrashReport>,
|
||||
}
|
||||
|
||||
impl LoadCartridgeStep {
|
||||
pub fn new(cartridge: Cartridge) -> Self {
|
||||
Self { cartridge, init_error: None }
|
||||
}
|
||||
|
||||
pub fn on_enter(&mut self, ctx: &mut PrometeuContext) {
|
||||
ctx.os.log(LogLevel::Info, LogSource::Pos, 0, format!("Loading cartridge: {}", self.cartridge.title));
|
||||
|
||||
ctx.os.log(
|
||||
LogLevel::Info,
|
||||
LogSource::Pos,
|
||||
0,
|
||||
format!("Loading cartridge: {}", self.cartridge.title),
|
||||
);
|
||||
|
||||
// Initialize Asset Manager
|
||||
ctx.hw.assets_mut().initialize_for_cartridge(
|
||||
self.cartridge.asset_table.clone(),
|
||||
self.cartridge.preload.clone(),
|
||||
self.cartridge.assets.clone()
|
||||
self.cartridge.assets.clone(),
|
||||
);
|
||||
|
||||
ctx.os.initialize_vm(ctx.vm, &self.cartridge);
|
||||
self.init_error = ctx.os.initialize_vm(ctx.vm, &self.cartridge).err();
|
||||
}
|
||||
|
||||
pub fn on_update(&mut self, ctx: &mut PrometeuContext) -> Option<FirmwareState> {
|
||||
if let Some(report) = self.init_error.take() {
|
||||
return Some(FirmwareState::AppCrashes(AppCrashesStep { report }));
|
||||
}
|
||||
|
||||
if self.cartridge.app_mode == AppMode::System {
|
||||
let id = ctx.hub.window_manager.add_window(
|
||||
self.cartridge.title.clone(),
|
||||
Rect { x: 40, y: 20, w: 240, h: 140 },
|
||||
Color::WHITE
|
||||
Color::WHITE,
|
||||
);
|
||||
ctx.hub.window_manager.set_focus(id);
|
||||
|
||||
|
||||
// System apps do not change the firmware state to GameRunning.
|
||||
// They run in the background or via windows in the Hub.
|
||||
return Some(FirmwareState::HubHome(HubHomeStep));
|
||||
@ -40,4 +59,4 @@ impl LoadCartridgeStep {
|
||||
}
|
||||
|
||||
pub fn on_exit(&mut self, _ctx: &mut PrometeuContext) {}
|
||||
}
|
||||
}
|
||||
@ -1,7 +1,7 @@
|
||||
use crate::firmware::boot_target::BootTarget;
|
||||
use crate::firmware::firmware_state::{FirmwareState, LaunchHubStep, SplashScreenStep};
|
||||
use crate::firmware::prometeu_context::PrometeuContext;
|
||||
use crate::log::{LogLevel, LogSource};
|
||||
use prometeu_hal::log::{LogLevel, LogSource};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ResetStep;
|
||||
@ -20,4 +20,4 @@ impl ResetStep {
|
||||
}
|
||||
|
||||
pub fn on_exit(&mut self, _ctx: &mut PrometeuContext) {}
|
||||
}
|
||||
}
|
||||
@ -1,7 +1,7 @@
|
||||
use crate::firmware::firmware_state::{FirmwareState, LaunchHubStep};
|
||||
use crate::firmware::prometeu_context::PrometeuContext;
|
||||
use crate::log::{LogLevel, LogSource};
|
||||
use crate::model::Color;
|
||||
use prometeu_hal::color::Color;
|
||||
use prometeu_hal::log::{LogLevel, LogSource};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SplashScreenStep {
|
||||
@ -17,7 +17,7 @@ impl SplashScreenStep {
|
||||
|
||||
pub fn on_update(&mut self, ctx: &mut PrometeuContext) -> Option<FirmwareState> {
|
||||
const ANIMATION_DURATION: u32 = 60; // 1 second at 60fps
|
||||
const TOTAL_DURATION: u32 = 240; // 4 seconds total (updated from 2s based on total_duration logic)
|
||||
const TOTAL_DURATION: u32 = 240; // 4 seconds total (updated from 2s based on total_duration logic)
|
||||
|
||||
// Update peripherals for input
|
||||
ctx.hw.pad_mut().begin_frame(ctx.signals);
|
||||
@ -28,7 +28,7 @@ impl SplashScreenStep {
|
||||
// Draw expanding square
|
||||
let (sw, sh) = ctx.hw.gfx().size();
|
||||
let max_size = (sw.min(sh) as i32 / 2).max(1);
|
||||
|
||||
|
||||
let current_size = if self.frame < ANIMATION_DURATION {
|
||||
(max_size * (self.frame as i32 + 1)) / ANIMATION_DURATION as i32
|
||||
} else {
|
||||
@ -37,7 +37,7 @@ impl SplashScreenStep {
|
||||
|
||||
let x = (sw as i32 - current_size) / 2;
|
||||
let y = (sh as i32 - current_size) / 2;
|
||||
|
||||
|
||||
ctx.hw.gfx_mut().fill_rect(x, y, current_size, current_size, Color::WHITE);
|
||||
ctx.hw.gfx_mut().present();
|
||||
|
||||
@ -57,4 +57,4 @@ impl SplashScreenStep {
|
||||
}
|
||||
|
||||
pub fn on_exit(&mut self, _ctx: &mut PrometeuContext) {}
|
||||
}
|
||||
}
|
||||
@ -1,17 +1,18 @@
|
||||
mod boot_target;
|
||||
#[allow(clippy::module_inception)]
|
||||
mod firmware;
|
||||
pub mod firmware_state;
|
||||
mod boot_target;
|
||||
|
||||
pub(crate) mod firmware_step_crash_screen;
|
||||
pub(crate) mod firmware_step_game_running;
|
||||
pub(crate) mod firmware_step_hub_home;
|
||||
pub(crate) mod firmware_step_launch_hub;
|
||||
pub(crate) mod firmware_step_load_cartridge;
|
||||
pub(crate) mod firmware_step_reset;
|
||||
pub(crate) mod firmware_step_splash_screen;
|
||||
pub(crate) mod firmware_step_launch_hub;
|
||||
pub(crate) mod firmware_step_hub_home;
|
||||
pub(crate) mod firmware_step_load_cartridge;
|
||||
pub(crate) mod firmware_step_game_running;
|
||||
pub(crate) mod firmware_step_crash_screen;
|
||||
mod prometeu_context;
|
||||
|
||||
pub use boot_target::BootTarget;
|
||||
pub use firmware::Firmware;
|
||||
pub use firmware_state::FirmwareState;
|
||||
pub use prometeu_context::PrometeuContext;
|
||||
pub use boot_target::BootTarget;
|
||||
@ -1,14 +1,13 @@
|
||||
use crate::firmware::boot_target::BootTarget;
|
||||
use crate::hardware::{HardwareBridge, InputSignals};
|
||||
use crate::prometeu_hub::PrometeuHub;
|
||||
use crate::prometeu_os::PrometeuOS;
|
||||
use crate::virtual_machine::VirtualMachine;
|
||||
use prometeu_hal::{HardwareBridge, InputSignals};
|
||||
use prometeu_system::{PrometeuHub, VirtualMachineRuntime};
|
||||
use prometeu_vm::VirtualMachine;
|
||||
|
||||
pub struct PrometeuContext<'a> {
|
||||
pub vm: &'a mut VirtualMachine,
|
||||
pub os: &'a mut PrometeuOS,
|
||||
pub os: &'a mut VirtualMachineRuntime,
|
||||
pub hub: &'a mut PrometeuHub,
|
||||
pub boot_target: &'a BootTarget,
|
||||
pub signals: &'a InputSignals,
|
||||
pub hw: &'a mut dyn HardwareBridge,
|
||||
}
|
||||
}
|
||||
3
crates/console/prometeu-firmware/src/lib.rs
Normal file
3
crates/console/prometeu-firmware/src/lib.rs
Normal file
@ -0,0 +1,3 @@
|
||||
pub mod firmware;
|
||||
|
||||
pub use firmware::*;
|
||||
@ -1,10 +1,10 @@
|
||||
[package]
|
||||
name = "prometeu-core"
|
||||
name = "prometeu-hal"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
license.workspace = true
|
||||
|
||||
[dependencies]
|
||||
prometeu-bytecode = { path = "../prometeu-bytecode" }
|
||||
serde = { version = "1.0.228", features = ["derive"] }
|
||||
serde_json = "1.0.149"
|
||||
prometeu-bytecode = { path = "../prometeu-bytecode" }
|
||||
serde_json = "1.0.149"
|
||||
163
crates/console/prometeu-hal/src/asset.rs
Normal file
163
crates/console/prometeu-hal/src/asset.rs
Normal file
@ -0,0 +1,163 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub type HandleId = u32;
|
||||
pub type AssetId = i32;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Deserialize, Serialize)]
|
||||
#[allow(non_camel_case_types)]
|
||||
pub enum BankType {
|
||||
GLYPH,
|
||||
SOUNDS,
|
||||
SCENE,
|
||||
// TILEMAPS,
|
||||
// BLOBS,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Deserialize, Serialize)]
|
||||
pub enum AssetCodec {
|
||||
#[serde(rename = "NONE")]
|
||||
None,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct AssetEntry {
|
||||
pub asset_id: AssetId,
|
||||
pub asset_name: String,
|
||||
pub bank_type: BankType,
|
||||
pub offset: u64,
|
||||
pub size: u64,
|
||||
pub decoded_size: u64,
|
||||
pub codec: AssetCodec,
|
||||
pub metadata: serde_json::Value,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct GlyphBankMetadata {
|
||||
pub tile_size: u32,
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
pub palette_count: u32,
|
||||
pub palette_authored: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct SoundBankMetadata {
|
||||
pub sample_rate: u32,
|
||||
pub channels: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
|
||||
pub struct SceneBankMetadata {}
|
||||
|
||||
pub const SCENE_PAYLOAD_MAGIC_V1: [u8; 4] = *b"SCNE";
|
||||
pub const SCENE_PAYLOAD_VERSION_V1: u16 = 1;
|
||||
pub const SCENE_LAYER_COUNT_V1: usize = 4;
|
||||
pub const SCENE_HEADER_BYTES_V1: usize = 12;
|
||||
pub const SCENE_LAYER_HEADER_BYTES_V1: usize = 28;
|
||||
pub const SCENE_TILE_RECORD_BYTES_V1: usize = 4;
|
||||
pub const SCENE_DECODED_LAYER_OVERHEAD_BYTES_V1: usize = 16;
|
||||
|
||||
impl AssetEntry {
|
||||
pub fn metadata_as_glyph_bank(&self) -> Result<GlyphBankMetadata, String> {
|
||||
if self.bank_type != BankType::GLYPH {
|
||||
return Err(format!(
|
||||
"Asset {} is not a GLYPH bank (type: {:?})",
|
||||
self.asset_id, self.bank_type
|
||||
));
|
||||
}
|
||||
serde_json::from_value(self.metadata.clone())
|
||||
.map_err(|e| format!("Invalid GLYPH metadata for asset {}: {}", self.asset_id, e))
|
||||
}
|
||||
|
||||
pub fn metadata_as_sound_bank(&self) -> Result<SoundBankMetadata, String> {
|
||||
if self.bank_type != BankType::SOUNDS {
|
||||
return Err(format!(
|
||||
"Asset {} is not a SOUNDS bank (type: {:?})",
|
||||
self.asset_id, self.bank_type
|
||||
));
|
||||
}
|
||||
serde_json::from_value(self.metadata.clone())
|
||||
.map_err(|e| format!("Invalid SOUNDS metadata for asset {}: {}", self.asset_id, e))
|
||||
}
|
||||
|
||||
pub fn metadata_as_scene_bank(&self) -> Result<SceneBankMetadata, String> {
|
||||
if self.bank_type != BankType::SCENE {
|
||||
return Err(format!(
|
||||
"Asset {} is not a SCENE bank (type: {:?})",
|
||||
self.asset_id, self.bank_type
|
||||
));
|
||||
}
|
||||
serde_json::from_value(self.metadata.clone())
|
||||
.map_err(|e| format!("Invalid SCENE metadata for asset {}: {}", self.asset_id, e))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct PreloadEntry {
|
||||
pub asset_id: AssetId,
|
||||
pub slot: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[repr(i32)]
|
||||
pub enum LoadStatus {
|
||||
PENDING = 0,
|
||||
LOADING = 1,
|
||||
READY = 2,
|
||||
COMMITTED = 3,
|
||||
CANCELED = 4,
|
||||
ERROR = 5,
|
||||
UnknownHandle = 6,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[repr(i32)]
|
||||
pub enum AssetLoadError {
|
||||
AssetNotFound = 3,
|
||||
SlotKindMismatch = 4,
|
||||
SlotIndexInvalid = 5,
|
||||
BackendError = 6,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[repr(i32)]
|
||||
pub enum AssetOpStatus {
|
||||
Ok = 0,
|
||||
UnknownHandle = 1,
|
||||
InvalidState = 2,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct BankTelemetry {
|
||||
pub bank_type: BankType,
|
||||
pub used_slots: usize,
|
||||
pub total_slots: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SlotStats {
|
||||
pub asset_id: Option<AssetId>,
|
||||
pub asset_name: Option<String>,
|
||||
pub generation: u32,
|
||||
pub resident_bytes: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub struct SlotRef {
|
||||
pub asset_type: BankType,
|
||||
pub index: usize,
|
||||
}
|
||||
|
||||
impl SlotRef {
|
||||
pub fn gfx(index: usize) -> Self {
|
||||
Self { asset_type: BankType::GLYPH, index }
|
||||
}
|
||||
|
||||
pub fn audio(index: usize) -> Self {
|
||||
Self { asset_type: BankType::SOUNDS, index }
|
||||
}
|
||||
|
||||
pub fn scene(index: usize) -> Self {
|
||||
Self { asset_type: BankType::SCENE, index }
|
||||
}
|
||||
}
|
||||
22
crates/console/prometeu-hal/src/asset_bridge.rs
Normal file
22
crates/console/prometeu-hal/src/asset_bridge.rs
Normal file
@ -0,0 +1,22 @@
|
||||
use crate::asset::{
|
||||
AssetEntry, AssetId, AssetLoadError, AssetOpStatus, BankTelemetry, HandleId, LoadStatus,
|
||||
PreloadEntry, SlotRef, SlotStats,
|
||||
};
|
||||
use crate::cartridge::AssetsPayloadSource;
|
||||
|
||||
pub trait AssetBridge {
|
||||
fn initialize_for_cartridge(
|
||||
&self,
|
||||
assets: Vec<AssetEntry>,
|
||||
preload: Vec<PreloadEntry>,
|
||||
assets_data: AssetsPayloadSource,
|
||||
);
|
||||
fn load(&self, asset_id: AssetId, slot_index: usize) -> Result<HandleId, AssetLoadError>;
|
||||
fn status(&self, handle: HandleId) -> LoadStatus;
|
||||
fn commit(&self, handle: HandleId) -> AssetOpStatus;
|
||||
fn cancel(&self, handle: HandleId) -> AssetOpStatus;
|
||||
fn apply_commits(&self);
|
||||
fn bank_telemetry(&self) -> Vec<BankTelemetry>;
|
||||
fn slot_info(&self, slot: SlotRef) -> SlotStats;
|
||||
fn shutdown(&self);
|
||||
}
|
||||
52
crates/console/prometeu-hal/src/audio_bridge.rs
Normal file
52
crates/console/prometeu-hal/src/audio_bridge.rs
Normal file
@ -0,0 +1,52 @@
|
||||
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);
|
||||
}
|
||||
@ -15,18 +15,19 @@ pub enum ButtonId {
|
||||
Select = 11,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Default, Clone, Copy, Debug)]
|
||||
pub struct Button {
|
||||
pub pressed: bool,
|
||||
pub released: bool,
|
||||
pub down: bool,
|
||||
pub hold_frames: u32,
|
||||
pub hold_frames: u16,
|
||||
}
|
||||
|
||||
impl Button {
|
||||
pub fn begin_frame(&mut self, is_down_now: bool) {
|
||||
let was_down = self.down;
|
||||
self.down = is_down_now.clone();
|
||||
self.down = is_down_now;
|
||||
|
||||
self.pressed = !was_down && self.down;
|
||||
self.released = was_down && !self.down;
|
||||
@ -37,4 +38,4 @@ impl Button {
|
||||
self.hold_frames = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
312
crates/console/prometeu-hal/src/cartridge.rs
Normal file
312
crates/console/prometeu-hal/src/cartridge.rs
Normal file
@ -0,0 +1,312 @@
|
||||
use crate::asset::{AssetEntry, PreloadEntry};
|
||||
use crate::syscalls::CapFlags;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fs::File;
|
||||
use std::io::{self, Cursor, Read, Seek, SeekFrom};
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub const ASSETS_PA_MAGIC: [u8; 4] = *b"ASPA";
|
||||
pub const ASSETS_PA_SCHEMA_VERSION: u32 = 1;
|
||||
pub const ASSETS_PA_PRELUDE_SIZE: usize = 32;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
|
||||
pub enum AppMode {
|
||||
Game,
|
||||
System,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Cartridge {
|
||||
pub app_id: u32,
|
||||
pub title: String,
|
||||
pub app_version: String,
|
||||
pub app_mode: AppMode,
|
||||
pub capabilities: CapFlags,
|
||||
pub program: Vec<u8>,
|
||||
pub assets: AssetsPayloadSource,
|
||||
pub asset_table: Vec<AssetEntry>,
|
||||
pub preload: Vec<PreloadEntry>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CartridgeDTO {
|
||||
pub app_id: u32,
|
||||
pub title: String,
|
||||
pub app_version: String,
|
||||
pub app_mode: AppMode,
|
||||
pub capabilities: CapFlags,
|
||||
pub program: Vec<u8>,
|
||||
pub assets: AssetsPayloadSource,
|
||||
pub asset_table: Vec<AssetEntry>,
|
||||
pub preload: Vec<PreloadEntry>,
|
||||
}
|
||||
|
||||
impl From<CartridgeDTO> for Cartridge {
|
||||
fn from(dto: CartridgeDTO) -> Self {
|
||||
Self {
|
||||
app_id: dto.app_id,
|
||||
title: dto.title,
|
||||
app_version: dto.app_version,
|
||||
app_mode: dto.app_mode,
|
||||
capabilities: dto.capabilities,
|
||||
program: dto.program,
|
||||
assets: dto.assets,
|
||||
asset_table: dto.asset_table,
|
||||
preload: dto.preload,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum AssetsPayloadSource {
|
||||
Memory(Arc<[u8]>),
|
||||
File(Arc<FileAssetsPayloadSource>),
|
||||
}
|
||||
|
||||
impl AssetsPayloadSource {
|
||||
pub fn empty() -> Self {
|
||||
Self::Memory(Arc::<[u8]>::from(Vec::<u8>::new()))
|
||||
}
|
||||
|
||||
pub fn from_bytes(bytes: Vec<u8>) -> Self {
|
||||
Self::Memory(Arc::<[u8]>::from(bytes))
|
||||
}
|
||||
|
||||
pub fn from_file(path: PathBuf, payload_offset: u64, payload_len: u64) -> Self {
|
||||
Self::File(Arc::new(FileAssetsPayloadSource { path, payload_offset, payload_len }))
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
match self {
|
||||
Self::Memory(bytes) => bytes.is_empty(),
|
||||
Self::File(source) => source.payload_len == 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn open_slice(&self, offset: u64, size: u64) -> io::Result<AssetsPayloadSlice> {
|
||||
match self {
|
||||
Self::Memory(bytes) => {
|
||||
let start =
|
||||
usize::try_from(offset).map_err(|_| invalid_input("asset offset overflow"))?;
|
||||
let len =
|
||||
usize::try_from(size).map_err(|_| invalid_input("asset size overflow"))?;
|
||||
let end =
|
||||
start.checked_add(len).ok_or_else(|| invalid_input("asset range overflow"))?;
|
||||
if end > bytes.len() {
|
||||
return Err(invalid_input("asset range out of bounds"));
|
||||
}
|
||||
|
||||
Ok(AssetsPayloadSlice::Memory { bytes: Arc::clone(bytes), start, len })
|
||||
}
|
||||
Self::File(source) => {
|
||||
let end = offset
|
||||
.checked_add(size)
|
||||
.ok_or_else(|| invalid_input("asset range overflow"))?;
|
||||
if end > source.payload_len {
|
||||
return Err(invalid_input("asset range out of bounds"));
|
||||
}
|
||||
|
||||
Ok(AssetsPayloadSlice::File { source: Arc::clone(source), offset, size })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct FileAssetsPayloadSource {
|
||||
pub path: PathBuf,
|
||||
pub payload_offset: u64,
|
||||
pub payload_len: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum AssetsPayloadSlice {
|
||||
Memory { bytes: Arc<[u8]>, start: usize, len: usize },
|
||||
File { source: Arc<FileAssetsPayloadSource>, offset: u64, size: u64 },
|
||||
}
|
||||
|
||||
impl AssetsPayloadSlice {
|
||||
pub fn open_reader(&self) -> io::Result<AssetsPayloadReader> {
|
||||
match self {
|
||||
Self::Memory { bytes, start, len } => {
|
||||
let data = Arc::<[u8]>::from(bytes[*start..*start + *len].to_vec());
|
||||
Ok(AssetsPayloadReader::Memory(Cursor::new(data)))
|
||||
}
|
||||
Self::File { source, offset, size } => {
|
||||
let mut file = File::open(&source.path)?;
|
||||
let absolute_start = source.payload_offset + offset;
|
||||
file.seek(SeekFrom::Start(absolute_start))?;
|
||||
Ok(AssetsPayloadReader::File(FileSliceReader {
|
||||
file,
|
||||
start: absolute_start,
|
||||
len: *size,
|
||||
position: 0,
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn read_all(&self) -> io::Result<Vec<u8>> {
|
||||
let mut reader = self.open_reader()?;
|
||||
let mut buffer = Vec::new();
|
||||
reader.read_to_end(&mut buffer)?;
|
||||
Ok(buffer)
|
||||
}
|
||||
}
|
||||
|
||||
pub enum AssetsPayloadReader {
|
||||
Memory(Cursor<Arc<[u8]>>),
|
||||
File(FileSliceReader),
|
||||
}
|
||||
|
||||
impl Read for AssetsPayloadReader {
|
||||
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
||||
match self {
|
||||
Self::Memory(reader) => reader.read(buf),
|
||||
Self::File(reader) => reader.read(buf),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Seek for AssetsPayloadReader {
|
||||
fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
|
||||
match self {
|
||||
Self::Memory(reader) => reader.seek(pos),
|
||||
Self::File(reader) => reader.seek(pos),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct FileSliceReader {
|
||||
file: File,
|
||||
start: u64,
|
||||
len: u64,
|
||||
position: u64,
|
||||
}
|
||||
|
||||
impl Read for FileSliceReader {
|
||||
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
||||
if self.position >= self.len {
|
||||
return Ok(0);
|
||||
}
|
||||
|
||||
let remaining = (self.len - self.position) as usize;
|
||||
let to_read = remaining.min(buf.len());
|
||||
let read = self.file.read(&mut buf[..to_read])?;
|
||||
self.position += read as u64;
|
||||
Ok(read)
|
||||
}
|
||||
}
|
||||
|
||||
impl Seek for FileSliceReader {
|
||||
fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
|
||||
let next = match pos {
|
||||
SeekFrom::Start(offset) => offset as i128,
|
||||
SeekFrom::Current(delta) => self.position as i128 + delta as i128,
|
||||
SeekFrom::End(delta) => self.len as i128 + delta as i128,
|
||||
};
|
||||
|
||||
if next < 0 || next as u64 > self.len {
|
||||
return Err(invalid_input("slice seek out of bounds"));
|
||||
}
|
||||
|
||||
let next = next as u64;
|
||||
self.file.seek(SeekFrom::Start(self.start + next))?;
|
||||
self.position = next;
|
||||
Ok(self.position)
|
||||
}
|
||||
}
|
||||
|
||||
fn invalid_input(message: &'static str) -> io::Error {
|
||||
io::Error::new(io::ErrorKind::InvalidInput, message)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct AssetsPackPrelude {
|
||||
pub magic: [u8; 4],
|
||||
pub schema_version: u32,
|
||||
pub header_len: u32,
|
||||
pub payload_offset: u64,
|
||||
pub flags: u32,
|
||||
pub reserved: u32,
|
||||
pub header_checksum: u32,
|
||||
}
|
||||
|
||||
impl AssetsPackPrelude {
|
||||
pub fn from_bytes(bytes: &[u8]) -> Option<Self> {
|
||||
if bytes.len() < ASSETS_PA_PRELUDE_SIZE {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut magic = [0_u8; 4];
|
||||
magic.copy_from_slice(&bytes[0..4]);
|
||||
|
||||
Some(Self {
|
||||
magic,
|
||||
schema_version: u32::from_le_bytes(bytes[4..8].try_into().ok()?),
|
||||
header_len: u32::from_le_bytes(bytes[8..12].try_into().ok()?),
|
||||
payload_offset: u64::from_le_bytes(bytes[12..20].try_into().ok()?),
|
||||
flags: u32::from_le_bytes(bytes[20..24].try_into().ok()?),
|
||||
reserved: u32::from_le_bytes(bytes[24..28].try_into().ok()?),
|
||||
header_checksum: u32::from_le_bytes(bytes[28..32].try_into().ok()?),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn to_bytes(self) -> [u8; ASSETS_PA_PRELUDE_SIZE] {
|
||||
let mut bytes = [0_u8; ASSETS_PA_PRELUDE_SIZE];
|
||||
bytes[0..4].copy_from_slice(&self.magic);
|
||||
bytes[4..8].copy_from_slice(&self.schema_version.to_le_bytes());
|
||||
bytes[8..12].copy_from_slice(&self.header_len.to_le_bytes());
|
||||
bytes[12..20].copy_from_slice(&self.payload_offset.to_le_bytes());
|
||||
bytes[20..24].copy_from_slice(&self.flags.to_le_bytes());
|
||||
bytes[24..28].copy_from_slice(&self.reserved.to_le_bytes());
|
||||
bytes[28..32].copy_from_slice(&self.header_checksum.to_le_bytes());
|
||||
bytes
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct AssetsPackHeader {
|
||||
#[serde(default)]
|
||||
pub asset_table: Vec<AssetEntry>,
|
||||
#[serde(default)]
|
||||
pub preload: Vec<PreloadEntry>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum CartridgeError {
|
||||
NotFound,
|
||||
InvalidFormat,
|
||||
InvalidManifest,
|
||||
UnsupportedVersion,
|
||||
MissingProgram,
|
||||
MissingAssets,
|
||||
IoError,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum Capability {
|
||||
None,
|
||||
System,
|
||||
Gfx,
|
||||
Audio,
|
||||
Fs,
|
||||
Log,
|
||||
Asset,
|
||||
Bank,
|
||||
All,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct CartridgeManifest {
|
||||
pub magic: String,
|
||||
pub cartridge_version: u32,
|
||||
pub app_id: u32,
|
||||
pub title: String,
|
||||
pub app_version: String,
|
||||
pub app_mode: AppMode,
|
||||
#[serde(default)]
|
||||
pub capabilities: Vec<Capability>,
|
||||
}
|
||||
586
crates/console/prometeu-hal/src/cartridge_loader.rs
Normal file
586
crates/console/prometeu-hal/src/cartridge_loader.rs
Normal file
@ -0,0 +1,586 @@
|
||||
use crate::asset::{AssetEntry, BankType};
|
||||
use crate::cartridge::{
|
||||
ASSETS_PA_MAGIC, ASSETS_PA_PRELUDE_SIZE, ASSETS_PA_SCHEMA_VERSION, AssetsPackHeader,
|
||||
AssetsPackPrelude, AssetsPayloadSource, Capability, Cartridge, CartridgeDTO, CartridgeError,
|
||||
CartridgeManifest,
|
||||
};
|
||||
use crate::syscalls::{CapFlags, caps};
|
||||
use std::collections::HashSet;
|
||||
use std::fs;
|
||||
use std::io::{Read, Seek, SeekFrom};
|
||||
use std::path::Path;
|
||||
|
||||
pub struct CartridgeLoader;
|
||||
|
||||
impl CartridgeLoader {
|
||||
pub fn load(path: impl AsRef<Path>) -> Result<Cartridge, CartridgeError> {
|
||||
let path = path.as_ref();
|
||||
if !path.exists() {
|
||||
return Err(CartridgeError::NotFound);
|
||||
}
|
||||
|
||||
if path.is_dir() {
|
||||
DirectoryCartridgeLoader::load(path)
|
||||
} else if path.extension().is_some_and(|ext| ext == "pmc") {
|
||||
PackedCartridgeLoader::load(path)
|
||||
} else {
|
||||
Err(CartridgeError::InvalidFormat)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct DirectoryCartridgeLoader;
|
||||
|
||||
impl DirectoryCartridgeLoader {
|
||||
pub fn load(path: &Path) -> Result<Cartridge, CartridgeError> {
|
||||
let manifest_path = path.join("manifest.json");
|
||||
if !manifest_path.exists() {
|
||||
return Err(CartridgeError::InvalidManifest);
|
||||
}
|
||||
|
||||
let manifest_content =
|
||||
fs::read_to_string(manifest_path).map_err(|_| CartridgeError::IoError)?;
|
||||
let manifest: CartridgeManifest =
|
||||
serde_json::from_str(&manifest_content).map_err(|_| CartridgeError::InvalidManifest)?;
|
||||
|
||||
// Additional validation as per requirements
|
||||
if manifest.magic != "PMTU" {
|
||||
return Err(CartridgeError::InvalidManifest);
|
||||
}
|
||||
if manifest.cartridge_version != 1 {
|
||||
return Err(CartridgeError::UnsupportedVersion);
|
||||
}
|
||||
|
||||
let capabilities = normalize_capabilities(&manifest.capabilities)?;
|
||||
|
||||
let program_path = path.join("program.pbx");
|
||||
if !program_path.exists() {
|
||||
return Err(CartridgeError::MissingProgram);
|
||||
}
|
||||
|
||||
let program = fs::read(program_path).map_err(|_| CartridgeError::IoError)?;
|
||||
|
||||
let assets_pa_path = path.join("assets.pa");
|
||||
let (assets, asset_table, preload) = if assets_pa_path.exists() {
|
||||
let parsed = parse_assets_pack(&assets_pa_path)?;
|
||||
(
|
||||
AssetsPayloadSource::from_file(
|
||||
assets_pa_path.clone(),
|
||||
parsed.payload_offset,
|
||||
parsed.payload_len,
|
||||
),
|
||||
parsed.header.asset_table,
|
||||
parsed.header.preload,
|
||||
)
|
||||
} else {
|
||||
if capabilities & caps::ASSET != 0 {
|
||||
return Err(CartridgeError::MissingAssets);
|
||||
}
|
||||
|
||||
(AssetsPayloadSource::empty(), Vec::new(), Vec::new())
|
||||
};
|
||||
|
||||
let dto = CartridgeDTO {
|
||||
app_id: manifest.app_id,
|
||||
title: manifest.title,
|
||||
app_version: manifest.app_version,
|
||||
app_mode: manifest.app_mode,
|
||||
capabilities,
|
||||
program,
|
||||
assets,
|
||||
asset_table,
|
||||
preload,
|
||||
};
|
||||
|
||||
Ok(Cartridge::from(dto))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PackedCartridgeLoader;
|
||||
|
||||
impl PackedCartridgeLoader {
|
||||
pub fn load(_path: &Path) -> Result<Cartridge, CartridgeError> {
|
||||
Err(CartridgeError::InvalidFormat)
|
||||
}
|
||||
}
|
||||
|
||||
fn normalize_capabilities(capabilities: &[Capability]) -> Result<CapFlags, CartridgeError> {
|
||||
let mut seen = HashSet::new();
|
||||
let mut normalized = caps::NONE;
|
||||
|
||||
for capability in capabilities {
|
||||
if !seen.insert(*capability) {
|
||||
return Err(CartridgeError::InvalidManifest);
|
||||
}
|
||||
|
||||
normalized |= match capability {
|
||||
Capability::None => caps::NONE,
|
||||
Capability::System => caps::SYSTEM,
|
||||
Capability::Gfx => caps::GFX,
|
||||
Capability::Audio => caps::AUDIO,
|
||||
Capability::Fs => caps::FS,
|
||||
Capability::Log => caps::LOG,
|
||||
Capability::Asset => caps::ASSET,
|
||||
Capability::Bank => caps::BANK,
|
||||
Capability::All => caps::ALL,
|
||||
};
|
||||
}
|
||||
|
||||
Ok(normalized)
|
||||
}
|
||||
|
||||
struct ParsedAssetsPack {
|
||||
header: AssetsPackHeader,
|
||||
payload_offset: u64,
|
||||
payload_len: u64,
|
||||
}
|
||||
|
||||
fn parse_assets_pack(path: &Path) -> Result<ParsedAssetsPack, CartridgeError> {
|
||||
let mut file = fs::File::open(path).map_err(|_| CartridgeError::IoError)?;
|
||||
let mut prelude_bytes = [0_u8; ASSETS_PA_PRELUDE_SIZE];
|
||||
file.read_exact(&mut prelude_bytes).map_err(|_| CartridgeError::InvalidFormat)?;
|
||||
|
||||
let prelude =
|
||||
AssetsPackPrelude::from_bytes(&prelude_bytes).ok_or(CartridgeError::InvalidFormat)?;
|
||||
|
||||
if prelude.magic != ASSETS_PA_MAGIC || prelude.schema_version != ASSETS_PA_SCHEMA_VERSION {
|
||||
return Err(CartridgeError::InvalidFormat);
|
||||
}
|
||||
|
||||
let header_start = ASSETS_PA_PRELUDE_SIZE;
|
||||
let header_len =
|
||||
usize::try_from(prelude.header_len).map_err(|_| CartridgeError::InvalidFormat)?;
|
||||
let header_end = header_start.checked_add(header_len).ok_or(CartridgeError::InvalidFormat)?;
|
||||
let payload_offset =
|
||||
usize::try_from(prelude.payload_offset).map_err(|_| CartridgeError::InvalidFormat)?;
|
||||
let file_len = usize::try_from(file.metadata().map_err(|_| CartridgeError::IoError)?.len())
|
||||
.map_err(|_| CartridgeError::InvalidFormat)?;
|
||||
|
||||
if payload_offset < header_start || header_end > file_len || payload_offset > file_len {
|
||||
return Err(CartridgeError::InvalidFormat);
|
||||
}
|
||||
if header_end != payload_offset {
|
||||
return Err(CartridgeError::InvalidFormat);
|
||||
}
|
||||
|
||||
file.seek(SeekFrom::Start(header_start as u64)).map_err(|_| CartridgeError::IoError)?;
|
||||
let mut header_bytes = vec![0_u8; header_len];
|
||||
file.read_exact(&mut header_bytes).map_err(|_| CartridgeError::InvalidFormat)?;
|
||||
let header: AssetsPackHeader =
|
||||
serde_json::from_slice(&header_bytes).map_err(|_| CartridgeError::InvalidFormat)?;
|
||||
validate_preload(&header.asset_table, &header.preload)?;
|
||||
let payload_len =
|
||||
u64::try_from(file_len - payload_offset).map_err(|_| CartridgeError::InvalidFormat)?;
|
||||
|
||||
Ok(ParsedAssetsPack { header, payload_offset: prelude.payload_offset, payload_len })
|
||||
}
|
||||
|
||||
fn validate_preload(
|
||||
asset_table: &[AssetEntry],
|
||||
preload: &[crate::asset::PreloadEntry],
|
||||
) -> Result<(), CartridgeError> {
|
||||
let mut claimed_slots = HashSet::<(BankType, usize)>::new();
|
||||
|
||||
for item in preload {
|
||||
let entry = asset_table
|
||||
.iter()
|
||||
.find(|entry| entry.asset_id == item.asset_id)
|
||||
.ok_or(CartridgeError::InvalidFormat)?;
|
||||
|
||||
if !claimed_slots.insert((entry.bank_type, item.slot)) {
|
||||
return Err(CartridgeError::InvalidFormat);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::asset::{AssetCodec, AssetEntry, BankType, PreloadEntry};
|
||||
use crate::cartridge::{ASSETS_PA_MAGIC, ASSETS_PA_SCHEMA_VERSION, AssetsPackPrelude};
|
||||
use crate::glyph_bank::GLYPH_BANK_PALETTE_COUNT_V1;
|
||||
use serde_json::json;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
static TEST_DIR_COUNTER: AtomicU64 = AtomicU64::new(0);
|
||||
|
||||
struct TestCartridgeDir {
|
||||
path: PathBuf,
|
||||
}
|
||||
|
||||
impl TestCartridgeDir {
|
||||
fn new(manifest: serde_json::Value) -> Self {
|
||||
let unique = TEST_DIR_COUNTER.fetch_add(1, Ordering::Relaxed);
|
||||
let timestamp = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.expect("system time must be after unix epoch")
|
||||
.as_nanos();
|
||||
let path = std::env::temp_dir()
|
||||
.join(format!("prometeu-hal-cartridge-loader-{}-{}", timestamp, unique));
|
||||
|
||||
fs::create_dir_all(&path).expect("must create temporary cartridge directory");
|
||||
fs::write(
|
||||
path.join("manifest.json"),
|
||||
serde_json::to_vec_pretty(&manifest).expect("manifest must serialize"),
|
||||
)
|
||||
.expect("must write manifest.json");
|
||||
fs::write(path.join("program.pbx"), [0x01_u8, 0x02, 0x03]).expect("must write program");
|
||||
|
||||
Self { path }
|
||||
}
|
||||
|
||||
fn path(&self) -> &Path {
|
||||
&self.path
|
||||
}
|
||||
|
||||
fn write_assets_pa(
|
||||
&self,
|
||||
asset_table: Vec<AssetEntry>,
|
||||
preload: Vec<PreloadEntry>,
|
||||
payload: &[u8],
|
||||
) {
|
||||
let header = serde_json::to_vec(&AssetsPackHeader { asset_table, preload })
|
||||
.expect("assets header must serialize");
|
||||
let payload_offset = (ASSETS_PA_PRELUDE_SIZE + header.len()) as u64;
|
||||
let prelude = AssetsPackPrelude {
|
||||
magic: ASSETS_PA_MAGIC,
|
||||
schema_version: ASSETS_PA_SCHEMA_VERSION,
|
||||
header_len: header.len() as u32,
|
||||
payload_offset,
|
||||
flags: 0,
|
||||
reserved: 0,
|
||||
header_checksum: 0,
|
||||
};
|
||||
|
||||
let mut bytes = prelude.to_bytes().to_vec();
|
||||
bytes.extend_from_slice(&header);
|
||||
bytes.extend_from_slice(payload);
|
||||
fs::write(self.path.join("assets.pa"), bytes).expect("must write assets.pa");
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for TestCartridgeDir {
|
||||
fn drop(&mut self) {
|
||||
let _ = fs::remove_dir_all(&self.path);
|
||||
}
|
||||
}
|
||||
|
||||
fn manifest_with_capabilities(capabilities: Option<Vec<&str>>) -> serde_json::Value {
|
||||
let mut manifest = json!({
|
||||
"magic": "PMTU",
|
||||
"cartridge_version": 1,
|
||||
"app_id": 1001,
|
||||
"title": "Example",
|
||||
"app_version": "1.0.0",
|
||||
"app_mode": "Game"
|
||||
});
|
||||
|
||||
if let Some(capabilities) = capabilities {
|
||||
manifest["capabilities"] = json!(capabilities);
|
||||
}
|
||||
|
||||
manifest
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_without_capabilities_defaults_to_none() {
|
||||
let dir = TestCartridgeDir::new(manifest_with_capabilities(None));
|
||||
|
||||
let cartridge = DirectoryCartridgeLoader::load(dir.path()).expect("cartridge must load");
|
||||
|
||||
assert_eq!(cartridge.capabilities, caps::NONE);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_with_single_capability_normalizes_to_flag() {
|
||||
let dir = TestCartridgeDir::new(manifest_with_capabilities(Some(vec!["gfx"])));
|
||||
|
||||
let cartridge = DirectoryCartridgeLoader::load(dir.path()).expect("cartridge must load");
|
||||
|
||||
assert_eq!(cartridge.capabilities, caps::GFX);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_with_multiple_capabilities_combines_flags() {
|
||||
let dir = TestCartridgeDir::new(manifest_with_capabilities(Some(vec!["gfx", "audio"])));
|
||||
|
||||
let cartridge = DirectoryCartridgeLoader::load(dir.path()).expect("cartridge must load");
|
||||
|
||||
assert_eq!(cartridge.capabilities, caps::GFX | caps::AUDIO);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_with_all_capability_normalizes_to_all_flags() {
|
||||
let dir = TestCartridgeDir::new(manifest_with_capabilities(Some(vec!["all"])));
|
||||
dir.write_assets_pa(vec![], vec![], &[]);
|
||||
|
||||
let cartridge = DirectoryCartridgeLoader::load(dir.path()).expect("cartridge must load");
|
||||
|
||||
assert_eq!(cartridge.capabilities, caps::ALL);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_with_none_capability_keeps_zero_flags() {
|
||||
let dir = TestCartridgeDir::new(manifest_with_capabilities(Some(vec!["none"])));
|
||||
|
||||
let cartridge = DirectoryCartridgeLoader::load(dir.path()).expect("cartridge must load");
|
||||
|
||||
assert_eq!(cartridge.capabilities, caps::NONE);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_with_duplicate_capabilities_fails() {
|
||||
let dir = TestCartridgeDir::new(manifest_with_capabilities(Some(vec!["gfx", "gfx"])));
|
||||
|
||||
let error = DirectoryCartridgeLoader::load(dir.path()).unwrap_err();
|
||||
|
||||
assert!(matches!(error, CartridgeError::InvalidManifest));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_with_unknown_capability_fails() {
|
||||
let dir = TestCartridgeDir::new(manifest_with_capabilities(Some(vec!["network"])));
|
||||
|
||||
let error = DirectoryCartridgeLoader::load(dir.path()).unwrap_err();
|
||||
|
||||
assert!(matches!(error, CartridgeError::InvalidManifest));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_with_legacy_input_capability_fails() {
|
||||
let dir = TestCartridgeDir::new(manifest_with_capabilities(Some(vec!["input"])));
|
||||
|
||||
let error = DirectoryCartridgeLoader::load(dir.path()).unwrap_err();
|
||||
|
||||
assert!(matches!(error, CartridgeError::InvalidManifest));
|
||||
}
|
||||
|
||||
fn test_asset_entry(offset: u64, size: u64) -> AssetEntry {
|
||||
AssetEntry {
|
||||
asset_id: 7,
|
||||
asset_name: "tiles".to_string(),
|
||||
bank_type: BankType::GLYPH,
|
||||
offset,
|
||||
size,
|
||||
decoded_size: 16 * 16 + (GLYPH_BANK_PALETTE_COUNT_V1 as u64 * 16 * 2),
|
||||
codec: AssetCodec::None,
|
||||
metadata: json!({
|
||||
"tile_size": 16,
|
||||
"width": 16,
|
||||
"height": 16,
|
||||
"palette_count": GLYPH_BANK_PALETTE_COUNT_V1,
|
||||
"palette_authored": GLYPH_BANK_PALETTE_COUNT_V1
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_with_asset_capability_requires_assets_pa() {
|
||||
let dir = TestCartridgeDir::new(manifest_with_capabilities(Some(vec!["asset"])));
|
||||
|
||||
let error = DirectoryCartridgeLoader::load(dir.path()).unwrap_err();
|
||||
|
||||
assert!(matches!(error, CartridgeError::MissingAssets));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_without_asset_capability_accepts_missing_assets_pa() {
|
||||
let dir = TestCartridgeDir::new(manifest_with_capabilities(Some(vec!["gfx"])));
|
||||
|
||||
let cartridge = DirectoryCartridgeLoader::load(dir.path()).expect("cartridge must load");
|
||||
|
||||
assert!(cartridge.assets.is_empty());
|
||||
assert!(cartridge.asset_table.is_empty());
|
||||
assert!(cartridge.preload.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_reads_asset_table_and_preload_from_assets_pa_header() {
|
||||
let dir = TestCartridgeDir::new(manifest_with_capabilities(Some(vec!["asset"])));
|
||||
let payload = vec![1_u8, 2, 3, 4];
|
||||
dir.write_assets_pa(
|
||||
vec![test_asset_entry(0, payload.len() as u64)],
|
||||
vec![PreloadEntry { asset_id: 7, slot: 2 }],
|
||||
&payload,
|
||||
);
|
||||
|
||||
let cartridge = DirectoryCartridgeLoader::load(dir.path()).expect("cartridge must load");
|
||||
|
||||
let slice = cartridge
|
||||
.assets
|
||||
.open_slice(0, payload.len() as u64)
|
||||
.expect("payload slice must open")
|
||||
.read_all()
|
||||
.expect("payload slice must read");
|
||||
assert_eq!(slice, payload);
|
||||
assert_eq!(cartridge.asset_table.len(), 1);
|
||||
assert_eq!(cartridge.asset_table[0].asset_name, "tiles");
|
||||
assert_eq!(cartridge.preload.len(), 1);
|
||||
assert_eq!(cartridge.preload[0].asset_id, 7);
|
||||
assert_eq!(cartridge.preload[0].slot, 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_rejects_preload_with_missing_asset_id() {
|
||||
let dir = TestCartridgeDir::new(manifest_with_capabilities(Some(vec!["asset"])));
|
||||
dir.write_assets_pa(
|
||||
vec![test_asset_entry(0, 4)],
|
||||
vec![PreloadEntry { asset_id: 999, slot: 2 }],
|
||||
&[1_u8, 2, 3, 4],
|
||||
);
|
||||
|
||||
let error = DirectoryCartridgeLoader::load(dir.path()).unwrap_err();
|
||||
|
||||
assert!(matches!(error, CartridgeError::InvalidFormat));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_rejects_preload_slot_clash_per_bank_type() {
|
||||
let dir = TestCartridgeDir::new(manifest_with_capabilities(Some(vec!["asset"])));
|
||||
let asset_table = vec![
|
||||
test_asset_entry(0, 4),
|
||||
AssetEntry {
|
||||
asset_id: 8,
|
||||
asset_name: "other_tiles".to_string(),
|
||||
bank_type: BankType::GLYPH,
|
||||
offset: 4,
|
||||
size: 4,
|
||||
decoded_size: 16 * 16 + (GLYPH_BANK_PALETTE_COUNT_V1 as u64 * 16 * 2),
|
||||
codec: AssetCodec::None,
|
||||
metadata: json!({
|
||||
"tile_size": 16,
|
||||
"width": 16,
|
||||
"height": 16,
|
||||
"palette_count": GLYPH_BANK_PALETTE_COUNT_V1,
|
||||
"palette_authored": GLYPH_BANK_PALETTE_COUNT_V1
|
||||
}),
|
||||
},
|
||||
];
|
||||
let preload =
|
||||
vec![PreloadEntry { asset_id: 7, slot: 2 }, PreloadEntry { asset_id: 8, slot: 2 }];
|
||||
dir.write_assets_pa(asset_table, preload, &[1_u8, 2, 3, 4, 5, 6, 7, 8]);
|
||||
|
||||
let error = DirectoryCartridgeLoader::load(dir.path()).unwrap_err();
|
||||
|
||||
assert!(matches!(error, CartridgeError::InvalidFormat));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_rejects_invalid_assets_pa_prelude() {
|
||||
let dir = TestCartridgeDir::new(manifest_with_capabilities(Some(vec!["asset"])));
|
||||
fs::write(dir.path().join("assets.pa"), b"not-a-valid-pack").expect("must write assets.pa");
|
||||
|
||||
let error = DirectoryCartridgeLoader::load(dir.path()).unwrap_err();
|
||||
|
||||
assert!(matches!(error, CartridgeError::InvalidFormat));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_rejects_invalid_assets_pa_header_json() {
|
||||
let dir = TestCartridgeDir::new(manifest_with_capabilities(Some(vec!["asset"])));
|
||||
let prelude = AssetsPackPrelude {
|
||||
magic: ASSETS_PA_MAGIC,
|
||||
schema_version: ASSETS_PA_SCHEMA_VERSION,
|
||||
header_len: 4,
|
||||
payload_offset: (ASSETS_PA_PRELUDE_SIZE + 4) as u64,
|
||||
flags: 0,
|
||||
reserved: 0,
|
||||
header_checksum: 0,
|
||||
};
|
||||
let mut bytes = prelude.to_bytes().to_vec();
|
||||
bytes.extend_from_slice(b"{no}");
|
||||
fs::write(dir.path().join("assets.pa"), bytes).expect("must write assets.pa");
|
||||
|
||||
let error = DirectoryCartridgeLoader::load(dir.path()).unwrap_err();
|
||||
|
||||
assert!(matches!(error, CartridgeError::InvalidFormat));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_rejects_unknown_codec_string_in_assets_pa_header() {
|
||||
let dir = TestCartridgeDir::new(manifest_with_capabilities(Some(vec!["asset"])));
|
||||
let header = serde_json::json!({
|
||||
"asset_table": [{
|
||||
"asset_id": 7,
|
||||
"asset_name": "tiles",
|
||||
"bank_type": "GLYPH",
|
||||
"offset": 0,
|
||||
"size": 4,
|
||||
"decoded_size": 768,
|
||||
"codec": "LZ4",
|
||||
"metadata": {
|
||||
"tile_size": 16,
|
||||
"width": 16,
|
||||
"height": 16,
|
||||
"palette_count": GLYPH_BANK_PALETTE_COUNT_V1,
|
||||
"palette_authored": GLYPH_BANK_PALETTE_COUNT_V1
|
||||
}
|
||||
}],
|
||||
"preload": []
|
||||
});
|
||||
let header_bytes = serde_json::to_vec(&header).expect("header must serialize");
|
||||
let prelude = AssetsPackPrelude {
|
||||
magic: ASSETS_PA_MAGIC,
|
||||
schema_version: ASSETS_PA_SCHEMA_VERSION,
|
||||
header_len: header_bytes.len() as u32,
|
||||
payload_offset: (ASSETS_PA_PRELUDE_SIZE + header_bytes.len()) as u64,
|
||||
flags: 0,
|
||||
reserved: 0,
|
||||
header_checksum: 0,
|
||||
};
|
||||
let mut bytes = prelude.to_bytes().to_vec();
|
||||
bytes.extend_from_slice(&header_bytes);
|
||||
bytes.extend_from_slice(&[1_u8, 2, 3, 4]);
|
||||
fs::write(dir.path().join("assets.pa"), bytes).expect("must write assets.pa");
|
||||
|
||||
let error = DirectoryCartridgeLoader::load(dir.path()).unwrap_err();
|
||||
|
||||
assert!(matches!(error, CartridgeError::InvalidFormat));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_rejects_legacy_raw_codec_string_in_assets_pa_header() {
|
||||
let dir = TestCartridgeDir::new(manifest_with_capabilities(Some(vec!["asset"])));
|
||||
let header = serde_json::json!({
|
||||
"asset_table": [{
|
||||
"asset_id": 7,
|
||||
"asset_name": "tiles",
|
||||
"bank_type": "GLYPH",
|
||||
"offset": 0,
|
||||
"size": 4,
|
||||
"decoded_size": 768,
|
||||
"codec": "RAW",
|
||||
"metadata": {
|
||||
"tile_size": 16,
|
||||
"width": 16,
|
||||
"height": 16,
|
||||
"palette_count": GLYPH_BANK_PALETTE_COUNT_V1,
|
||||
"palette_authored": GLYPH_BANK_PALETTE_COUNT_V1
|
||||
}
|
||||
}],
|
||||
"preload": []
|
||||
});
|
||||
let header_bytes = serde_json::to_vec(&header).expect("header must serialize");
|
||||
let prelude = AssetsPackPrelude {
|
||||
magic: ASSETS_PA_MAGIC,
|
||||
schema_version: ASSETS_PA_SCHEMA_VERSION,
|
||||
header_len: header_bytes.len() as u32,
|
||||
payload_offset: (ASSETS_PA_PRELUDE_SIZE + header_bytes.len()) as u64,
|
||||
flags: 0,
|
||||
reserved: 0,
|
||||
header_checksum: 0,
|
||||
};
|
||||
let mut bytes = prelude.to_bytes().to_vec();
|
||||
bytes.extend_from_slice(&header_bytes);
|
||||
bytes.extend_from_slice(&[1_u8, 2, 3, 4]);
|
||||
fs::write(dir.path().join("assets.pa"), bytes).expect("must write assets.pa");
|
||||
|
||||
let error = DirectoryCartridgeLoader::load(dir.path()).unwrap_err();
|
||||
|
||||
assert!(matches!(error, CartridgeError::InvalidFormat));
|
||||
}
|
||||
}
|
||||
@ -1,10 +1,13 @@
|
||||
/// Simple RGB565 color (0bRRRRRGGGGGGBBBBB).
|
||||
/// - 5 bits Red
|
||||
/// - 6 bits Green
|
||||
/// - 5 bits Blue
|
||||
/// Represents a 16-bit color in the RGB565 format.
|
||||
///
|
||||
/// There is no alpha channel.
|
||||
/// Transparency is handled via Color Key or Blend Mode.
|
||||
/// The RGB565 format is a common high-color representation for embedded systems:
|
||||
/// - **Red**: 5 bits (0..31)
|
||||
/// - **Green**: 6 bits (0..63)
|
||||
/// - **Blue**: 5 bits (0..31)
|
||||
///
|
||||
/// Prometeu does not have a hardware alpha channel. Transparency is achieved
|
||||
/// by using a specific color key (Magenta / 0xF81F) which the GFX engine
|
||||
/// skips during rendering.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
pub struct Color(pub u16);
|
||||
|
||||
@ -52,7 +55,7 @@ impl Color {
|
||||
pub const fn gray_scale(c: u8) -> Self {
|
||||
Self::rgb(c, c, c)
|
||||
}
|
||||
|
||||
|
||||
pub const fn from_raw(raw: u16) -> Self {
|
||||
Self(raw)
|
||||
}
|
||||
@ -69,5 +72,4 @@ impl Color {
|
||||
let hex = r8 << 16 | g8 << 8 | b8;
|
||||
hex as i32
|
||||
}
|
||||
|
||||
}
|
||||
10
crates/console/prometeu-hal/src/composer_status.rs
Normal file
10
crates/console/prometeu-hal/src/composer_status.rs
Normal file
@ -0,0 +1,10 @@
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[repr(i32)]
|
||||
pub enum ComposerOpStatus {
|
||||
Ok = 0,
|
||||
SceneUnavailable = 1,
|
||||
ArgRangeInvalid = 2,
|
||||
BankInvalid = 3,
|
||||
LayerInvalid = 4,
|
||||
SpriteOverflow = 5,
|
||||
}
|
||||
@ -1,7 +1,6 @@
|
||||
use crate::cartridge::AppMode;
|
||||
use prometeu_bytecode::Value;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use crate::model::AppMode;
|
||||
|
||||
use crate::virtual_machine::Value;
|
||||
|
||||
pub const DEVTOOLS_PROTOCOL_VERSION: u32 = 1;
|
||||
|
||||
@ -34,22 +33,11 @@ pub enum DebugCommand {
|
||||
#[serde(tag = "type")]
|
||||
pub enum DebugResponse {
|
||||
#[serde(rename = "handshake")]
|
||||
Handshake {
|
||||
protocol_version: u32,
|
||||
runtime_version: String,
|
||||
cartridge: HandshakeCartridge,
|
||||
},
|
||||
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,
|
||||
},
|
||||
GetState { pc: usize, stack_top: Vec<Value>, frame_index: u64, app_id: u32 },
|
||||
#[serde(rename = "breakpoints")]
|
||||
Breakpoints {
|
||||
pcs: Vec<usize>,
|
||||
},
|
||||
Breakpoints { pcs: Vec<usize> },
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
@ -64,44 +52,60 @@ pub struct HandshakeCartridge {
|
||||
#[serde(tag = "event")]
|
||||
pub enum DebugEvent {
|
||||
#[serde(rename = "breakpointHit")]
|
||||
BreakpointHit {
|
||||
pc: usize,
|
||||
frame_index: u64,
|
||||
},
|
||||
BreakpointHit { pc: usize, frame_index: u64 },
|
||||
#[serde(rename = "log")]
|
||||
Log {
|
||||
level: String,
|
||||
source: String,
|
||||
msg: String,
|
||||
},
|
||||
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,
|
||||
gfx_used_bytes: usize,
|
||||
gfx_inflight_bytes: usize,
|
||||
gfx_slots_occupied: u32,
|
||||
audio_used_bytes: usize,
|
||||
audio_inflight_bytes: usize,
|
||||
audio_slots_occupied: u32,
|
||||
glyph_slots_used: u32,
|
||||
glyph_slots_total: u32,
|
||||
sound_slots_used: u32,
|
||||
sound_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,
|
||||
},
|
||||
Cert { rule: String, used: u64, limit: u64, frame_index: u64 },
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::virtual_machine::Value;
|
||||
|
||||
#[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,
|
||||
};
|
||||
|
||||
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() {
|
||||
@ -119,4 +123,21 @@ mod tests {
|
||||
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"));
|
||||
}
|
||||
}
|
||||
80
crates/console/prometeu-hal/src/gfx_bridge.rs
Normal file
80
crates/console/prometeu-hal/src/gfx_bridge.rs
Normal file
@ -0,0 +1,80 @@
|
||||
use crate::color::Color;
|
||||
use crate::scene_viewport_cache::SceneViewportCache;
|
||||
use crate::scene_viewport_resolver::ResolverUpdate;
|
||||
use crate::sprite::Sprite;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum BlendMode {
|
||||
None,
|
||||
Half,
|
||||
HalfPlus,
|
||||
HalfMinus,
|
||||
Full,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[repr(i32)]
|
||||
pub enum GfxOpStatus {
|
||||
Ok = 0,
|
||||
AssetNotFound = 1,
|
||||
InvalidSpriteIndex = 2,
|
||||
ArgRangeInvalid = 3,
|
||||
BankInvalid = 4,
|
||||
}
|
||||
|
||||
pub trait GfxBridge {
|
||||
fn size(&self) -> (usize, usize);
|
||||
fn front_buffer(&self) -> &[u16];
|
||||
fn clear(&mut self, color: Color);
|
||||
fn fill_rect_blend(&mut self, x: i32, y: i32, w: i32, h: i32, color: Color, mode: BlendMode);
|
||||
fn fill_rect(&mut self, x: i32, y: i32, w: i32, h: i32, color: Color);
|
||||
fn draw_pixel(&mut self, x: i32, y: i32, color: Color);
|
||||
fn draw_line(&mut self, x0: i32, y0: i32, x1: i32, y1: i32, color: Color);
|
||||
fn draw_circle(&mut self, xc: i32, yc: i32, r: i32, color: Color);
|
||||
fn draw_circle_points(&mut self, xc: i32, yc: i32, x: i32, y: i32, color: Color);
|
||||
fn fill_circle(&mut self, xc: i32, yc: i32, r: i32, color: Color);
|
||||
fn draw_circle_lines(&mut self, xc: i32, yc: i32, x: i32, y: i32, color: Color);
|
||||
fn draw_disc(&mut self, x: i32, y: i32, r: i32, border_color: Color, fill_color: Color);
|
||||
fn draw_rect(&mut self, x: i32, y: i32, w: i32, h: i32, color: Color);
|
||||
fn draw_square(
|
||||
&mut self,
|
||||
x: i32,
|
||||
y: i32,
|
||||
w: i32,
|
||||
h: i32,
|
||||
border_color: Color,
|
||||
fill_color: Color,
|
||||
);
|
||||
fn draw_horizontal_line(&mut self, x0: i32, x1: i32, y: i32, color: Color);
|
||||
fn draw_vertical_line(&mut self, x: i32, y0: i32, y1: i32, color: Color);
|
||||
fn present(&mut self);
|
||||
/// Render the canonical game frame with no bound scene.
|
||||
///
|
||||
/// Deferred `gfx.*` overlay/debug primitives are intentionally outside this
|
||||
/// contract and are drained by a separate final overlay stage.
|
||||
fn render_no_scene_frame(&mut self);
|
||||
/// Render the canonical scene-backed game frame from cache/resolver state.
|
||||
///
|
||||
/// Deferred `gfx.*` overlay/debug primitives are intentionally outside this
|
||||
/// contract and are drained by a separate final overlay stage.
|
||||
fn render_scene_from_cache(&mut self, cache: &SceneViewportCache, update: &ResolverUpdate);
|
||||
fn load_frame_sprites(&mut self, sprites: &[Sprite]);
|
||||
/// Submit text into the `gfx.*` primitive path.
|
||||
///
|
||||
/// Under the accepted runtime contract this is not the canonical game
|
||||
/// composition path; it belongs to the deferred final overlay/debug family.
|
||||
fn draw_text(&mut self, x: i32, y: i32, text: &str, color: Color);
|
||||
fn draw_char(&mut self, x: i32, y: i32, c: char, color: Color);
|
||||
|
||||
fn sprite(&self, index: usize) -> &Sprite;
|
||||
fn sprite_mut(&mut self, index: usize) -> &mut Sprite;
|
||||
|
||||
fn scene_fade_level(&self) -> u8;
|
||||
fn set_scene_fade_level(&mut self, level: u8);
|
||||
fn scene_fade_color(&self) -> Color;
|
||||
fn set_scene_fade_color(&mut self, color: Color);
|
||||
fn hud_fade_level(&self) -> u8;
|
||||
fn set_hud_fade_level(&mut self, level: u8);
|
||||
fn hud_fade_color(&self) -> Color;
|
||||
fn set_hud_fade_color(&mut self, color: Color);
|
||||
}
|
||||
7
crates/console/prometeu-hal/src/glyph.rs
Normal file
7
crates/console/prometeu-hal/src/glyph.rs
Normal file
@ -0,0 +1,7 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, Deserialize, Serialize)]
|
||||
pub struct Glyph {
|
||||
pub glyph_id: u16,
|
||||
pub palette_id: u8,
|
||||
}
|
||||
@ -1,7 +1,11 @@
|
||||
use crate::model::Color;
|
||||
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)]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize, Serialize)]
|
||||
pub enum TileSize {
|
||||
/// 8x8 pixels.
|
||||
Size8 = 8,
|
||||
@ -12,11 +16,13 @@ pub enum TileSize {
|
||||
}
|
||||
|
||||
/// A container for graphical assets.
|
||||
///
|
||||
/// A TileBank stores both the raw pixel data (as palette indices) and the
|
||||
/// color palettes themselves. This encapsulates all the information needed
|
||||
/// to render a set of tiles.
|
||||
pub struct TileBank {
|
||||
///
|
||||
/// 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.
|
||||
@ -24,22 +30,23 @@ pub struct TileBank {
|
||||
/// Height of the full bank sheet in pixels.
|
||||
pub height: usize,
|
||||
|
||||
/// Pixel data stored as 4-bit indices (packed into 8-bit values).
|
||||
/// Decoded pixel data stored as one palette index per pixel.
|
||||
/// Serialized payloads are packed; runtime memory is expanded.
|
||||
/// Index 0 is always reserved for transparency.
|
||||
pub pixel_indices: Vec<u8>,
|
||||
/// Table of 64 palettes, each containing 16 RGB565 colors, total of 1024 colors for a bank.
|
||||
pub palettes: [[Color; 16]; 64],
|
||||
/// Runtime-facing v1 palette table: 64 palettes of 16 RGB565 colors each.
|
||||
pub palettes: [[Color; GLYPH_BANK_COLORS_PER_PALETTE]; GLYPH_BANK_PALETTE_COUNT_V1],
|
||||
}
|
||||
|
||||
impl TileBank {
|
||||
/// Creates an empty tile bank with the specified dimensions.
|
||||
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], // Index 0 = Transparent
|
||||
palettes: [[Color::BLACK; 16]; 64],
|
||||
palettes: [[Color::BLACK; GLYPH_BANK_COLORS_PER_PALETTE]; GLYPH_BANK_PALETTE_COUNT_V1],
|
||||
}
|
||||
}
|
||||
|
||||
@ -64,12 +71,16 @@ impl TileBank {
|
||||
|
||||
/// Maps a 4-bit index to a real RGB565 Color using the specified palette.
|
||||
pub fn resolve_color(&self, palette_id: u8, pixel_index: u8) -> Color {
|
||||
// Hardware Rule: Index 0 is always transparent.
|
||||
// Hardware Rule: Index 0 is always transparent.
|
||||
// We use Magenta as the 'transparent' signal color during composition.
|
||||
if pixel_index == 0 {
|
||||
return Color::COLOR_KEY;
|
||||
}
|
||||
|
||||
self.palettes[palette_id as usize][pixel_index as usize]
|
||||
self.palettes
|
||||
.get(palette_id as usize)
|
||||
.and_then(|palette| palette.get(pixel_index as usize))
|
||||
.copied()
|
||||
.unwrap_or(Color::COLOR_KEY)
|
||||
}
|
||||
}
|
||||
}
|
||||
31
crates/console/prometeu-hal/src/hardware_bridge.rs
Normal file
31
crates/console/prometeu-hal/src/hardware_bridge.rs
Normal file
@ -0,0 +1,31 @@
|
||||
use crate::asset_bridge::AssetBridge;
|
||||
use crate::audio_bridge::AudioBridge;
|
||||
use crate::gfx_bridge::GfxBridge;
|
||||
use crate::pad_bridge::PadBridge;
|
||||
use crate::sprite::Sprite;
|
||||
use crate::touch_bridge::TouchBridge;
|
||||
|
||||
pub trait HardwareBridge {
|
||||
fn begin_frame(&mut self);
|
||||
fn bind_scene(&mut self, scene_bank_id: usize) -> bool;
|
||||
fn unbind_scene(&mut self);
|
||||
fn set_camera(&mut self, x: i32, y: i32);
|
||||
fn emit_sprite(&mut self, sprite: Sprite) -> bool;
|
||||
fn render_frame(&mut self);
|
||||
fn has_glyph_bank(&self, bank_id: usize) -> bool;
|
||||
|
||||
fn gfx(&self) -> &dyn GfxBridge;
|
||||
fn gfx_mut(&mut self) -> &mut dyn GfxBridge;
|
||||
|
||||
fn audio(&self) -> &dyn AudioBridge;
|
||||
fn audio_mut(&mut self) -> &mut dyn AudioBridge;
|
||||
|
||||
fn pad(&self) -> &dyn PadBridge;
|
||||
fn pad_mut(&mut self) -> &mut dyn PadBridge;
|
||||
|
||||
fn touch(&self) -> &dyn TouchBridge;
|
||||
fn touch_mut(&mut self) -> &mut dyn TouchBridge;
|
||||
|
||||
fn assets(&self) -> &dyn AssetBridge;
|
||||
fn assets_mut(&mut self) -> &mut dyn AssetBridge;
|
||||
}
|
||||
24
crates/console/prometeu-hal/src/host_context.rs
Normal file
24
crates/console/prometeu-hal/src/host_context.rs
Normal file
@ -0,0 +1,24 @@
|
||||
use crate::hardware_bridge::HardwareBridge;
|
||||
use crate::vm_fault::VmFault;
|
||||
|
||||
pub struct HostContext<'a> {
|
||||
pub hw: Option<&'a mut dyn HardwareBridge>,
|
||||
}
|
||||
|
||||
impl<'a> HostContext<'a> {
|
||||
pub fn new(hw: Option<&'a mut dyn HardwareBridge>) -> Self {
|
||||
Self { hw }
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn require_hw(&mut self) -> Result<&mut dyn HardwareBridge, VmFault> {
|
||||
match &mut self.hw {
|
||||
Some(hw) => Ok(*hw),
|
||||
None => Err(VmFault::Unavailable),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait HostContextProvider {
|
||||
fn make_ctx(&'_ mut self) -> HostContext<'_>;
|
||||
}
|
||||
27
crates/console/prometeu-hal/src/host_return.rs
Normal file
27
crates/console/prometeu-hal/src/host_return.rs
Normal file
@ -0,0 +1,27 @@
|
||||
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));
|
||||
}
|
||||
}
|
||||
@ -15,12 +15,9 @@ pub struct InputSignals {
|
||||
|
||||
pub start_signal: bool,
|
||||
pub select_signal: bool,
|
||||
|
||||
|
||||
// TOUCH
|
||||
pub f_signal: bool,
|
||||
pub x_pos: i32,
|
||||
pub y_pos: i32,
|
||||
}
|
||||
|
||||
impl InputSignals {
|
||||
}
|
||||
47
crates/console/prometeu-hal/src/lib.rs
Normal file
47
crates/console/prometeu-hal/src/lib.rs
Normal file
@ -0,0 +1,47 @@
|
||||
pub mod asset;
|
||||
pub mod asset_bridge;
|
||||
pub mod audio_bridge;
|
||||
pub mod button;
|
||||
pub mod cartridge;
|
||||
pub mod cartridge_loader;
|
||||
pub mod color;
|
||||
pub mod composer_status;
|
||||
pub mod debugger_protocol;
|
||||
pub mod gfx_bridge;
|
||||
pub mod glyph;
|
||||
pub mod glyph_bank;
|
||||
pub mod hardware_bridge;
|
||||
pub mod host_context;
|
||||
pub mod host_return;
|
||||
pub mod input_signals;
|
||||
pub mod log;
|
||||
pub mod native_helpers;
|
||||
pub mod native_interface;
|
||||
pub mod pad_bridge;
|
||||
pub mod sample;
|
||||
pub mod scene_bank;
|
||||
pub mod scene_layer;
|
||||
pub mod scene_viewport_cache;
|
||||
pub mod scene_viewport_resolver;
|
||||
pub mod sound_bank;
|
||||
pub mod sprite;
|
||||
pub mod syscalls;
|
||||
pub mod telemetry;
|
||||
pub mod tile;
|
||||
pub mod tilemap;
|
||||
pub mod touch_bridge;
|
||||
pub mod vm_fault;
|
||||
pub mod window;
|
||||
|
||||
pub use asset_bridge::AssetBridge;
|
||||
pub use audio_bridge::{AudioBridge, AudioOpStatus, LoopMode};
|
||||
pub use composer_status::ComposerOpStatus;
|
||||
pub use gfx_bridge::{BlendMode, GfxBridge, GfxOpStatus};
|
||||
pub use hardware_bridge::HardwareBridge;
|
||||
pub use host_context::{HostContext, HostContextProvider};
|
||||
pub use host_return::HostReturn;
|
||||
pub use input_signals::InputSignals;
|
||||
pub use native_helpers::{expect_bool, expect_int};
|
||||
pub use native_interface::{NativeInterface, SyscallId};
|
||||
pub use pad_bridge::PadBridge;
|
||||
pub use touch_bridge::TouchBridge;
|
||||
@ -1,5 +1,4 @@
|
||||
use crate::log::LogLevel;
|
||||
use crate::log::LogSource;
|
||||
use crate::log::{LogLevel, LogSource};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct LogEvent {
|
||||
@ -1,10 +1,13 @@
|
||||
use std::collections::VecDeque;
|
||||
use crate::log::{LogEvent, LogLevel, LogSource};
|
||||
use std::collections::VecDeque;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicU32, Ordering};
|
||||
|
||||
pub struct LogService {
|
||||
events: VecDeque<LogEvent>,
|
||||
capacity: usize,
|
||||
next_seq: u64,
|
||||
pub logs_count: Arc<AtomicU32>,
|
||||
}
|
||||
|
||||
impl LogService {
|
||||
@ -13,10 +16,19 @@ impl LogService {
|
||||
events: VecDeque::with_capacity(capacity),
|
||||
capacity,
|
||||
next_seq: 0,
|
||||
logs_count: Arc::new(AtomicU32::new(0)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn log(&mut self, ts_ms: u64, frame: u64, level: LogLevel, source: LogSource, tag: u16, msg: String) {
|
||||
pub fn log(
|
||||
&mut self,
|
||||
ts_ms: u64,
|
||||
frame: u64,
|
||||
level: LogLevel,
|
||||
source: LogSource,
|
||||
tag: u16,
|
||||
msg: String,
|
||||
) {
|
||||
if self.events.len() >= self.capacity {
|
||||
self.events.pop_front();
|
||||
}
|
||||
@ -30,6 +42,11 @@ impl LogService {
|
||||
msg,
|
||||
});
|
||||
self.next_seq += 1;
|
||||
self.logs_count.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
pub fn reset_count(&mut self) {
|
||||
self.logs_count.store(0, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
pub fn get_recent(&self, n: usize) -> Vec<LogEvent> {
|
||||
@ -55,7 +72,7 @@ mod tests {
|
||||
service.log(100, 1, LogLevel::Info, LogSource::Pos, 0, "Log 1".to_string());
|
||||
service.log(110, 1, LogLevel::Info, LogSource::Pos, 0, "Log 2".to_string());
|
||||
service.log(120, 1, LogLevel::Info, LogSource::Pos, 0, "Log 3".to_string());
|
||||
|
||||
|
||||
assert_eq!(service.events.len(), 3);
|
||||
assert_eq!(service.events[0].msg, "Log 1");
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
mod log_level;
|
||||
mod log_source;
|
||||
mod log_event;
|
||||
mod log_level;
|
||||
mod log_service;
|
||||
mod log_source;
|
||||
|
||||
pub use log_level::LogLevel;
|
||||
pub use log_source::LogSource;
|
||||
pub use log_event::LogEvent;
|
||||
pub use log_level::LogLevel;
|
||||
pub use log_service::LogService;
|
||||
pub use log_source::LogSource;
|
||||
17
crates/console/prometeu-hal/src/native_helpers.rs
Normal file
17
crates/console/prometeu-hal/src/native_helpers.rs
Normal file
@ -0,0 +1,17 @@
|
||||
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)))
|
||||
}
|
||||
21
crates/console/prometeu-hal/src/native_interface.rs
Normal file
21
crates/console/prometeu-hal/src/native_interface.rs
Normal file
@ -0,0 +1,21 @@
|
||||
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>;
|
||||
}
|
||||
20
crates/console/prometeu-hal/src/pad_bridge.rs
Normal file
20
crates/console/prometeu-hal/src/pad_bridge.rs
Normal file
@ -0,0 +1,20 @@
|
||||
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;
|
||||
}
|
||||
@ -7,12 +7,7 @@ pub struct Sample {
|
||||
|
||||
impl Sample {
|
||||
pub fn new(sample_rate: u32, data: Vec<i16>) -> Self {
|
||||
Self {
|
||||
sample_rate,
|
||||
data,
|
||||
loop_start: None,
|
||||
loop_end: None,
|
||||
}
|
||||
Self { sample_rate, data, loop_start: None, loop_end: None }
|
||||
}
|
||||
|
||||
pub fn with_loop(mut self, start: u32, end: u32) -> Self {
|
||||
50
crates/console/prometeu-hal/src/scene_bank.rs
Normal file
50
crates/console/prometeu-hal/src/scene_bank.rs
Normal file
@ -0,0 +1,50 @@
|
||||
use crate::scene_layer::SceneLayer;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct SceneBank {
|
||||
pub layers: [SceneLayer; 4],
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::glyph::Glyph;
|
||||
use crate::glyph_bank::TileSize;
|
||||
use crate::scene_layer::ParallaxFactor;
|
||||
use crate::tile::Tile;
|
||||
use crate::tilemap::TileMap;
|
||||
|
||||
fn layer(glyph_bank_id: u8, parallax_x: f32, parallax_y: f32, glyph_id: u16) -> SceneLayer {
|
||||
SceneLayer {
|
||||
active: true,
|
||||
glyph_bank_id,
|
||||
tile_size: TileSize::Size16,
|
||||
parallax_factor: ParallaxFactor { x: parallax_x, y: parallax_y },
|
||||
tilemap: TileMap {
|
||||
width: 1,
|
||||
height: 1,
|
||||
tiles: vec![Tile {
|
||||
active: true,
|
||||
glyph: Glyph { glyph_id, palette_id: glyph_bank_id },
|
||||
flip_x: false,
|
||||
flip_y: false,
|
||||
}],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scene_bank_owns_exactly_four_layers() {
|
||||
let scene = SceneBank {
|
||||
layers: [
|
||||
layer(0, 1.0, 1.0, 10),
|
||||
layer(1, 0.5, 1.0, 11),
|
||||
layer(2, 1.0, 0.5, 12),
|
||||
layer(3, 0.25, 0.25, 13),
|
||||
],
|
||||
};
|
||||
|
||||
assert_eq!(scene.layers.len(), 4);
|
||||
assert_eq!(scene.layers[3].tilemap.tiles[0].glyph.glyph_id, 13);
|
||||
}
|
||||
}
|
||||
59
crates/console/prometeu-hal/src/scene_layer.rs
Normal file
59
crates/console/prometeu-hal/src/scene_layer.rs
Normal file
@ -0,0 +1,59 @@
|
||||
use crate::glyph_bank::TileSize;
|
||||
use crate::tilemap::TileMap;
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct ParallaxFactor {
|
||||
pub x: f32,
|
||||
pub y: f32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct SceneLayer {
|
||||
pub active: bool,
|
||||
pub glyph_bank_id: u8,
|
||||
pub tile_size: TileSize,
|
||||
pub parallax_factor: ParallaxFactor,
|
||||
pub tilemap: TileMap,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::glyph::Glyph;
|
||||
use crate::tile::Tile;
|
||||
|
||||
#[test]
|
||||
fn scene_layer_preserves_parallax_factor_and_tilemap_ownership() {
|
||||
let layer = SceneLayer {
|
||||
active: true,
|
||||
glyph_bank_id: 7,
|
||||
tile_size: TileSize::Size16,
|
||||
parallax_factor: ParallaxFactor { x: 0.5, y: 0.75 },
|
||||
tilemap: TileMap {
|
||||
width: 2,
|
||||
height: 1,
|
||||
tiles: vec![
|
||||
Tile {
|
||||
active: true,
|
||||
glyph: Glyph { glyph_id: 21, palette_id: 3 },
|
||||
flip_x: false,
|
||||
flip_y: false,
|
||||
},
|
||||
Tile {
|
||||
active: false,
|
||||
glyph: Glyph { glyph_id: 22, palette_id: 4 },
|
||||
flip_x: true,
|
||||
flip_y: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
assert_eq!(layer.glyph_bank_id, 7);
|
||||
assert_eq!(layer.parallax_factor.x, 0.5);
|
||||
assert_eq!(layer.parallax_factor.y, 0.75);
|
||||
assert_eq!(layer.tilemap.width, 2);
|
||||
assert_eq!(layer.tilemap.tiles[1].glyph.glyph_id, 22);
|
||||
assert!(layer.tilemap.tiles[1].flip_x);
|
||||
}
|
||||
}
|
||||
565
crates/console/prometeu-hal/src/scene_viewport_cache.rs
Normal file
565
crates/console/prometeu-hal/src/scene_viewport_cache.rs
Normal file
@ -0,0 +1,565 @@
|
||||
use crate::glyph_bank::TileSize;
|
||||
use crate::scene_bank::SceneBank;
|
||||
use crate::scene_layer::SceneLayer;
|
||||
use crate::tile::Tile;
|
||||
|
||||
const FLAG_FLIP_X: u8 = 0b0000_0001;
|
||||
const FLAG_FLIP_Y: u8 = 0b0000_0010;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
pub struct CachedTileEntry {
|
||||
pub active: bool,
|
||||
pub glyph_id: u16,
|
||||
pub palette_id: u8,
|
||||
pub flags: u8,
|
||||
pub glyph_bank_id: u8,
|
||||
}
|
||||
|
||||
impl CachedTileEntry {
|
||||
pub fn flip_x(self) -> bool {
|
||||
(self.flags & FLAG_FLIP_X) != 0
|
||||
}
|
||||
|
||||
pub fn flip_y(self) -> bool {
|
||||
(self.flags & FLAG_FLIP_Y) != 0
|
||||
}
|
||||
|
||||
fn from_tile(layer: &SceneLayer, tile: Tile) -> Self {
|
||||
let mut flags = 0_u8;
|
||||
if tile.flip_x {
|
||||
flags |= FLAG_FLIP_X;
|
||||
}
|
||||
if tile.flip_y {
|
||||
flags |= FLAG_FLIP_Y;
|
||||
}
|
||||
|
||||
Self {
|
||||
active: tile.active,
|
||||
glyph_id: tile.glyph.glyph_id,
|
||||
palette_id: tile.glyph.palette_id,
|
||||
flags,
|
||||
glyph_bank_id: layer.glyph_bank_id,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct ViewportRegion {
|
||||
pub x: usize,
|
||||
pub y: usize,
|
||||
pub width: usize,
|
||||
pub height: usize,
|
||||
}
|
||||
|
||||
impl ViewportRegion {
|
||||
pub const fn new(x: usize, y: usize, width: usize, height: usize) -> Self {
|
||||
Self { x, y, width, height }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct SceneViewportLayerCache {
|
||||
width: usize,
|
||||
height: usize,
|
||||
logical_origin_x: i32,
|
||||
logical_origin_y: i32,
|
||||
ring_origin_x: usize,
|
||||
ring_origin_y: usize,
|
||||
pub glyph_bank_id: u8,
|
||||
pub tile_size: TileSize,
|
||||
entries: Vec<CachedTileEntry>,
|
||||
pub valid: bool,
|
||||
}
|
||||
|
||||
impl SceneViewportLayerCache {
|
||||
pub fn new(layer: &SceneLayer, width: usize, height: usize) -> Self {
|
||||
Self {
|
||||
width,
|
||||
height,
|
||||
logical_origin_x: 0,
|
||||
logical_origin_y: 0,
|
||||
ring_origin_x: 0,
|
||||
ring_origin_y: 0,
|
||||
glyph_bank_id: layer.glyph_bank_id,
|
||||
tile_size: layer.tile_size,
|
||||
entries: vec![CachedTileEntry::default(); width * height],
|
||||
valid: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn width(&self) -> usize {
|
||||
self.width
|
||||
}
|
||||
|
||||
pub fn height(&self) -> usize {
|
||||
self.height
|
||||
}
|
||||
|
||||
pub fn logical_origin(&self) -> (i32, i32) {
|
||||
(self.logical_origin_x, self.logical_origin_y)
|
||||
}
|
||||
|
||||
pub fn ring_origin(&self) -> (usize, usize) {
|
||||
(self.ring_origin_x, self.ring_origin_y)
|
||||
}
|
||||
|
||||
pub fn entry(&self, cache_x: usize, cache_y: usize) -> CachedTileEntry {
|
||||
self.entries[self.physical_index(cache_x, cache_y)]
|
||||
}
|
||||
|
||||
pub fn invalidate_all(&mut self) {
|
||||
self.entries.fill(CachedTileEntry::default());
|
||||
self.valid = false;
|
||||
}
|
||||
|
||||
pub fn move_window_to(&mut self, origin_x: i32, origin_y: i32) {
|
||||
let delta_x = origin_x - self.logical_origin_x;
|
||||
let delta_y = origin_y - self.logical_origin_y;
|
||||
|
||||
self.logical_origin_x = origin_x;
|
||||
self.logical_origin_y = origin_y;
|
||||
|
||||
self.ring_origin_x = Self::wrapped_origin(self.ring_origin_x, delta_x, self.width);
|
||||
self.ring_origin_y = Self::wrapped_origin(self.ring_origin_y, delta_y, self.height);
|
||||
}
|
||||
|
||||
pub fn move_window_by(&mut self, delta_x: i32, delta_y: i32) {
|
||||
self.move_window_to(self.logical_origin_x + delta_x, self.logical_origin_y + delta_y);
|
||||
}
|
||||
|
||||
pub fn refresh_line(&mut self, layer: &SceneLayer, cache_y: usize) {
|
||||
self.refresh_region(layer, ViewportRegion::new(0, cache_y, self.width, 1));
|
||||
}
|
||||
|
||||
pub fn refresh_column(&mut self, layer: &SceneLayer, cache_x: usize) {
|
||||
self.refresh_region(layer, ViewportRegion::new(cache_x, 0, 1, self.height));
|
||||
}
|
||||
|
||||
pub fn refresh_region(&mut self, layer: &SceneLayer, region: ViewportRegion) {
|
||||
self.glyph_bank_id = layer.glyph_bank_id;
|
||||
self.tile_size = layer.tile_size;
|
||||
|
||||
let max_x = region.x.saturating_add(region.width).min(self.width);
|
||||
let max_y = region.y.saturating_add(region.height).min(self.height);
|
||||
|
||||
for cache_y in region.y..max_y {
|
||||
for cache_x in region.x..max_x {
|
||||
let entry = self.materialize_entry(layer, cache_x, cache_y);
|
||||
let idx = self.physical_index(cache_x, cache_y);
|
||||
self.entries[idx] = entry;
|
||||
}
|
||||
}
|
||||
|
||||
self.valid = true;
|
||||
}
|
||||
|
||||
pub fn refresh_all(&mut self, layer: &SceneLayer) {
|
||||
self.refresh_region(layer, ViewportRegion::new(0, 0, self.width, self.height));
|
||||
}
|
||||
|
||||
fn materialize_entry(
|
||||
&self,
|
||||
layer: &SceneLayer,
|
||||
cache_x: usize,
|
||||
cache_y: usize,
|
||||
) -> CachedTileEntry {
|
||||
let scene_x = self.logical_origin_x + cache_x as i32;
|
||||
let scene_y = self.logical_origin_y + cache_y as i32;
|
||||
|
||||
if scene_x < 0 || scene_y < 0 {
|
||||
return CachedTileEntry::default();
|
||||
}
|
||||
|
||||
let tile_x = scene_x as usize;
|
||||
let tile_y = scene_y as usize;
|
||||
if tile_x >= layer.tilemap.width || tile_y >= layer.tilemap.height {
|
||||
return CachedTileEntry::default();
|
||||
}
|
||||
|
||||
let tile = layer.tilemap.tiles[tile_y * layer.tilemap.width + tile_x];
|
||||
CachedTileEntry::from_tile(layer, tile)
|
||||
}
|
||||
|
||||
fn physical_index(&self, cache_x: usize, cache_y: usize) -> usize {
|
||||
let physical_x = (self.ring_origin_x + cache_x) % self.width;
|
||||
let physical_y = (self.ring_origin_y + cache_y) % self.height;
|
||||
physical_y * self.width + physical_x
|
||||
}
|
||||
|
||||
fn wrapped_origin(current: usize, delta: i32, span: usize) -> usize {
|
||||
if span == 0 {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let span_i32 = span as i32;
|
||||
let current_i32 = current as i32;
|
||||
(current_i32 + delta).rem_euclid(span_i32) as usize
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct SceneViewportCache {
|
||||
width: usize,
|
||||
height: usize,
|
||||
pub layers: [SceneViewportLayerCache; 4],
|
||||
}
|
||||
|
||||
impl SceneViewportCache {
|
||||
pub fn new(scene: &SceneBank, width: usize, height: usize) -> Self {
|
||||
Self {
|
||||
width,
|
||||
height,
|
||||
layers: std::array::from_fn(|i| {
|
||||
SceneViewportLayerCache::new(&scene.layers[i], width, height)
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn width(&self) -> usize {
|
||||
self.width
|
||||
}
|
||||
|
||||
pub fn height(&self) -> usize {
|
||||
self.height
|
||||
}
|
||||
|
||||
pub fn invalidate_all(&mut self) {
|
||||
for layer in &mut self.layers {
|
||||
layer.invalidate_all();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn move_layer_window_to(&mut self, layer_idx: usize, origin_x: i32, origin_y: i32) {
|
||||
self.layers[layer_idx].move_window_to(origin_x, origin_y);
|
||||
}
|
||||
|
||||
pub fn move_layer_window_by(&mut self, layer_idx: usize, delta_x: i32, delta_y: i32) {
|
||||
self.layers[layer_idx].move_window_by(delta_x, delta_y);
|
||||
}
|
||||
|
||||
pub fn refresh_layer_line(&mut self, scene: &SceneBank, layer_idx: usize, cache_y: usize) {
|
||||
self.layers[layer_idx].refresh_line(&scene.layers[layer_idx], cache_y);
|
||||
}
|
||||
|
||||
pub fn refresh_layer_column(&mut self, scene: &SceneBank, layer_idx: usize, cache_x: usize) {
|
||||
self.layers[layer_idx].refresh_column(&scene.layers[layer_idx], cache_x);
|
||||
}
|
||||
|
||||
pub fn refresh_layer_region(
|
||||
&mut self,
|
||||
scene: &SceneBank,
|
||||
layer_idx: usize,
|
||||
region: ViewportRegion,
|
||||
) {
|
||||
self.layers[layer_idx].refresh_region(&scene.layers[layer_idx], region);
|
||||
}
|
||||
|
||||
pub fn refresh_layer_all(&mut self, scene: &SceneBank, layer_idx: usize) {
|
||||
self.layers[layer_idx].refresh_all(&scene.layers[layer_idx]);
|
||||
}
|
||||
|
||||
pub fn materialize_all_layers(&mut self, scene: &SceneBank) {
|
||||
for layer_idx in 0..self.layers.len() {
|
||||
self.refresh_layer_all(scene, layer_idx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::glyph::Glyph;
|
||||
use crate::glyph_bank::TileSize;
|
||||
use crate::scene_layer::ParallaxFactor;
|
||||
use crate::tile::Tile;
|
||||
use crate::tilemap::TileMap;
|
||||
|
||||
fn make_tile(glyph_id: u16, palette_id: u8, flip_x: bool, flip_y: bool) -> Tile {
|
||||
Tile { active: true, glyph: Glyph { glyph_id, palette_id }, flip_x, flip_y }
|
||||
}
|
||||
|
||||
fn make_layer(glyph_bank_id: u8, base_glyph: u16) -> SceneLayer {
|
||||
let mut tiles = Vec::new();
|
||||
for y in 0..4 {
|
||||
for x in 0..4 {
|
||||
tiles.push(make_tile(
|
||||
base_glyph + (y * 4 + x) as u16,
|
||||
glyph_bank_id,
|
||||
x % 2 == 0,
|
||||
y % 2 == 1,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
SceneLayer {
|
||||
active: true,
|
||||
glyph_bank_id,
|
||||
tile_size: TileSize::Size16,
|
||||
parallax_factor: ParallaxFactor { x: 1.0, y: 1.0 },
|
||||
tilemap: TileMap { width: 4, height: 4, tiles },
|
||||
}
|
||||
}
|
||||
|
||||
fn make_scene() -> SceneBank {
|
||||
SceneBank {
|
||||
layers: [
|
||||
make_layer(1, 100),
|
||||
make_layer(2, 200),
|
||||
make_layer(3, 300),
|
||||
make_layer(4, 400),
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn layer_cache_wraps_ring_origin_under_window_movement() {
|
||||
let scene = make_scene();
|
||||
let mut cache = SceneViewportCache::new(&scene, 3, 3);
|
||||
|
||||
cache.move_layer_window_by(0, 1, 2);
|
||||
assert_eq!(cache.layers[0].logical_origin(), (1, 2));
|
||||
assert_eq!(cache.layers[0].ring_origin(), (1, 2));
|
||||
|
||||
cache.move_layer_window_by(0, 3, 2);
|
||||
assert_eq!(cache.layers[0].logical_origin(), (4, 4));
|
||||
assert_eq!(cache.layers[0].ring_origin(), (1, 1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn layer_cache_wraps_ring_origin_for_negative_and_large_movements() {
|
||||
let scene = make_scene();
|
||||
let mut cache = SceneViewportCache::new(&scene, 3, 3);
|
||||
|
||||
cache.move_layer_window_by(0, -1, -4);
|
||||
assert_eq!(cache.layers[0].logical_origin(), (-1, -4));
|
||||
assert_eq!(cache.layers[0].ring_origin(), (2, 2));
|
||||
|
||||
cache.move_layer_window_by(0, 7, 8);
|
||||
assert_eq!(cache.layers[0].logical_origin(), (6, 4));
|
||||
assert_eq!(cache.layers[0].ring_origin(), (0, 1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn move_window_to_matches_incremental_ring_movement() {
|
||||
let scene = make_scene();
|
||||
let mut direct = SceneViewportCache::new(&scene, 4, 4);
|
||||
let mut incremental = SceneViewportCache::new(&scene, 4, 4);
|
||||
|
||||
direct.move_layer_window_to(0, 9, -6);
|
||||
incremental.move_layer_window_by(0, 5, -2);
|
||||
incremental.move_layer_window_by(0, 4, -4);
|
||||
|
||||
assert_eq!(direct.layers[0].logical_origin(), incremental.layers[0].logical_origin());
|
||||
assert_eq!(direct.layers[0].ring_origin(), incremental.layers[0].ring_origin());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cache_entry_fields_are_derived_from_scene_tiles() {
|
||||
let scene = make_scene();
|
||||
let mut cache = SceneViewportCache::new(&scene, 2, 2);
|
||||
|
||||
cache.refresh_layer_all(&scene, 0);
|
||||
let entry = cache.layers[0].entry(1, 1);
|
||||
|
||||
assert!(entry.active);
|
||||
assert_eq!(entry.glyph_id, 105);
|
||||
assert_eq!(entry.palette_id, 1);
|
||||
assert_eq!(entry.glyph_bank_id, 1);
|
||||
assert!(!entry.flip_x());
|
||||
assert!(entry.flip_y());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn line_refresh_only_updates_the_requested_line() {
|
||||
let scene = make_scene();
|
||||
let mut cache = SceneViewportCache::new(&scene, 3, 3);
|
||||
|
||||
cache.refresh_layer_line(&scene, 0, 1);
|
||||
|
||||
assert_eq!(cache.layers[0].entry(0, 0), CachedTileEntry::default());
|
||||
assert_eq!(cache.layers[0].entry(0, 1).glyph_id, 104);
|
||||
assert_eq!(cache.layers[0].entry(2, 1).glyph_id, 106);
|
||||
assert_eq!(cache.layers[0].entry(0, 2), CachedTileEntry::default());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn column_refresh_only_updates_the_requested_column() {
|
||||
let scene = make_scene();
|
||||
let mut cache = SceneViewportCache::new(&scene, 3, 3);
|
||||
|
||||
cache.refresh_layer_column(&scene, 1, 2);
|
||||
|
||||
assert_eq!(cache.layers[1].entry(0, 0), CachedTileEntry::default());
|
||||
assert_eq!(cache.layers[1].entry(2, 0).glyph_id, 202);
|
||||
assert_eq!(cache.layers[1].entry(2, 2).glyph_id, 210);
|
||||
assert_eq!(cache.layers[1].entry(1, 2), CachedTileEntry::default());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn region_refresh_only_updates_the_requested_area() {
|
||||
let scene = make_scene();
|
||||
let mut cache = SceneViewportCache::new(&scene, 3, 3);
|
||||
|
||||
cache.refresh_layer_region(&scene, 2, ViewportRegion::new(1, 1, 2, 2));
|
||||
|
||||
assert_eq!(cache.layers[2].entry(0, 0), CachedTileEntry::default());
|
||||
assert_eq!(cache.layers[2].entry(1, 1).glyph_id, 305);
|
||||
assert_eq!(cache.layers[2].entry(2, 2).glyph_id, 310);
|
||||
assert_eq!(cache.layers[2].entry(0, 2), CachedTileEntry::default());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scene_swap_invalidation_clears_all_layers() {
|
||||
let scene = make_scene();
|
||||
let mut cache = SceneViewportCache::new(&scene, 2, 2);
|
||||
cache.materialize_all_layers(&scene);
|
||||
|
||||
cache.invalidate_all();
|
||||
|
||||
for layer in &cache.layers {
|
||||
assert!(!layer.valid);
|
||||
for y in 0..cache.height() {
|
||||
for x in 0..cache.width() {
|
||||
assert_eq!(layer.entry(x, y), CachedTileEntry::default());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn corner_style_region_update_does_not_touch_outside_tiles() {
|
||||
let scene = make_scene();
|
||||
let mut cache = SceneViewportCache::new(&scene, 4, 4);
|
||||
cache.materialize_all_layers(&scene);
|
||||
|
||||
let before = cache.layers[3].entry(1, 1);
|
||||
cache.layers[3].invalidate_all();
|
||||
cache.refresh_layer_region(&scene, 3, ViewportRegion::new(2, 2, 2, 2));
|
||||
|
||||
assert_eq!(cache.layers[3].entry(0, 0), CachedTileEntry::default());
|
||||
assert_eq!(cache.layers[3].entry(1, 1), CachedTileEntry::default());
|
||||
assert_ne!(cache.layers[3].entry(2, 2), CachedTileEntry::default());
|
||||
assert_eq!(before.glyph_id, 405);
|
||||
assert_eq!(cache.layers[3].entry(3, 3).glyph_id, 415);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn refresh_after_wrapped_window_move_materializes_new_logical_tiles() {
|
||||
let scene = make_scene();
|
||||
let mut cache = SceneViewportCache::new(&scene, 3, 3);
|
||||
|
||||
cache.refresh_layer_all(&scene, 0);
|
||||
cache.move_layer_window_to(0, 1, 2);
|
||||
cache.refresh_layer_all(&scene, 0);
|
||||
|
||||
assert_eq!(cache.layers[0].logical_origin(), (1, 2));
|
||||
assert_eq!(cache.layers[0].ring_origin(), (1, 2));
|
||||
assert_eq!(cache.layers[0].entry(0, 0).glyph_id, 109);
|
||||
assert_eq!(cache.layers[0].entry(1, 0).glyph_id, 110);
|
||||
assert_eq!(cache.layers[0].entry(2, 0).glyph_id, 111);
|
||||
assert_eq!(cache.layers[0].entry(0, 1).glyph_id, 113);
|
||||
assert_eq!(cache.layers[0].entry(2, 1).glyph_id, 115);
|
||||
assert_eq!(cache.layers[0].entry(0, 2), CachedTileEntry::default());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn partial_refresh_uses_wrapped_physical_slots_after_window_move() {
|
||||
let scene = make_scene();
|
||||
let mut cache = SceneViewportCache::new(&scene, 3, 3);
|
||||
|
||||
cache.move_layer_window_to(0, 1, 0);
|
||||
cache.refresh_layer_column(&scene, 0, 0);
|
||||
|
||||
assert_eq!(cache.layers[0].ring_origin(), (1, 0));
|
||||
assert_eq!(cache.layers[0].entry(0, 0).glyph_id, 101);
|
||||
assert_eq!(cache.layers[0].entry(0, 1).glyph_id, 105);
|
||||
assert_eq!(cache.layers[0].entry(0, 2).glyph_id, 109);
|
||||
assert_eq!(cache.layers[0].entry(1, 0), CachedTileEntry::default());
|
||||
assert_eq!(cache.layers[0].entry(2, 0), CachedTileEntry::default());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn out_of_bounds_logical_origins_materialize_default_entries_after_wrap() {
|
||||
let scene = make_scene();
|
||||
let mut cache = SceneViewportCache::new(&scene, 2, 2);
|
||||
|
||||
cache.move_layer_window_to(0, -2, 3);
|
||||
cache.refresh_layer_all(&scene, 0);
|
||||
|
||||
for y in 0..2 {
|
||||
for x in 0..2 {
|
||||
assert_eq!(cache.layers[0].entry(x, y), CachedTileEntry::default());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ringbuffer_preserves_logical_tile_mapping_across_long_mixed_movements() {
|
||||
let scene = make_scene();
|
||||
let mut cache = SceneViewportCache::new(&scene, 3, 3);
|
||||
let motions = [
|
||||
(1, 0),
|
||||
(0, 1),
|
||||
(2, 2),
|
||||
(-1, 0),
|
||||
(0, -2),
|
||||
(4, 1),
|
||||
(-3, 3),
|
||||
(5, -4),
|
||||
(-6, 2),
|
||||
(3, -3),
|
||||
(7, 7),
|
||||
(-8, -5),
|
||||
];
|
||||
|
||||
for &(dx, dy) in &motions {
|
||||
cache.move_layer_window_by(0, dx, dy);
|
||||
cache.refresh_layer_all(&scene, 0);
|
||||
|
||||
let (origin_x, origin_y) = cache.layers[0].logical_origin();
|
||||
for cache_y in 0..cache.height() {
|
||||
for cache_x in 0..cache.width() {
|
||||
let expected_scene_x = origin_x + cache_x as i32;
|
||||
let expected_scene_y = origin_y + cache_y as i32;
|
||||
|
||||
let expected = if expected_scene_x < 0
|
||||
|| expected_scene_y < 0
|
||||
|| expected_scene_x as usize >= scene.layers[0].tilemap.width
|
||||
|| expected_scene_y as usize >= scene.layers[0].tilemap.height
|
||||
{
|
||||
CachedTileEntry::default()
|
||||
} else {
|
||||
let tile_x = expected_scene_x as usize;
|
||||
let tile_y = expected_scene_y as usize;
|
||||
let tile = scene.layers[0].tilemap.tiles
|
||||
[tile_y * scene.layers[0].tilemap.width + tile_x];
|
||||
CachedTileEntry::from_tile(&scene.layers[0], tile)
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
cache.layers[0].entry(cache_x, cache_y),
|
||||
expected,
|
||||
"mismatch at logical origin ({}, {}), cache ({}, {})",
|
||||
origin_x,
|
||||
origin_y,
|
||||
cache_x,
|
||||
cache_y
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn materialization_populates_all_four_layers() {
|
||||
let scene = make_scene();
|
||||
let mut cache = SceneViewportCache::new(&scene, 2, 2);
|
||||
|
||||
cache.materialize_all_layers(&scene);
|
||||
|
||||
assert_eq!(cache.layers[0].entry(0, 0).glyph_id, 100);
|
||||
assert_eq!(cache.layers[1].entry(0, 0).glyph_id, 200);
|
||||
assert_eq!(cache.layers[2].entry(1, 1).glyph_id, 305);
|
||||
assert_eq!(cache.layers[3].entry(1, 0).glyph_id, 401);
|
||||
}
|
||||
}
|
||||
536
crates/console/prometeu-hal/src/scene_viewport_resolver.rs
Normal file
536
crates/console/prometeu-hal/src/scene_viewport_resolver.rs
Normal file
@ -0,0 +1,536 @@
|
||||
use crate::glyph_bank::TileSize;
|
||||
use crate::scene_bank::SceneBank;
|
||||
use crate::scene_viewport_cache::ViewportRegion;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
pub struct TileAnchor {
|
||||
pub x: i32,
|
||||
pub y: i32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum CacheRefreshRequest {
|
||||
InvalidateLayer { layer_index: usize },
|
||||
RefreshLine { layer_index: usize, cache_y: usize },
|
||||
RefreshColumn { layer_index: usize, cache_x: usize },
|
||||
RefreshRegion { layer_index: usize, region: ViewportRegion },
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct LayerCopyRequest {
|
||||
pub layer_index: usize,
|
||||
pub tile_size: TileSize,
|
||||
pub viewport_width_px: i32,
|
||||
pub viewport_height_px: i32,
|
||||
pub source_offset_x_px: i32,
|
||||
pub source_offset_y_px: i32,
|
||||
pub cache_origin_tile_x: i32,
|
||||
pub cache_origin_tile_y: i32,
|
||||
pub camera_x_px: i32,
|
||||
pub camera_y_px: i32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct ResolverUpdate {
|
||||
pub master_anchor: TileAnchor,
|
||||
pub layer_anchors: [TileAnchor; 4],
|
||||
pub refresh_requests: Vec<CacheRefreshRequest>,
|
||||
pub copy_requests: [LayerCopyRequest; 4],
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct SceneViewportResolver {
|
||||
viewport_width_px: i32,
|
||||
viewport_height_px: i32,
|
||||
cache_width_tiles: usize,
|
||||
cache_height_tiles: usize,
|
||||
hysteresis_safe_px: i32,
|
||||
hysteresis_trigger_px: i32,
|
||||
initialized: bool,
|
||||
master_anchor: TileAnchor,
|
||||
layer_anchors: [TileAnchor; 4],
|
||||
}
|
||||
|
||||
impl SceneViewportResolver {
|
||||
pub fn new(
|
||||
viewport_width_px: i32,
|
||||
viewport_height_px: i32,
|
||||
cache_width_tiles: usize,
|
||||
cache_height_tiles: usize,
|
||||
hysteresis_safe_px: i32,
|
||||
hysteresis_trigger_px: i32,
|
||||
) -> Self {
|
||||
Self {
|
||||
viewport_width_px,
|
||||
viewport_height_px,
|
||||
cache_width_tiles,
|
||||
cache_height_tiles,
|
||||
hysteresis_safe_px,
|
||||
hysteresis_trigger_px,
|
||||
initialized: false,
|
||||
master_anchor: TileAnchor::default(),
|
||||
layer_anchors: [TileAnchor::default(); 4],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn reset_scene(&mut self) -> Vec<CacheRefreshRequest> {
|
||||
self.initialized = false;
|
||||
vec![
|
||||
CacheRefreshRequest::InvalidateLayer { layer_index: 0 },
|
||||
CacheRefreshRequest::InvalidateLayer { layer_index: 1 },
|
||||
CacheRefreshRequest::InvalidateLayer { layer_index: 2 },
|
||||
CacheRefreshRequest::InvalidateLayer { layer_index: 3 },
|
||||
]
|
||||
}
|
||||
|
||||
pub fn update(
|
||||
&mut self,
|
||||
scene: &SceneBank,
|
||||
camera_x_px: i32,
|
||||
camera_y_px: i32,
|
||||
) -> ResolverUpdate {
|
||||
let mut refresh_requests = Vec::new();
|
||||
let camera_center_x = camera_x_px + self.viewport_width_px / 2;
|
||||
let camera_center_y = camera_y_px + self.viewport_height_px / 2;
|
||||
|
||||
let layer_inputs: [(i32, i32, i32, i32, i32); 4] = std::array::from_fn(|i| {
|
||||
let layer = &scene.layers[i];
|
||||
let tile_size_px = layer.tile_size as i32;
|
||||
let layer_camera_x_px = ((camera_x_px as f32) * layer.parallax_factor.x).floor() as i32;
|
||||
let layer_camera_y_px = ((camera_y_px as f32) * layer.parallax_factor.y).floor() as i32;
|
||||
let layer_center_x_px = layer_camera_x_px + self.viewport_width_px / 2;
|
||||
let layer_center_y_px = layer_camera_y_px + self.viewport_height_px / 2;
|
||||
(
|
||||
tile_size_px,
|
||||
layer_camera_x_px,
|
||||
layer_camera_y_px,
|
||||
layer_center_x_px,
|
||||
layer_center_y_px,
|
||||
)
|
||||
});
|
||||
|
||||
if !self.initialized {
|
||||
self.master_anchor = self.initial_anchor(
|
||||
scene.layers[0].tilemap.width,
|
||||
scene.layers[0].tilemap.height,
|
||||
scene.layers[0].tile_size as i32,
|
||||
camera_center_x,
|
||||
camera_center_y,
|
||||
);
|
||||
|
||||
for (layer_index, layer) in scene.layers.iter().enumerate() {
|
||||
let (_, _, _, layer_center_x_px, layer_center_y_px) = layer_inputs[layer_index];
|
||||
self.layer_anchors[layer_index] = self.initial_anchor(
|
||||
layer.tilemap.width,
|
||||
layer.tilemap.height,
|
||||
layer.tile_size as i32,
|
||||
layer_center_x_px,
|
||||
layer_center_y_px,
|
||||
);
|
||||
refresh_requests.push(CacheRefreshRequest::RefreshRegion {
|
||||
layer_index,
|
||||
region: ViewportRegion::new(
|
||||
0,
|
||||
0,
|
||||
self.cache_width_tiles,
|
||||
self.cache_height_tiles,
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
self.initialized = true;
|
||||
} else {
|
||||
let layer0 = &scene.layers[0];
|
||||
self.master_anchor = self.advance_anchor(
|
||||
self.master_anchor,
|
||||
camera_center_x,
|
||||
camera_center_y,
|
||||
layer0.tile_size as i32,
|
||||
layer0.tilemap.width,
|
||||
layer0.tilemap.height,
|
||||
);
|
||||
|
||||
for (layer_index, layer) in scene.layers.iter().enumerate() {
|
||||
let previous = self.layer_anchors[layer_index];
|
||||
let (tile_size_px, _, _, layer_center_x_px, layer_center_y_px) =
|
||||
layer_inputs[layer_index];
|
||||
let next = self.advance_anchor(
|
||||
previous,
|
||||
layer_center_x_px,
|
||||
layer_center_y_px,
|
||||
tile_size_px,
|
||||
layer.tilemap.width,
|
||||
layer.tilemap.height,
|
||||
);
|
||||
self.layer_anchors[layer_index] = next;
|
||||
self.emit_refresh_requests(layer_index, previous, next, &mut refresh_requests);
|
||||
}
|
||||
}
|
||||
|
||||
let copy_requests = std::array::from_fn(|layer_index| {
|
||||
let layer = &scene.layers[layer_index];
|
||||
let (tile_size_px, layer_camera_x_px, layer_camera_y_px, _, _) =
|
||||
layer_inputs[layer_index];
|
||||
let anchor = self.layer_anchors[layer_index];
|
||||
let cache_origin_tile_x = anchor.x - (self.cache_width_tiles as i32 / 2);
|
||||
let cache_origin_tile_y = anchor.y - (self.cache_height_tiles as i32 / 2);
|
||||
let cache_origin_x_px = cache_origin_tile_x * tile_size_px;
|
||||
let cache_origin_y_px = cache_origin_tile_y * tile_size_px;
|
||||
|
||||
LayerCopyRequest {
|
||||
layer_index,
|
||||
tile_size: layer.tile_size,
|
||||
viewport_width_px: self.viewport_width_px,
|
||||
viewport_height_px: self.viewport_height_px,
|
||||
source_offset_x_px: layer_camera_x_px - cache_origin_x_px,
|
||||
source_offset_y_px: layer_camera_y_px - cache_origin_y_px,
|
||||
cache_origin_tile_x,
|
||||
cache_origin_tile_y,
|
||||
camera_x_px: layer_camera_x_px,
|
||||
camera_y_px: layer_camera_y_px,
|
||||
}
|
||||
});
|
||||
|
||||
ResolverUpdate {
|
||||
master_anchor: self.master_anchor,
|
||||
layer_anchors: self.layer_anchors,
|
||||
refresh_requests,
|
||||
copy_requests,
|
||||
}
|
||||
}
|
||||
|
||||
fn initial_anchor(
|
||||
&self,
|
||||
scene_width_tiles: usize,
|
||||
scene_height_tiles: usize,
|
||||
tile_size_px: i32,
|
||||
camera_center_x_px: i32,
|
||||
camera_center_y_px: i32,
|
||||
) -> TileAnchor {
|
||||
let proposed = TileAnchor {
|
||||
x: camera_center_x_px.div_euclid(tile_size_px),
|
||||
y: camera_center_y_px.div_euclid(tile_size_px),
|
||||
};
|
||||
self.clamp_anchor(proposed, scene_width_tiles, scene_height_tiles)
|
||||
}
|
||||
|
||||
fn advance_anchor(
|
||||
&self,
|
||||
current: TileAnchor,
|
||||
camera_center_x_px: i32,
|
||||
camera_center_y_px: i32,
|
||||
tile_size_px: i32,
|
||||
scene_width_tiles: usize,
|
||||
scene_height_tiles: usize,
|
||||
) -> TileAnchor {
|
||||
let mut next = current;
|
||||
|
||||
loop {
|
||||
let center_x_px = next.x * tile_size_px + tile_size_px / 2;
|
||||
let drift_x = camera_center_x_px - center_x_px;
|
||||
if drift_x.abs() <= self.hysteresis_safe_px {
|
||||
break;
|
||||
}
|
||||
if drift_x > self.hysteresis_trigger_px {
|
||||
next.x += 1;
|
||||
continue;
|
||||
}
|
||||
if drift_x < -self.hysteresis_trigger_px {
|
||||
next.x -= 1;
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
loop {
|
||||
let center_y_px = next.y * tile_size_px + tile_size_px / 2;
|
||||
let drift_y = camera_center_y_px - center_y_px;
|
||||
if drift_y.abs() <= self.hysteresis_safe_px {
|
||||
break;
|
||||
}
|
||||
if drift_y > self.hysteresis_trigger_px {
|
||||
next.y += 1;
|
||||
continue;
|
||||
}
|
||||
if drift_y < -self.hysteresis_trigger_px {
|
||||
next.y -= 1;
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
self.clamp_anchor(next, scene_width_tiles, scene_height_tiles)
|
||||
}
|
||||
|
||||
fn clamp_anchor(
|
||||
&self,
|
||||
proposed: TileAnchor,
|
||||
scene_width_tiles: usize,
|
||||
scene_height_tiles: usize,
|
||||
) -> TileAnchor {
|
||||
TileAnchor {
|
||||
x: self.clamp_axis(proposed.x, scene_width_tiles, self.cache_width_tiles),
|
||||
y: self.clamp_axis(proposed.y, scene_height_tiles, self.cache_height_tiles),
|
||||
}
|
||||
}
|
||||
|
||||
fn clamp_axis(&self, proposed: i32, scene_tiles: usize, cache_tiles: usize) -> i32 {
|
||||
let half = (cache_tiles / 2) as i32;
|
||||
if scene_tiles <= cache_tiles {
|
||||
return half;
|
||||
}
|
||||
|
||||
let min = half;
|
||||
let max = (scene_tiles - cache_tiles) as i32 + half;
|
||||
proposed.clamp(min, max)
|
||||
}
|
||||
|
||||
fn emit_refresh_requests(
|
||||
&self,
|
||||
layer_index: usize,
|
||||
previous: TileAnchor,
|
||||
next: TileAnchor,
|
||||
requests: &mut Vec<CacheRefreshRequest>,
|
||||
) {
|
||||
let delta_x = next.x - previous.x;
|
||||
let delta_y = next.y - previous.y;
|
||||
|
||||
if delta_x == 0 && delta_y == 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
if delta_x == 0 {
|
||||
self.emit_line_requests(layer_index, delta_y, requests);
|
||||
return;
|
||||
}
|
||||
|
||||
if delta_y == 0 {
|
||||
self.emit_column_requests(layer_index, delta_x, requests);
|
||||
return;
|
||||
}
|
||||
|
||||
self.emit_corner_region_requests(layer_index, delta_x, delta_y, requests);
|
||||
}
|
||||
|
||||
fn emit_line_requests(
|
||||
&self,
|
||||
layer_index: usize,
|
||||
delta_y: i32,
|
||||
requests: &mut Vec<CacheRefreshRequest>,
|
||||
) {
|
||||
let count = delta_y.unsigned_abs() as usize;
|
||||
if delta_y > 0 {
|
||||
for offset in 0..count {
|
||||
requests.push(CacheRefreshRequest::RefreshLine {
|
||||
layer_index,
|
||||
cache_y: self.cache_height_tiles - count + offset,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
for cache_y in 0..count {
|
||||
requests.push(CacheRefreshRequest::RefreshLine { layer_index, cache_y });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn emit_column_requests(
|
||||
&self,
|
||||
layer_index: usize,
|
||||
delta_x: i32,
|
||||
requests: &mut Vec<CacheRefreshRequest>,
|
||||
) {
|
||||
let count = delta_x.unsigned_abs() as usize;
|
||||
if delta_x > 0 {
|
||||
for offset in 0..count {
|
||||
requests.push(CacheRefreshRequest::RefreshColumn {
|
||||
layer_index,
|
||||
cache_x: self.cache_width_tiles - count + offset,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
for cache_x in 0..count {
|
||||
requests.push(CacheRefreshRequest::RefreshColumn { layer_index, cache_x });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn emit_corner_region_requests(
|
||||
&self,
|
||||
layer_index: usize,
|
||||
delta_x: i32,
|
||||
delta_y: i32,
|
||||
requests: &mut Vec<CacheRefreshRequest>,
|
||||
) {
|
||||
let width = delta_x.unsigned_abs() as usize;
|
||||
let height = delta_y.unsigned_abs() as usize;
|
||||
|
||||
let primary_x = if delta_x > 0 { self.cache_width_tiles - width } else { 0 };
|
||||
requests.push(CacheRefreshRequest::RefreshRegion {
|
||||
layer_index,
|
||||
region: ViewportRegion::new(primary_x, 0, width, self.cache_height_tiles),
|
||||
});
|
||||
|
||||
let secondary_y = if delta_y > 0 { self.cache_height_tiles - height } else { 0 };
|
||||
let secondary_x = if delta_x > 0 { 0 } else { width };
|
||||
let secondary_width = self.cache_width_tiles.saturating_sub(width);
|
||||
|
||||
if secondary_width > 0 {
|
||||
requests.push(CacheRefreshRequest::RefreshRegion {
|
||||
layer_index,
|
||||
region: ViewportRegion::new(secondary_x, secondary_y, secondary_width, height),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::glyph::Glyph;
|
||||
use crate::glyph_bank::TileSize;
|
||||
use crate::scene_layer::{ParallaxFactor, SceneLayer};
|
||||
use crate::tile::Tile;
|
||||
use crate::tilemap::TileMap;
|
||||
|
||||
fn make_layer(
|
||||
tile_size: TileSize,
|
||||
parallax_x: f32,
|
||||
parallax_y: f32,
|
||||
width: usize,
|
||||
height: usize,
|
||||
) -> SceneLayer {
|
||||
let mut tiles = Vec::new();
|
||||
for i in 0..(width * height) {
|
||||
tiles.push(Tile {
|
||||
active: true,
|
||||
glyph: Glyph { glyph_id: i as u16, palette_id: 0 },
|
||||
flip_x: false,
|
||||
flip_y: false,
|
||||
});
|
||||
}
|
||||
|
||||
SceneLayer {
|
||||
active: true,
|
||||
glyph_bank_id: 1,
|
||||
tile_size,
|
||||
parallax_factor: ParallaxFactor { x: parallax_x, y: parallax_y },
|
||||
tilemap: TileMap { width, height, tiles },
|
||||
}
|
||||
}
|
||||
|
||||
fn make_scene() -> SceneBank {
|
||||
SceneBank {
|
||||
layers: [
|
||||
make_layer(TileSize::Size16, 1.0, 1.0, 64, 64),
|
||||
make_layer(TileSize::Size16, 0.5, 0.5, 64, 64),
|
||||
make_layer(TileSize::Size16, 1.0, 0.75, 64, 64),
|
||||
make_layer(TileSize::Size16, 0.25, 1.0, 64, 64),
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn first_update_initializes_master_and_layer_anchors() {
|
||||
let scene = make_scene();
|
||||
let mut resolver = SceneViewportResolver::new(320, 180, 25, 16, 12, 20);
|
||||
|
||||
let update = resolver.update(&scene, 0, 0);
|
||||
|
||||
assert_eq!(update.master_anchor, TileAnchor { x: 12, y: 8 });
|
||||
assert_eq!(update.layer_anchors[0], TileAnchor { x: 12, y: 8 });
|
||||
assert_eq!(update.layer_anchors[1], TileAnchor { x: 12, y: 8 });
|
||||
assert_eq!(update.refresh_requests.len(), 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn per_layer_copy_requests_follow_parallax_factor() {
|
||||
let scene = make_scene();
|
||||
let mut resolver = SceneViewportResolver::new(320, 180, 25, 16, 12, 20);
|
||||
|
||||
let update = resolver.update(&scene, 32, 48);
|
||||
|
||||
assert_eq!(update.copy_requests[0].camera_x_px, 32);
|
||||
assert_eq!(update.copy_requests[0].camera_y_px, 48);
|
||||
assert_eq!(update.copy_requests[1].camera_x_px, 16);
|
||||
assert_eq!(update.copy_requests[1].camera_y_px, 24);
|
||||
assert_eq!(update.copy_requests[3].camera_x_px, 8);
|
||||
assert_eq!(update.copy_requests[3].camera_y_px, 48);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hysteresis_prevents_small_back_and_forth_refresh_churn() {
|
||||
let scene = make_scene();
|
||||
let mut resolver = SceneViewportResolver::new(320, 180, 25, 16, 12, 20);
|
||||
|
||||
let _ = resolver.update(&scene, 0, 0);
|
||||
let update = resolver.update(&scene, 8, 0);
|
||||
|
||||
assert!(update.refresh_requests.is_empty());
|
||||
assert_eq!(update.master_anchor, TileAnchor { x: 12, y: 8 });
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn repeated_high_speed_movement_advances_in_tile_steps() {
|
||||
let scene = make_scene();
|
||||
let mut resolver = SceneViewportResolver::new(320, 180, 25, 16, 12, 20);
|
||||
|
||||
let _ = resolver.update(&scene, 0, 0);
|
||||
let update = resolver.update(&scene, 64, 0);
|
||||
|
||||
assert!(update.master_anchor.x > 12);
|
||||
assert!(update.refresh_requests.iter().any(|request| matches!(
|
||||
request,
|
||||
CacheRefreshRequest::RefreshColumn { layer_index: 0, .. }
|
||||
| CacheRefreshRequest::RefreshRegion { layer_index: 0, .. }
|
||||
)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn anchors_clamp_near_scene_edges() {
|
||||
let scene = make_scene();
|
||||
let mut resolver = SceneViewportResolver::new(320, 180, 25, 16, 12, 20);
|
||||
|
||||
let update = resolver.update(&scene, 10_000, 10_000);
|
||||
|
||||
assert_eq!(update.master_anchor, TileAnchor { x: 51, y: 56 });
|
||||
assert_eq!(update.layer_anchors[0], TileAnchor { x: 51, y: 56 });
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn corner_trigger_converts_to_non_overlapping_region_requests() {
|
||||
let scene = make_scene();
|
||||
let mut resolver = SceneViewportResolver::new(320, 180, 25, 16, 12, 20);
|
||||
|
||||
let _ = resolver.update(&scene, 0, 0);
|
||||
let update = resolver.update(&scene, 64, 80);
|
||||
|
||||
let regions: Vec<_> = update
|
||||
.refresh_requests
|
||||
.iter()
|
||||
.filter_map(|request| match request {
|
||||
CacheRefreshRequest::RefreshRegion { layer_index: 0, region } => Some(*region),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
|
||||
assert_eq!(regions.len(), 2);
|
||||
assert_eq!(regions[0].x + regions[0].width, 25);
|
||||
assert_eq!(regions[0].height, 16);
|
||||
assert_eq!(regions[1].width, 24);
|
||||
assert_eq!(regions[1].height, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_scene_requests_full_invalidation_for_all_layers() {
|
||||
let mut resolver = SceneViewportResolver::new(320, 180, 25, 16, 12, 20);
|
||||
|
||||
let requests = resolver.reset_scene();
|
||||
|
||||
assert_eq!(requests.len(), 4);
|
||||
assert!(
|
||||
requests
|
||||
.iter()
|
||||
.all(|request| matches!(request, CacheRefreshRequest::InvalidateLayer { .. }))
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,9 +1,9 @@
|
||||
use crate::model::Sample;
|
||||
use crate::sample::Sample;
|
||||
use std::sync::Arc;
|
||||
|
||||
/// A container for audio assets.
|
||||
///
|
||||
/// A SoundBank stores multiple audio samples that can be played by the
|
||||
///
|
||||
/// A SoundBank stores multiple audio samples that can be played by the
|
||||
/// audio subsystem.
|
||||
pub struct SoundBank {
|
||||
pub samples: Vec<Arc<Sample>>,
|
||||
@ -1,13 +1,14 @@
|
||||
use crate::model::Tile;
|
||||
use crate::glyph::Glyph;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct Sprite {
|
||||
pub tile: Tile,
|
||||
pub glyph: Glyph,
|
||||
pub x: i32,
|
||||
pub y: i32,
|
||||
pub layer: u8,
|
||||
pub bank_id: u8,
|
||||
pub active: bool,
|
||||
pub flip_x: bool,
|
||||
pub flip_y: bool,
|
||||
pub priority: u8,
|
||||
}
|
||||
}
|
||||
161
crates/console/prometeu-hal/src/syscalls.rs
Normal file
161
crates/console/prometeu-hal/src/syscalls.rs
Normal file
@ -0,0 +1,161 @@
|
||||
mod domains;
|
||||
mod registry;
|
||||
mod resolver;
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
pub mod caps;
|
||||
|
||||
pub use resolver::{
|
||||
DeclaredLoadError, LoadError, SyscallIdentity, SyscallResolved,
|
||||
resolve_declared_program_syscalls, resolve_program_syscalls, resolve_syscall,
|
||||
};
|
||||
|
||||
/// Enumeration of all System Calls (Syscalls) available in the Prometeu environment.
|
||||
///
|
||||
/// Syscalls are the primary mechanism for a program running in the Virtual Machine
|
||||
/// to interact with the outside world (Hardware, OS, Filesystem).
|
||||
///
|
||||
/// Each Syscall has a unique 32-bit ID. The IDs are grouped by category:
|
||||
/// - **0x0xxx**: System & OS Control
|
||||
/// - **0x1xxx**: Graphics (GFX)
|
||||
/// - **0x11xx**: Frame Composer orchestration
|
||||
/// - **0x2xxx**: Reserved for legacy input syscalls (disabled for v1 VM-owned input)
|
||||
/// - **0x3xxx**: Audio (PCM & Mixing)
|
||||
/// - **0x4xxx**: Filesystem (Sandboxed I/O)
|
||||
/// - **0x5xxx**: Logging & Debugging
|
||||
/// - **0x6xxx**: Asset Loading & Memory Banks
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[repr(u32)]
|
||||
pub enum Syscall {
|
||||
SystemHasCart = 0x0001,
|
||||
SystemRunCart = 0x0002,
|
||||
GfxClear = 0x1001,
|
||||
GfxFillRect = 0x1002,
|
||||
GfxDrawLine = 0x1003,
|
||||
GfxDrawCircle = 0x1004,
|
||||
GfxDrawDisc = 0x1005,
|
||||
GfxDrawSquare = 0x1006,
|
||||
GfxDrawText = 0x1008,
|
||||
GfxClear565 = 0x1010,
|
||||
ComposerBindScene = 0x1101,
|
||||
ComposerUnbindScene = 0x1102,
|
||||
ComposerSetCamera = 0x1103,
|
||||
ComposerEmitSprite = 0x1104,
|
||||
AudioPlaySample = 0x3001,
|
||||
AudioPlay = 0x3002,
|
||||
FsOpen = 0x4001,
|
||||
FsRead = 0x4002,
|
||||
FsWrite = 0x4003,
|
||||
FsClose = 0x4004,
|
||||
FsListDir = 0x4005,
|
||||
FsExists = 0x4006,
|
||||
FsDelete = 0x4007,
|
||||
MemSlotCount = 0x4201,
|
||||
MemSlotStat = 0x4202,
|
||||
MemSlotRead = 0x4203,
|
||||
MemSlotWrite = 0x4204,
|
||||
MemSlotCommit = 0x4205,
|
||||
MemSlotClear = 0x4206,
|
||||
LogWrite = 0x5001,
|
||||
LogWriteTag = 0x5002,
|
||||
AssetLoad = 0x6001,
|
||||
AssetStatus = 0x6002,
|
||||
AssetCommit = 0x6003,
|
||||
AssetCancel = 0x6004,
|
||||
BankInfo = 0x6101,
|
||||
BankSlotInfo = 0x6102,
|
||||
}
|
||||
|
||||
/// Canonical metadata describing a syscall using the unified slot-based ABI.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct SyscallMeta {
|
||||
pub id: u32,
|
||||
pub module: &'static str,
|
||||
pub name: &'static str,
|
||||
pub version: u16,
|
||||
pub arg_slots: u8,
|
||||
pub ret_slots: u16,
|
||||
pub caps: CapFlags,
|
||||
pub determinism: Determinism,
|
||||
pub may_allocate: bool,
|
||||
pub cost_hint: u32,
|
||||
}
|
||||
|
||||
/// Bitflags representing capabilities required to invoke a syscall.
|
||||
pub type CapFlags = u64;
|
||||
|
||||
/// Determinism flags for a syscall.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Determinism {
|
||||
Unknown,
|
||||
Deterministic,
|
||||
NonDeterministic,
|
||||
}
|
||||
|
||||
/// Pairing of a strongly-typed syscall and its metadata.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct SyscallRegistryEntry {
|
||||
pub syscall: Syscall,
|
||||
pub meta: SyscallMeta,
|
||||
}
|
||||
|
||||
impl SyscallRegistryEntry {
|
||||
/// Starts the builder with mandatory fields and sensible default values.
|
||||
pub const fn builder(syscall: Syscall, module: &'static str, name: &'static str) -> Self {
|
||||
Self {
|
||||
syscall,
|
||||
meta: SyscallMeta {
|
||||
id: syscall as u32,
|
||||
module,
|
||||
name,
|
||||
version: 1, // Default for new syscalls
|
||||
arg_slots: 0,
|
||||
ret_slots: 0,
|
||||
caps: 0,
|
||||
determinism: Determinism::Deterministic,
|
||||
may_allocate: false,
|
||||
cost_hint: 1,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn version(mut self, n: u16) -> Self {
|
||||
self.meta.version = n;
|
||||
self
|
||||
}
|
||||
|
||||
pub const fn args(mut self, n: u8) -> Self {
|
||||
self.meta.arg_slots = n;
|
||||
self
|
||||
}
|
||||
|
||||
pub const fn rets(mut self, n: u16) -> Self {
|
||||
self.meta.ret_slots = n;
|
||||
self
|
||||
}
|
||||
|
||||
pub const fn caps(mut self, caps: CapFlags) -> Self {
|
||||
self.meta.caps = caps;
|
||||
self
|
||||
}
|
||||
|
||||
pub const fn non_deterministic(mut self) -> Self {
|
||||
self.meta.determinism = Determinism::NonDeterministic;
|
||||
self
|
||||
}
|
||||
|
||||
pub const fn may_allocate(mut self) -> Self {
|
||||
self.meta.may_allocate = true;
|
||||
self
|
||||
}
|
||||
|
||||
pub const fn cost(mut self, cost: u32) -> Self {
|
||||
self.meta.cost_hint = cost;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
pub fn meta_for(syscall: Syscall) -> &'static SyscallMeta {
|
||||
registry::meta_for(syscall)
|
||||
}
|
||||
11
crates/console/prometeu-hal/src/syscalls/caps.rs
Normal file
11
crates/console/prometeu-hal/src/syscalls/caps.rs
Normal file
@ -0,0 +1,11 @@
|
||||
use super::CapFlags;
|
||||
|
||||
pub const NONE: CapFlags = 0;
|
||||
pub const SYSTEM: CapFlags = 1 << 0;
|
||||
pub const GFX: CapFlags = 1 << 1;
|
||||
pub const AUDIO: CapFlags = 1 << 2;
|
||||
pub const FS: CapFlags = 1 << 3;
|
||||
pub const LOG: CapFlags = 1 << 4;
|
||||
pub const ASSET: CapFlags = 1 << 5;
|
||||
pub const BANK: CapFlags = 1 << 6;
|
||||
pub const ALL: CapFlags = SYSTEM | GFX | AUDIO | FS | LOG | ASSET | BANK;
|
||||
27
crates/console/prometeu-hal/src/syscalls/domains/asset.rs
Normal file
27
crates/console/prometeu-hal/src/syscalls/domains/asset.rs
Normal file
@ -0,0 +1,27 @@
|
||||
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),
|
||||
];
|
||||
14
crates/console/prometeu-hal/src/syscalls/domains/audio.rs
Normal file
14
crates/console/prometeu-hal/src/syscalls/domains/audio.rs
Normal file
@ -0,0 +1,14 @@
|
||||
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),
|
||||
];
|
||||
12
crates/console/prometeu-hal/src/syscalls/domains/bank.rs
Normal file
12
crates/console/prometeu-hal/src/syscalls/domains/bank.rs
Normal file
@ -0,0 +1,12 @@
|
||||
use crate::syscalls::{Syscall, SyscallRegistryEntry, caps};
|
||||
|
||||
pub(crate) const ENTRIES: &[SyscallRegistryEntry] = &[
|
||||
SyscallRegistryEntry::builder(Syscall::BankInfo, "bank", "info")
|
||||
.args(1)
|
||||
.rets(1)
|
||||
.caps(caps::BANK),
|
||||
SyscallRegistryEntry::builder(Syscall::BankSlotInfo, "bank", "slot_info")
|
||||
.args(2)
|
||||
.rets(1)
|
||||
.caps(caps::BANK),
|
||||
];
|
||||
22
crates/console/prometeu-hal/src/syscalls/domains/composer.rs
Normal file
22
crates/console/prometeu-hal/src/syscalls/domains/composer.rs
Normal file
@ -0,0 +1,22 @@
|
||||
use crate::syscalls::{Syscall, SyscallRegistryEntry, caps};
|
||||
|
||||
pub(crate) const ENTRIES: &[SyscallRegistryEntry] = &[
|
||||
SyscallRegistryEntry::builder(Syscall::ComposerBindScene, "composer", "bind_scene")
|
||||
.args(1)
|
||||
.rets(1)
|
||||
.caps(caps::GFX)
|
||||
.cost(5),
|
||||
SyscallRegistryEntry::builder(Syscall::ComposerUnbindScene, "composer", "unbind_scene")
|
||||
.rets(1)
|
||||
.caps(caps::GFX)
|
||||
.cost(2),
|
||||
SyscallRegistryEntry::builder(Syscall::ComposerSetCamera, "composer", "set_camera")
|
||||
.args(2)
|
||||
.caps(caps::GFX)
|
||||
.cost(2),
|
||||
SyscallRegistryEntry::builder(Syscall::ComposerEmitSprite, "composer", "emit_sprite")
|
||||
.args(9)
|
||||
.rets(1)
|
||||
.caps(caps::GFX)
|
||||
.cost(5),
|
||||
];
|
||||
68
crates/console/prometeu-hal/src/syscalls/domains/fs.rs
Normal file
68
crates/console/prometeu-hal/src/syscalls/domains/fs.rs
Normal file
@ -0,0 +1,68 @@
|
||||
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),
|
||||
];
|
||||
36
crates/console/prometeu-hal/src/syscalls/domains/gfx.rs
Normal file
36
crates/console/prometeu-hal/src/syscalls/domains/gfx.rs
Normal file
@ -0,0 +1,36 @@
|
||||
use crate::syscalls::{Syscall, SyscallRegistryEntry, caps};
|
||||
|
||||
pub(crate) const ENTRIES: &[SyscallRegistryEntry] = &[
|
||||
SyscallRegistryEntry::builder(Syscall::GfxClear, "gfx", "clear")
|
||||
.args(1)
|
||||
.caps(caps::GFX)
|
||||
.cost(20),
|
||||
SyscallRegistryEntry::builder(Syscall::GfxFillRect, "gfx", "fill_rect")
|
||||
.args(5)
|
||||
.caps(caps::GFX)
|
||||
.cost(20),
|
||||
SyscallRegistryEntry::builder(Syscall::GfxDrawLine, "gfx", "draw_line")
|
||||
.args(5)
|
||||
.caps(caps::GFX)
|
||||
.cost(5),
|
||||
SyscallRegistryEntry::builder(Syscall::GfxDrawCircle, "gfx", "draw_circle")
|
||||
.args(4)
|
||||
.caps(caps::GFX)
|
||||
.cost(5),
|
||||
SyscallRegistryEntry::builder(Syscall::GfxDrawDisc, "gfx", "draw_disc")
|
||||
.args(5)
|
||||
.caps(caps::GFX)
|
||||
.cost(5),
|
||||
SyscallRegistryEntry::builder(Syscall::GfxDrawSquare, "gfx", "draw_square")
|
||||
.args(6)
|
||||
.caps(caps::GFX)
|
||||
.cost(5),
|
||||
SyscallRegistryEntry::builder(Syscall::GfxDrawText, "gfx", "draw_text")
|
||||
.args(4)
|
||||
.caps(caps::GFX)
|
||||
.cost(20),
|
||||
SyscallRegistryEntry::builder(Syscall::GfxClear565, "gfx", "clear_565")
|
||||
.args(1)
|
||||
.caps(caps::GFX)
|
||||
.cost(20),
|
||||
];
|
||||
14
crates/console/prometeu-hal/src/syscalls/domains/log.rs
Normal file
14
crates/console/prometeu-hal/src/syscalls/domains/log.rs
Normal file
@ -0,0 +1,14 @@
|
||||
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),
|
||||
];
|
||||
22
crates/console/prometeu-hal/src/syscalls/domains/mod.rs
Normal file
22
crates/console/prometeu-hal/src/syscalls/domains/mod.rs
Normal file
@ -0,0 +1,22 @@
|
||||
mod asset;
|
||||
mod audio;
|
||||
mod bank;
|
||||
mod composer;
|
||||
mod fs;
|
||||
mod gfx;
|
||||
mod log;
|
||||
mod system;
|
||||
|
||||
use super::SyscallRegistryEntry;
|
||||
|
||||
pub(crate) fn all_entries() -> impl Iterator<Item = &'static SyscallRegistryEntry> {
|
||||
system::ENTRIES
|
||||
.iter()
|
||||
.chain(gfx::ENTRIES.iter())
|
||||
.chain(composer::ENTRIES.iter())
|
||||
.chain(audio::ENTRIES.iter())
|
||||
.chain(fs::ENTRIES.iter())
|
||||
.chain(log::ENTRIES.iter())
|
||||
.chain(asset::ENTRIES.iter())
|
||||
.chain(bank::ENTRIES.iter())
|
||||
}
|
||||
11
crates/console/prometeu-hal/src/syscalls/domains/system.rs
Normal file
11
crates/console/prometeu-hal/src/syscalls/domains/system.rs
Normal file
@ -0,0 +1,11 @@
|
||||
use crate::syscalls::{Syscall, SyscallRegistryEntry, caps};
|
||||
|
||||
pub(crate) const ENTRIES: &[SyscallRegistryEntry] = &[
|
||||
SyscallRegistryEntry::builder(Syscall::SystemHasCart, "system", "has_cart")
|
||||
.rets(1)
|
||||
.caps(caps::SYSTEM),
|
||||
SyscallRegistryEntry::builder(Syscall::SystemRunCart, "system", "run_cart")
|
||||
.caps(caps::SYSTEM)
|
||||
.non_deterministic()
|
||||
.cost(50),
|
||||
];
|
||||
105
crates/console/prometeu-hal/src/syscalls/registry.rs
Normal file
105
crates/console/prometeu-hal/src/syscalls/registry.rs
Normal file
@ -0,0 +1,105 @@
|
||||
use super::{Syscall, SyscallMeta, domains};
|
||||
|
||||
pub(crate) fn meta_for(syscall: Syscall) -> &'static SyscallMeta {
|
||||
for entry in domains::all_entries() {
|
||||
if entry.syscall == syscall {
|
||||
return &entry.meta;
|
||||
}
|
||||
}
|
||||
panic!("Missing SyscallMeta for {:?}", syscall);
|
||||
}
|
||||
|
||||
impl Syscall {
|
||||
pub fn from_u32(id: u32) -> Option<Self> {
|
||||
match id {
|
||||
0x0001 => Some(Self::SystemHasCart),
|
||||
0x0002 => Some(Self::SystemRunCart),
|
||||
0x1001 => Some(Self::GfxClear),
|
||||
0x1002 => Some(Self::GfxFillRect),
|
||||
0x1003 => Some(Self::GfxDrawLine),
|
||||
0x1004 => Some(Self::GfxDrawCircle),
|
||||
0x1005 => Some(Self::GfxDrawDisc),
|
||||
0x1006 => Some(Self::GfxDrawSquare),
|
||||
0x1008 => Some(Self::GfxDrawText),
|
||||
0x1010 => Some(Self::GfxClear565),
|
||||
0x1101 => Some(Self::ComposerBindScene),
|
||||
0x1102 => Some(Self::ComposerUnbindScene),
|
||||
0x1103 => Some(Self::ComposerSetCamera),
|
||||
0x1104 => Some(Self::ComposerEmitSprite),
|
||||
0x3001 => Some(Self::AudioPlaySample),
|
||||
0x3002 => Some(Self::AudioPlay),
|
||||
0x4001 => Some(Self::FsOpen),
|
||||
0x4002 => Some(Self::FsRead),
|
||||
0x4003 => Some(Self::FsWrite),
|
||||
0x4004 => Some(Self::FsClose),
|
||||
0x4005 => Some(Self::FsListDir),
|
||||
0x4006 => Some(Self::FsExists),
|
||||
0x4007 => Some(Self::FsDelete),
|
||||
0x4201 => Some(Self::MemSlotCount),
|
||||
0x4202 => Some(Self::MemSlotStat),
|
||||
0x4203 => Some(Self::MemSlotRead),
|
||||
0x4204 => Some(Self::MemSlotWrite),
|
||||
0x4205 => Some(Self::MemSlotCommit),
|
||||
0x4206 => Some(Self::MemSlotClear),
|
||||
0x5001 => Some(Self::LogWrite),
|
||||
0x5002 => Some(Self::LogWriteTag),
|
||||
0x6001 => Some(Self::AssetLoad),
|
||||
0x6002 => Some(Self::AssetStatus),
|
||||
0x6003 => Some(Self::AssetCommit),
|
||||
0x6004 => Some(Self::AssetCancel),
|
||||
0x6101 => Some(Self::BankInfo),
|
||||
0x6102 => Some(Self::BankSlotInfo),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn args_count(&self) -> usize {
|
||||
super::meta_for(*self).arg_slots as usize
|
||||
}
|
||||
|
||||
pub fn results_count(&self) -> usize {
|
||||
super::meta_for(*self).ret_slots as usize
|
||||
}
|
||||
|
||||
pub fn name(&self) -> &'static str {
|
||||
match self {
|
||||
Self::SystemHasCart => "SystemHasCart",
|
||||
Self::SystemRunCart => "SystemRunCart",
|
||||
Self::GfxClear => "GfxClear",
|
||||
Self::GfxFillRect => "GfxFillRect",
|
||||
Self::GfxDrawLine => "GfxDrawLine",
|
||||
Self::GfxDrawCircle => "GfxDrawCircle",
|
||||
Self::GfxDrawDisc => "GfxDrawDisc",
|
||||
Self::GfxDrawSquare => "GfxDrawSquare",
|
||||
Self::GfxDrawText => "GfxDrawText",
|
||||
Self::GfxClear565 => "GfxClear565",
|
||||
Self::ComposerBindScene => "ComposerBindScene",
|
||||
Self::ComposerUnbindScene => "ComposerUnbindScene",
|
||||
Self::ComposerSetCamera => "ComposerSetCamera",
|
||||
Self::ComposerEmitSprite => "ComposerEmitSprite",
|
||||
Self::AudioPlaySample => "AudioPlaySample",
|
||||
Self::AudioPlay => "AudioPlay",
|
||||
Self::FsOpen => "FsOpen",
|
||||
Self::FsRead => "FsRead",
|
||||
Self::FsWrite => "FsWrite",
|
||||
Self::FsClose => "FsClose",
|
||||
Self::FsListDir => "FsListDir",
|
||||
Self::FsExists => "FsExists",
|
||||
Self::FsDelete => "FsDelete",
|
||||
Self::MemSlotCount => "MemSlotCount",
|
||||
Self::MemSlotStat => "MemSlotStat",
|
||||
Self::MemSlotRead => "MemSlotRead",
|
||||
Self::MemSlotWrite => "MemSlotWrite",
|
||||
Self::MemSlotCommit => "MemSlotCommit",
|
||||
Self::MemSlotClear => "MemSlotClear",
|
||||
Self::LogWrite => "LogWrite",
|
||||
Self::LogWriteTag => "LogWriteTag",
|
||||
Self::AssetLoad => "AssetLoad",
|
||||
Self::AssetStatus => "AssetStatus",
|
||||
Self::AssetCommit => "AssetCommit",
|
||||
Self::AssetCancel => "AssetCancel",
|
||||
Self::BankInfo => "BankInfo",
|
||||
Self::BankSlotInfo => "BankSlotInfo",
|
||||
}
|
||||
}
|
||||
}
|
||||
156
crates/console/prometeu-hal/src/syscalls/resolver.rs
Normal file
156
crates/console/prometeu-hal/src/syscalls/resolver.rs
Normal file
@ -0,0 +1,156 @@
|
||||
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)
|
||||
}
|
||||
416
crates/console/prometeu-hal/src/syscalls/tests.rs
Normal file
416
crates/console/prometeu-hal/src/syscalls/tests.rs
Normal file
@ -0,0 +1,416 @@
|
||||
use super::*;
|
||||
|
||||
fn all_syscalls() -> Vec<Syscall> {
|
||||
domains::all_entries().map(|entry| entry.syscall).collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_syscall_has_metadata() {
|
||||
for sc in all_syscalls() {
|
||||
let m = meta_for(sc);
|
||||
assert_eq!(m.id, sc as u32, "id mismatch for {:?}", sc);
|
||||
assert!(!m.module.is_empty(), "module must be non-empty for id=0x{:08X}", m.id);
|
||||
assert!(!m.name.is_empty(), "name must be non-empty for id=0x{:08X}", m.id);
|
||||
assert!(m.version > 0, "version must be > 0 for id=0x{:08X}", m.id);
|
||||
}
|
||||
|
||||
use std::collections::HashSet;
|
||||
let mut ids = HashSet::new();
|
||||
let mut identities = HashSet::new();
|
||||
let mut count = 0usize;
|
||||
for entry in domains::all_entries() {
|
||||
count += 1;
|
||||
assert!(ids.insert(entry.meta.id), "duplicate syscall id 0x{:08X}", entry.meta.id);
|
||||
let parsed = Syscall::from_u32(entry.meta.id).expect("id not recognized by enum mapping");
|
||||
assert_eq!(parsed as u32, entry.meta.id);
|
||||
|
||||
let key = (entry.meta.module, entry.meta.name, entry.meta.version);
|
||||
assert!(
|
||||
identities.insert(key),
|
||||
"duplicate canonical identity: ({}.{}, v{})",
|
||||
entry.meta.module,
|
||||
entry.meta.name,
|
||||
entry.meta.version
|
||||
);
|
||||
}
|
||||
|
||||
assert_eq!(count, all_syscalls().len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolver_returns_expected_id_for_known_identity() {
|
||||
let id = resolve_syscall("gfx", "clear", 1).expect("known identity must resolve");
|
||||
assert_eq!(id.id, 0x1001);
|
||||
assert_eq!(id.meta.module, "gfx");
|
||||
assert_eq!(id.meta.name, "clear");
|
||||
assert_eq!(id.meta.version, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolver_rejects_unknown_identity() {
|
||||
let res = resolve_syscall("gfx", "nonexistent", 1);
|
||||
assert!(res.is_none());
|
||||
|
||||
let requested = [SyscallIdentity { module: "gfx", name: "nonexistent", version: 1 }];
|
||||
let err = resolve_program_syscalls(&requested, 0).unwrap_err();
|
||||
match err {
|
||||
LoadError::UnknownSyscall { module, name, version } => {
|
||||
assert_eq!(module, "gfx");
|
||||
assert_eq!(name, "nonexistent");
|
||||
assert_eq!(version, 1);
|
||||
}
|
||||
_ => panic!("expected UnknownSyscall error"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolver_rejects_removed_legacy_gfx_set_sprite_identity() {
|
||||
assert!(resolve_syscall("gfx", "set_sprite", 1).is_none());
|
||||
|
||||
let requested = [SyscallIdentity { module: "gfx", name: "set_sprite", version: 1 }];
|
||||
let err = resolve_program_syscalls(&requested, caps::ALL).unwrap_err();
|
||||
assert_eq!(
|
||||
err,
|
||||
LoadError::UnknownSyscall { module: "gfx".into(), name: "set_sprite".into(), version: 1 }
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolver_enforces_capabilities() {
|
||||
let requested = [SyscallIdentity { module: "gfx", name: "clear", version: 1 }];
|
||||
let err = resolve_program_syscalls(&requested, 0).unwrap_err();
|
||||
match err {
|
||||
LoadError::MissingCapability { required, provided, module, name, version } => {
|
||||
assert_eq!(module, "gfx");
|
||||
assert_eq!(name, "clear");
|
||||
assert_eq!(version, 1);
|
||||
assert_ne!(required, 0);
|
||||
assert_eq!(provided, 0);
|
||||
}
|
||||
_ => panic!("expected MissingCapability error"),
|
||||
}
|
||||
|
||||
let ok = resolve_program_syscalls(&requested, caps::GFX).expect("must resolve with caps");
|
||||
assert_eq!(ok.len(), 1);
|
||||
assert_eq!(ok[0].id, 0x1001);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn declared_resolver_returns_expected_id_for_known_identity() {
|
||||
let declared = [prometeu_bytecode::SyscallDecl {
|
||||
module: "gfx".into(),
|
||||
name: "clear".into(),
|
||||
version: 1,
|
||||
arg_slots: 1,
|
||||
ret_slots: 0,
|
||||
}];
|
||||
|
||||
let ok =
|
||||
resolve_declared_program_syscalls(&declared, caps::GFX).expect("must resolve with ABI");
|
||||
assert_eq!(ok.len(), 1);
|
||||
assert_eq!(ok[0].id, 0x1001);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn declared_resolver_rejects_unknown_identity() {
|
||||
let declared = [prometeu_bytecode::SyscallDecl {
|
||||
module: "gfx".into(),
|
||||
name: "nonexistent".into(),
|
||||
version: 1,
|
||||
arg_slots: 1,
|
||||
ret_slots: 0,
|
||||
}];
|
||||
|
||||
let err = resolve_declared_program_syscalls(&declared, caps::GFX).unwrap_err();
|
||||
assert_eq!(
|
||||
err,
|
||||
DeclaredLoadError::UnknownSyscall {
|
||||
module: "gfx".into(),
|
||||
name: "nonexistent".into(),
|
||||
version: 1,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn declared_resolver_rejects_missing_capability() {
|
||||
let declared = [prometeu_bytecode::SyscallDecl {
|
||||
module: "gfx".into(),
|
||||
name: "clear".into(),
|
||||
version: 1,
|
||||
arg_slots: 1,
|
||||
ret_slots: 0,
|
||||
}];
|
||||
|
||||
let err = resolve_declared_program_syscalls(&declared, caps::NONE).unwrap_err();
|
||||
assert_eq!(
|
||||
err,
|
||||
DeclaredLoadError::MissingCapability {
|
||||
required: caps::GFX,
|
||||
provided: caps::NONE,
|
||||
module: "gfx".into(),
|
||||
name: "clear".into(),
|
||||
version: 1,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn declared_resolver_rejects_abi_mismatch() {
|
||||
let declared = [prometeu_bytecode::SyscallDecl {
|
||||
module: "gfx".into(),
|
||||
name: "draw_line".into(),
|
||||
version: 1,
|
||||
arg_slots: 4,
|
||||
ret_slots: 0,
|
||||
}];
|
||||
|
||||
let err = resolve_declared_program_syscalls(&declared, caps::GFX).unwrap_err();
|
||||
assert_eq!(
|
||||
err,
|
||||
DeclaredLoadError::AbiMismatch {
|
||||
module: "gfx".into(),
|
||||
name: "draw_line".into(),
|
||||
version: 1,
|
||||
declared_arg_slots: 4,
|
||||
declared_ret_slots: 0,
|
||||
expected_arg_slots: 5,
|
||||
expected_ret_slots: 0,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn status_first_syscall_signatures_are_pinned() {
|
||||
let clear = meta_for(Syscall::GfxClear);
|
||||
assert_eq!(clear.arg_slots, 1);
|
||||
assert_eq!(clear.ret_slots, 0);
|
||||
|
||||
let fill_rect = meta_for(Syscall::GfxFillRect);
|
||||
assert_eq!(fill_rect.arg_slots, 5);
|
||||
assert_eq!(fill_rect.ret_slots, 0);
|
||||
|
||||
let draw_line = meta_for(Syscall::GfxDrawLine);
|
||||
assert_eq!(draw_line.arg_slots, 5);
|
||||
assert_eq!(draw_line.ret_slots, 0);
|
||||
|
||||
let draw_circle = meta_for(Syscall::GfxDrawCircle);
|
||||
assert_eq!(draw_circle.arg_slots, 4);
|
||||
assert_eq!(draw_circle.ret_slots, 0);
|
||||
|
||||
let draw_disc = meta_for(Syscall::GfxDrawDisc);
|
||||
assert_eq!(draw_disc.arg_slots, 5);
|
||||
assert_eq!(draw_disc.ret_slots, 0);
|
||||
|
||||
let draw_square = meta_for(Syscall::GfxDrawSquare);
|
||||
assert_eq!(draw_square.arg_slots, 6);
|
||||
assert_eq!(draw_square.ret_slots, 0);
|
||||
|
||||
let draw_text = meta_for(Syscall::GfxDrawText);
|
||||
assert_eq!(draw_text.arg_slots, 4);
|
||||
assert_eq!(draw_text.ret_slots, 0);
|
||||
|
||||
let clear_565 = meta_for(Syscall::GfxClear565);
|
||||
assert_eq!(clear_565.arg_slots, 1);
|
||||
assert_eq!(clear_565.ret_slots, 0);
|
||||
|
||||
let bind_scene = meta_for(Syscall::ComposerBindScene);
|
||||
assert_eq!(bind_scene.arg_slots, 1);
|
||||
assert_eq!(bind_scene.ret_slots, 1);
|
||||
|
||||
let unbind_scene = meta_for(Syscall::ComposerUnbindScene);
|
||||
assert_eq!(unbind_scene.arg_slots, 0);
|
||||
assert_eq!(unbind_scene.ret_slots, 1);
|
||||
|
||||
let set_camera = meta_for(Syscall::ComposerSetCamera);
|
||||
assert_eq!(set_camera.arg_slots, 2);
|
||||
assert_eq!(set_camera.ret_slots, 0);
|
||||
|
||||
let emit_sprite = meta_for(Syscall::ComposerEmitSprite);
|
||||
assert_eq!(emit_sprite.arg_slots, 9);
|
||||
assert_eq!(emit_sprite.ret_slots, 1);
|
||||
|
||||
let audio_play_sample = meta_for(Syscall::AudioPlaySample);
|
||||
assert_eq!(audio_play_sample.arg_slots, 5);
|
||||
assert_eq!(audio_play_sample.ret_slots, 1);
|
||||
|
||||
let audio_play = meta_for(Syscall::AudioPlay);
|
||||
assert_eq!(audio_play.arg_slots, 7);
|
||||
assert_eq!(audio_play.ret_slots, 1);
|
||||
|
||||
let asset_load = meta_for(Syscall::AssetLoad);
|
||||
assert_eq!(asset_load.arg_slots, 2);
|
||||
assert_eq!(asset_load.ret_slots, 2);
|
||||
|
||||
let asset_commit = meta_for(Syscall::AssetCommit);
|
||||
assert_eq!(asset_commit.arg_slots, 1);
|
||||
assert_eq!(asset_commit.ret_slots, 1);
|
||||
|
||||
let asset_cancel = meta_for(Syscall::AssetCancel);
|
||||
assert_eq!(asset_cancel.arg_slots, 1);
|
||||
assert_eq!(asset_cancel.ret_slots, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn declared_resolver_rejects_legacy_status_first_signatures() {
|
||||
let declared = vec![
|
||||
prometeu_bytecode::SyscallDecl {
|
||||
module: "composer".into(),
|
||||
name: "bind_scene".into(),
|
||||
version: 1,
|
||||
arg_slots: 1,
|
||||
ret_slots: 0,
|
||||
},
|
||||
prometeu_bytecode::SyscallDecl {
|
||||
module: "audio".into(),
|
||||
name: "play_sample".into(),
|
||||
version: 1,
|
||||
arg_slots: 5,
|
||||
ret_slots: 0,
|
||||
},
|
||||
prometeu_bytecode::SyscallDecl {
|
||||
module: "audio".into(),
|
||||
name: "play".into(),
|
||||
version: 1,
|
||||
arg_slots: 7,
|
||||
ret_slots: 0,
|
||||
},
|
||||
prometeu_bytecode::SyscallDecl {
|
||||
module: "asset".into(),
|
||||
name: "load".into(),
|
||||
version: 1,
|
||||
arg_slots: 3,
|
||||
ret_slots: 2,
|
||||
},
|
||||
prometeu_bytecode::SyscallDecl {
|
||||
module: "asset".into(),
|
||||
name: "commit".into(),
|
||||
version: 1,
|
||||
arg_slots: 1,
|
||||
ret_slots: 0,
|
||||
},
|
||||
prometeu_bytecode::SyscallDecl {
|
||||
module: "asset".into(),
|
||||
name: "cancel".into(),
|
||||
version: 1,
|
||||
arg_slots: 1,
|
||||
ret_slots: 0,
|
||||
},
|
||||
];
|
||||
|
||||
for decl in declared {
|
||||
let err = resolve_declared_program_syscalls(std::slice::from_ref(&decl), caps::ALL)
|
||||
.expect_err("legacy signature must be rejected");
|
||||
match err {
|
||||
DeclaredLoadError::AbiMismatch {
|
||||
module,
|
||||
name,
|
||||
version,
|
||||
declared_arg_slots,
|
||||
declared_ret_slots,
|
||||
expected_arg_slots,
|
||||
expected_ret_slots,
|
||||
} => {
|
||||
assert_eq!(module, decl.module);
|
||||
assert_eq!(name, decl.name);
|
||||
assert_eq!(version, decl.version);
|
||||
assert_eq!(declared_arg_slots, decl.arg_slots);
|
||||
assert_eq!(declared_ret_slots, decl.ret_slots);
|
||||
assert!(
|
||||
expected_arg_slots != declared_arg_slots
|
||||
|| expected_ret_slots != declared_ret_slots
|
||||
);
|
||||
}
|
||||
other => panic!("expected AbiMismatch, got {:?}", other),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn declared_resolver_accepts_mixed_status_first_surface_as_a_single_module() {
|
||||
let declared = vec![
|
||||
prometeu_bytecode::SyscallDecl {
|
||||
module: "composer".into(),
|
||||
name: "bind_scene".into(),
|
||||
version: 1,
|
||||
arg_slots: 1,
|
||||
ret_slots: 1,
|
||||
},
|
||||
prometeu_bytecode::SyscallDecl {
|
||||
module: "composer".into(),
|
||||
name: "unbind_scene".into(),
|
||||
version: 1,
|
||||
arg_slots: 0,
|
||||
ret_slots: 1,
|
||||
},
|
||||
prometeu_bytecode::SyscallDecl {
|
||||
module: "composer".into(),
|
||||
name: "emit_sprite".into(),
|
||||
version: 1,
|
||||
arg_slots: 9,
|
||||
ret_slots: 1,
|
||||
},
|
||||
prometeu_bytecode::SyscallDecl {
|
||||
module: "audio".into(),
|
||||
name: "play".into(),
|
||||
version: 1,
|
||||
arg_slots: 7,
|
||||
ret_slots: 1,
|
||||
},
|
||||
prometeu_bytecode::SyscallDecl {
|
||||
module: "asset".into(),
|
||||
name: "load".into(),
|
||||
version: 1,
|
||||
arg_slots: 2,
|
||||
ret_slots: 2,
|
||||
},
|
||||
prometeu_bytecode::SyscallDecl {
|
||||
module: "asset".into(),
|
||||
name: "commit".into(),
|
||||
version: 1,
|
||||
arg_slots: 1,
|
||||
ret_slots: 1,
|
||||
},
|
||||
];
|
||||
|
||||
let resolved =
|
||||
resolve_declared_program_syscalls(&declared, caps::GFX | caps::AUDIO | caps::ASSET)
|
||||
.expect("mixed status-first surface must resolve together");
|
||||
|
||||
assert_eq!(resolved.len(), declared.len());
|
||||
assert_eq!(resolved[0].meta.ret_slots, 1);
|
||||
assert_eq!(resolved[1].meta.ret_slots, 1);
|
||||
assert_eq!(resolved[2].meta.ret_slots, 1);
|
||||
assert_eq!(resolved[3].meta.ret_slots, 1);
|
||||
assert_eq!(resolved[4].meta.ret_slots, 2);
|
||||
assert_eq!(resolved[5].meta.ret_slots, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn memcard_syscall_signatures_are_pinned() {
|
||||
let slot_count = meta_for(Syscall::MemSlotCount);
|
||||
assert_eq!(slot_count.module, "mem");
|
||||
assert_eq!(slot_count.name, "slot_count");
|
||||
assert_eq!(slot_count.arg_slots, 0);
|
||||
assert_eq!(slot_count.ret_slots, 2);
|
||||
|
||||
let slot_stat = meta_for(Syscall::MemSlotStat);
|
||||
assert_eq!(slot_stat.arg_slots, 1);
|
||||
assert_eq!(slot_stat.ret_slots, 5);
|
||||
|
||||
let slot_read = meta_for(Syscall::MemSlotRead);
|
||||
assert_eq!(slot_read.arg_slots, 3);
|
||||
assert_eq!(slot_read.ret_slots, 3);
|
||||
|
||||
let slot_write = meta_for(Syscall::MemSlotWrite);
|
||||
assert_eq!(slot_write.arg_slots, 3);
|
||||
assert_eq!(slot_write.ret_slots, 2);
|
||||
|
||||
let slot_commit = meta_for(Syscall::MemSlotCommit);
|
||||
assert_eq!(slot_commit.arg_slots, 1);
|
||||
assert_eq!(slot_commit.ret_slots, 1);
|
||||
|
||||
let slot_clear = meta_for(Syscall::MemSlotClear);
|
||||
assert_eq!(slot_clear.arg_slots, 1);
|
||||
assert_eq!(slot_clear.ret_slots, 1);
|
||||
}
|
||||
308
crates/console/prometeu-hal/src/telemetry.rs
Normal file
308
crates/console/prometeu-hal/src/telemetry.rs
Normal file
@ -0,0 +1,308 @@
|
||||
use crate::log::{LogLevel, LogService, LogSource};
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicU32, AtomicU64, AtomicUsize, Ordering};
|
||||
|
||||
#[derive(Debug, Clone, Copy, Default)]
|
||||
pub struct TelemetryFrame {
|
||||
pub frame_index: u64,
|
||||
pub vm_steps: u32,
|
||||
pub cycles_used: u64,
|
||||
pub cycles_budget: u64,
|
||||
pub syscalls: u32,
|
||||
pub host_cpu_time_us: u64,
|
||||
pub completed_logical_frames: u32,
|
||||
pub violations: u32,
|
||||
|
||||
// Bank telemetry
|
||||
pub glyph_slots_used: u32,
|
||||
pub glyph_slots_total: u32,
|
||||
pub sound_slots_used: u32,
|
||||
pub sound_slots_total: u32,
|
||||
|
||||
// RAM (Heap)
|
||||
pub heap_used_bytes: usize,
|
||||
pub heap_max_bytes: usize,
|
||||
|
||||
// Log Pressure from the last completed logical frame
|
||||
pub logs_count: u32,
|
||||
}
|
||||
|
||||
/// Thread-safe, atomic telemetry storage for real-time monitoring by the host.
|
||||
/// This follows the push-based model from DEC-0005 to avoid expensive scans or locks.
|
||||
#[derive(Debug, Default)]
|
||||
pub struct AtomicTelemetry {
|
||||
pub frame_index: AtomicU64,
|
||||
pub cycles_used: AtomicU64,
|
||||
pub cycles_budget: AtomicU64,
|
||||
pub syscalls: AtomicU32,
|
||||
pub host_cpu_time_us: AtomicU64,
|
||||
pub vm_steps: AtomicU32,
|
||||
pub completed_logical_frames: AtomicU32,
|
||||
pub violations: AtomicU32,
|
||||
|
||||
// Bank telemetry
|
||||
pub glyph_slots_used: AtomicU32,
|
||||
pub glyph_slots_total: AtomicU32,
|
||||
pub sound_slots_used: AtomicU32,
|
||||
pub sound_slots_total: AtomicU32,
|
||||
|
||||
// RAM (Heap)
|
||||
pub heap_used_bytes: AtomicUsize,
|
||||
pub heap_max_bytes: AtomicUsize,
|
||||
|
||||
// Transient in-flight log counter for the current logical frame
|
||||
pub current_logs_count: Arc<AtomicU32>,
|
||||
// Persisted log count from the last completed logical frame
|
||||
pub logs_count: AtomicU32,
|
||||
}
|
||||
|
||||
impl AtomicTelemetry {
|
||||
pub fn new(current_logs_count: Arc<AtomicU32>) -> Self {
|
||||
Self { current_logs_count, ..Default::default() }
|
||||
}
|
||||
|
||||
/// Snapshots the current atomic state into a TelemetryFrame.
|
||||
pub fn snapshot(&self) -> TelemetryFrame {
|
||||
TelemetryFrame {
|
||||
frame_index: self.frame_index.load(Ordering::Relaxed),
|
||||
cycles_used: self.cycles_used.load(Ordering::Relaxed),
|
||||
cycles_budget: self.cycles_budget.load(Ordering::Relaxed),
|
||||
syscalls: self.syscalls.load(Ordering::Relaxed),
|
||||
host_cpu_time_us: self.host_cpu_time_us.load(Ordering::Relaxed),
|
||||
completed_logical_frames: self.completed_logical_frames.load(Ordering::Relaxed),
|
||||
violations: self.violations.load(Ordering::Relaxed),
|
||||
glyph_slots_used: self.glyph_slots_used.load(Ordering::Relaxed),
|
||||
glyph_slots_total: self.glyph_slots_total.load(Ordering::Relaxed),
|
||||
sound_slots_used: self.sound_slots_used.load(Ordering::Relaxed),
|
||||
sound_slots_total: self.sound_slots_total.load(Ordering::Relaxed),
|
||||
heap_used_bytes: self.heap_used_bytes.load(Ordering::Relaxed),
|
||||
heap_max_bytes: self.heap_max_bytes.load(Ordering::Relaxed),
|
||||
logs_count: self.logs_count.load(Ordering::Relaxed),
|
||||
vm_steps: self.vm_steps.load(Ordering::Relaxed),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn reset(&self) {
|
||||
self.frame_index.store(0, Ordering::Relaxed);
|
||||
self.cycles_used.store(0, Ordering::Relaxed);
|
||||
self.syscalls.store(0, Ordering::Relaxed);
|
||||
self.host_cpu_time_us.store(0, Ordering::Relaxed);
|
||||
self.completed_logical_frames.store(0, Ordering::Relaxed);
|
||||
self.violations.store(0, Ordering::Relaxed);
|
||||
self.glyph_slots_used.store(0, Ordering::Relaxed);
|
||||
self.glyph_slots_total.store(0, Ordering::Relaxed);
|
||||
self.sound_slots_used.store(0, Ordering::Relaxed);
|
||||
self.sound_slots_total.store(0, Ordering::Relaxed);
|
||||
self.heap_used_bytes.store(0, Ordering::Relaxed);
|
||||
self.vm_steps.store(0, Ordering::Relaxed);
|
||||
self.logs_count.store(0, Ordering::Relaxed);
|
||||
self.current_logs_count.store(0, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Default)]
|
||||
pub struct CertificationConfig {
|
||||
pub enabled: bool,
|
||||
pub cycles_budget_per_frame: Option<u64>,
|
||||
pub max_syscalls_per_frame: Option<u32>,
|
||||
pub max_host_cpu_us_per_frame: Option<u64>,
|
||||
pub max_glyph_slots_used: Option<u32>,
|
||||
pub max_sound_slots_used: Option<u32>,
|
||||
pub max_heap_bytes: Option<usize>,
|
||||
pub max_logs_per_frame: Option<u32>,
|
||||
}
|
||||
|
||||
pub struct Certifier {
|
||||
pub config: CertificationConfig,
|
||||
}
|
||||
|
||||
impl Certifier {
|
||||
pub fn new(config: CertificationConfig) -> Self {
|
||||
Self { config }
|
||||
}
|
||||
|
||||
pub fn evaluate(
|
||||
&self,
|
||||
telemetry: &TelemetryFrame,
|
||||
log_service: &mut LogService,
|
||||
ts_ms: u64,
|
||||
) -> usize {
|
||||
if !self.config.enabled {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let mut violations = 0;
|
||||
|
||||
// 1. Cycles
|
||||
if let Some(budget) = self.config.cycles_budget_per_frame
|
||||
&& telemetry.cycles_used > budget
|
||||
{
|
||||
log_service.log(
|
||||
ts_ms,
|
||||
telemetry.frame_index,
|
||||
LogLevel::Warn,
|
||||
LogSource::Pos,
|
||||
0xCA01,
|
||||
format!(
|
||||
"Cert: cycles_used exceeded budget ({} > {})",
|
||||
telemetry.cycles_used, budget
|
||||
),
|
||||
);
|
||||
violations += 1;
|
||||
}
|
||||
|
||||
// 2. Syscalls
|
||||
if let Some(limit) = self.config.max_syscalls_per_frame
|
||||
&& telemetry.syscalls > limit
|
||||
{
|
||||
log_service.log(
|
||||
ts_ms,
|
||||
telemetry.frame_index,
|
||||
LogLevel::Warn,
|
||||
LogSource::Pos,
|
||||
0xCA02,
|
||||
format!(
|
||||
"Cert: syscalls per frame exceeded limit ({} > {})",
|
||||
telemetry.syscalls, limit
|
||||
),
|
||||
);
|
||||
violations += 1;
|
||||
}
|
||||
|
||||
// 3. CPU Time
|
||||
if let Some(limit) = self.config.max_host_cpu_us_per_frame
|
||||
&& telemetry.host_cpu_time_us > limit
|
||||
{
|
||||
log_service.log(
|
||||
ts_ms,
|
||||
telemetry.frame_index,
|
||||
LogLevel::Warn,
|
||||
LogSource::Pos,
|
||||
0xCA03,
|
||||
format!(
|
||||
"Cert: host_cpu_time_us exceeded limit ({} > {})",
|
||||
telemetry.host_cpu_time_us, limit
|
||||
),
|
||||
);
|
||||
violations += 1;
|
||||
}
|
||||
|
||||
// 4. GLYPH bank slots
|
||||
if let Some(limit) = self.config.max_glyph_slots_used
|
||||
&& telemetry.glyph_slots_used > limit
|
||||
{
|
||||
log_service.log(
|
||||
ts_ms,
|
||||
telemetry.frame_index,
|
||||
LogLevel::Warn,
|
||||
LogSource::Pos,
|
||||
0xCA04,
|
||||
format!(
|
||||
"Cert: GLYPH bank exceeded slot limit ({} > {})",
|
||||
telemetry.glyph_slots_used, limit
|
||||
),
|
||||
);
|
||||
violations += 1;
|
||||
}
|
||||
|
||||
// 5. SOUNDS bank slots
|
||||
if let Some(limit) = self.config.max_sound_slots_used
|
||||
&& telemetry.sound_slots_used > limit
|
||||
{
|
||||
log_service.log(
|
||||
ts_ms,
|
||||
telemetry.frame_index,
|
||||
LogLevel::Warn,
|
||||
LogSource::Pos,
|
||||
0xCA05,
|
||||
format!(
|
||||
"Cert: SOUNDS bank exceeded slot limit ({} > {})",
|
||||
telemetry.sound_slots_used, limit
|
||||
),
|
||||
);
|
||||
violations += 1;
|
||||
}
|
||||
|
||||
// 6. Heap Memory
|
||||
if let Some(limit) = self.config.max_heap_bytes
|
||||
&& telemetry.heap_used_bytes > limit
|
||||
{
|
||||
log_service.log(
|
||||
ts_ms,
|
||||
telemetry.frame_index,
|
||||
LogLevel::Warn,
|
||||
LogSource::Pos,
|
||||
0xCA06,
|
||||
format!(
|
||||
"Cert: Heap memory exceeded limit ({} > {})",
|
||||
telemetry.heap_used_bytes, limit
|
||||
),
|
||||
);
|
||||
violations += 1;
|
||||
}
|
||||
|
||||
// 7. Log Pressure
|
||||
if let Some(limit) = self.config.max_logs_per_frame
|
||||
&& telemetry.logs_count > limit
|
||||
{
|
||||
log_service.log(
|
||||
ts_ms,
|
||||
telemetry.frame_index,
|
||||
LogLevel::Warn,
|
||||
LogSource::Pos,
|
||||
0xCA07,
|
||||
format!("Cert: Log pressure exceeded limit ({} > {})", telemetry.logs_count, limit),
|
||||
);
|
||||
violations += 1;
|
||||
}
|
||||
|
||||
violations
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::log::LogService;
|
||||
|
||||
#[test]
|
||||
fn test_certifier_violations() {
|
||||
let mut ls = LogService::new(10);
|
||||
let config = CertificationConfig {
|
||||
enabled: true,
|
||||
cycles_budget_per_frame: Some(100),
|
||||
max_syscalls_per_frame: Some(5),
|
||||
max_host_cpu_us_per_frame: Some(1000),
|
||||
max_glyph_slots_used: Some(1),
|
||||
..Default::default()
|
||||
};
|
||||
let cert = Certifier::new(config);
|
||||
|
||||
let mut tel = TelemetryFrame::default();
|
||||
tel.cycles_used = 150;
|
||||
tel.syscalls = 10;
|
||||
tel.host_cpu_time_us = 500;
|
||||
tel.glyph_slots_used = 2;
|
||||
|
||||
let violations = cert.evaluate(&tel, &mut ls, 1000);
|
||||
assert_eq!(violations, 3);
|
||||
|
||||
let logs = ls.get_recent(10);
|
||||
assert_eq!(logs.len(), 3);
|
||||
assert!(logs[0].msg.contains("cycles_used"));
|
||||
assert!(logs[1].msg.contains("syscalls"));
|
||||
assert!(logs[2].msg.contains("GLYPH bank"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn snapshot_uses_persisted_last_frame_logs() {
|
||||
let current = Arc::new(AtomicU32::new(7));
|
||||
let tel = AtomicTelemetry::new(Arc::clone(¤t));
|
||||
tel.logs_count.store(3, Ordering::Relaxed);
|
||||
|
||||
let snapshot = tel.snapshot();
|
||||
|
||||
assert_eq!(snapshot.logs_count, 3);
|
||||
assert_eq!(current.load(Ordering::Relaxed), 7);
|
||||
}
|
||||
}
|
||||
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