implement PLN-0043 runtime 270p core migration

This commit is contained in:
bQUARKz 2026-04-27 06:42:44 +01:00
parent d10d5aa7db
commit 4ca53046bf
Signed by: bquarkz
SSH Key Fingerprint: SHA256:Z7dgqoglWwoK6j6u4QC87OveEq74WOhFN+gitsxtkf8
5 changed files with 58 additions and 39 deletions

View File

@ -340,6 +340,7 @@ impl FrameComposer {
mod tests {
use super::*;
use crate::gfx::Gfx;
use crate::hardware::Hardware;
use crate::memory_banks::{
GlyphBankPoolAccess, GlyphBankPoolInstaller, MemoryBanks, SceneBankPoolInstaller,
};
@ -405,10 +406,14 @@ mod tests {
#[test]
fn frame_composer_starts_unbound_with_empty_owned_state() {
let frame_composer =
FrameComposer::new(320, 180, Arc::new(MemoryBanks::new()), make_glyph_slot_index(&[]));
let frame_composer = FrameComposer::new(
Hardware::W,
Hardware::H,
Arc::new(MemoryBanks::new()),
make_glyph_slot_index(&[]),
);
assert_eq!(frame_composer.viewport_size(), (320, 180));
assert_eq!(frame_composer.viewport_size(), (Hardware::W, Hardware::H));
assert_eq!(frame_composer.active_scene_id(), None);
assert!(frame_composer.active_scene().is_none());
assert_eq!(frame_composer.scene_status(), SceneStatus::Unbound);
@ -425,7 +430,8 @@ mod tests {
let banks = Arc::new(MemoryBanks::new());
banks.install_scene_bank(3, Arc::new(make_scene()));
let frame_composer = FrameComposer::new(320, 180, banks, make_glyph_slot_index(&[(1, 0)]));
let frame_composer =
FrameComposer::new(Hardware::W, Hardware::H, banks, make_glyph_slot_index(&[(1, 0)]));
let scene =
frame_composer.scene_bank_slot(3).expect("scene bank slot 3 should be resident");
@ -442,7 +448,7 @@ mod tests {
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, make_glyph_slot_index(&[(1, 0)]));
FrameComposer::new(Hardware::W, Hardware::H, banks, make_glyph_slot_index(&[(1, 0)]));
assert!(frame_composer.bind_scene(3));
@ -462,7 +468,7 @@ mod tests {
banks.install_scene_bank(1, Arc::new(make_scene()));
let mut frame_composer =
FrameComposer::new(320, 180, banks, make_glyph_slot_index(&[(1, 0)]));
FrameComposer::new(Hardware::W, Hardware::H, banks, make_glyph_slot_index(&[(1, 0)]));
assert!(frame_composer.bind_scene(1));
frame_composer.unbind_scene();
@ -476,8 +482,12 @@ mod tests {
#[test]
fn set_camera_stores_top_left_pixel_coordinates() {
let mut frame_composer =
FrameComposer::new(320, 180, Arc::new(MemoryBanks::new()), make_glyph_slot_index(&[]));
let mut frame_composer = FrameComposer::new(
Hardware::W,
Hardware::H,
Arc::new(MemoryBanks::new()),
make_glyph_slot_index(&[]),
);
frame_composer.set_camera(-12, 48);
@ -490,17 +500,21 @@ mod tests {
banks.install_scene_bank(0, Arc::new(make_scene()));
let mut frame_composer =
FrameComposer::new(320, 180, banks, make_glyph_slot_index(&[(1, 0)]));
FrameComposer::new(Hardware::W, Hardware::H, banks, make_glyph_slot_index(&[(1, 0)]));
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));
assert_eq!((cache.width(), cache.height()), (65, 38));
}
#[test]
fn missing_scene_binding_falls_back_to_no_scene_state() {
let mut frame_composer =
FrameComposer::new(320, 180, Arc::new(MemoryBanks::new()), make_glyph_slot_index(&[]));
let mut frame_composer = FrameComposer::new(
Hardware::W,
Hardware::H,
Arc::new(MemoryBanks::new()),
make_glyph_slot_index(&[]),
);
assert!(!frame_composer.bind_scene(7));
@ -514,7 +528,8 @@ mod tests {
let banks = Arc::new(MemoryBanks::new());
banks.install_scene_bank(0, Arc::new(make_scene()));
let mut frame_composer = FrameComposer::new(320, 180, banks, make_glyph_slot_index(&[]));
let mut frame_composer =
FrameComposer::new(Hardware::W, Hardware::H, banks, make_glyph_slot_index(&[]));
let panic = catch_unwind(AssertUnwindSafe(|| frame_composer.bind_scene(0)))
.expect_err("bind_scene should panic when dependency is missing");
@ -649,8 +664,12 @@ mod tests {
#[test]
fn frame_composer_emits_ordered_sprites_for_rendering() {
let mut frame_composer =
FrameComposer::new(320, 180, Arc::new(MemoryBanks::new()), make_glyph_slot_index(&[]));
let mut frame_composer = FrameComposer::new(
Hardware::W,
Hardware::H,
Arc::new(MemoryBanks::new()),
make_glyph_slot_index(&[]),
);
frame_composer.begin_frame();
assert!(frame_composer.emit_sprite(Sprite {

View File

@ -21,7 +21,7 @@ use std::sync::Arc;
/// for easier testing and different host implementations (Desktop, Web, etc.).
///
/// ### Console Specifications:
/// - **Resolution**: 320x180 (16:9 Aspect Ratio).
/// - **Resolution**: 480x270 (16:9 Aspect Ratio).
/// - **Color Depth**: RGB565 (16-bit).
/// - **Audio**: Stereo, Command-based mixing.
/// - **Input**: 12-button Digital Gamepad + Absolute Touch/Mouse.
@ -115,9 +115,9 @@ impl HardwareBridge for Hardware {
impl Hardware {
/// Internal hardware width in pixels.
pub const W: usize = 320;
pub const W: usize = 480;
/// Internal hardware height in pixels.
pub const H: usize = 180;
pub const H: usize = 270;
/// Creates a fresh hardware instance with default settings.
pub fn new() -> Self {

View File

@ -433,20 +433,20 @@ mod tests {
#[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 mut resolver = SceneViewportResolver::new(480, 270, 35, 21, 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.master_anchor, TileAnchor { x: 17, y: 10 });
assert_eq!(update.layer_anchors[0], TileAnchor { x: 17, y: 10 });
assert_eq!(update.layer_anchors[1], TileAnchor { x: 17, y: 10 });
assert_eq!(update.refresh_requests.len(), 4);
}
#[test]
fn per_layer_copy_requests_follow_parallax_factor() {
let scene = make_scene();
let mut resolver = SceneViewportResolver::new(320, 180, 25, 16, 12, 20);
let mut resolver = SceneViewportResolver::new(480, 270, 35, 21, 12, 20);
let update = resolver.update(&scene, 32, 48);
@ -461,24 +461,24 @@ mod tests {
#[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 mut resolver = SceneViewportResolver::new(480, 270, 35, 21, 12, 20);
let _ = resolver.update(&scene, 0, 0);
let update = resolver.update(&scene, 8, 0);
assert!(update.refresh_requests.is_empty());
assert_eq!(update.master_anchor, TileAnchor { x: 12, y: 8 });
assert_eq!(update.master_anchor, TileAnchor { x: 17, y: 10 });
}
#[test]
fn repeated_high_speed_movement_advances_in_tile_steps() {
let scene = make_scene();
let mut resolver = SceneViewportResolver::new(320, 180, 25, 16, 12, 20);
let mut resolver = SceneViewportResolver::new(480, 270, 35, 21, 12, 20);
let _ = resolver.update(&scene, 0, 0);
let update = resolver.update(&scene, 64, 0);
assert!(update.master_anchor.x > 12);
assert!(update.master_anchor.x > 17);
assert!(update.refresh_requests.iter().any(|request| matches!(
request,
CacheRefreshRequest::RefreshColumn { layer_index: 0, .. }
@ -489,18 +489,18 @@ mod tests {
#[test]
fn anchors_clamp_near_scene_edges() {
let scene = make_scene();
let mut resolver = SceneViewportResolver::new(320, 180, 25, 16, 12, 20);
let mut resolver = SceneViewportResolver::new(480, 270, 35, 21, 12, 20);
let update = resolver.update(&scene, 10_000, 10_000);
assert_eq!(update.master_anchor, TileAnchor { x: 51, y: 56 });
assert_eq!(update.layer_anchors[0], TileAnchor { x: 51, y: 56 });
assert_eq!(update.master_anchor, TileAnchor { x: 46, y: 53 });
assert_eq!(update.layer_anchors[0], TileAnchor { x: 46, y: 53 });
}
#[test]
fn corner_trigger_converts_to_non_overlapping_region_requests() {
let scene = make_scene();
let mut resolver = SceneViewportResolver::new(320, 180, 25, 16, 12, 20);
let mut resolver = SceneViewportResolver::new(480, 270, 35, 21, 12, 20);
let _ = resolver.update(&scene, 0, 0);
let update = resolver.update(&scene, 64, 80);
@ -515,15 +515,15 @@ mod tests {
.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);
assert_eq!(regions[0].x + regions[0].width, 35);
assert_eq!(regions[0].height, 21);
assert_eq!(regions[1].width, 34);
assert_eq!(regions[1].height, 2);
}
#[test]
fn reset_scene_requests_full_invalidation_for_all_layers() {
let mut resolver = SceneViewportResolver::new(320, 180, 25, 16, 12, 20);
let mut resolver = SceneViewportResolver::new(480, 270, 35, 21, 12, 20);
let requests = resolver.reset_scene();

View File

@ -28,4 +28,4 @@
{"type":"discussion","id":"DSC-0017","status":"done","ticket":"asset-entry-metadata-normalization-contract","title":"Asset Entry Metadata Normalization Contract","created_at":"2026-03-27","updated_at":"2026-04-09","tags":[],"agendas":[{"id":"AGD-0016","file":"workflow/agendas/AGD-0016-asset-entry-metadata-normalization-contract.md","status":"done","created_at":"2026-03-27","updated_at":"2026-04-09"}],"decisions":[{"id":"DEC-0004","file":"workflow/decisions/DEC-0004-asset-entry-metadata-normalization-contract.md","status":"accepted","created_at":"2026-04-09","updated_at":"2026-04-09"}],"plans":[],"lessons":[{"id":"LSN-0023","file":"lessons/DSC-0017-asset-metadata-normalization/LSN-0023-typed-asset-metadata-helpers.md","status":"done","created_at":"2026-04-09","updated_at":"2026-04-09"}]}
{"type":"discussion","id":"DSC-0018","status":"done","ticket":"asset-load-asset-id-int-contract","title":"Asset Load Asset ID Int Contract","created_at":"2026-03-27","updated_at":"2026-03-27","tags":["asset","runtime","abi"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0019","file":"lessons/DSC-0018-asset-load-asset-id-int-contract/LSN-0019-asset-load-id-abi-convergence.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"}]}
{"type":"discussion","id":"DSC-0019","status":"done","ticket":"jenkinsfile-correction","title":"Jenkinsfile Correction and Relocation","created_at":"2026-04-07","updated_at":"2026-04-07","tags":["ci","jenkins"],"agendas":[{"id":"AGD-0017","file":"workflow/agendas/AGD-0017-jenkinsfile-correction.md","status":"done","created_at":"2026-04-07","updated_at":"2026-04-07"}],"decisions":[{"id":"DEC-0002","file":"workflow/decisions/DEC-0002-jenkinsfile-strategy.md","status":"accepted","created_at":"2026-04-07","updated_at":"2026-04-07"}],"plans":[{"id":"PLN-0002","file":"workflow/plans/PLN-0002-jenkinsfile-execution.md","status":"done","created_at":"2026-04-07","updated_at":"2026-04-07"}],"lessons":[{"id":"LSN-0020","file":"lessons/DSC-0019-jenkins-ci-standardization/LSN-0020-jenkins-standard-relocation.md","status":"done","created_at":"2026-04-07","updated_at":"2026-04-07"}]}
{"type":"discussion","id":"DSC-0030","status":"open","ticket":"internal-viewport-270p","title":"Agenda - Internal Viewport 270p (480x270)","created_at":"2026-04-27","updated_at":"2026-04-27","tags":["gfx","runtime","viewport","resolution","frame-composer","host"],"agendas":[{"id":"AGD-0030","file":"workflow/agendas/AGD-0030-internal-viewport-270p.md","status":"done","created_at":"2026-04-27","updated_at":"2026-04-27"}],"decisions":[{"id":"DEC-0022","file":"workflow/decisions/DEC-0022-internal-viewport-270p.md","status":"accepted","created_at":"2026-04-27","updated_at":"2026-04-27","ref_agenda":"AGD-0030"}],"plans":[{"id":"PLN-0043","file":"workflow/plans/PLN-0043-runtime-270p-core-migration.md","status":"review","created_at":"2026-04-27","updated_at":"2026-04-27","ref_decisions":["DEC-0022"]},{"id":"PLN-0044","file":"workflow/plans/PLN-0044-host-and-tests-270p-propagation.md","status":"review","created_at":"2026-04-27","updated_at":"2026-04-27","ref_decisions":["DEC-0022"]},{"id":"PLN-0045","file":"workflow/plans/PLN-0045-specs-and-docs-270p-publication.md","status":"review","created_at":"2026-04-27","updated_at":"2026-04-27","ref_decisions":["DEC-0022"]}],"lessons":[]}
{"type":"discussion","id":"DSC-0030","status":"open","ticket":"internal-viewport-270p","title":"Agenda - Internal Viewport 270p (480x270)","created_at":"2026-04-27","updated_at":"2026-04-27","tags":["gfx","runtime","viewport","resolution","frame-composer","host"],"agendas":[{"id":"AGD-0030","file":"workflow/agendas/AGD-0030-internal-viewport-270p.md","status":"done","created_at":"2026-04-27","updated_at":"2026-04-27"}],"decisions":[{"id":"DEC-0022","file":"workflow/decisions/DEC-0022-internal-viewport-270p.md","status":"accepted","created_at":"2026-04-27","updated_at":"2026-04-27","ref_agenda":"AGD-0030"}],"plans":[{"id":"PLN-0043","file":"workflow/plans/PLN-0043-runtime-270p-core-migration.md","status":"done","created_at":"2026-04-27","updated_at":"2026-04-27","ref_decisions":["DEC-0022"]},{"id":"PLN-0044","file":"workflow/plans/PLN-0044-host-and-tests-270p-propagation.md","status":"review","created_at":"2026-04-27","updated_at":"2026-04-27","ref_decisions":["DEC-0022"]},{"id":"PLN-0045","file":"workflow/plans/PLN-0045-specs-and-docs-270p-publication.md","status":"review","created_at":"2026-04-27","updated_at":"2026-04-27","ref_decisions":["DEC-0022"]}],"lessons":[]}

View File

@ -2,9 +2,9 @@
id: PLN-0043
ticket: internal-viewport-270p
title: Runtime 270p Core Migration
status: review
status: done
created: 2026-04-27
completed:
completed: 2026-04-27
tags: [gfx, runtime, viewport, resolution, frame-composer]
---