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 { assemble(s).expect("assemble") } pub fn generate() -> Result<()> { let mut rom: Vec = 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) { // 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, 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> { 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) { 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 { 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 { 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::() }) .sum() } fn encode_scene_payload(scene: &SceneBank) -> Vec { 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 ); } }