Preserve explicit original HD details and rebuild Linux and Windows runtimes

This commit is contained in:
raptorreplica committed 2026-10-17 16:31:08 +11:00
1 parent 1bc0a9ceff
commit 9015a6c955
13 files changed
+739 -33

No files matched your search

+103
View File
@@ -58,6 +58,7 @@ pub enum OverlayResult {
Applied,
InvalidCanvas,
NonOpaque,
InvalidOriginalDetails,
}
pub struct Replacement {
@@ -66,6 +67,10 @@ pub struct Replacement {
pub position: [i32; 2],
pub logical_size: [usize; 2],
}
pub struct RetainedOriginalPlacement {
pub replacement_index: usize,
pub details: crate::hd::OriginalDetailCells,
}
pub struct Composition {
pub image: Sprite,
pub results: Vec<OverlayResult>,
@@ -120,6 +125,13 @@ impl TerrainHd {
scale: usize,
palette: &[[u8; 3]; 256],
) -> Composition {
self.compose_retaining_original(context, replacements, &[], scale, palette)
}
/// Original detail is explicit metadata and is patched only into the FINAL
/// layer. Pristine HD edge inputs and all unprotected samples stay unchanged.
pub fn compose_retaining_original(&mut self, context: &Sprite,
replacements: &[Replacement], retained: &[RetainedOriginalPlacement],
scale: usize, palette: &[[u8; 3]; 256]) -> Composition {
let mut image = context.clone();
let mut results = Vec::with_capacity(replacements.len());
let mut accepted = BTreeMap::new();
@@ -133,6 +145,30 @@ impl TerrainHd {
if accepted.is_empty() {
return Composition { image, results };
}
// Validate/project all accepted protected footprints before composition.
// Conflicting ownership fails the entire opt-in context to Original;
// default/no-retention composition remains exactly the old route.
let mut protected = BTreeMap::new();
let valid_details = retained.iter().all(|retention| {
let Some(tile) = replacements.get(retention.replacement_index) else { return false };
if accepted.get(&(tile.position, tile.logical_size)) != Some(&retention.replacement_index) { return true; }
if retention.details.logical_size() != tile.logical_size { return false; }
for &[x, y] in retention.details.cells() {
if x as usize >= tile.logical_size[0] || y as usize >= tile.logical_size[1] { return false; }
for dy in 0..scale { for dx in 0..scale {
let xx = (tile.position[0] as i64 + x as i64) * scale as i64 + dx as i64;
let yy = (tile.position[1] as i64 + y as i64) * scale as i64 + dy as i64;
if xx < 0 || yy < 0 || xx >= context.width as i64 || yy >= context.height as i64 { continue; }
let pixel = yy as usize * context.width + xx as usize;
if protected.insert(pixel, retention.replacement_index)
.is_some_and(|owner| owner != retention.replacement_index) { return false; }
}}
}
true
});
if !valid_details {
return Composition { image: context.clone(), results: vec![OverlayResult::InvalidOriginalDetails; replacements.len()] };
}
self.prepare_palette(palette);
// Keep a pristine joined HD context. Every edge reads this same layer,
// so the result cannot depend on the overlay order or previous blending.
@@ -257,6 +293,9 @@ impl TerrainHd {
}
}
}
// AFTER all tile edges/corners: copy authentic joined Original context.
// These samples were never read by shared_target/tile_edges.
for pixel in protected.keys() { image.pixels[*pixel] = context.pixels[*pixel]; }
Composition { image, results }
}
@@ -609,3 +648,67 @@ fn edge_weight(distance: usize, band: usize) -> usize {
let denominator = band * band * band;
(distance * distance * (3 * band - 2 * distance) * (LEVELS - 1) + denominator / 2) / denominator
}
#[cfg(test)]
mod retained_original_join_tests {
use super::*;
fn detail(index:usize,cells:Vec<[u32;2]>) -> RetainedOriginalPlacement {
RetainedOriginalPlacement {replacement_index:index,
details:crate::hd::OriginalDetailCells::for_test(cells,[32,32])}
}
fn palette()->[[u8;3];256] {std::array::from_fn(|i|[i as u8;3])}
#[test]
fn edge_protection_is_after_all_joins_no_neighbor_leak_at_any_scale() {
for scale in 1..=4 {
let context=Sprite {width:64*scale,height:32*scale,pixels:vec![17;64*32*scale*scale]};
let tiles=vec![Replacement{image:Sprite{width:32*scale,height:32*scale,pixels:vec![60;32*32*scale*scale]},position:[0,0],logical_size:[32,32]},
Replacement{image:Sprite{width:32*scale,height:32*scale,pixels:vec![80;32*32*scale*scale]},position:[32,0],logical_size:[32,32]}];
let mut t=TerrainHd::default();let base=t.compose(&context,&tiles,scale,&palette()).image;
let result=t.compose_retaining_original(&context,&tiles,&[detail(0,vec![[31,16]])],scale,&palette());
assert!(result.results.iter().all(|r|*r==OverlayResult::Applied));
for y in 0..context.height {for x in 0..context.width {
let i=y*context.width+x;let protected=x/scale==31&&y/scale==16;
assert_eq!(result.image.pixels[i],if protected{context.pixels[i]}else{base.pixels[i]});
}}
let empty=t.compose_retaining_original(&context,&tiles,&[],scale,&palette()).image;
assert_eq!(base,empty);
}
}
#[test]
fn duplicate_protected_owners_fail_to_original_not_last_write() {
let context=Sprite{width:64,height:32,pixels:vec![17;64*32]};
let tiles=vec![Replacement{image:Sprite{width:32,height:32,pixels:vec![60;1024]},position:[0,0],logical_size:[32,32]},
Replacement{image:Sprite{width:32,height:32,pixels:vec![80;1024]},position:[1,0],logical_size:[32,32]}];
let result=TerrainHd::default().compose_retaining_original(&context,&tiles,
&[detail(0,vec![[1,0]]),detail(1,vec![[0,0]])],1,&palette());
assert_eq!(result.image,context);assert!(result.results.iter().all(|r|*r==OverlayResult::InvalidOriginalDetails));
}
#[test]
fn reserved_detail_after_joins_preserves_functional_labels_and_never_leaks_to_neighbors() {
let mut logical=Sprite{width:64,height:32,pixels:vec![17;64*32]};
logical.pixels[15*64+31]=252;logical.pixels[16*64+31]=253;
logical.pixels[15*64+32]=243;logical.pixels[16*64+32]=240;
for scale in 1..=4 {
let context=crate::render::upscale(&logical,scale).unwrap();
let hd=vec![Replacement{image:Sprite{width:32*scale,height:32*scale,pixels:vec![60;32*32*scale*scale]},position:[0,0],logical_size:[32,32]},Replacement{image:Sprite{width:32*scale,height:32*scale,pixels:vec![80;32*32*scale*scale]},position:[32,0],logical_size:[32,32]}];
let mut compositor=TerrainHd::default();
let baseline=compositor.compose(&context,&hd,scale,&palette()).image;
let hybrid=compositor.compose_retaining_original(&context,&hd,&[detail(0,vec![[31,15],[31,16]])],scale,&palette()).image;
let mut phase=palette();phase[252]=[255,0,0];phase[253]=[0,255,0];
let mut phase2=phase;phase2[252]=[0,0,0];phase2[253]=[255,255,255];
let rgba1=crate::render::Canvas{image:hybrid.clone(),scale}.rgba(&phase,false);
let rgba2=crate::render::Canvas{image:hybrid.clone(),scale}.rgba(&phase2,false);
for y in 0..context.height {for x in 0..context.width {
let i=y*context.width+x;let protected=x/scale==31&&(y/scale==15||y/scale==16);
assert_eq!(hybrid.pixels[i],if protected{context.pixels[i]}else{baseline.pixels[i]});
if protected&&(hybrid.pixels[i]==252||hybrid.pixels[i]==253){assert_ne!(&rgba1[i*4..i*4+3],&rgba2[i*4..i*4+3]);}
}}
let mixed=&hd[..1];let base_mixed=compositor.compose(&context,mixed,scale,&palette()).image;
let retained_mixed=compositor.compose_retaining_original(&context,mixed,&[detail(0,vec![[31,15],[31,16]])],scale,&palette()).image;
for y in 0..context.height {for x in 32*scale..context.width {
let i=y*context.width+x;assert_eq!(retained_mixed.pixels[i],base_mixed.pixels[i],"foreign Original fallback unchanged");
}}
}
}
}