Editor for papermario-dx mods
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

add render modes, geometry modes, and depth sorting to map rendering

- Project crate: add GeometryMode and RenderMode enums, texture
coords (u/v) on Vertex
- Star Rod interop: parse render mode (property 0x5C) and geometry
mode flags from map XML
- GBI: per-node geometry mode and other_mode_low, Z buffer clearing
- Map editor: sort nodes by PM64's RenderTaskBasePriorities table,
using three-list bucketing (NEAR/MID/FAR) with per-bucket sort
direction for correct opaque and translucent draw order

+1153 -164
+1
Cargo.lock
··· 3919 3919 name = "star_rod_interop" 3920 3920 version = "0.1.0" 3921 3921 dependencies = [ 3922 + "log", 3922 3923 "loro", 3923 3924 "loroscope", 3924 3925 "project",
+1
Cargo.toml
··· 24 24 raw-window-handle = "0.6" 25 25 roxmltree = "0.21" 26 26 rfd = "0.15" 27 + log = "0.4" 27 28 thiserror = "2" 28 29 anyhow = "1" 29 30
+2
crates/kammy/src/app.rs
··· 495 495 v.set_x(x); 496 496 v.set_y(y); 497 497 v.set_z(z); 498 + v.set_u(0.0); 499 + v.set_v(0.0); 498 500 let c = v.color(); 499 501 c.set_r(r); 500 502 c.set_g(g);
+356 -13
crates/kammy/src/editor/map.rs
··· 10 10 11 11 pub mod camera; 12 12 13 - use pm64::gbi::{NodeData, TriangleData, VertexData}; 13 + use pm64::gbi::{self, NodeData, TriangleData, VertexData}; 14 14 use project::{ModelNode, Project}; 15 15 16 16 use super::{Editor, EditorContext, EditorId}; ··· 69 69 ); 70 70 self.camera.handle_input(&interact_response); 71 71 72 - let nodes = extract_nodes(ctx.project, &self.scene_key); 72 + let mut nodes = extract_nodes(ctx.project, &self.scene_key); 73 + sort_nodes_by_render_priority(&mut nodes, self.camera.eye_position()); 74 + let nodes: Vec<NodeData> = nodes.into_iter().map(|(_, n)| n).collect(); 73 75 #[expect( 74 76 clippy::cast_possible_truncation, 75 77 clippy::as_conversions, ··· 92 94 } 93 95 } 94 96 95 - /// Extracts all model nodes from the given scene's model tree into plain render data. 96 - fn extract_nodes(project: &Project, scene_key: &str) -> Vec<NodeData> { 97 + /// Extracts all model nodes from the given scene's model tree into plain render data, 98 + /// paired with their sort priority from PM64's `RenderTaskBasePriorities[]` table. 99 + fn extract_nodes(project: &Project, scene_key: &str) -> Vec<(i32, NodeData)> { 97 100 let Some(scene) = project.scenes().get(scene_key) else { 98 101 return Vec::new(); 99 102 }; ··· 106 109 } 107 110 108 111 /// Recursively collects a node and its children into the output list. 109 - fn collect_node(tree: &loroscope::Tree<ModelNode>, id: loro::TreeID, out: &mut Vec<NodeData>) { 112 + fn collect_node( 113 + tree: &loroscope::Tree<ModelNode>, 114 + id: loro::TreeID, 115 + out: &mut Vec<(i32, NodeData)>, 116 + ) { 110 117 let Some(node) = tree.get(id) else { return }; 111 118 112 119 let tri_list = node.triangles(); ··· 119 126 v2: extract_vertex(&tri.v2()), 120 127 }); 121 128 } 122 - out.push(NodeData { triangles }); 129 + let geometry_mode = match node.geometry_mode() { 130 + project::GeometryMode::VertexColor => gbi::GeometryMode::VertexColor, 131 + project::GeometryMode::VertexColorCullBack => gbi::GeometryMode::VertexColorCullBack, 132 + project::GeometryMode::Lit => gbi::GeometryMode::Lit, 133 + project::GeometryMode::LitCullBack => gbi::GeometryMode::LitCullBack, 134 + }; 135 + let render_mode = node.render_mode(); 136 + out.push(( 137 + render_mode_to_sort_priority(&render_mode), 138 + NodeData { 139 + triangles, 140 + geometry_mode, 141 + other_mode_low: render_mode_to_other_mode_low(&render_mode), 142 + }, 143 + )); 123 144 124 145 if let Some(children) = tree.children(id) { 125 146 for child_id in children { ··· 128 149 } 129 150 } 130 151 131 - /// Converts a CRDT vertex accessor into a plain `VertexData`. 152 + /// Returns the RDP `othermode_low` bits for a project render mode preset. 153 + /// 154 + /// Values derived from PM64's `ModelRenderModes[]` table in model.c, using 155 + /// the `RENDER_CLASS_1CYC` display lists. Each value is the full 156 + /// `G_RM_*1 | G_RM_*2` that `gsDPSetRenderMode` would encode. 157 + /// 158 + /// Modes not handled by the 1CYC switch in the game (SHADOW, `PASS_THROUGH`, 159 + /// BEHIND variants, etc.) use their closest equivalent rather than the 160 + /// game's default fallback to `SURFACE_OPA`. 161 + fn render_mode_to_other_mode_low(mode: &project::RenderMode) -> u32 { 162 + match mode { 163 + // G_RM_AA_ZB_OPA_SURF 164 + project::RenderMode::SolidAaZbLayer0 165 + | project::RenderMode::SurfaceOpa 166 + | project::RenderMode::PassThrough => 0x0055_2078, 167 + // G_RM_ZB_OPA_SURF 168 + project::RenderMode::SurfaceOpaNoAa => 0x0055_2230, 169 + // G_RM_AA_OPA_SURF 170 + project::RenderMode::SurfaceOpaNoZb | project::RenderMode::SurfaceOpaNoZbBehind => { 171 + 0x0055_2048 172 + } 173 + // G_RM_AA_ZB_OPA_DECAL 174 + project::RenderMode::DecalOpa => 0x0055_2D58, 175 + // G_RM_ZB_OPA_DECAL 176 + project::RenderMode::DecalOpaNoAa => 0x0055_2E10, 177 + // G_RM_AA_ZB_OPA_INTER 178 + project::RenderMode::IntersectingOpa => 0x0055_2478, 179 + // G_RM_AA_ZB_TEX_EDGE 180 + project::RenderMode::Alphatest | project::RenderMode::AlphatestOnesided => 0x0055_3078, 181 + // G_RM_AA_TEX_EDGE 182 + project::RenderMode::AlphatestNoZb | project::RenderMode::AlphatestNoZbBehind => { 183 + 0x0055_3048 184 + } 185 + // G_RM_AA_ZB_XLU_SURF 186 + project::RenderMode::SurfaceXluLayer1 187 + | project::RenderMode::SurfaceXluLayer2 188 + | project::RenderMode::SurfaceXluLayer3 189 + | project::RenderMode::Shadow => 0x0050_49D8, 190 + // G_RM_ZB_XLU_SURF 191 + project::RenderMode::SurfaceXluNoAa => 0x0050_4A50, 192 + // G_RM_AA_XLU_SURF 193 + project::RenderMode::SurfaceXluNoZb | project::RenderMode::SurfaceXluNoZbBehind => { 194 + 0x0050_41C8 195 + } 196 + // Custom: AA_EN | Z_CMP | Z_UPD | IM_RD | CLR_ON_CVG | CVG_DST_WRAP | ZMODE_XLU | FORCE_BL 197 + project::RenderMode::SurfaceXluZbZupd | project::RenderMode::SurfaceXluAaZbZupd => { 198 + 0x0050_49F8 199 + } 200 + // G_RM_AA_ZB_XLU_DECAL 201 + project::RenderMode::DecalXlu | project::RenderMode::DecalXluAhead => 0x0050_4DD8, 202 + // G_RM_ZB_OVL_SURF 203 + project::RenderMode::DecalXluNoAa => 0x0050_4F50, 204 + // G_RM_AA_ZB_XLU_INTER 205 + project::RenderMode::IntersectingXlu => 0x0050_45D8, 206 + // Custom: IM_RD | CVG_DST_SAVE | ZMODE_XLU | FORCE_BL (cloud without Z compare) 207 + project::RenderMode::CloudNoZcmp => 0x0050_4B40, 208 + // G_RM_ZB_CLD_SURF 209 + project::RenderMode::Cloud => 0x0050_4B50, 210 + // G_RM_CLD_SURF 211 + project::RenderMode::CloudNoZb => 0x0050_4340, 212 + } 213 + } 214 + 132 215 #[expect( 133 216 clippy::cast_possible_truncation, 134 - clippy::cast_sign_loss, 135 217 clippy::as_conversions, 136 - reason = "vertex coords are small integers that fit in i16/u8" 218 + reason = "vertex coords are small integers that fit in i16" 137 219 )] 138 220 fn extract_vertex(v: &project::Vertex) -> VertexData { 139 221 let c = v.color(); ··· 141 223 x: v.x() as i16, 142 224 y: v.y() as i16, 143 225 z: v.z() as i16, 144 - r: c.r() as u8, 145 - g: c.g() as u8, 146 - b: c.b() as u8, 147 - a: c.a() as u8, 226 + tc_s: v.u() as i16, 227 + tc_t: v.v() as i16, 228 + r: clamp_color_channel(c.r()), 229 + g: clamp_color_channel(c.g()), 230 + b: clamp_color_channel(c.b()), 231 + a: clamp_color_channel(c.a()), 232 + } 233 + } 234 + 235 + #[expect( 236 + clippy::cast_sign_loss, 237 + clippy::as_conversions, 238 + reason = "value is clamped to 0..=255" 239 + )] 240 + fn clamp_color_channel(value: i64) -> u8 { 241 + if !(0..=255).contains(&value) { 242 + tracing::warn!(value, "color channel outside 0..=255, clamping"); 243 + } 244 + value.clamp(0, 255) as u8 245 + } 246 + 247 + /// Render-mode sort priority from PM64's `RenderTaskBasePriorities[]` table (model.c). 248 + /// 249 + /// Each render mode maps to a base priority. The effective sort key is 250 + /// `base_priority + camera_distance`, which determines both the rendering 251 + /// pass (NEAR / MID / FAR) and within-pass ordering. 252 + fn render_mode_to_sort_priority(mode: &project::RenderMode) -> i32 { 253 + match mode { 254 + project::RenderMode::SolidAaZbLayer0 => -100_000, 255 + project::RenderMode::SurfaceOpaNoZb 256 + | project::RenderMode::AlphatestNoZb 257 + | project::RenderMode::SurfaceXluNoZb => 0, 258 + project::RenderMode::CloudNoZb => 700_000, 259 + project::RenderMode::SurfaceOpa 260 + | project::RenderMode::SurfaceOpaNoAa 261 + | project::RenderMode::DecalOpa 262 + | project::RenderMode::DecalOpaNoAa 263 + | project::RenderMode::IntersectingOpa 264 + | project::RenderMode::Alphatest 265 + | project::RenderMode::AlphatestOnesided => 1_000_000, 266 + project::RenderMode::SurfaceOpaNoZbBehind => 4_000_000, 267 + project::RenderMode::AlphatestNoZbBehind => 4_250_000, 268 + project::RenderMode::SurfaceXluNoZbBehind | project::RenderMode::CloudNoZcmp => 4_500_000, 269 + project::RenderMode::IntersectingXlu | project::RenderMode::PassThrough => 5_500_000, 270 + project::RenderMode::SurfaceXluLayer3 => 6_000_000, 271 + project::RenderMode::DecalXluAhead | project::RenderMode::Shadow => 6_500_000, 272 + project::RenderMode::DecalXlu | project::RenderMode::DecalXluNoAa => 7_000_000, 273 + project::RenderMode::SurfaceXluLayer2 => 7_500_000, 274 + project::RenderMode::SurfaceXluLayer1 275 + | project::RenderMode::SurfaceXluNoAa 276 + | project::RenderMode::SurfaceXluZbZupd 277 + | project::RenderMode::SurfaceXluAaZbZupd 278 + | project::RenderMode::Cloud => 8_000_000, 279 + } 280 + } 281 + 282 + /// Sorts nodes by PM64's render task priority system. 283 + /// 284 + /// Mirrors `queue_render_task()` and `execute_render_tasks()` in model.c. 285 + /// Each node's effective sort key is `sort_priority + camera_distance`. 286 + /// Nodes are bucketed into three render passes based on this key: 287 + /// 288 + /// - **NEAR** (key < 800K): background layers, no-ZB modes. Sorted descending. 289 + /// - **MID** (800K–3M): standard opaque geometry. Sorted ascending (front-to-back). 290 + /// - **FAR** (≥ 3M): translucent layers, shadows, behind-geometry. Sorted descending 291 + /// (back-to-front). 292 + /// 293 + /// Rendered in order: NEAR, MID, FAR. 294 + fn sort_nodes_by_render_priority(nodes: &mut [(i32, NodeData)], eye: [f32; 3]) { 295 + nodes.sort_by(|(pri_a, node_a), (pri_b, node_b)| { 296 + let dist_a = node_camera_dist(node_a, eye); 297 + let dist_b = node_camera_dist(node_b, eye); 298 + let eff_a = f64::from(*pri_a) + f64::from(dist_a); 299 + let eff_b = f64::from(*pri_b) + f64::from(dist_b); 300 + let (bucket_a, val_a) = sort_key(eff_a); 301 + let (bucket_b, val_b) = sort_key(eff_b); 302 + bucket_a 303 + .cmp(&bucket_b) 304 + .then_with(|| val_a.total_cmp(&val_b)) 305 + }); 306 + } 307 + 308 + /// Maps an effective distance to a sort key that replicates PM64's three-list 309 + /// sort order within a single comparison. 310 + /// 311 + /// - NEAR (< 800K): bucket 0, descending → negate within bucket 312 + /// - MID (800K–3M): bucket 1, ascending → use as-is 313 + /// - FAR (≥ 3M): bucket 2, descending → negate within bucket 314 + fn sort_key(effective_dist: f64) -> (u8, f64) { 315 + if effective_dist < 800_000.0 { 316 + (0, -effective_dist) 317 + } else if effective_dist < 3_000_000.0 { 318 + (1, effective_dist) 319 + } else { 320 + (2, -effective_dist) 148 321 } 149 322 } 150 323 324 + /// Euclidean distance from `eye` to the centroid of a node's vertices. 325 + fn node_camera_dist(node: &NodeData, eye: [f32; 3]) -> f32 { 326 + let c = node_centroid(node); 327 + let dx = c[0] - eye[0]; 328 + let dy = c[1] - eye[1]; 329 + let dz = c[2] - eye[2]; 330 + (dx * dx + dy * dy + dz * dz).sqrt() 331 + } 332 + 333 + /// Computes the average position of all vertices in a node. 334 + fn node_centroid(node: &NodeData) -> [f32; 3] { 335 + let mut sum = [0.0f32; 3]; 336 + let mut count = 0.0f32; 337 + for tri in &node.triangles { 338 + for v in [&tri.v0, &tri.v1, &tri.v2] { 339 + sum[0] += f32::from(v.x); 340 + sum[1] += f32::from(v.y); 341 + sum[2] += f32::from(v.z); 342 + count += 1.0; 343 + } 344 + } 345 + if count == 0.0 { 346 + return [0.0; 3]; 347 + } 348 + [sum[0] / count, sum[1] / count, sum[2] / count] 349 + } 350 + 151 351 /// Paper Mario NTSC VI configuration (osViModeNtscLan1 + osViSetSpecialFeatures). 152 352 /// 153 353 /// Matches the game's 320x240 16-bit non-interlaced mode with gamma off, ··· 169 369 y_scale: 0x400, 170 370 } 171 371 } 372 + 373 + #[cfg(test)] 374 + mod tests { 375 + use super::*; 376 + use pm64::gbi::{GeometryMode, NodeData, TriangleData, VertexData}; 377 + 378 + fn make_node(x: i16, y: i16, z: i16) -> NodeData { 379 + let v = |dx: i16, dy: i16| VertexData { 380 + x: x + dx, 381 + y: y + dy, 382 + z, 383 + tc_s: 0, 384 + tc_t: 0, 385 + r: 255, 386 + g: 0, 387 + b: 0, 388 + a: 255, 389 + }; 390 + NodeData { 391 + triangles: vec![TriangleData { 392 + v0: v(-10, -10), 393 + v1: v(10, -10), 394 + v2: v(0, 10), 395 + }], 396 + geometry_mode: GeometryMode::VertexColor, 397 + other_mode_low: 0x0055_2078, 398 + } 399 + } 400 + 401 + /// Standard opaque priority (1_000_000). 402 + const PRI_OPA: i32 = 1_000_000; 403 + /// XLU layer 1 priority (8_000_000). 404 + const PRI_XLU1: i32 = 8_000_000; 405 + /// XLU layer 2 priority (7_500_000). 406 + const PRI_XLU2: i32 = 7_500_000; 407 + /// Decal XLU priority (7_000_000). 408 + const PRI_DECAL_XLU: i32 = 7_000_000; 409 + 410 + #[test] 411 + fn opaque_before_translucent() { 412 + let mut nodes = vec![ 413 + (PRI_XLU1, make_node(0, 0, 0)), 414 + (PRI_OPA, make_node(0, 0, 0)), 415 + ]; 416 + sort_nodes_by_render_priority(&mut nodes, [0.0, 0.0, 100.0]); 417 + assert_eq!( 418 + nodes[0].0, PRI_OPA, 419 + "opaque (MID) should come before XLU (FAR)" 420 + ); 421 + assert_eq!(nodes[1].0, PRI_XLU1); 422 + } 423 + 424 + #[test] 425 + fn opaque_sorted_front_to_back() { 426 + let mut nodes = vec![ 427 + (PRI_OPA, make_node(0, 0, -500)), // far 428 + (PRI_OPA, make_node(0, 0, 0)), // near 429 + (PRI_OPA, make_node(0, 0, -200)), // mid-distance 430 + ]; 431 + let eye = [0.0, 0.0, 100.0]; 432 + sort_nodes_by_render_priority(&mut nodes, eye); 433 + 434 + let dists: Vec<f32> = nodes 435 + .iter() 436 + .map(|(_, n)| node_camera_dist(n, eye)) 437 + .collect(); 438 + assert!( 439 + dists[0] <= dists[1] && dists[1] <= dists[2], 440 + "opaque (MID list, ascending) should be front-to-back: {dists:?}" 441 + ); 442 + } 443 + 444 + #[test] 445 + fn translucent_sorted_back_to_front() { 446 + let mut nodes = vec![ 447 + (PRI_XLU1, make_node(0, 0, 0)), // near 448 + (PRI_XLU1, make_node(0, 0, -500)), // far 449 + (PRI_XLU1, make_node(0, 0, -200)), // mid-distance 450 + ]; 451 + let eye = [0.0, 0.0, 100.0]; 452 + sort_nodes_by_render_priority(&mut nodes, eye); 453 + 454 + let dists: Vec<f32> = nodes 455 + .iter() 456 + .map(|(_, n)| node_camera_dist(n, eye)) 457 + .collect(); 458 + assert!( 459 + dists[0] >= dists[1] && dists[1] >= dists[2], 460 + "translucent (FAR list, descending) should be back-to-front: {dists:?}" 461 + ); 462 + } 463 + 464 + #[test] 465 + fn xlu_layers_render_in_priority_order() { 466 + // Different XLU layers at the same distance should sort by priority band. 467 + // FAR list is descending, so highest effective dist (XLU1=8M) comes first, 468 + // then XLU2=7.5M, then DECAL_XLU=7M. 469 + let mut nodes = vec![ 470 + (PRI_DECAL_XLU, make_node(0, 0, 0)), 471 + (PRI_XLU1, make_node(0, 0, 0)), 472 + (PRI_XLU2, make_node(0, 0, 0)), 473 + ]; 474 + sort_nodes_by_render_priority(&mut nodes, [0.0, 0.0, 100.0]); 475 + 476 + assert_eq!(nodes[0].0, PRI_XLU1); 477 + assert_eq!(nodes[1].0, PRI_XLU2); 478 + assert_eq!(nodes[2].0, PRI_DECAL_XLU); 479 + } 480 + 481 + #[test] 482 + fn centroid_averages_vertices() { 483 + let node = make_node(100, 200, 300); 484 + let c = node_centroid(&node); 485 + // Triangle offsets are (-10,-10), (10,-10), (0,10) so 486 + // x-centroid = 100, y-centroid = 200 + (-10-10+10)/3 ≈ 196.67, z = 300 487 + assert!((c[0] - 100.0).abs() < 0.1); 488 + assert!((c[1] - 196.67).abs() < 0.1); 489 + assert!((c[2] - 300.0).abs() < 0.1); 490 + } 491 + 492 + #[test] 493 + fn empty_node_centroid_is_origin() { 494 + let node = NodeData { 495 + triangles: vec![], 496 + geometry_mode: GeometryMode::VertexColor, 497 + other_mode_low: 0, 498 + }; 499 + assert_eq!(node_centroid(&node), [0.0; 3]); 500 + } 501 + 502 + #[test] 503 + fn sort_key_buckets_correctly() { 504 + // NEAR bucket: effective < 800K 505 + let (bucket, _) = sort_key(500_000.0); 506 + assert_eq!(bucket, 0); 507 + // MID bucket: 800K–3M 508 + let (bucket, _) = sort_key(1_500_000.0); 509 + assert_eq!(bucket, 1); 510 + // FAR bucket: ≥ 3M 511 + let (bucket, _) = sort_key(8_000_000.0); 512 + assert_eq!(bucket, 2); 513 + } 514 + }
+1 -1
crates/kammy/src/editor/map/camera.rs
··· 61 61 } 62 62 } 63 63 64 - fn eye_position(&self) -> [f32; 3] { 64 + pub fn eye_position(&self) -> [f32; 3] { 65 65 let cy = self.yaw.cos(); 66 66 let sy = self.yaw.sin(); 67 67 let cp = self.pitch.cos();
+4
crates/kammy/src/tests/map.rs
··· 34 34 x, 35 35 y, 36 36 z, 37 + tc_s: 0, 38 + tc_t: 0, 37 39 r: 255, 38 40 g: 0, 39 41 b: 0, ··· 46 48 v1: red(200, -200, 0), 47 49 v2: red(0, 200, 0), 48 50 }], 51 + geometry_mode: pm64::gbi::GeometryMode::VertexColor, 52 + other_mode_low: 0x0055_2078, 49 53 }] 50 54 } 51 55
+272 -125
crates/pm64/src/gbi.rs
··· 7 7 //! Converts plain vertex/triangle data into F3DEX2-compatible display list 8 8 //! commands that the N64 RSP can execute. 9 9 10 - /// A vertex with position and color data in N64-native ranges. 11 10 #[derive(Clone, Debug, PartialEq)] 12 11 pub struct VertexData { 13 12 pub x: i16, 14 13 pub y: i16, 15 14 pub z: i16, 15 + /// Texture S coordinate (10.5 fixed-point). 16 + pub tc_s: i16, 17 + /// Texture T coordinate (10.5 fixed-point). 18 + pub tc_t: i16, 16 19 pub r: u8, 17 20 pub g: u8, 18 21 pub b: u8, 19 22 pub a: u8, 20 23 } 21 24 22 - /// A triangle referencing three vertices. 23 25 #[derive(Clone, Debug, PartialEq)] 24 26 pub struct TriangleData { 25 27 pub v0: VertexData, ··· 27 29 pub v2: VertexData, 28 30 } 29 31 30 - /// A model node's geometry in plain (non-CRDT) form. 32 + #[derive(Clone, Copy, Debug, PartialEq, Eq, Default)] 33 + pub enum GeometryMode { 34 + #[default] 35 + VertexColor, 36 + VertexColorCullBack, 37 + /// RGBA bytes are normals, not colors. 38 + Lit, 39 + /// RGBA bytes are normals, not colors. 40 + LitCullBack, 41 + } 42 + 43 + impl GeometryMode { 44 + pub fn to_flags(self) -> u32 { 45 + match self { 46 + Self::VertexColor => G_SHADE | G_SHADING_SMOOTH, 47 + Self::VertexColorCullBack => G_SHADE | G_SHADING_SMOOTH | G_CULL_BACK, 48 + Self::Lit => G_SHADE | G_SHADING_SMOOTH | G_LIGHTING, 49 + Self::LitCullBack => G_SHADE | G_SHADING_SMOOTH | G_LIGHTING | G_CULL_BACK, 50 + } 51 + } 52 + } 53 + 54 + /// Plain (non-CRDT) model node, ready for GBI reconstruction. 31 55 #[derive(Clone, Debug, PartialEq)] 32 56 pub struct NodeData { 33 - /// Triangles belonging to this node. 34 57 pub triangles: Vec<TriangleData>, 58 + pub geometry_mode: GeometryMode, 59 + pub other_mode_low: u32, 35 60 } 36 61 37 - /// N64 camera matrices in s15.16 fixed-point format (64 bytes each). 62 + /// s15.16 fixed-point matrices (64 bytes each). 38 63 #[derive(Clone, Debug, PartialEq, Eq)] 39 64 pub struct CameraMatrices { 40 - /// Projection matrix (64 bytes, s15.16 fixed-point). 41 65 pub projection: [u8; 64], 42 - /// Modelview matrix (64 bytes, s15.16 fixed-point). 43 66 pub modelview: [u8; 64], 44 67 } 45 68 46 - /// Output of GBI display list reconstruction. 47 69 #[derive(Clone, Debug)] 48 70 pub struct GbiOutput { 49 - /// RDP command words (each command is 2 words = 64 bits). 71 + /// Paired command words (each command = 2 words). 50 72 pub commands: Vec<u32>, 51 - /// Packed vertex data to be placed in RDRAM. 52 73 pub vertex_data: Vec<u8>, 53 - /// N64 `Vp` viewport struct (16 bytes, big-endian `i16` fields). 74 + /// N64 `Vp` struct (16 bytes, big-endian). 54 75 pub viewport_data: [u8; 16], 55 76 } 56 77 ··· 69 90 const G_MV_VIEWPORT: u32 = 8; 70 91 const G_FILLRECT: u32 = 0xF6; 71 92 const G_SETFILLCOLOR: u32 = 0xF7; 93 + const G_SETZIMG: u32 = 0xFE; 72 94 const G_RDPPIPESYNC: u32 = 0xE7; 73 95 const G_RDPFULLSYNC: u32 = 0xE9; 74 96 75 97 // Geometry mode flags 76 98 const G_SHADE: u32 = 0x0000_0004; 99 + const G_LIGHTING: u32 = 0x0002_0000; 100 + const G_CULL_BACK: u32 = 0x0000_0400; 77 101 const G_SHADING_SMOOTH: u32 = 0x0020_0000; 78 102 79 103 // Matrix flags ··· 90 114 91 115 /// Packs a single vertex into 16 bytes (N64 Vtx format). 92 116 /// 93 - /// Layout: `x:i16 y:i16 z:i16 flag:0 tc_s:0 tc_t:0 r:u8 g:u8 b:u8 a:u8` 117 + /// Layout: `x:i16 y:i16 z:i16 flag:0 tc_s:i16 tc_t:i16 r:u8 g:u8 b:u8 a:u8` 94 118 fn pack_vertex(v: &VertexData) -> [u8; 16] { 95 119 let mut buf = [0u8; 16]; 96 120 buf[0..2].copy_from_slice(&v.x.to_be_bytes()); 97 121 buf[2..4].copy_from_slice(&v.y.to_be_bytes()); 98 122 buf[4..6].copy_from_slice(&v.z.to_be_bytes()); 99 - // flag[6..8] = 0, tc_s[8..10] = 0, tc_t[10..12] = 0 123 + // flag[6..8] = 0 124 + buf[8..10].copy_from_slice(&v.tc_s.to_be_bytes()); 125 + buf[10..12].copy_from_slice(&v.tc_t.to_be_bytes()); 100 126 buf[12] = v.r; 101 127 buf[13] = v.g; 102 128 buf[14] = v.b; ··· 143 169 vp 144 170 } 145 171 172 + /// RDRAM addresses for GBI display list reconstruction. 173 + #[derive(Clone, Debug)] 174 + pub struct RdramLayout { 175 + pub fb: u32, 176 + pub zb: u32, 177 + pub vertex: u32, 178 + pub projection: u32, 179 + pub modelview: u32, 180 + pub viewport: u32, 181 + } 182 + 146 183 /// Reconstructs a GBI display list from model nodes and camera matrices. 147 184 /// 148 185 /// The output display list sets up the framebuffer, scissor, render modes, 149 186 /// matrices, then draws all triangles with vertex coloring. 150 - /// 151 - /// # Arguments 152 - /// - `nodes`: Model geometry to render. 153 - /// - `camera`: Camera projection and modelview matrices in N64 format. 154 - /// - `fb_addr`: RDRAM address for the framebuffer. 155 - /// - `vertex_addr`: RDRAM address where vertex data will be placed. 156 - /// - `proj_addr`: RDRAM address of the projection matrix. 157 - /// - `mv_addr`: RDRAM address of the modelview matrix. 158 - /// - `viewport_addr`: RDRAM address where the viewport struct will be placed. 159 - pub fn reconstruct( 160 - nodes: &[NodeData], 161 - _camera: &CameraMatrices, 162 - fb_addr: u32, 163 - vertex_addr: u32, 164 - proj_addr: u32, 165 - mv_addr: u32, 166 - viewport_addr: u32, 167 - ) -> GbiOutput { 187 + pub fn reconstruct(nodes: &[NodeData], _camera: &CameraMatrices, rdram: &RdramLayout) -> GbiOutput { 168 188 let mut commands: Vec<u32> = Vec::new(); 169 189 let mut vertex_data: Vec<u8> = Vec::new(); 170 190 171 - // Build the N64 Vp struct (scale + translate, each 4 × i16, big-endian) 172 191 let viewport_data = pack_viewport(FB_WIDTH, FB_HEIGHT); 173 192 174 193 // SetColorImage: RGBA 16-bit, width=320 175 - // fmt=0 (RGBA), siz=G_IM_SIZ_16b(2), width-1 176 194 commands.push((G_SETCOLORIMAGE << 24) | (2 << 19) | (FB_WIDTH - 1)); 177 - commands.push(fb_addr); 195 + commands.push(rdram.fb); 178 196 179 - // SetScissor 180 197 commands.push(G_SETSCISSOR << 24); 181 198 commands.push(((FB_WIDTH << 2) << 12) | (FB_HEIGHT << 2)); 182 199 183 - // Clear the framebuffer with a dark background 200 + // Clear framebuffer (FILL cycle type) 184 201 commands.push(G_RDPPIPESYNC << 24); 185 202 commands.push(0); 186 - // SetOtherMode: FILL cycle type (bits 52-53 = 3 → 0x0030_0000 in high word) 187 203 commands.push((G_RDPSETOTHERMODE << 24) | 0x0030_0000); 188 204 commands.push(0); 189 - // SetFillColor: dark blue-gray (48, 48, 64) in RGBA5551, duplicated for 32-bit 205 + commands.push(G_SETFILLCOLOR << 24); 206 + commands.push(0x3191_3191); // dark blue-gray in RGBA5551 207 + commands.push((G_FILLRECT << 24) | ((((FB_WIDTH - 1) << 2) << 12) | ((FB_HEIGHT - 1) << 2))); 208 + commands.push(0); 209 + 210 + // Clear Z buffer by temporarily pointing SetColorImage at the Z buffer address 211 + commands.push(G_RDPPIPESYNC << 24); 212 + commands.push(0); 213 + commands.push((G_SETCOLORIMAGE << 24) | (2 << 19) | (FB_WIDTH - 1)); 214 + commands.push(rdram.zb); 190 215 commands.push(G_SETFILLCOLOR << 24); 191 - commands.push(0x3191_3191); 192 - // FillRect: full screen (0,0)-(319,239) in 10.2 fixed-point 216 + commands.push(0xFFFC_FFFC); 193 217 commands.push((G_FILLRECT << 24) | ((((FB_WIDTH - 1) << 2) << 12) | ((FB_HEIGHT - 1) << 2))); 194 218 commands.push(0); 195 219 196 - // RDP pipe sync before switching to 1-cycle rendering 220 + // Restore color image and set Z image 197 221 commands.push(G_RDPPIPESYNC << 24); 198 222 commands.push(0); 223 + commands.push((G_SETCOLORIMAGE << 24) | (2 << 19) | (FB_WIDTH - 1)); 224 + commands.push(rdram.fb); 225 + commands.push((G_SETZIMG << 24) | (2 << 19)); 226 + commands.push(rdram.zb); 199 227 200 - // SetOtherMode: 1-cycle, texture perspective, G_RM_OPA_SURF render mode 201 - commands.push((G_RDPSETOTHERMODE << 24) | 0x0008_0000); 202 - commands.push(0x0F0A_4000); 228 + // 1-cycle mode 229 + commands.push(G_RDPPIPESYNC << 24); 230 + commands.push(0); 203 231 204 - // SetCombine: G_CC_SHADE — output = vertex shade color 205 - // Encoding: (0-0)*0+SHADE for both RGB and alpha, both cycles. 232 + // G_CC_SHADE 206 233 commands.push(G_SETCOMBINE << 24); 207 234 commands.push(0x0002_0904); 208 235 209 - // Set geometry mode: AND mask = 0 clears all bits, then OR sets shade + smooth 210 - commands.push(G_SETGEOMETRYMODE << 24); 211 - commands.push(G_SHADE | G_SHADING_SMOOTH); 212 - 213 - // gSPViewport — maps clip-space to screen coordinates 214 - // gDma2p encoding: size=(16-1)>>3=1, idx=G_MV_VIEWPORT(8), ofs=0 236 + // gSPViewport 215 237 commands.push((G_MOVEMEM << 24) | (1 << 19) | G_MV_VIEWPORT); 216 - commands.push(viewport_addr); 238 + commands.push(rdram.viewport); 217 239 218 - // Load projection matrix 219 240 // F3DEX2's gSPMatrix XORs the push flag: NOPUSH(0) ^ PUSH(1) = 1 220 241 let mtx_proj_flags = u32::from(G_MTX_PROJECTION | G_MTX_LOAD | G_MTX_NOPUSH) ^ 1; 221 242 commands.push((G_MTX << 24) | 0x0038_0000 | mtx_proj_flags); 222 - commands.push(proj_addr); 243 + commands.push(rdram.projection); 223 244 224 - // Load modelview matrix 225 245 let mtx_mv_flags = u32::from(G_MTX_LOAD | G_MTX_NOPUSH) ^ 1; 226 246 commands.push((G_MTX << 24) | 0x0038_0000 | mtx_mv_flags); 227 - commands.push(mv_addr); 247 + commands.push(rdram.modelview); 248 + 249 + let mut current_geom_flags: Option<u32> = None; 250 + let mut current_other_mode_low: Option<u32> = None; 251 + let mut global_vert_offset: usize = 0; 252 + 253 + for node in nodes { 254 + if node.triangles.is_empty() { 255 + continue; 256 + } 257 + 258 + let geom_flags = node.geometry_mode.to_flags(); 259 + if current_geom_flags != Some(geom_flags) { 260 + // AND mask = 0 clears all, OR sets new mode 261 + commands.push(G_SETGEOMETRYMODE << 24); 262 + commands.push(geom_flags); 263 + current_geom_flags = Some(geom_flags); 264 + } 265 + 266 + let other_mode_low = node.other_mode_low; 267 + if current_other_mode_low != Some(other_mode_low) { 268 + commands.push(G_RDPPIPESYNC << 24); 269 + commands.push(0); 270 + commands.push((G_RDPSETOTHERMODE << 24) | 0x0008_0000); 271 + commands.push(other_mode_low); 272 + current_other_mode_low = Some(other_mode_low); 273 + } 228 274 229 - // Collect all triangles and deduplicate vertices 230 - let all_tris: Vec<&TriangleData> = nodes.iter().flat_map(|n| &n.triangles).collect(); 275 + let mut unique_verts: Vec<VertexData> = Vec::new(); 276 + let mut tri_indices: Vec<[usize; 3]> = Vec::new(); 231 277 232 - let mut unique_verts: Vec<VertexData> = Vec::new(); 233 - let mut tri_indices: Vec<[usize; 3]> = Vec::new(); 278 + for tri in &node.triangles { 279 + let mut indices = [0usize; 3]; 280 + for (vi, v) in [&tri.v0, &tri.v1, &tri.v2].iter().enumerate() { 281 + indices[vi] = if let Some(idx) = unique_verts.iter().position(|uv| uv == *v) { 282 + idx 283 + } else { 284 + unique_verts.push((*v).clone()); 285 + unique_verts.len() - 1 286 + }; 287 + } 288 + tri_indices.push(indices); 289 + } 234 290 235 - for tri in &all_tris { 236 - let mut indices = [0usize; 3]; 237 - for (vi, v) in [&tri.v0, &tri.v1, &tri.v2].iter().enumerate() { 238 - indices[vi] = if let Some(idx) = unique_verts.iter().position(|uv| uv == *v) { 239 - idx 240 - } else { 241 - unique_verts.push((*v).clone()); 242 - unique_verts.len() - 1 243 - }; 291 + for v in &unique_verts { 292 + vertex_data.extend_from_slice(&pack_vertex(v)); 244 293 } 245 - tri_indices.push(indices); 246 - } 247 294 248 - // Pack vertex data 249 - for v in &unique_verts { 250 - vertex_data.extend_from_slice(&pack_vertex(v)); 251 - } 295 + let total_verts = unique_verts.len(); 296 + let mut batch_start = 0; 252 297 253 - // Emit vertex loads and triangle commands in batches of MAX_VERTS_PER_BATCH 254 - let total_verts = unique_verts.len(); 255 - let mut batch_start = 0; 298 + while batch_start < total_verts { 299 + let batch_end = (batch_start + MAX_VERTS_PER_BATCH).min(total_verts); 300 + let batch_count = batch_end - batch_start; 256 301 257 - while batch_start < total_verts { 258 - let batch_end = (batch_start + MAX_VERTS_PER_BATCH).min(total_verts); 259 - let batch_count = batch_end - batch_start; 302 + let n = idx_u32(batch_count); 303 + let v0 = 0u32; 304 + commands.push((G_VTX << 24) | (n << 12) | ((v0 + n) << 1)); 305 + commands.push(rdram.vertex + idx_u32(global_vert_offset + batch_start) * 16); 260 306 261 - // F3DEX2 gSPVertex: word0 = (G_VTX << 24) | (n << 12) | ((v0 + n) << 1) 262 - let n = idx_u32(batch_count); 263 - let v0 = 0u32; 264 - commands.push((G_VTX << 24) | (n << 12) | ((v0 + n) << 1)); 265 - commands.push(vertex_addr + idx_u32(batch_start) * 16); 307 + let batch_tris: Vec<&[usize; 3]> = tri_indices 308 + .iter() 309 + .filter(|idx| idx.iter().all(|&i| i >= batch_start && i < batch_end)) 310 + .collect(); 266 311 267 - // Emit triangles that reference vertices in this batch 268 - let batch_tris: Vec<&[usize; 3]> = tri_indices 269 - .iter() 270 - .filter(|idx| idx.iter().all(|&i| i >= batch_start && i < batch_end)) 271 - .collect(); 312 + let mut ti = 0; 313 + while ti + 1 < batch_tris.len() { 314 + let t0 = batch_tris[ti]; 315 + let t1 = batch_tris[ti + 1]; 316 + let (a0, b0, c0) = ( 317 + idx_u32(t0[0] - batch_start), 318 + idx_u32(t0[1] - batch_start), 319 + idx_u32(t0[2] - batch_start), 320 + ); 321 + let (a1, b1, c1) = ( 322 + idx_u32(t1[0] - batch_start), 323 + idx_u32(t1[1] - batch_start), 324 + idx_u32(t1[2] - batch_start), 325 + ); 326 + commands.push((G_TRI2 << 24) | ((a0 * 2) << 16) | ((b0 * 2) << 8) | (c0 * 2)); 327 + commands.push(((a1 * 2) << 16) | ((b1 * 2) << 8) | (c1 * 2)); 328 + ti += 2; 329 + } 330 + if ti < batch_tris.len() { 331 + let t = batch_tris[ti]; 332 + let (a, b, c) = ( 333 + idx_u32(t[0] - batch_start), 334 + idx_u32(t[1] - batch_start), 335 + idx_u32(t[2] - batch_start), 336 + ); 337 + commands.push((G_TRI1 << 24) | ((a * 2) << 16) | ((b * 2) << 8) | (c * 2)); 338 + commands.push(0); 339 + } 272 340 273 - let mut ti = 0; 274 - while ti + 1 < batch_tris.len() { 275 - // gSP2Triangles 276 - let t0 = batch_tris[ti]; 277 - let t1 = batch_tris[ti + 1]; 278 - let (a0, b0, c0) = ( 279 - idx_u32(t0[0] - batch_start), 280 - idx_u32(t0[1] - batch_start), 281 - idx_u32(t0[2] - batch_start), 282 - ); 283 - let (a1, b1, c1) = ( 284 - idx_u32(t1[0] - batch_start), 285 - idx_u32(t1[1] - batch_start), 286 - idx_u32(t1[2] - batch_start), 287 - ); 288 - commands.push((G_TRI2 << 24) | ((a0 * 2) << 16) | ((b0 * 2) << 8) | (c0 * 2)); 289 - commands.push(((a1 * 2) << 16) | ((b1 * 2) << 8) | (c1 * 2)); 290 - ti += 2; 291 - } 292 - if ti < batch_tris.len() { 293 - // gSP1Triangle for the remaining triangle 294 - let t = batch_tris[ti]; 295 - let (a, b, c) = ( 296 - idx_u32(t[0] - batch_start), 297 - idx_u32(t[1] - batch_start), 298 - idx_u32(t[2] - batch_start), 299 - ); 300 - commands.push((G_TRI1 << 24) | ((a * 2) << 16) | ((b * 2) << 8) | (c * 2)); 301 - commands.push(0); 341 + batch_start = batch_end; 302 342 } 303 343 304 - batch_start = batch_end; 344 + global_vert_offset += total_verts; 305 345 } 306 346 307 347 // Full sync + End display list ··· 328 368 x, 329 369 y, 330 370 z, 371 + tc_s: 0, 372 + tc_t: 0, 331 373 r: 255, 332 374 g: 0, 333 375 b: 0, ··· 343 385 v1: red_vertex(100, 0, 0), 344 386 v2: red_vertex(50, 100, 0), 345 387 }], 388 + geometry_mode: GeometryMode::VertexColor, 389 + other_mode_low: 0x0055_2078, 346 390 }]; 347 391 348 392 let camera = CameraMatrices { ··· 350 394 modelview: [0u8; 64], 351 395 }; 352 396 353 - let output = reconstruct(&nodes, &camera, 0x100, 0x1000, 0x2000, 0x2040, 0x2080); 397 + let output = reconstruct( 398 + &nodes, 399 + &camera, 400 + &RdramLayout { 401 + fb: 0x100, 402 + zb: 0x26000, 403 + vertex: 0x1000, 404 + projection: 0x2000, 405 + modelview: 0x2040, 406 + viewport: 0x2080, 407 + }, 408 + ); 354 409 355 410 // Should have commands (setup + vertex load + 1 triangle + sync + enddl) 356 411 assert!(output.commands.len() >= 4); ··· 371 426 x: -100, 372 427 y: 200, 373 428 z: -300, 429 + tc_s: 512, 430 + tc_t: -1024, 374 431 r: 128, 375 432 g: 64, 376 433 b: 32, ··· 384 441 assert_eq!(x, -100); 385 442 assert_eq!(y, 200); 386 443 assert_eq!(z, -300); 444 + let tc_s = i16::from_be_bytes([packed[8], packed[9]]); 445 + let tc_t = i16::from_be_bytes([packed[10], packed[11]]); 446 + assert_eq!(tc_s, 512); 447 + assert_eq!(tc_t, -1024); 387 448 assert_eq!(packed[12], 128); 388 449 assert_eq!(packed[13], 64); 389 450 assert_eq!(packed[14], 32); ··· 403 464 } 404 465 }) 405 466 .collect(); 406 - let nodes = vec![NodeData { triangles }]; 467 + let nodes = vec![NodeData { 468 + triangles, 469 + geometry_mode: GeometryMode::VertexColor, 470 + other_mode_low: 0x0055_2078, 471 + }]; 407 472 408 473 let camera = CameraMatrices { 409 474 projection: [0u8; 64], 410 475 modelview: [0u8; 64], 411 476 }; 412 477 413 - let output = reconstruct(&nodes, &camera, 0x100, 0x1000, 0x2000, 0x2040, 0x2080); 478 + let output = reconstruct( 479 + &nodes, 480 + &camera, 481 + &RdramLayout { 482 + fb: 0x100, 483 + zb: 0x26000, 484 + vertex: 0x1000, 485 + projection: 0x2000, 486 + modelview: 0x2040, 487 + viewport: 0x2080, 488 + }, 489 + ); 414 490 415 491 // Should have 120 unique vertices = 120 * 16 = 1920 bytes 416 492 assert_eq!(output.vertex_data.len(), 1920); ··· 426 502 vtx_count >= 4, 427 503 "should have multiple vertex batches, got {vtx_count}" 428 504 ); 505 + } 506 + 507 + #[test] 508 + fn per_node_geometry_mode() { 509 + let nodes = vec![ 510 + NodeData { 511 + triangles: vec![TriangleData { 512 + v0: red_vertex(0, 0, 0), 513 + v1: red_vertex(100, 0, 0), 514 + v2: red_vertex(50, 100, 0), 515 + }], 516 + geometry_mode: GeometryMode::VertexColor, 517 + other_mode_low: 0x0055_2078, 518 + }, 519 + NodeData { 520 + triangles: vec![TriangleData { 521 + v0: red_vertex(200, 0, 0), 522 + v1: red_vertex(300, 0, 0), 523 + v2: red_vertex(250, 100, 0), 524 + }], 525 + geometry_mode: GeometryMode::LitCullBack, 526 + other_mode_low: 0x0055_2078, 527 + }, 528 + ]; 529 + 530 + let camera = CameraMatrices { 531 + projection: [0u8; 64], 532 + modelview: [0u8; 64], 533 + }; 534 + 535 + let output = reconstruct( 536 + &nodes, 537 + &camera, 538 + &RdramLayout { 539 + fb: 0x100, 540 + zb: 0x26000, 541 + vertex: 0x1000, 542 + projection: 0x2000, 543 + modelview: 0x2040, 544 + viewport: 0x2080, 545 + }, 546 + ); 547 + 548 + // Should have two SetGeometryMode commands (one per node since modes differ) 549 + let geom_mode_count = output 550 + .commands 551 + .iter() 552 + .step_by(2) 553 + .filter(|&&w| (w >> 24) == G_SETGEOMETRYMODE) 554 + .count(); 555 + assert_eq!( 556 + geom_mode_count, 2, 557 + "should have 2 SetGeometryMode commands, got {geom_mode_count}" 558 + ); 559 + 560 + // The second SetGeometryMode should have G_LIGHTING and G_CULL_BACK flags 561 + let geom_mode_words: Vec<u32> = output 562 + .commands 563 + .chunks(2) 564 + .filter(|pair| (pair[0] >> 24) == G_SETGEOMETRYMODE) 565 + .map(|pair| pair[1]) 566 + .collect(); 567 + assert_eq!(geom_mode_words.len(), 2); 568 + assert_eq!(geom_mode_words[0], G_SHADE | G_SHADING_SMOOTH); 569 + assert_eq!( 570 + geom_mode_words[1], 571 + G_SHADE | G_SHADING_SMOOTH | G_LIGHTING | G_CULL_BACK 572 + ); 573 + 574 + // Should have 6 vertices (3 per node, no cross-node dedup) 575 + assert_eq!(output.vertex_data.len(), 96); 429 576 } 430 577 }
+29 -18
crates/pm64/src/render.rs
··· 34 34 // host-side byte offsets (usize). We store them as usize and convert 35 35 // to u32 when writing to the OSTask fields via `addr_u32()`. 36 36 const FB_ADDR: usize = 0x0000_0100; 37 + /// Z buffer placed right after framebuffer (320 * 240 * 2 = 0x25800 bytes). 38 + const ZB_ADDR: usize = 0x0002_6000; 37 39 const F3DEX2_TEXT_ADDR: usize = 0x0010_0000; 38 40 const F3DEX2_DATA_ADDR: usize = 0x0010_2000; 39 41 const DL_ADDR: usize = 0x0012_0000; ··· 45 47 const DRAM_STACK_ADDR: usize = 0x001C_0000; 46 48 47 49 const RDRAM_SIZE: u32 = 4 * 1024 * 1024; // 4 MB 50 + 51 + const RDRAM_LAYOUT: gbi::RdramLayout = gbi::RdramLayout { 52 + fb: addr_u32(FB_ADDR), 53 + zb: addr_u32(ZB_ADDR), 54 + vertex: addr_u32(VERTEX_ADDR), 55 + projection: addr_u32(PROJ_MTX_ADDR), 56 + modelview: addr_u32(MV_MTX_ADDR), 57 + viewport: addr_u32(VIEWPORT_ADDR), 58 + }; 48 59 49 60 /// Converts a RDRAM address constant (usize) to u32 for the RSP. 50 61 #[expect( ··· 172 183 /// The returned slice borrows from the internal RSP device and is valid 173 184 /// until the next call to `render`. 174 185 pub fn render(&mut self, nodes: &[NodeData], camera: &CameraMatrices) -> &[u32] { 175 - let gbi_output = gbi::reconstruct( 176 - nodes, 177 - camera, 178 - addr_u32(FB_ADDR), 179 - addr_u32(VERTEX_ADDR), 180 - addr_u32(PROJ_MTX_ADDR), 181 - addr_u32(MV_MTX_ADDR), 182 - addr_u32(VIEWPORT_ADDR), 183 - ); 186 + let gbi_output = gbi::reconstruct(nodes, camera, &RDRAM_LAYOUT); 184 187 self.render_gbi(&gbi_output, camera) 185 188 } 186 189 ··· 191 194 camera: &CameraMatrices, 192 195 sink: &mut dyn rsp::RdpSink, 193 196 ) { 194 - let gbi_output = gbi::reconstruct( 195 - nodes, 196 - camera, 197 - addr_u32(FB_ADDR), 198 - addr_u32(VERTEX_ADDR), 199 - addr_u32(PROJ_MTX_ADDR), 200 - addr_u32(MV_MTX_ADDR), 201 - addr_u32(VIEWPORT_ADDR), 202 - ); 197 + let gbi_output = gbi::reconstruct(nodes, camera, &RDRAM_LAYOUT); 203 198 self.prepare_frame(&gbi_output, camera); 204 199 self.device.run_with_sink(sink); 205 200 } ··· 376 371 #[test] 377 372 fn single_red_triangle_produces_rdp_commands() { 378 373 let nodes = vec![NodeData { 374 + geometry_mode: gbi::GeometryMode::VertexColor, 375 + other_mode_low: 0x0055_2078, 379 376 triangles: vec![TriangleData { 380 377 v0: VertexData { 381 378 x: 0, 382 379 y: 0, 383 380 z: 0, 381 + tc_s: 0, 382 + tc_t: 0, 384 383 r: 255, 385 384 g: 0, 386 385 b: 0, ··· 390 389 x: 100, 391 390 y: 0, 392 391 z: 0, 392 + tc_s: 0, 393 + tc_t: 0, 393 394 r: 255, 394 395 g: 0, 395 396 b: 0, ··· 399 400 x: 50, 400 401 y: 100, 401 402 z: 0, 403 + tc_s: 0, 404 + tc_t: 0, 402 405 r: 255, 403 406 g: 0, 404 407 b: 0, ··· 431 434 } 432 435 433 436 let nodes = vec![NodeData { 437 + geometry_mode: gbi::GeometryMode::VertexColor, 438 + other_mode_low: 0x0055_2078, 434 439 triangles: vec![TriangleData { 435 440 v0: VertexData { 436 441 x: 0, 437 442 y: 0, 438 443 z: 0, 444 + tc_s: 0, 445 + tc_t: 0, 439 446 r: 255, 440 447 g: 0, 441 448 b: 0, ··· 445 452 x: 100, 446 453 y: 0, 447 454 z: 0, 455 + tc_s: 0, 456 + tc_t: 0, 448 457 r: 255, 449 458 g: 0, 450 459 b: 0, ··· 454 463 x: 50, 455 464 y: 100, 456 465 z: 0, 466 + tc_s: 0, 467 + tc_t: 0, 457 468 r: 255, 458 469 g: 0, 459 470 b: 0,
+73 -6
crates/project/src/lib.rs
··· 9 9 10 10 use loroscope::loroscope; 11 11 12 - /// An RGBA color with integer channels (0–255 range by convention). 13 12 #[loroscope] 14 13 #[derive(Debug)] 15 14 pub struct ColorRgba { ··· 19 18 pub a: i64, 20 19 } 21 20 22 - /// A vertex with 3D position and vertex color. 23 21 #[loroscope] 24 22 #[derive(Debug)] 25 23 pub struct Vertex { 26 24 pub x: f64, 27 25 pub y: f64, 28 26 pub z: f64, 27 + pub u: f64, 28 + pub v: f64, 29 29 pub color: ColorRgba, 30 30 } 31 31 32 - /// A triangle defined by three vertices. 33 32 #[loroscope] 34 33 #[derive(Debug)] 35 34 pub struct Triangle { ··· 38 37 pub v2: Vertex, 39 38 } 40 39 41 - /// A named node in the model tree, containing a list of triangles. 40 + /// Controls whether vertex RGBA bytes are colors or normals, and whether backface culling is on. 41 + #[loroscope] 42 + #[derive(Debug)] 43 + #[expect(missing_docs, reason = "loroscope macro doesn't forward variant docs")] 44 + pub enum GeometryMode { 45 + VertexColor, 46 + VertexColorCullBack, 47 + /// RGBA bytes are normals, not colors. 48 + Lit, 49 + /// RGBA bytes are normals, not colors. 50 + LitCullBack, 51 + } 52 + 53 + /// RDP render mode preset. Maps to PM64's Property 0x5C values in Star Rod XML. 54 + #[loroscope] 55 + #[derive(Debug)] 56 + #[expect(missing_docs, reason = "loroscope macro doesn't forward variant docs")] 57 + pub enum RenderMode { 58 + SolidAaZbLayer0, // 0x00 59 + SurfaceOpa, // 0x01 60 + SurfaceOpaNoAa, // 0x03 61 + SurfaceOpaNoZb, // 0x04 62 + DecalOpa, // 0x05 63 + DecalOpaNoAa, // 0x07 64 + IntersectingOpa, // 0x09 65 + Alphatest, // 0x0D 66 + AlphatestOnesided, // 0x0F 67 + AlphatestNoZb, // 0x10 68 + SurfaceXluLayer1, // 0x11 69 + SurfaceXluNoAa, // 0x13 70 + SurfaceXluNoZb, // 0x14 71 + SurfaceXluZbZupd, // 0x15 72 + SurfaceXluLayer2, // 0x16 73 + DecalXlu, // 0x1A 74 + DecalXluNoAa, // 0x1C 75 + DecalXluAhead, // 0x1E 76 + Shadow, // 0x20 77 + SurfaceXluLayer3, // 0x22 78 + IntersectingXlu, // 0x26 79 + PassThrough, // 0x28 80 + SurfaceXluAaZbZupd, // 0x29 81 + SurfaceOpaNoZbBehind, // 0x2A 82 + AlphatestNoZbBehind, // 0x2B 83 + SurfaceXluNoZbBehind, // 0x2C 84 + CloudNoZcmp, // 0x2D 85 + Cloud, // 0x2E 86 + CloudNoZb, // 0x2F 87 + } 88 + 42 89 #[loroscope] 43 90 #[derive(Debug)] 44 91 pub struct ModelNode { 45 - /// Display name of this model node. 46 92 pub name: String, 47 - /// The triangles that make up this node's geometry. 93 + pub geometry_mode: GeometryMode, 94 + pub render_mode: RenderMode, 48 95 pub triangles: List<Triangle>, 49 96 } 50 97 ··· 172 219 let (_id, node) = scene.model_tree().create_root(); 173 220 node.set_name("root"); 174 221 assert_eq!(scene.model_tree().roots().len(), 1); 222 + } 223 + 224 + #[test] 225 + fn geometry_mode_get_set() { 226 + let root = TestRoot::new(); 227 + let tree = root.model_tree(); 228 + let (_id, node) = tree.create_root(); 229 + node.set_name("test"); 230 + 231 + // Default should be VertexColor (first variant) 232 + assert!(matches!(node.geometry_mode(), GeometryMode::VertexColor)); 233 + 234 + node.set_geometry_mode_lit_cull_back(); 235 + assert!(matches!(node.geometry_mode(), GeometryMode::LitCullBack)); 236 + 237 + node.set_geometry_mode_vertex_color_cull_back(); 238 + assert!(matches!( 239 + node.geometry_mode(), 240 + GeometryMode::VertexColorCullBack 241 + )); 175 242 } 176 243 }
+1
crates/star_rod_interop/Cargo.toml
··· 10 10 [dependencies] 11 11 project = { path = "../project" } 12 12 loroscope = { path = "../loroscope" } 13 + log = { workspace = true } 13 14 loro = { workspace = true } 14 15 roxmltree = { workspace = true } 15 16 thiserror = { workspace = true }
+378
crates/star_rod_interop/src/lib.rs
··· 103 103 continue; 104 104 }; 105 105 106 + // Parse render mode from <PropertyList> → <Property v="5C,0,VALUE"/> 107 + if let Some(prop_list) = model_el.children().find(|n| n.has_tag_name("PropertyList")) 108 + && let Some(render_mode) = parse_render_mode_property(&prop_list) 109 + { 110 + set_render_mode(&node, &render_mode); 111 + } 112 + 106 113 // Find <ShapeMesh> → <DisplayList> → <TriangleBatch>* 107 114 let shape_mesh = model_el.children().find(|n| n.has_tag_name("ShapeMesh")); 108 115 let display_list = 109 116 shape_mesh.and_then(|sm| sm.children().find(|n| n.has_tag_name("DisplayList"))); 110 117 111 118 if let Some(dl) = display_list { 119 + let geom_mode = parse_geometry_mode(&dl); 120 + set_geometry_mode(&node, &geom_mode); 121 + 112 122 for batch in dl.children().filter(|n| n.has_tag_name("TriangleBatch")) { 113 123 parse_triangle_batch(&batch, &node)?; 114 124 } ··· 144 154 Ok(()) 145 155 } 146 156 157 + const G_LIGHTING: u32 = 0x0002_0000; 158 + const G_CULL_BACK: u32 = 0x0000_0400; 159 + 160 + /// Parses geometry mode from the last `D9` (`SetGeometryMode`) command in a `<DisplayList>`. 161 + /// Format: `D9AAAAAAA,OOOOOOO` where O is the OR bits. Uses the last D9 with OR != 0. 162 + fn parse_geometry_mode(dl: &roxmltree::Node<'_, '_>) -> project::GeometryMode { 163 + let mut or_bits: Option<u32> = None; 164 + 165 + for f3dex2 in dl.children().filter(|n| n.has_tag_name("F3DEX2")) { 166 + let Some(cmd) = f3dex2.attribute("cmd") else { 167 + continue; 168 + }; 169 + let Some((word0_str, word1_str)) = cmd.split_once(',') else { 170 + continue; 171 + }; 172 + let Ok(word0) = u32::from_str_radix(word0_str.trim(), 16) else { 173 + continue; 174 + }; 175 + if (word0 >> 24) != 0xD9 { 176 + continue; 177 + } 178 + let Ok(word1) = u32::from_str_radix(word1_str.trim(), 16) else { 179 + continue; 180 + }; 181 + if word1 != 0 { 182 + or_bits = Some(word1); 183 + } 184 + } 185 + 186 + match or_bits { 187 + Some(bits) => { 188 + let has_lighting = bits & G_LIGHTING != 0; 189 + let has_cull_back = bits & G_CULL_BACK != 0; 190 + match (has_lighting, has_cull_back) { 191 + (false, false) => project::GeometryMode::VertexColor, 192 + (false, true) => project::GeometryMode::VertexColorCullBack, 193 + (true, false) => project::GeometryMode::Lit, 194 + (true, true) => project::GeometryMode::LitCullBack, 195 + } 196 + } 197 + None => project::GeometryMode::VertexColor, 198 + } 199 + } 200 + 201 + fn set_geometry_mode(node: &project::ModelNode, mode: &project::GeometryMode) { 202 + match mode { 203 + project::GeometryMode::VertexColor => node.set_geometry_mode_vertex_color(), 204 + project::GeometryMode::VertexColorCullBack => { 205 + node.set_geometry_mode_vertex_color_cull_back(); 206 + } 207 + project::GeometryMode::Lit => node.set_geometry_mode_lit(), 208 + project::GeometryMode::LitCullBack => node.set_geometry_mode_lit_cull_back(), 209 + } 210 + } 211 + 212 + /// Parses `<Property v="5C,0,VALUE"/>` from a `<PropertyList>` into a render mode. 213 + fn parse_render_mode_property(prop_list: &roxmltree::Node<'_, '_>) -> Option<project::RenderMode> { 214 + for prop in prop_list.children().filter(|n| n.has_tag_name("Property")) { 215 + let Some(v) = prop.attribute("v") else { 216 + continue; 217 + }; 218 + let parts: Vec<&str> = v.split(',').collect(); 219 + if parts.len() < 3 { 220 + continue; 221 + } 222 + let Ok(prop_type) = u32::from_str_radix(parts[0].trim(), 16) else { 223 + continue; 224 + }; 225 + if prop_type != 0x5C { 226 + continue; 227 + } 228 + let Ok(value) = u32::from_str_radix(parts[2].trim(), 16) else { 229 + continue; 230 + }; 231 + return match value { 232 + 0x00 => Some(project::RenderMode::SolidAaZbLayer0), 233 + 0x01 => Some(project::RenderMode::SurfaceOpa), 234 + 0x03 => Some(project::RenderMode::SurfaceOpaNoAa), 235 + 0x04 => Some(project::RenderMode::SurfaceOpaNoZb), 236 + 0x05 => Some(project::RenderMode::DecalOpa), 237 + 0x07 => Some(project::RenderMode::DecalOpaNoAa), 238 + 0x09 => Some(project::RenderMode::IntersectingOpa), 239 + 0x0D => Some(project::RenderMode::Alphatest), 240 + 0x0F => Some(project::RenderMode::AlphatestOnesided), 241 + 0x10 => Some(project::RenderMode::AlphatestNoZb), 242 + 0x11 => Some(project::RenderMode::SurfaceXluLayer1), 243 + 0x13 => Some(project::RenderMode::SurfaceXluNoAa), 244 + 0x14 => Some(project::RenderMode::SurfaceXluNoZb), 245 + 0x15 => Some(project::RenderMode::SurfaceXluZbZupd), 246 + 0x16 => Some(project::RenderMode::SurfaceXluLayer2), 247 + 0x1A => Some(project::RenderMode::DecalXlu), 248 + 0x1C => Some(project::RenderMode::DecalXluNoAa), 249 + 0x1E => Some(project::RenderMode::DecalXluAhead), 250 + 0x20 => Some(project::RenderMode::Shadow), 251 + 0x22 => Some(project::RenderMode::SurfaceXluLayer3), 252 + 0x26 => Some(project::RenderMode::IntersectingXlu), 253 + 0x28 => Some(project::RenderMode::PassThrough), 254 + 0x29 => Some(project::RenderMode::SurfaceXluAaZbZupd), 255 + 0x2A => Some(project::RenderMode::SurfaceOpaNoZbBehind), 256 + 0x2B => Some(project::RenderMode::AlphatestNoZbBehind), 257 + 0x2C => Some(project::RenderMode::SurfaceXluNoZbBehind), 258 + 0x2D => Some(project::RenderMode::CloudNoZcmp), 259 + 0x2E => Some(project::RenderMode::Cloud), 260 + 0x2F => Some(project::RenderMode::CloudNoZb), 261 + unknown => { 262 + log::warn!( 263 + "unknown render mode Property 0x5C value: {unknown:#X}, defaulting to Alphatest" 264 + ); 265 + Some(project::RenderMode::Alphatest) 266 + } 267 + }; 268 + } 269 + None 270 + } 271 + 272 + fn set_render_mode(node: &project::ModelNode, mode: &project::RenderMode) { 273 + match mode { 274 + project::RenderMode::SolidAaZbLayer0 => node.set_render_mode_solid_aa_zb_layer0(), 275 + project::RenderMode::SurfaceOpa => node.set_render_mode_surface_opa(), 276 + project::RenderMode::SurfaceOpaNoAa => node.set_render_mode_surface_opa_no_aa(), 277 + project::RenderMode::SurfaceOpaNoZb => node.set_render_mode_surface_opa_no_zb(), 278 + project::RenderMode::DecalOpa => node.set_render_mode_decal_opa(), 279 + project::RenderMode::DecalOpaNoAa => node.set_render_mode_decal_opa_no_aa(), 280 + project::RenderMode::IntersectingOpa => node.set_render_mode_intersecting_opa(), 281 + project::RenderMode::Alphatest => node.set_render_mode_alphatest(), 282 + project::RenderMode::AlphatestOnesided => node.set_render_mode_alphatest_onesided(), 283 + project::RenderMode::AlphatestNoZb => node.set_render_mode_alphatest_no_zb(), 284 + project::RenderMode::SurfaceXluLayer1 => node.set_render_mode_surface_xlu_layer1(), 285 + project::RenderMode::SurfaceXluNoAa => node.set_render_mode_surface_xlu_no_aa(), 286 + project::RenderMode::SurfaceXluNoZb => node.set_render_mode_surface_xlu_no_zb(), 287 + project::RenderMode::SurfaceXluZbZupd => node.set_render_mode_surface_xlu_zb_zupd(), 288 + project::RenderMode::SurfaceXluLayer2 => node.set_render_mode_surface_xlu_layer2(), 289 + project::RenderMode::DecalXlu => node.set_render_mode_decal_xlu(), 290 + project::RenderMode::DecalXluNoAa => node.set_render_mode_decal_xlu_no_aa(), 291 + project::RenderMode::DecalXluAhead => node.set_render_mode_decal_xlu_ahead(), 292 + project::RenderMode::Shadow => node.set_render_mode_shadow(), 293 + project::RenderMode::SurfaceXluLayer3 => node.set_render_mode_surface_xlu_layer3(), 294 + project::RenderMode::IntersectingXlu => node.set_render_mode_intersecting_xlu(), 295 + project::RenderMode::PassThrough => node.set_render_mode_pass_through(), 296 + project::RenderMode::SurfaceXluAaZbZupd => node.set_render_mode_surface_xlu_aa_zb_zupd(), 297 + project::RenderMode::SurfaceOpaNoZbBehind => { 298 + node.set_render_mode_surface_opa_no_zb_behind(); 299 + } 300 + project::RenderMode::AlphatestNoZbBehind => node.set_render_mode_alphatest_no_zb_behind(), 301 + project::RenderMode::SurfaceXluNoZbBehind => { 302 + node.set_render_mode_surface_xlu_no_zb_behind(); 303 + } 304 + project::RenderMode::CloudNoZcmp => node.set_render_mode_cloud_no_zcmp(), 305 + project::RenderMode::Cloud => node.set_render_mode_cloud(), 306 + project::RenderMode::CloudNoZb => node.set_render_mode_cloud_no_zb(), 307 + } 308 + } 309 + 147 310 /// Parses a `<TriangleBatch>` element and adds its triangles to the node. 148 311 fn parse_triangle_batch( 149 312 batch: &roxmltree::Node<'_, '_>, ··· 211 374 x: f64, 212 375 y: f64, 213 376 z: f64, 377 + u: f64, 378 + v: f64, 214 379 r: i64, 215 380 g: i64, 216 381 b: i64, ··· 234 399 }); 235 400 } 236 401 402 + let (u, v) = if let Some(uv) = el.attribute("uv") { 403 + let tc = parse_f64_csv(uv, "Vertex", "uv")?; 404 + if tc.len() != 2 { 405 + return Err(ImportError::InvalidAttribute { 406 + element: "Vertex".to_owned(), 407 + attr: "uv".to_owned(), 408 + reason: format!("expected 2 components, got {}", tc.len()), 409 + }); 410 + } 411 + (tc[0], tc[1]) 412 + } else { 413 + (0.0, 0.0) 414 + }; 415 + 237 416 let (r, g, b, a) = if let Some(rgba) = el.attribute("rgba") { 238 417 let c = parse_i64_csv(rgba, "Vertex", "rgba")?; 239 418 if c.len() != 4 { ··· 252 431 x: coords[0], 253 432 y: coords[1], 254 433 z: coords[2], 434 + u, 435 + v, 255 436 r, 256 437 g, 257 438 b, ··· 264 445 crdt.set_x(v.x); 265 446 crdt.set_y(v.y); 266 447 crdt.set_z(v.z); 448 + crdt.set_u(v.u); 449 + crdt.set_v(v.v); 267 450 let color = crdt.color(); 268 451 color.set_r(v.r); 269 452 color.set_g(v.g); ··· 537 720 538 721 import_map_xml(xml, &scene).unwrap(); 539 722 assert_eq!(scene.display_name(), "kmr_20"); 723 + } 724 + 725 + #[test] 726 + fn uv_coordinates_parsed() { 727 + let scene = make_scene(); 728 + let xml = r#"<?xml version="1.0"?> 729 + <Map desc="" background="" textures=""> 730 + <ModelTree> 731 + <Node name="Root" id="0"/> 732 + </ModelTree> 733 + <Models> 734 + <Model ver="2" type="MODEL" lightset="0"> 735 + <MapObject name="Root" id="0"/> 736 + <ShapeMesh ver="3" texture="t"> 737 + <DisplayList> 738 + <TriangleBatch ver="0"> 739 + <VertexTable> 740 + <Vertex xyz="0,0,0" uv="512,1024" rgba="255,255,255,255"/> 741 + <Vertex xyz="1,0,0" uv="-128,256" rgba="255,255,255,255"/> 742 + <Vertex xyz="0,1,0" uv="0,0" rgba="255,255,255,255"/> 743 + </VertexTable> 744 + <TriangleList> 745 + <Triangle ijk="0,1,2"/> 746 + </TriangleList> 747 + </TriangleBatch> 748 + </DisplayList> 749 + </ShapeMesh> 750 + </Model> 751 + </Models> 752 + </Map>"#; 753 + 754 + import_map_xml(xml, &scene).unwrap(); 755 + let tri = scene 756 + .model_tree() 757 + .get(scene.model_tree().roots()[0]) 758 + .unwrap() 759 + .triangles() 760 + .get(0) 761 + .unwrap(); 762 + assert_eq!(tri.v0().u(), 512.0); 763 + assert_eq!(tri.v0().v(), 1024.0); 764 + assert_eq!(tri.v1().u(), -128.0); 765 + assert_eq!(tri.v1().v(), 256.0); 766 + assert_eq!(tri.v2().u(), 0.0); 767 + assert_eq!(tri.v2().v(), 0.0); 768 + } 769 + 770 + #[test] 771 + fn missing_uv_defaults_to_zero() { 772 + let scene = make_scene(); 773 + let xml = r#"<?xml version="1.0"?> 774 + <Map desc="" background="" textures=""> 775 + <ModelTree> 776 + <Node name="Root" id="0"/> 777 + </ModelTree> 778 + <Models> 779 + <Model ver="2" type="MODEL" lightset="0"> 780 + <MapObject name="Root" id="0"/> 781 + <ShapeMesh ver="3" texture="t"> 782 + <DisplayList> 783 + <TriangleBatch ver="0"> 784 + <VertexTable> 785 + <Vertex xyz="0,0,0"/> 786 + <Vertex xyz="1,0,0"/> 787 + <Vertex xyz="0,1,0"/> 788 + </VertexTable> 789 + <TriangleList> 790 + <Triangle ijk="0,1,2"/> 791 + </TriangleList> 792 + </TriangleBatch> 793 + </DisplayList> 794 + </ShapeMesh> 795 + </Model> 796 + </Models> 797 + </Map>"#; 798 + 799 + import_map_xml(xml, &scene).unwrap(); 800 + let tri = scene 801 + .model_tree() 802 + .get(scene.model_tree().roots()[0]) 803 + .unwrap() 804 + .triangles() 805 + .get(0) 806 + .unwrap(); 807 + assert_eq!(tri.v0().u(), 0.0); 808 + assert_eq!(tri.v0().v(), 0.0); 809 + } 810 + 811 + #[test] 812 + fn geometry_mode_from_f3dex2() { 813 + let scene = make_scene(); 814 + let xml = r#"<?xml version="1.0"?> 815 + <Map desc="" background="" textures=""> 816 + <ModelTree> 817 + <Node name="Root" id="0"/> 818 + </ModelTree> 819 + <Models> 820 + <Model ver="2" type="MODEL" lightset="0"> 821 + <MapObject name="Root" id="0"/> 822 + <ShapeMesh ver="3" texture="t"> 823 + <DisplayList> 824 + <F3DEX2 cmd="D9000000,0"/> 825 + <F3DEX2 cmd="D9000000,00220400"/> 826 + <TriangleBatch ver="0"> 827 + <VertexTable> 828 + <Vertex xyz="0,0,0"/> 829 + <Vertex xyz="1,0,0"/> 830 + <Vertex xyz="0,1,0"/> 831 + </VertexTable> 832 + <TriangleList> 833 + <Triangle ijk="0,1,2"/> 834 + </TriangleList> 835 + </TriangleBatch> 836 + </DisplayList> 837 + </ShapeMesh> 838 + </Model> 839 + </Models> 840 + </Map>"#; 841 + 842 + import_map_xml(xml, &scene).unwrap(); 843 + let node = scene 844 + .model_tree() 845 + .get(scene.model_tree().roots()[0]) 846 + .unwrap(); 847 + // 0x00220400 = G_SHADING_SMOOTH | G_LIGHTING | G_CULL_BACK 848 + assert!(matches!( 849 + node.geometry_mode(), 850 + project::GeometryMode::LitCullBack 851 + )); 852 + } 853 + 854 + #[test] 855 + fn missing_f3dex2_defaults_to_vertex_color() { 856 + let scene = make_scene(); 857 + let xml = r#"<?xml version="1.0"?> 858 + <Map desc="" background="" textures=""> 859 + <ModelTree> 860 + <Node name="Root" id="0"/> 861 + </ModelTree> 862 + <Models> 863 + <Model ver="2" type="MODEL" lightset="0"> 864 + <MapObject name="Root" id="0"/> 865 + <ShapeMesh ver="3" texture="t"> 866 + <DisplayList> 867 + <TriangleBatch ver="0"> 868 + <VertexTable> 869 + <Vertex xyz="0,0,0"/> 870 + <Vertex xyz="1,0,0"/> 871 + <Vertex xyz="0,1,0"/> 872 + </VertexTable> 873 + <TriangleList> 874 + <Triangle ijk="0,1,2"/> 875 + </TriangleList> 876 + </TriangleBatch> 877 + </DisplayList> 878 + </ShapeMesh> 879 + </Model> 880 + </Models> 881 + </Map>"#; 882 + 883 + import_map_xml(xml, &scene).unwrap(); 884 + let node = scene 885 + .model_tree() 886 + .get(scene.model_tree().roots()[0]) 887 + .unwrap(); 888 + assert!(matches!( 889 + node.geometry_mode(), 890 + project::GeometryMode::VertexColor 891 + )); 892 + } 893 + 894 + #[test] 895 + fn render_mode_from_property() { 896 + let scene = make_scene(); 897 + let xml = r#"<?xml version="1.0"?> 898 + <Map desc="" background="" textures=""> 899 + <ModelTree> 900 + <Node name="Root" id="0"/> 901 + </ModelTree> 902 + <Models> 903 + <Model ver="2" type="MODEL" lightset="0"> 904 + <MapObject name="Root" id="0"/> 905 + <PropertyList> 906 + <Property v="5C,0,1A"/> 907 + </PropertyList> 908 + </Model> 909 + </Models> 910 + </Map>"#; 911 + 912 + import_map_xml(xml, &scene).unwrap(); 913 + let node = scene 914 + .model_tree() 915 + .get(scene.model_tree().roots()[0]) 916 + .unwrap(); 917 + assert!(matches!(node.render_mode(), project::RenderMode::DecalXlu)); 540 918 } 541 919 }
+35 -1
crates/star_rod_interop/tests/import.rs
··· 54 54 </Model> 55 55 <Model ver="2" type="MODEL" lightset="0"> 56 56 <MapObject name="Ground" id="0"/> 57 + <PropertyList> 58 + <Property v="5C,0,0D"/> 59 + </PropertyList> 57 60 <ShapeMesh ver="3" texture="grass_tex"> 58 61 <DisplayList> 59 62 <F3DEX2 cmd="E7000000,0"/> 63 + <F3DEX2 cmd="D9000000,0"/> 64 + <F3DEX2 cmd="D9000000,00200004"/> 60 65 <TriangleBatch ver="0"> 61 66 <VertexTable> 62 67 <Vertex xyz="-200,0,-200" uv="0,0" rgba="80,140,80,255"/> ··· 74 79 </Model> 75 80 <Model ver="2" type="MODEL" lightset="0"> 76 81 <MapObject name="Trunk" id="3"/> 82 + <PropertyList> 83 + <Property v="5C,0,1A"/> 84 + </PropertyList> 77 85 <ShapeMesh ver="3" texture="bark_tex"> 78 86 <DisplayList> 87 + <F3DEX2 cmd="D9000000,00220400"/> 79 88 <TriangleBatch ver="0"> 80 89 <VertexTable> 81 90 <Vertex xyz="90,0,0" rgba="139,90,43,255"/> ··· 115 124 assert_eq!(ground.name(), "Ground"); 116 125 assert_eq!(ground.triangles().len(), 2); 117 126 118 - // Spot-check a vertex 127 + // Verify Ground geometry mode (D9 sets G_SHADE | G_SHADING_SMOOTH, no lighting) 128 + assert!(matches!( 129 + ground.geometry_mode(), 130 + project::GeometryMode::VertexColor 131 + )); 132 + 133 + // Verify Ground render mode (Property 0x5C = 0x0D = Alphatest) 134 + assert!(matches!( 135 + ground.render_mode(), 136 + project::RenderMode::Alphatest 137 + )); 138 + 139 + // Spot-check a vertex with UV 119 140 let tri0 = ground.triangles().get(0).unwrap(); 120 141 assert_eq!(tri0.v0().x(), -200.0); 142 + assert_eq!(tri0.v0().u(), 0.0); 143 + assert_eq!(tri0.v0().v(), 0.0); 121 144 assert_eq!(tri0.v0().color().r(), 80); 145 + // Second vertex has uv="0,1024" 146 + assert_eq!(tri0.v1().u(), 0.0); 147 + assert_eq!(tri0.v1().v(), 1024.0); 122 148 123 149 // Check tree subtree: Tree -> [Trunk, Leaves] 124 150 let tree_node_id = children[2]; ··· 131 157 let trunk = tree.get(tree_children[0]).unwrap(); 132 158 assert_eq!(trunk.name(), "Trunk"); 133 159 assert_eq!(trunk.triangles().len(), 1); 160 + 161 + // Trunk has D9 with G_LIGHTING | G_CULL_BACK → LitCullBack 162 + assert!(matches!( 163 + trunk.geometry_mode(), 164 + project::GeometryMode::LitCullBack 165 + )); 166 + // Trunk has Property 0x5C = 0x1A → DecalXlu 167 + assert!(matches!(trunk.render_mode(), project::RenderMode::DecalXlu)); 134 168 135 169 let leaves = tree.get(tree_children[1]).unwrap(); 136 170 assert_eq!(leaves.name(), "Leaves");