Support original sprite palettes for faithful HD terrain remasters
This commit is contained in:
1 parent
4ab5f79909
commit
68669fc683
20 files changed
+960
-43
No files matched your search
@@ -1140,3 +1140,40 @@ fn canonical_terrain_candidate_preserves_all_declared_contexts_and_neighbor_pair
|
||||
output.display()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn checked_original_palette_domain_reaches_public_terrain_joins_without_changing_the_png() {
|
||||
use sha2::{Digest,Sha256};
|
||||
use raptor_replica::terrain_hd::PaletteConstraintPlacement;
|
||||
let temp=tempfile::tempdir().unwrap();let data=temp.path().join("catalog");let pack=temp.path().join("hd");
|
||||
std::fs::create_dir_all(data.join("misc")).unwrap();std::fs::create_dir(&pack).unwrap();
|
||||
let source=[1_i32,0,0,4,4].into_iter().flat_map(i32::to_le_bytes)
|
||||
.chain((0..16).map(|i|if i%2==0 {10_u8}else{20_u8})).collect::<Vec<_>>();
|
||||
std::fs::write(data.join("tile"),&source).unwrap();
|
||||
let raw_palette:Vec<u8>=(0..256).flat_map(|i|[i as u8%64;3]).collect();
|
||||
std::fs::write(data.join("misc/PALETTE_DAT"),&raw_palette).unwrap();
|
||||
std::fs::write(data.join("INDEX"),format!("1 768 misc/PALETTE_DAT\n120 {} tile\n",source.len())).unwrap();
|
||||
let raw=pack.join("raw.png");image::RgbaImage::from_pixel(16,16,image::Rgba([220,190,150,255])).save(&raw).unwrap();
|
||||
let raw_sha=Sha256::digest(std::fs::read(&raw).unwrap());
|
||||
let manifest=json!({"format_version":1,"name":"synthetic policy integration","sequences":[
|
||||
{"name":"checked Original","base_asset":0x120,"frame_count":1,"frames":[
|
||||
{"frame":0,"file":"raw.png","logical_width":4,"logical_height":4,"origin":[0,0],
|
||||
"source_sha256":format!("{:x}",Sha256::digest(&source)),"palette_constraint":"original-painted-ordinary-v1"}]}]});
|
||||
std::fs::write(pack.join("hd.json"),serde_json::to_vec(&manifest).unwrap()).unwrap();
|
||||
let catalog=Catalog::open(&data).unwrap();let p=catalog.palette().unwrap();let mut hd=HdAssets::load(&pack,&catalog);
|
||||
assert!(hd.diagnostics.is_empty());
|
||||
for scale in 1..=4 {
|
||||
assert!(hd.terrain_sprite(0x120,scale,&p).is_err(),"a legacy consumer must not silently discard the domain");
|
||||
let (sprite,details,domain)=hd.terrain_sprite_with_palette(0x120,scale,&p).unwrap().unwrap();
|
||||
assert!(details.is_none());let domain=domain.unwrap();assert_eq!(domain.indices(),[10,20]);
|
||||
assert!(sprite.pixels.iter().all(|&i|domain.contains(i)));
|
||||
let context=solid(8*scale,4*scale,10);let neighbor=solid(4*scale,4*scale,100);
|
||||
let tiles=[Replacement {image:sprite,position:[0,0],logical_size:[4,4]},
|
||||
Replacement {image:neighbor,position:[4,0],logical_size:[4,4]}];
|
||||
let result=TerrainHd::default().compose_with_palette_constraints(&context,&tiles,&[],
|
||||
&[PaletteConstraintPlacement {replacement_index:0,palette:domain}],scale,&p);
|
||||
assert_eq!(result.results,vec![OverlayResult::Applied,OverlayResult::Applied]);
|
||||
for y in 0..4*scale {for x in 0..4*scale {assert!([10,20].contains(&result.image.pixels[y*result.image.width+x]));}}
|
||||
}
|
||||
assert_eq!(Sha256::digest(std::fs::read(raw).unwrap()),raw_sha,"quantization must not rewrite the source PNG");
|
||||
}
|
||||
@@ -127,10 +127,31 @@ class RustSourceCompatibility(unittest.TestCase):
|
||||
start=s.index('fn '+name+'(');left=s.index('{',start);depth=1;end=left+1
|
||||
while depth:depth+=(s[end]=='{')-(s[end]=='}');end+=1
|
||||
return s[start:end]
|
||||
for name in ['source_reference_budget','retain_original_detail_samples','sprite_with_policy','terrain_sprite']:
|
||||
# Keep the old fingerprints. Strip only the explicit new opt-in branch;
|
||||
# the None/default sampler and Original-retention kernel must remain exact.
|
||||
for name in ['source_reference_budget','retain_original_detail_samples']:
|
||||
self.assertEqual(expected[name],hashlib.sha256(function(new,name).encode()).hexdigest(),name)
|
||||
sampler = function(new,'sprite_with_policy')
|
||||
opt_in = """PalettePolicy::SourceClasses | PalettePolicy::SourceClassesBaseline =>
|
||||
replacement.original_palette.as_ref().map_or_else(
|
||||
|| closest(palette,rgb,source_label),
|
||||
|domain| domain.closest(palette,rgb.map(i32::from))),"""
|
||||
self.assertEqual(sampler.count(opt_in),1)
|
||||
sampler=sampler.replace(opt_in,'PalettePolicy::SourceClasses | PalettePolicy::SourceClassesBaseline => closest(palette, rgb, source_label),')
|
||||
self.assertEqual(expected['sprite_with_policy'],hashlib.sha256(sampler.encode()).hexdigest())
|
||||
implementation=function(new,'terrain_sprite_impl')
|
||||
signature='fn terrain_sprite(&mut self, id: u32, scale: usize, palette: &[[u8; 3]; 256])\n -> Result<Option<(Sprite, Option<OriginalDetailCells>)>>\n {'
|
||||
normalized=signature+implementation[implementation.index('{')+1:]
|
||||
self.assertEqual(expected['terrain_sprite'],hashlib.sha256(normalized.encode()).hexdigest())
|
||||
wrapper=function(new,'terrain_sprite')
|
||||
self.assertIn('palette_constraint.is_some()',wrapper)
|
||||
self.assertIn('self.terrain_sprite_impl(id,scale,palette)',wrapper)
|
||||
for name,old_bytes,call in [('frame_key','b"raptor-retained-original-details-v2-painted0255"','original_detail_policy_tag(frame.original_detail_cells.len(), false)'),('original_detail_cache_identity','b"retained-original-rejected-fallback-v2-painted0255"','original_detail_policy_tag(frame.original_detail_cells.len(), true)')]:
|
||||
self.assertEqual(expected[name],hashlib.sha256(function(new,name).replace(call,old_bytes).encode()).hexdigest(),name)
|
||||
body=function(new,name)
|
||||
if name=='frame_key':
|
||||
branch=' if frame.palette_constraint.is_some() {\n digest.update(b"raptor-hd-original-painted-ordinary-palette-v1");\n }\n'
|
||||
self.assertEqual(body.count(branch),1);body=body.replace(branch,'')
|
||||
self.assertEqual(expected[name],hashlib.sha256(body.replace(call,old_bytes).encode()).hexdigest(),name)
|
||||
def test_audit_still_calls_actual_payload_validator(self):
|
||||
tree=ast.parse((ROOT/'tools/hd_art_audit.py').read_text())
|
||||
node=next(n for n in tree.body if isinstance(n,ast.FunctionDef) and n.name=='validate_hd_frame')
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
import unittest,sys,struct,copy,importlib.util
|
||||
from pathlib import Path
|
||||
sys.dont_write_bytecode=True
|
||||
sys.path.insert(0,str(Path(__file__).resolve().parents[1]/'tools'))
|
||||
import hd_frame_policy as policy
|
||||
import hd_candidate_review as candidate
|
||||
import hd_register_reviewed as registrar
|
||||
def frame():return dict(frame=0,file='raw.png',logical_width=2,logical_height=2,origin=[0,0],source_sha256='0'*64)
|
||||
def payload(indices):return struct.pack('<iiiii',1,0,0,2,2)+bytes(indices)
|
||||
class PalettePolicyTests(unittest.TestCase):
|
||||
def test_default_dictionary_unchanged(self):
|
||||
f=frame();before=copy.deepcopy(f);policy.validate_frame_policy(f,payload([10,20,10,20]));self.assertEqual(f,before);self.assertNotIn('palette_constraint',f)
|
||||
def test_null_remains_default(self):
|
||||
f=frame();f['palette_constraint']=None;policy.validate_frame_policy(f,payload([240,255,10,20]));self.assertIsNone(f['palette_constraint'])
|
||||
def test_exact_enum_source_is_valid(self):
|
||||
f=frame();f['palette_constraint']='original-painted-ordinary-v1';policy.validate_frame_policy(f,payload([0,20,239,20]))
|
||||
def test_wrong_types_names_and_lists_reject(self):
|
||||
for value in [True,False,0,1,1.0,[],{},'ordinary','original-painted-ordinary-v2']:
|
||||
with self.subTest(value=value),self.assertRaises(ValueError):
|
||||
f=frame();f['palette_constraint']=value;policy.validate_frame_policy(f,payload([10]*4))
|
||||
def test_reserved_240_to255_reject(self):
|
||||
for i in range(240,256):
|
||||
with self.subTest(i=i),self.assertRaises(ValueError):
|
||||
f=frame();f['palette_constraint']='original-painted-ordinary-v1';policy.validate_frame_policy(f,payload([10,20,i,20]))
|
||||
def test_transparent_rle_source_rejects(self):
|
||||
f=frame();f['palette_constraint']='original-painted-ordinary-v1';p=struct.pack('<iiiii',0,0,0,2,2)+struct.pack('<iiii',0,0,-1,0)
|
||||
with self.assertRaises(ValueError):policy.validate_frame_policy(f,p)
|
||||
def test_canvas_mismatch_rejects(self):
|
||||
f=frame();f['palette_constraint']='original-painted-ordinary-v1';p=struct.pack('<iiiii',1,0,0,1,4)+bytes([10]*4)
|
||||
with self.assertRaises(ValueError):policy.validate_frame_policy(f,p)
|
||||
def test_malformed_authentic_source_payloads_reject_before_palette_derivation(self):
|
||||
f=frame();f['palette_constraint']='original-painted-ordinary-v1'
|
||||
invalid=[b'',payload([10,20,10]),struct.pack('<iiiii',2,0,0,2,2)+bytes([10]*4),
|
||||
struct.pack('<iiiii',1,0,0,0,2),
|
||||
struct.pack('<iiiii',0,0,0,2,2)+struct.pack('<iiii',1,0,0,2)+bytes([10,20])]
|
||||
for p in invalid:
|
||||
with self.subTest(payload=p),self.assertRaises(ValueError):policy.validate_frame_policy(f,p)
|
||||
def test_metadata_only_does_not_claim_source_validation(self):
|
||||
f=frame();f['palette_constraint']='original-painted-ordinary-v1';policy.validate_frame_policy(f)
|
||||
def test_unknown_fields_stay_rejected(self):
|
||||
f=frame();f['allowed_palette_colors']=[10]
|
||||
with self.assertRaises(ValueError):policy.validate_frame_policy(f,payload([10]*4))
|
||||
def test_retained_and_nearest_existing_policies_still_validate(self):
|
||||
f=frame();f.update(palette_constraint='original-painted-ordinary-v1',original_detail_cells=[[0,0]],native_nearest_cues=[[1,1]])
|
||||
policy.validate_frame_policy(f,payload([10]*4))
|
||||
|
||||
# The registrar entry point is exercised in memory with synthetic catalog/QA
|
||||
# identities. No PNG decode, process, registrar main or writer is permitted.
|
||||
import json,hashlib,tempfile
|
||||
from unittest import mock
|
||||
import test_hd_retained_original_policy as retained_fixture
|
||||
from PIL import Image
|
||||
|
||||
class PaletteBindingAndRegistrarTests(unittest.TestCase):
|
||||
write_json=staticmethod(retained_fixture.ActualRegistrarProvenance.write_json)
|
||||
def setUp(self):
|
||||
retained_fixture.ActualRegistrarProvenance.setUp(self)
|
||||
raw=payload([10,20,10,20]);self.f=frame();self.f['source_sha256']=hashlib.sha256(raw).hexdigest();self.f['palette_constraint']='original-painted-ordinary-v1'
|
||||
self.candidate['sequences'][0]['frames']=[self.f];self.rows=candidate.proposal_frames(self.candidate);digest=candidate.binding_digest(self.rows)
|
||||
self.review.pop('retained_original_detail_review');self.review['frame_bindings_sha256']=digest
|
||||
self.review['palette_constraint_review']=dict(policy_version='original-painted-ordinary-v1',frame_bindings_sha256=digest,unchanged_generated_raw=True,independent_allscale_and_genuine_context_review=True)
|
||||
self.write_json(self.job_dir/'candidate.json',self.candidate)
|
||||
self.write_json(self.job_dir/'job.json',dict(job_id='synthetic-complete1',targets=[dict(asset='00001',source_sha256=self.f['source_sha256'],logical_canvas=[2,2])]))
|
||||
data=self.project/'data';(data/'misc').mkdir(parents=True);(data/'tile').write_bytes(raw);(data/'INDEX').write_text(f'1 {len(raw)} tile\n');(data/'misc/PALETTE_DAT').write_bytes(bytes(i%64 for i in range(256) for _ in range(3)))
|
||||
def color(i):v=(i%64<<2)|(i%64>>4);return dict(index=i,RGB=[v,v,v])
|
||||
qa=json.loads((self.root/'qa.json').read_text());qa['frame_bindings_sha256']=digest
|
||||
report=dict(asset='00001',source_sha256=self.f['source_sha256'],source_palette='PALETTE_DAT',logical_canvas=[2,2],source_mask=dict(palette_indices_240_to_255_counts=[0]*16),hd_masks=[dict(scale=s,mask=dict(visible_pixels=4*s*s,palette_indices_240_to_255_counts=[0]*16)) for s in range(1,5)],palette_constraint=dict(policy='original-painted-ordinary-v1',applied=True,source_indices=[10,20],source_colors=[color(10),color(20)],scales=[dict(scale=s,visible_pixels=4*s*s,outside_original_palette=0) for s in range(1,5)]))
|
||||
qa['frames']=[report];self.qa=qa;self.write_json(self.root/'qa.json',qa)
|
||||
def approve(self):return retained_fixture.ActualRegistrarProvenance.approve(self)
|
||||
def test_real_approved_job_exact_palette_scope_passes_under_all_writer_process_image_traps(self):
|
||||
_,review,rows=self.approve();self.assertEqual(rows,self.rows);self.assertIs(review['palette_constraint_review']['unchanged_generated_raw'],True)
|
||||
def test_old_raw_review_or_default_sampler_evidence_cannot_approve_the_new_mode(self):
|
||||
original=self.review.pop('palette_constraint_review')
|
||||
with self.assertRaisesRegex(ValueError,'explicit palette'):self.approve()
|
||||
self.review['palette_constraint_review']=original;del self.qa['frames'][0]['palette_constraint'];self.write_json(self.root/'qa.json',self.qa)
|
||||
with self.assertRaisesRegex(ValueError,'exact SourceClasses'):self.approve()
|
||||
def test_each_treatment_scope_boolean_version_and_binding_is_required(self):
|
||||
base=self.review['palette_constraint_review']
|
||||
for key,value in dict(policy_version='other',frame_bindings_sha256='0'*64,unchanged_generated_raw=1,independent_allscale_and_genuine_context_review='yes').items():
|
||||
self.review['palette_constraint_review']=dict(base,**{key:value})
|
||||
with self.subTest(key=key),self.assertRaises(ValueError):self.approve()
|
||||
self.review['palette_constraint_review']=base
|
||||
def test_measured_indices_colors_static_mode_and_typed_scales_cannot_alias(self):
|
||||
original=copy.deepcopy(self.qa)
|
||||
mutations=[lambda e:e.update(source_indices=[True,20]),lambda e:e.update(source_indices=[20,10]),lambda e:e.update(source_colors=[dict(index=10,RGB=[0,0,0]),e['source_colors'][1]]),lambda e:e.update(applied=1),lambda e:e['scales'][0].update(scale=True),lambda e:e['scales'][1].update(visible_pixels=16.0),lambda e:e['scales'][2].update(outside_original_palette=1),lambda e:e.update(scales=[{},None])]
|
||||
for change in mutations:
|
||||
self.qa=copy.deepcopy(original);change(self.qa['frames'][0]['palette_constraint']);self.write_json(self.root/'qa.json',self.qa)
|
||||
with self.subTest(change=change),self.assertRaises(ValueError):self.approve()
|
||||
def test_actual_source_and_rgb_domain_are_checked_from_catalog_not_supplied_qa(self):
|
||||
(self.project/'data/tile').write_bytes(payload([10,240,10,20]))
|
||||
with self.assertRaisesRegex(ValueError,'authentic opaque ordinary'):self.approve()
|
||||
def test_candidate_digest_binds_policy_while_default_dictionary_stays_unchanged(self):
|
||||
plain=copy.deepcopy(self.candidate);plain['sequences'][0]['frames'][0].pop('palette_constraint')
|
||||
self.assertNotEqual(candidate.binding_digest(candidate.proposal_frames(plain)),candidate.binding_digest(self.rows))
|
||||
self.assertNotIn('palette_constraint',candidate.proposal_frames(plain)['00001'])
|
||||
|
||||
if __name__=="__main__":unittest.main(verbosity=2)
|
||||
Reference in new issue
Block a user