diff options
Diffstat (limited to 'crates')
| -rw-r--r-- | crates/fparkan-fx/src/atmosphere.rs | 38 | ||||
| -rw-r--r-- | crates/fparkan-material/src/lib.rs | 57 | ||||
| -rw-r--r-- | crates/fparkan-terrain-format/src/lib.rs | 346 | ||||
| -rw-r--r-- | crates/fparkan-terrain/src/lib.rs | 48 | ||||
| -rw-r--r-- | crates/fparkan-texm/src/lib.rs | 19 |
5 files changed, 489 insertions, 19 deletions
diff --git a/crates/fparkan-fx/src/atmosphere.rs b/crates/fparkan-fx/src/atmosphere.rs index 2681b00..da3a98b 100644 --- a/crates/fparkan-fx/src/atmosphere.rs +++ b/crates/fparkan-fx/src/atmosphere.rs @@ -1139,8 +1139,7 @@ fn sky_sample(key: &InterpolatedKey) -> SkySample { fn sun_sample(key: &InterpolatedKey, birth: Option<SunBirth>) -> SunSample { let source = key.colors[12].rgba(); let intensity = key.parameters[2]; - // Native selector-0 outputs 5/6 are palette[9]/[14] (Terrain.dll RVA 0x3DFB0); - // CSun uses slot 5 alpha for flare opacity (+0x4B3AB) and slot 6 RGB for secondary light (+0x4B4BE). + // CSun's sprite and flare opacity use palette[10]; its secondary light uses palette[14]. SunSample { active: birth.is_some(), rgb: [ @@ -1149,7 +1148,7 @@ fn sun_sample(key: &InterpolatedKey, birth: Option<SunBirth>) -> SunSample { source[2] * intensity, ], values: [key.parameters[0], key.parameters[1]], - packed: [key.colors[9], key.colors[14]], + packed: [key.colors[10], key.colors[14]], intensity, birth, } @@ -1590,8 +1589,8 @@ mod tests { #[test] fn sun_packed_slots_feed_native_flare_alpha_and_secondary_light() { let mut palette = [0xFF00_0000_u32; KEY_COLOR_COUNT]; - palette[9] = 0x4011_2233; - palette[10] = 0xFFAA_BBCC; + palette[9] = 0xFF00_0000; + palette[10] = 0x4011_2233; palette[12] = 0xFFFF_FFFF; palette[14] = 0x8044_5566; let bytes = document(2, |out| { @@ -1608,7 +1607,7 @@ mod tests { let sampled = parsed.sample_day(0, 0.0); assert!(sampled.sun.active); - assert_eq!(sampled.sun.packed[0], PackedColor(palette[9])); + assert_eq!(sampled.sun.packed[0], PackedColor(palette[10])); assert_eq!(sampled.sun.packed[1], PackedColor(palette[14])); let birth = sampled.sun.birth.expect("sun birth"); @@ -1617,7 +1616,7 @@ mod tests { 0.0, crate::sky::SunConfig::from_birth(birth), ); - assert_eq!(sun.sprite_color, PackedColor(palette[9]).rgba()); + assert_eq!(sun.sprite_color, PackedColor(palette[10]).rgba()); assert_eq!(sun.secondary_color, PackedColor(palette[14]).rgba()[..3]); let optics = crate::sky::sun_optics( @@ -1635,6 +1634,31 @@ mod tests { } #[test] + fn sun_sprite_uses_interpolated_palette_ten() { + let mut first = [0xFF00_0000_u32; KEY_COLOR_COUNT]; + first[9] = 0xFF00_0000; + first[10] = 0x32FF_2800; + first[12] = 0xFFFF_FFFF; + let mut second = first; + second[10] = 0x4BFF_4A00; + let bytes = document(2, |out| { + key(out, 0, 0, 0, first, [0.0, 0.0, 1.0, 0.0], &[]); + key(out, 12, 0, 1, second, [0.0, 0.0, 1.0, 0.0], &[]); + }); + let mut parsed = TypedAtmosphere::parse(&bytes).expect("parse"); + for key in &mut parsed.tracks[0].keys { + key.strings[0] = AtmosphereString { + raw: b"sun".to_vec(), + text: "sun".to_owned(), + }; + } + + let sampled = parsed.sample_day(0, 7.2 * 3_600.0); + assert_eq!(sampled.sun.packed[0], PackedColor(0x41FF_3C00)); + assert_ne!(sampled.sun.packed[0], PackedColor(first[9])); + } + + #[test] fn wraps_last_key_to_first_and_emits_events() { let palette = [0xFF00_0000_u32; KEY_COLOR_COUNT]; let bytes = document(2, |out| { diff --git a/crates/fparkan-material/src/lib.rs b/crates/fparkan-material/src/lib.rs index da0b716..082d10c 100644 --- a/crates/fparkan-material/src/lib.rs +++ b/crates/fparkan-material/src/lib.rs @@ -717,6 +717,10 @@ fn interpolate_coefficients( mask: u32, fraction: f32, ) -> MaterialCoefficients { + // World3D 0x3030 receives source and destination wrappers one dword before + // each D3DMATERIAL7 body. Its +4/+14/+24/+34 offsets therefore select + // parameters 4..6, 0..2, 8..10, and 12..14; +0x20 selects parameter 3. + // The other alpha/extra fields, power, and page remain from the left phase. let left = left.coefficients(); let right = right.coefficients(); let mut output = left; @@ -1306,6 +1310,59 @@ mod tests { } #[test] + fn material_animation_interpolation_masks_match_native_rebased_offsets() { + let left = phase_with_parameters( + [ + 10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120, 130, 140, 150, 160, 170, 180, + ], + b"LEFT", + ); + let right = phase_with_parameters( + [ + 70, 80, 90, 100, 110, 120, 130, 140, 150, 160, 170, 180, 190, 200, 210, 220, 230, + 240, + ], + b"RIGHT", + ); + let rgb = |first: u8| [first, first + 10, first + 20].map(|value| f32::from(value) / 255.0); + let cases = [ + (0, rgb(10), rgb(50), rgb(90), rgb(130), 0.4), + (1, rgb(40), rgb(50), rgb(90), rgb(130), 0.4), + (2, rgb(10), rgb(80), rgb(90), rgb(130), 0.4), + (4, rgb(10), rgb(50), rgb(120), rgb(130), 0.4), + (8, rgb(10), rgb(50), rgb(90), rgb(160), 0.4), + (16, rgb(10), rgb(50), rgb(90), rgb(130), 0.7), + (0x1f, rgb(40), rgb(80), rgb(120), rgb(160), 0.7), + ]; + + for (mask, additive, directional, specular, extra, opacity) in cases { + let output = interpolate_coefficients(&left, &right, mask, 0.5); + for (actual, expected) in [ + (output.additive_rgb, additive), + (output.directional_rgb, directional), + (output.specular_rgb, specular), + (output.extra_rgb, extra), + ] { + for channel in 0..3 { + assert!( + (actual[channel] - expected[channel]).abs() < 1e-6, + "mask {mask:#x}, channel {channel}: got {}, expected {}", + actual[channel], + expected[channel] + ); + } + } + + assert!((output.opacity - opacity).abs() < 1e-6, "mask {mask:#x}"); + assert_eq!(output.directional_extra, 80.0 / 255.0); + assert_eq!(output.specular_extra, 120.0 / 255.0); + assert_eq!(output.extra_alpha, 160.0 / 255.0); + assert_eq!(output.power, 170); + assert_eq!(output.page_index, -76); + } + } + + #[test] fn material_ping_pong_reverses_local_time_and_clamp_holds_last_phase() { let phases = vec![ phase_with_parameters([0; 18], b"A"), diff --git a/crates/fparkan-terrain-format/src/lib.rs b/crates/fparkan-terrain-format/src/lib.rs index 639fc57..88bd325 100644 --- a/crates/fparkan-terrain-format/src/lib.rs +++ b/crates/fparkan-terrain-format/src/lib.rs @@ -28,6 +28,7 @@ const REQUIRED_TYPES: [u32; 9] = [ const AREAL_PREFIX_SIZE: usize = 56; const SLOT_HEADER_SIZE: usize = 0x8c; const SLOT_STRIDE: usize = 68; +const NODE38_STRIDE: usize = 38; const GRID_HIT_COUNT_BITS: u32 = 10; const GRID_POOL_OFFSET_MASK: u32 = (1 << 22) - 1; @@ -152,6 +153,129 @@ pub struct TerrainFace28 { pub raw: [u8; 28], } +/// Terrain detail level selected by one Node38 slot reference. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum TerrainLod { + /// Near terrain geometry. + Lod0, + /// Far terrain geometry. + Lod1, +} + +/// Node and detail level that own one source face. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct TerrainFaceLod { + /// Index into [`LandMeshDocument::faces`]. + pub face_index: usize, + /// Index into the Node38 stream. + pub node_index: usize, + /// Selected terrain detail level. + pub lod: TerrainLod, +} + +/// Error in the terrain Node38-to-face slot mapping. +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum TerrainLodError { + /// Nonempty terrain has no Node38 records to select its slots. + MissingNodes { face_count: usize }, + /// Node stream size is not a whole number of 38-byte records. + InvalidNodeStreamSize { size: usize }, + /// Terrain uses a slot group beyond its two supported detail levels. + UnsupportedNodeSlot { + /// Node containing the unsupported slot. + node: usize, + /// Index in the 15-entry Node38 slot map. + slot_index: usize, + /// Raw terrain slot id. + slot: u16, + }, + /// A Node38 entry references a missing Slot68 record. + SlotOutOfRange { + /// Node containing the reference. + node: usize, + /// Selected detail level. + lod: TerrainLod, + /// Raw terrain slot id. + slot: u16, + /// Number of decoded Slot68 records. + slot_count: usize, + }, + /// A Slot68 triangle range exceeds the source face array. + FaceRangeOutOfBounds { + /// Node containing the slot. + node: usize, + /// Selected detail level. + lod: TerrainLod, + /// Raw terrain slot id. + slot: u16, + /// First source face. + start: usize, + /// Number of faces. + count: usize, + /// Number of source faces. + face_count: usize, + }, + /// A source face belongs to more than one terrain slot. + DuplicateFaceAssignment { face: usize }, + /// A source face is absent from every supported terrain slot. + MissingFaceAssignment { face: usize }, + /// A checked index calculation overflowed. + IntegerOverflow, +} + +impl std::fmt::Display for TerrainLodError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::MissingNodes { face_count } => write!( + f, + "terrain has {face_count} faces but no Node38 records" + ), + Self::InvalidNodeStreamSize { size } => write!( + f, + "terrain Node38 stream has {size} bytes, not a multiple of {NODE38_STRIDE}" + ), + Self::UnsupportedNodeSlot { + node, + slot_index, + slot, + } => write!( + f, + "terrain Node38 {node} uses unsupported slot group {slot_index} ({slot})" + ), + Self::SlotOutOfRange { + node, + lod, + slot, + slot_count, + } => write!( + f, + "terrain Node38 {node} LOD {lod:?} references slot {slot} outside {slot_count} slots" + ), + Self::FaceRangeOutOfBounds { + node, + lod, + slot, + start, + count, + face_count, + } => write!( + f, + "terrain Node38 {node} LOD {lod:?} slot {slot} range {start}..{} exceeds {face_count} faces", + start.saturating_add(*count) + ), + Self::DuplicateFaceAssignment { face } => { + write!(f, "terrain face {face} belongs to more than one slot") + } + Self::MissingFaceAssignment { face } => { + write!(f, "terrain face {face} is not assigned to a supported slot") + } + Self::IntegerOverflow => write!(f, "terrain slot index overflow"), + } + } +} + +impl std::error::Error for TerrainLodError {} + impl TerrainFace28 { /// Decodes the proven two-table selector layout of [`Self::material_tag`]. #[must_use] @@ -273,6 +397,106 @@ pub struct LandMeshDocument { } impl LandMeshDocument { + /// Resolves each source face to the Node38 and terrain LOD that own it. + /// + /// The supported terrain layout uses slot-map entries 0 and 1 for LOD0 + /// and LOD1. The other 13 entries must be empty. Every source face must be + /// covered exactly once; empty terrain may omit Node38 records. + /// + /// # Errors + /// + /// Returns [`TerrainLodError`] for missing or unsupported slots, invalid + /// ranges, overlaps, or uncovered faces. + pub fn face_lod_assignments(&self) -> Result<Vec<TerrainFaceLod>, TerrainLodError> { + if self.faces.is_empty() && self.nodes_raw.is_empty() { + return Ok(Vec::new()); + } + if !self.nodes_raw.len().is_multiple_of(NODE38_STRIDE) { + return Err(TerrainLodError::InvalidNodeStreamSize { + size: self.nodes_raw.len(), + }); + } + if self.nodes_raw.is_empty() { + return Err(TerrainLodError::MissingNodes { + face_count: self.faces.len(), + }); + } + + let mut assignments = vec![None; self.faces.len()]; + for (node_index, node) in self.nodes_raw.chunks_exact(NODE38_STRIDE).enumerate() { + for slot_index in 2..15 { + let offset = 8 + slot_index * 2; + let slot = u16::from_le_bytes([node[offset], node[offset + 1]]); + if slot != u16::MAX { + return Err(TerrainLodError::UnsupportedNodeSlot { + node: node_index, + slot_index, + slot, + }); + } + } + + for (lod_index, lod) in [TerrainLod::Lod0, TerrainLod::Lod1].into_iter().enumerate() { + let offset = 8 + lod_index * 2; + let slot = u16::from_le_bytes([node[offset], node[offset + 1]]); + if slot == u16::MAX { + continue; + } + let slot_index = usize::from(slot); + let raw = self.slots.slots_raw.get(slot_index).ok_or( + TerrainLodError::SlotOutOfRange { + node: node_index, + lod, + slot, + slot_count: self.slots.slots_raw.len(), + }, + )?; + let start = usize::from(u16::from_le_bytes([raw[0], raw[1]])); + let count = usize::from(u16::from_le_bytes([raw[2], raw[3]])); + let end = start + .checked_add(count) + .ok_or(TerrainLodError::IntegerOverflow)?; + if end > self.faces.len() { + return Err(TerrainLodError::FaceRangeOutOfBounds { + node: node_index, + lod, + slot, + start, + count, + face_count: self.faces.len(), + }); + } + for face_index in start..end { + let assignment = assignments.get_mut(face_index).ok_or( + TerrainLodError::FaceRangeOutOfBounds { + node: node_index, + lod, + slot, + start, + count, + face_count: self.faces.len(), + }, + )?; + if assignment.is_some() { + return Err(TerrainLodError::DuplicateFaceAssignment { face: face_index }); + } + *assignment = Some(TerrainFaceLod { + face_index, + node_index, + lod, + }); + } + } + } + assignments + .into_iter() + .enumerate() + .map(|(face_index, assignment)| { + assignment.ok_or(TerrainLodError::MissingFaceAssignment { face: face_index }) + }) + .collect() + } + /// Resolves the type-11 material pairs addressed by one 68-byte slot. /// /// Returns `None` when the slot is absent or its pair range does not fit @@ -1564,6 +1788,80 @@ mod tests { } #[test] + fn terrain_face_lod_assignments_keep_interleaved_source_order_and_reject_missing_nodes() { + let mut nodes_raw = vec![u8::MAX; 2 * NODE38_STRIDE]; + for (node_index, [lod0_slot, lod1_slot]) in [[0_u16, 1_u16], [2, 3]].into_iter().enumerate() + { + let offset = node_index * NODE38_STRIDE + 8; + nodes_raw[offset..offset + 2].copy_from_slice(&lod0_slot.to_le_bytes()); + nodes_raw[offset + 2..offset + 4].copy_from_slice(&lod1_slot.to_le_bytes()); + } + let mut slots_raw = vec![[0_u8; SLOT_STRIDE]; 4]; + for (raw, start) in slots_raw.iter_mut().zip([0_u16, 1, 2, 3]) { + raw[..2].copy_from_slice(&start.to_le_bytes()); + raw[2..4].copy_from_slice(&1_u16.to_le_bytes()); + } + let mesh = LandMeshDocument { + streams: Vec::new(), + nodes_raw, + slots: TerrainSlotTable { + header_raw: Vec::new(), + slots_raw, + }, + positions: Vec::new(), + normals: Vec::new(), + uv0: Vec::new(), + accelerator: Vec::new(), + aux14: Vec::new(), + aux18: Vec::new(), + faces: (0..4).map(|_| terrain_face([0, 1, 2])).collect(), + }; + + let assignments = mesh.face_lod_assignments().expect("complete slot mapping"); + assert_eq!( + assignments, + vec![ + TerrainFaceLod { + face_index: 0, + node_index: 0, + lod: TerrainLod::Lod0, + }, + TerrainFaceLod { + face_index: 1, + node_index: 0, + lod: TerrainLod::Lod1, + }, + TerrainFaceLod { + face_index: 2, + node_index: 1, + lod: TerrainLod::Lod0, + }, + TerrainFaceLod { + face_index: 3, + node_index: 1, + lod: TerrainLod::Lod1, + }, + ] + ); + + let mut missing_nodes = mesh.clone(); + missing_nodes.nodes_raw.clear(); + assert_eq!( + missing_nodes.face_lod_assignments(), + Err(TerrainLodError::MissingNodes { face_count: 4 }) + ); + missing_nodes.faces.clear(); + assert_eq!(missing_nodes.face_lod_assignments(), Ok(Vec::new())); + + let mut overlapping = mesh; + overlapping.slots.slots_raw[3][..2].copy_from_slice(&2_u16.to_le_bytes()); + assert_eq!( + overlapping.face_lod_assignments(), + Err(TerrainLodError::DuplicateFaceAssignment { face: 2 }) + ); + } + + #[test] fn slot_material_pairs_use_type11_pair_table_and_preserve_flags() { let nres = decode_nres(&minimal_land_msh(&face([0, 1, 2], [None, None, None]))).expect("nres"); @@ -1836,6 +2134,42 @@ Generator 1 .unwrap_or_else(|err| panic!("{corpus} {path:?}: {err}")); let document = decode_land_msh(&nres).unwrap_or_else(|err| panic!("{corpus} {path:?}: {err}")); + let assignments = document + .face_lod_assignments() + .unwrap_or_else(|err| panic!("{corpus} {path:?}: {err}")); + assert_eq!(assignments.len(), document.faces.len(), "{corpus} {path:?}"); + for (face_index, assignment) in assignments.iter().enumerate() { + assert_eq!(assignment.face_index, face_index, "{corpus} {path:?}"); + } + let lod0_faces = assignments + .iter() + .filter(|assignment| assignment.lod == TerrainLod::Lod0) + .count(); + let lod1_faces = assignments + .iter() + .filter(|assignment| assignment.lod == TerrainLod::Lod1) + .count(); + assert_eq!( + lod0_faces + lod1_faces, + document.faces.len(), + "{corpus} {path:?}" + ); + if corpus == "IS" { + let map_name = path + .parent() + .and_then(Path::file_name) + .and_then(|name| name.to_str()) + .map(str::to_ascii_lowercase); + let expected_lod0 = match map_name.as_deref() { + Some("automap") => Some(1_912), + Some("prol") => Some(5_868), + Some("tut_1") => Some(5_046), + _ => None, + }; + if let Some(expected) = expected_lod0 { + assert_eq!(lod0_faces, expected, "{corpus} {path:?} LOD0 face count"); + } + } files += 1; vertices += document.positions.len(); faces += document.faces.len(); @@ -2127,6 +2461,18 @@ Generator 1 out } + fn terrain_face(vertices: [u16; 3]) -> TerrainFace28 { + TerrainFace28 { + flags: FullSurfaceMask(0), + material_tag: 0, + aux_tag: 0, + vertices, + neighbors: [None; 3], + tail_raw: *b"TAILFACE", + raw: face(vertices, [None; 3]), + } + } + fn entry(type_id: u32, attr1: u32, attr3: u32, payload: &[u8]) -> TestEntry<'_> { TestEntry { type_id, diff --git a/crates/fparkan-terrain/src/lib.rs b/crates/fparkan-terrain/src/lib.rs index cd2a874..a014b61 100644 --- a/crates/fparkan-terrain/src/lib.rs +++ b/crates/fparkan-terrain/src/lib.rs @@ -9,7 +9,7 @@ use std::collections::VecDeque; /// Applications access this semantic terrain API through this terrain crate /// rather than taking a direct dependency on the binary-format parser. pub use fparkan_terrain_format::{ - FullSurfaceMask, TerrainMaterialLayers, TerrainMaterialSelection, + FullSurfaceMask, TerrainLod, TerrainLodError, TerrainMaterialLayers, TerrainMaterialSelection, }; /// Terrain world. @@ -91,6 +91,8 @@ pub enum TerrainError { /// Position count. position_count: usize, }, + /// Terrain Node38/Slot68 records do not assign every face to one LOD. + InvalidLodMapping(TerrainLodError), } impl std::fmt::Display for TerrainError { @@ -117,11 +119,23 @@ impl std::fmt::Display for TerrainError { f, "terrain face {face} references vertex {vertex} outside {position_count} positions" ), + Self::InvalidLodMapping(error) => write!(f, "invalid terrain LOD mapping: {error}"), } } } -impl std::error::Error for TerrainError {} +impl std::error::Error for TerrainError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Self::InvalidLodMapping(error) => Some(error), + Self::Unsupported + | Self::TooManyAreals { .. } + | Self::InvalidGridReference { .. } + | Self::InvalidArealReference { .. } + | Self::InvalidSurfaceVertex { .. } => None, + } + } +} /// Surface query. pub trait SurfaceQuery { @@ -247,8 +261,8 @@ impl TerrainWorld { /// /// # Errors /// - /// Returns [`TerrainError`] if a face cannot be represented by runtime - /// indexes. + /// Returns [`TerrainError`] if terrain LOD metadata or a face cannot be + /// represented by runtime indexes. pub fn from_land_msh(mesh: &LandMeshDocument) -> Result<Self, TerrainError> { Ok(Self { surfaces: build_surfaces(mesh)?, @@ -878,8 +892,19 @@ impl RuntimeGrid { } fn build_surfaces(mesh: &LandMeshDocument) -> Result<Vec<RuntimeTriangle>, TerrainError> { - let mut triangles = Vec::with_capacity(mesh.faces.len()); - for (face_index, face) in mesh.faces.iter().enumerate() { + let assignments = mesh + .face_lod_assignments() + .map_err(TerrainError::InvalidLodMapping)?; + let lod0_count = assignments + .iter() + .filter(|assignment| assignment.lod == TerrainLod::Lod0) + .count(); + let mut triangles = Vec::with_capacity(lod0_count); + for (face, assignment) in mesh.faces.iter().zip(assignments.iter()) { + if assignment.lod != TerrainLod::Lod0 { + continue; + } + let face_index = assignment.face_index; let vertices = [ surface_vertex(mesh, face_index, face.vertices[0])?, surface_vertex(mesh, face_index, face.vertices[1])?, @@ -1243,7 +1268,7 @@ mod tests { let face0 = terrain_face(FullSurfaceMask(0x0000_0001), [0, 1, 2]); let face1 = terrain_face(FullSurfaceMask(0x0000_0002), [1, 3, 2]); - LandMeshDocument { + let mut mesh = LandMeshDocument { streams: Vec::<TerrainStream>::new(), nodes_raw: Vec::new(), slots: TerrainSlotTable { @@ -1262,7 +1287,14 @@ mod tests { aux14: Vec::new(), aux18: Vec::new(), faces: vec![face0, face1], - } + }; + let mut node = vec![u8::MAX; 38]; + node[8..10].copy_from_slice(&0_u16.to_le_bytes()); + let mut slot = [0_u8; 68]; + slot[2..4].copy_from_slice(&(mesh.faces.len() as u16).to_le_bytes()); + mesh.nodes_raw = node; + mesh.slots.slots_raw.push(slot); + mesh } fn terrain_face( diff --git a/crates/fparkan-texm/src/lib.rs b/crates/fparkan-texm/src/lib.rs index 3578dd0..4b7ffaa 100644 --- a/crates/fparkan-texm/src/lib.rs +++ b/crates/fparkan-texm/src/lib.rs @@ -665,10 +665,10 @@ fn decode_mip_rgba8_internal( continue; } let out = index.saturating_mul(4); - rgba[out] = palette[palette_offset]; + rgba[out] = palette[palette_offset + 2]; rgba[out + 1] = palette[palette_offset + 1]; - rgba[out + 2] = palette[palette_offset + 2]; - rgba[out + 3] = palette[palette_offset + 3]; + rgba[out + 2] = palette[palette_offset]; + rgba[out + 3] = 255; } } DiskPixelFormat::Rgb565 => decode_words(level_data, pixel_count, &mut rgba, decode_rgb565), @@ -996,6 +996,17 @@ mod tests { } #[test] + fn indexed_palette_decodes_bgrx_as_opaque_rgba() { + let document = + decode_texm(Arc::from(indexed_payload().into_boxed_slice())).expect("indexed document"); + + assert_eq!( + decode_mip_rgba8(&document, 0).expect("rgba").rgba8, + [30, 20, 10, 255] + ); + } + + #[test] fn channel_expansion_boundary_values_are_stable() { let document = decode_texm(Arc::from( payload(2, 1, 565, &[&[0x00, 0x00, 0xFF, 0xFF]]).into_boxed_slice(), @@ -1155,7 +1166,7 @@ mod tests { fn indexed_payload() -> Vec<u8> { let mut palette = [0_u8; 1024]; - palette[4..8].copy_from_slice(&[10, 20, 30, 255]); + palette[4..8].copy_from_slice(&[10, 20, 30, 0]); let mut out = header(1, 1, 1, 0); out.extend_from_slice(&palette); out.push(1); |
