implement PLN-0043 runtime 270p core migration
This commit is contained in:
parent
d10d5aa7db
commit
4ca53046bf
@ -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 {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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();
|
||||
|
||||
|
||||
@ -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":[]}
|
||||
|
||||
@ -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]
|
||||
---
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user