bQUARKz 76254928e6
All checks were successful
Intrepid/Prometeu/Runtime/pipeline/pr-master This commit looks good
stress test cart fixes
2026-04-18 16:24:20 +01:00

447 lines
15 KiB
Rust

use anyhow::Result;
use prometeu_bytecode::assembler::assemble;
use prometeu_bytecode::model::{
BytecodeModule, ConstantPoolEntry, DebugInfo, Export, FunctionMeta, SyscallDecl,
};
use prometeu_hal::asset::{
AssetCodec, AssetEntry, BankType, PreloadEntry, SCENE_DECODED_LAYER_OVERHEAD_BYTES_V1,
SCENE_LAYER_COUNT_V1, SCENE_PAYLOAD_MAGIC_V1, SCENE_PAYLOAD_VERSION_V1,
};
use prometeu_hal::cartridge::{
AssetsPackHeader, AssetsPackPrelude, ASSETS_PA_MAGIC, ASSETS_PA_PRELUDE_SIZE,
ASSETS_PA_SCHEMA_VERSION,
};
use prometeu_hal::color::Color;
use prometeu_hal::glyph::Glyph;
use prometeu_hal::glyph_bank::{
TileSize, GLYPH_BANK_COLORS_PER_PALETTE, GLYPH_BANK_PALETTE_COUNT_V1,
};
use prometeu_hal::scene_bank::SceneBank;
use prometeu_hal::scene_layer::{ParallaxFactor, SceneLayer};
use prometeu_hal::tile::Tile;
use prometeu_hal::tilemap::TileMap;
use std::fs;
use std::mem::size_of;
use std::path::PathBuf;
fn asm(s: &str) -> Vec<u8> {
assemble(s).expect("assemble")
}
pub fn generate() -> Result<()> {
let mut rom: Vec<u8> = Vec::new();
let syscalls = vec![
SyscallDecl {
module: "gfx".into(),
name: "clear_565".into(),
version: 1,
arg_slots: 1,
ret_slots: 0,
},
SyscallDecl {
module: "gfx".into(),
name: "draw_text".into(),
version: 1,
arg_slots: 4,
ret_slots: 0,
},
SyscallDecl {
module: "log".into(),
name: "write".into(),
version: 1,
arg_slots: 2,
ret_slots: 0,
},
SyscallDecl {
module: "composer".into(),
name: "bind_scene".into(),
version: 1,
arg_slots: 1,
ret_slots: 1,
},
SyscallDecl {
module: "composer".into(),
name: "set_camera".into(),
version: 1,
arg_slots: 2,
ret_slots: 0,
},
SyscallDecl {
module: "composer".into(),
name: "emit_sprite".into(),
version: 1,
arg_slots: 9,
ret_slots: 1,
},
];
heavy_load(&mut rom);
// light_load(&mut rom);
let functions = vec![FunctionMeta {
code_offset: 0,
code_len: rom.len() as u32,
param_slots: 0,
local_slots: 2,
return_slots: 0,
max_stack_slots: 32,
}];
let module = BytecodeModule {
version: 0,
const_pool: vec![
ConstantPoolEntry::String("stress".into()),
ConstantPoolEntry::String("frame".into()),
ConstantPoolEntry::String("overlay".into()),
ConstantPoolEntry::String("composer".into()),
],
functions,
code: rom,
debug_info: Some(DebugInfo {
pc_to_span: vec![],
function_names: vec![(0, "main".into())],
}),
exports: vec![Export { symbol: "main".into(), func_idx: 0 }],
syscalls,
};
let bytes = module.serialize();
let mut out_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
out_dir.pop(); // pbxgen-stress
out_dir.pop(); // tools
out_dir.pop(); // crates
out_dir.push("test-cartridges");
out_dir.push("stress-console");
fs::create_dir_all(&out_dir)?;
fs::write(out_dir.join("program.pbx"), bytes)?;
fs::write(out_dir.join("assets.pa"), build_assets_pack()?)?;
fs::write(out_dir.join("manifest.json"), b"{\n \"magic\": \"PMTU\",\n \"cartridge_version\": 1,\n \"app_id\": 1,\n \"title\": \"Stress Console\",\n \"app_version\": \"0.1.0\",\n \"app_mode\": \"Game\",\n \"capabilities\": [\"gfx\", \"log\", \"asset\"]\n}\n")?;
Ok(())
}
fn heavy_load(rom: &mut Vec<u8>) {
// Single function 0: main
// Global 0 = frame counter
// Global 1 = scene bound flag
// Local 0 = sprite row
// Local 1 = sprite col
rom.extend(asm("GET_GLOBAL 0\nPUSH_I32 1\nADD\nSET_GLOBAL 0"));
rom.extend(asm("GET_GLOBAL 1\nPUSH_I32 0\nEQ"));
let jif_bind_done_offset = rom.len() + 2;
rom.extend(asm("JMP_IF_FALSE 0"));
rom.extend(asm("PUSH_I32 0\nHOSTCALL 3\nPOP_N 1\nPUSH_I32 1\nSET_GLOBAL 1"));
let bind_done_target = rom.len() as u32;
rom.extend(asm("PUSH_I32 0\nHOSTCALL 0"));
rom.extend(asm(
"GET_GLOBAL 0\nPUSH_I32 2\nMUL\nPUSH_I32 192\nMOD\nGET_GLOBAL 0\nPUSH_I32 76\nMOD\nHOSTCALL 4",
));
rom.extend(asm("PUSH_I32 0\nSET_LOCAL 0"));
let row_loop_start = rom.len() as u32;
rom.extend(asm("GET_LOCAL 0\nPUSH_I32 16\nLT"));
let jif_row_end_offset = rom.len() + 2;
rom.extend(asm("JMP_IF_FALSE 0"));
rom.extend(asm("PUSH_I32 0\nSET_LOCAL 1"));
let col_loop_start = rom.len() as u32;
rom.extend(asm("GET_LOCAL 1\nPUSH_I32 32\nLT"));
let jif_col_end_offset = rom.len() + 2;
rom.extend(asm("JMP_IF_FALSE 0"));
rom.extend(asm(
"PUSH_I32 0\n\
GET_LOCAL 0\nPUSH_I32 32\nMUL\nGET_LOCAL 1\nADD\nGET_GLOBAL 0\nADD\nPUSH_I32 15\nMOD\nPUSH_I32 1\nADD\n\
GET_LOCAL 1\nPUSH_I32 10\nMUL\nGET_GLOBAL 0\nPUSH_I32 10\nMOD\nADD\nPUSH_I32 320\nMOD\n\
GET_LOCAL 0\nPUSH_I32 10\nMUL\nGET_GLOBAL 0\nPUSH_I32 2\nMUL\nPUSH_I32 10\nMOD\nADD\nPUSH_I32 180\nMOD\n\
GET_LOCAL 0\nGET_LOCAL 1\nADD\nGET_GLOBAL 0\nADD\nPUSH_I32 4\nMOD\n\
PUSH_I32 0\n\
GET_LOCAL 1\nGET_GLOBAL 0\nADD\nPUSH_I32 1\nBIT_AND\nPUSH_I32 0\nNEQ\n\
GET_LOCAL 0\nGET_GLOBAL 0\nADD\nPUSH_I32 1\nBIT_AND\nPUSH_I32 0\nNEQ\n\
GET_LOCAL 0\nGET_LOCAL 1\nADD\nPUSH_I32 4\nMOD\n\
HOSTCALL 5\nPOP_N 1",
));
rom.extend(asm("GET_LOCAL 1\nPUSH_I32 1\nADD\nSET_LOCAL 1"));
let jmp_col_loop_offset = rom.len() + 2;
rom.extend(asm("JMP 0"));
let col_loop_end = rom.len() as u32;
rom.extend(asm("GET_LOCAL 0\nPUSH_I32 1\nADD\nSET_LOCAL 0"));
let jmp_row_loop_offset = rom.len() + 2;
rom.extend(asm("JMP 0"));
let row_loop_end = rom.len() as u32;
rom.extend(asm("GET_GLOBAL 0\nPUSH_I32 3\nMUL\nPUSH_I32 220\nMOD\n\
PUSH_I32 8\n\
PUSH_CONST 0\n\
GET_GLOBAL 0\nPUSH_I32 2047\nMUL\nPUSH_I32 65535\nBIT_AND\n\
HOSTCALL 1"));
rom.extend(asm("PUSH_I32 12\n\
GET_GLOBAL 0\nPUSH_I32 2\nMUL\nPUSH_I32 120\nMOD\nPUSH_I32 24\nADD\n\
PUSH_CONST 1\n\
GET_GLOBAL 0\nPUSH_I32 4093\nMUL\nPUSH_I32 65535\nBIT_AND\n\
HOSTCALL 1"));
rom.extend(asm("PUSH_I32 220\n\
GET_GLOBAL 0\nPUSH_I32 5\nMUL\nPUSH_I32 140\nMOD\n\
PUSH_CONST 2\n\
GET_GLOBAL 0\nPUSH_I32 1237\nMUL\nPUSH_I32 65535\nBIT_AND\n\
HOSTCALL 1"));
rom.extend(asm("GET_GLOBAL 0\nPUSH_I32 4\nMUL\nPUSH_I32 180\nMOD\nPUSH_I32 80\nADD\n\
GET_GLOBAL 0\nPUSH_I32 3\nMUL\nPUSH_I32 90\nMOD\nPUSH_I32 70\nADD\n\
PUSH_CONST 3\n\
GET_GLOBAL 0\nPUSH_I32 3001\nMUL\nPUSH_I32 65535\nBIT_AND\n\
HOSTCALL 1"));
rom.extend(asm("GET_GLOBAL 0\nPUSH_I32 60\nMOD\nPUSH_I32 0\nEQ"));
let jif_log_offset = rom.len() + 2;
rom.extend(asm("JMP_IF_FALSE 0"));
rom.extend(asm("PUSH_I32 2\nPUSH_CONST 0\nHOSTCALL 2"));
let after_log = rom.len() as u32;
rom.extend(asm("FRAME_SYNC\nRET"));
let patch = |buf: &mut Vec<u8>, imm_offset: usize, target: u32| {
buf[imm_offset..imm_offset + 4].copy_from_slice(&target.to_le_bytes());
};
patch(rom, jif_bind_done_offset, bind_done_target);
patch(rom, jif_row_end_offset, row_loop_end);
patch(rom, jif_col_end_offset, col_loop_end);
patch(rom, jmp_col_loop_offset, col_loop_start);
patch(rom, jmp_row_loop_offset, row_loop_start);
patch(rom, jif_log_offset, after_log);
}
fn build_assets_pack() -> Result<Vec<u8>> {
let (glyph_entry, glyph_payload) = build_glyph_asset();
let scene = build_scene_bank();
let scene_payload = encode_scene_payload(&scene);
let scene_entry = AssetEntry {
asset_id: 1,
asset_name: "stress_scene".into(),
bank_type: BankType::SCENE,
offset: glyph_payload.len() as u64,
size: scene_payload.len() as u64,
decoded_size: expected_scene_decoded_size(&scene) as u64,
codec: AssetCodec::None,
metadata: serde_json::json!({}),
};
let asset_table = vec![glyph_entry, scene_entry];
let preload =
vec![PreloadEntry { asset_id: 0, slot: 0 }, PreloadEntry { asset_id: 1, slot: 0 }];
let payload_len = glyph_payload.len() + scene_payload.len();
let header = serde_json::to_vec(&AssetsPackHeader { asset_table, preload })?;
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(&glyph_payload);
bytes.extend_from_slice(&scene_payload);
debug_assert_eq!(bytes.len(), payload_offset as usize + payload_len);
Ok(bytes)
}
fn build_glyph_asset() -> (AssetEntry, Vec<u8>) {
let pixel_indices = vec![1_u8; 8 * 8];
let mut payload = pack_4bpp(&pixel_indices);
payload.extend_from_slice(&build_palette_bytes());
let entry = AssetEntry {
asset_id: 0,
asset_name: "stress_square".into(),
bank_type: BankType::GLYPH,
offset: 0,
size: payload.len() as u64,
decoded_size: (8 * 8 + GLYPH_BANK_PALETTE_COUNT_V1 * GLYPH_BANK_COLORS_PER_PALETTE * 2)
as u64,
codec: AssetCodec::None,
metadata: serde_json::json!({
"tile_size": 8,
"width": 8,
"height": 8,
"palette_count": GLYPH_BANK_PALETTE_COUNT_V1,
"palette_authored": GLYPH_BANK_PALETTE_COUNT_V1
}),
};
(entry, payload)
}
fn build_palette_bytes() -> Vec<u8> {
let mut bytes =
Vec::with_capacity(GLYPH_BANK_PALETTE_COUNT_V1 * GLYPH_BANK_COLORS_PER_PALETTE * 2);
for palette_id in 0..GLYPH_BANK_PALETTE_COUNT_V1 {
for color_index in 0..GLYPH_BANK_COLORS_PER_PALETTE {
let color = if color_index == 1 { stress_color(palette_id) } else { Color::BLACK };
bytes.extend_from_slice(&color.raw().to_le_bytes());
}
}
bytes
}
fn stress_color(palette_id: usize) -> Color {
let r = ((palette_id * 53) % 256) as u8;
let g = ((palette_id * 97 + 64) % 256) as u8;
let b = ((palette_id * 29 + 128) % 256) as u8;
Color::rgb(r, g, b)
}
fn pack_4bpp(indices: &[u8]) -> Vec<u8> {
let mut packed = Vec::with_capacity(indices.len().div_ceil(2));
for chunk in indices.chunks(2) {
let hi = chunk[0] & 0x0f;
let lo = chunk.get(1).copied().unwrap_or(0) & 0x0f;
packed.push((hi << 4) | lo);
}
packed
}
fn build_scene_bank() -> SceneBank {
let mut layers = std::array::from_fn(|layer_index| {
let mut tiles = vec![
Tile {
active: false,
glyph: Glyph { glyph_id: 0, palette_id: layer_index as u8 + 1 },
flip_x: false,
flip_y: false,
};
64 * 32
];
for step in 0..8 {
let x = 4 + step * 7 + layer_index * 2;
let y = 2 + step * 3 + layer_index * 2;
let index = y * 64 + x;
tiles[index] = Tile {
active: true,
glyph: Glyph { glyph_id: 0, palette_id: layer_index as u8 + 1 },
flip_x: false,
flip_y: false,
};
}
SceneLayer {
active: true,
glyph_bank_id: 0,
tile_size: TileSize::Size8,
parallax_factor: match layer_index {
0 => ParallaxFactor { x: 1.0, y: 1.0 },
1 => ParallaxFactor { x: 0.75, y: 0.75 },
2 => ParallaxFactor { x: 0.5, y: 0.5 },
_ => ParallaxFactor { x: 0.25, y: 0.25 },
},
tilemap: TileMap { width: 64, height: 32, tiles },
}
});
// Keep the farthest layer a bit sparser so the diagonal remains visually readable.
for step in 0..4 {
let x = 10 + step * 12;
let y = 4 + step * 5;
let index = y * 64 + x;
layers[3].tilemap.tiles[index].active = false;
}
SceneBank { layers }
}
fn expected_scene_decoded_size(scene: &SceneBank) -> usize {
scene
.layers
.iter()
.map(|layer| {
SCENE_DECODED_LAYER_OVERHEAD_BYTES_V1 + layer.tilemap.tiles.len() * size_of::<Tile>()
})
.sum()
}
fn encode_scene_payload(scene: &SceneBank) -> Vec<u8> {
let mut data = Vec::new();
data.extend_from_slice(&SCENE_PAYLOAD_MAGIC_V1);
data.extend_from_slice(&SCENE_PAYLOAD_VERSION_V1.to_le_bytes());
data.extend_from_slice(&(SCENE_LAYER_COUNT_V1 as u16).to_le_bytes());
data.extend_from_slice(&0_u32.to_le_bytes());
for layer in &scene.layers {
let layer_flags = if layer.active { 0b0000_0001 } else { 0 };
data.push(layer_flags);
data.push(layer.glyph_bank_id);
data.push(layer.tile_size as u8);
data.push(0);
data.extend_from_slice(&layer.parallax_factor.x.to_le_bytes());
data.extend_from_slice(&layer.parallax_factor.y.to_le_bytes());
data.extend_from_slice(&(layer.tilemap.width as u32).to_le_bytes());
data.extend_from_slice(&(layer.tilemap.height as u32).to_le_bytes());
data.extend_from_slice(&(layer.tilemap.tiles.len() as u32).to_le_bytes());
data.extend_from_slice(&0_u32.to_le_bytes());
for tile in &layer.tilemap.tiles {
let mut tile_flags = 0_u8;
if tile.active {
tile_flags |= 0b0000_0001;
}
if tile.flip_x {
tile_flags |= 0b0000_0010;
}
if tile.flip_y {
tile_flags |= 0b0000_0100;
}
data.push(tile_flags);
data.push(tile.glyph.palette_id);
data.extend_from_slice(&tile.glyph.glyph_id.to_le_bytes());
}
}
data
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn assets_pack_contains_preloaded_glyph_and_scene_assets() {
let bytes = build_assets_pack().expect("assets pack");
let prelude =
AssetsPackPrelude::from_bytes(&bytes[..ASSETS_PA_PRELUDE_SIZE]).expect("prelude");
assert_eq!(prelude.magic, ASSETS_PA_MAGIC);
assert_eq!(prelude.schema_version, ASSETS_PA_SCHEMA_VERSION);
let header_start = ASSETS_PA_PRELUDE_SIZE;
let header_end = header_start + prelude.header_len as usize;
let header: AssetsPackHeader =
serde_json::from_slice(&bytes[header_start..header_end]).expect("header");
assert_eq!(header.asset_table.len(), 2);
assert_eq!(header.preload.len(), 2);
assert_eq!(header.asset_table[0].bank_type, BankType::GLYPH);
assert_eq!(header.asset_table[1].bank_type, BankType::SCENE);
assert_eq!(header.preload[0].slot, 0);
assert_eq!(header.preload[1].slot, 0);
assert_eq!(header.asset_table[0].offset, 0);
assert_eq!(header.asset_table[1].offset, header.asset_table[0].size);
assert_eq!(
bytes.len(),
prelude.payload_offset as usize
+ header.asset_table[0].size as usize
+ header.asset_table[1].size as usize
);
}
}