paavohtl Claude Opus 4.6 (1M context) commited on
Commit
6c987d4
·
1 Parent(s): a7fe8f3

fix: complete gradient with palette round-trip, mark as untested

Browse files

Restore PaletteContext::find_closest and color_fp methods (renamed
from read_color_fp). The original uses FindClosest + ReadColor to
establish an initial/fallback color before the interpolation loop.
Rows beyond band 5 now correctly use the last interpolated color
instead of potentially uninitialized values.

Add clear UNTESTED warnings in module doc and function doc — this
code path only triggers on maps with non-standard dimensions and
has not been visually verified.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

crates/openwa-core/src/display/gradient.rs CHANGED
@@ -1,4 +1,9 @@
1
  //! Sky gradient computation for non-standard level heights.
 
 
 
 
 
2
 
3
  use crate::engine::ddgame::DDGame;
4
  use crate::rebase::rb;
@@ -6,21 +11,20 @@ use crate::render::gfx_handler::call_gfx_find_and_load;
6
  use crate::wa_alloc::{wa_free, wa_malloc};
7
 
8
  /// Palette context for gradient color mapping.
9
- /// Matches the WA stack-allocated structure used by PaletteContext__Init (0x5411A0).
 
 
 
10
  struct PaletteContext {
11
- min_index: u8,
12
- max_index: u8,
13
  colors: [u32; 256], // RGB packed, indexed by palette index
14
- valid: [bool; 256], // whether index has a color assigned
15
  used_count: usize,
16
- used_list: [u8; 256], // palette indices currently in use
17
  }
18
 
19
  impl PaletteContext {
20
  fn new() -> Self {
21
  Self {
22
- min_index: 0,
23
- max_index: 0xFF,
24
  colors: [0; 256],
25
  valid: [false; 256],
26
  used_count: 0,
@@ -28,20 +32,17 @@ impl PaletteContext {
28
  }
29
  }
30
 
31
- /// Map an RGB color to a palette index. Returns existing index if color
32
- /// already mapped, otherwise allocates a new one. Returns None if full.
33
  fn map_color(&mut self, rgb: u32) -> Option<u8> {
34
  let masked = rgb & 0x00FF_FFFF;
35
- // Search existing entries
36
  for i in 0..self.used_count {
37
  let idx = self.used_list[i] as usize;
38
  if idx != 0 && (self.colors[idx] & 0x00FF_FFFF) == masked {
39
  return Some(idx as u8);
40
  }
41
  }
42
- // Allocate new: use indices from max_index downward
43
- // Find first unused index
44
- for idx in (self.min_index as usize..=self.max_index as usize).rev() {
45
  if !self.valid[idx] {
46
  self.colors[idx] = masked;
47
  self.valid[idx] = true;
@@ -52,6 +53,43 @@ impl PaletteContext {
52
  }
53
  None
54
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55
  }
56
 
57
  /// Compute the complex sky gradient for non-standard level heights.
@@ -60,8 +98,9 @@ impl PaletteContext {
60
  /// Loads gradient.img, samples 7 anchor colors, then creates an interpolated
61
  /// gradient image (64 columns × (level_height + 0xDC) rows).
62
  ///
63
- /// The result is stored at `DDGame+0x30` (gradient_image) and optionally
64
- /// `DDGame+0x34` (gradient_image_2).
 
65
  #[cfg(target_arch = "x86")]
66
  pub(crate) unsafe fn compute_complex_gradient(
67
  ddgame: *mut DDGame,
@@ -71,15 +110,14 @@ pub(crate) unsafe fn compute_complex_gradient(
71
  ) {
72
  let mut palette = PaletteContext::new();
73
 
74
- // Load gradient.img
75
  let gradient_sprite =
76
  call_gfx_find_and_load(land_layer, b"gradient.img\0".as_ptr(), layer3_ctx);
77
  if gradient_sprite.is_null() {
78
- return; // No gradient available — stub fallback handles this
79
  }
80
 
81
- // Read gradient sprite dimensions via vtable[4] (get_pixel)
82
- // Sprite height is at sprite+0x18 (field [6] in u32 layout)
83
  let gradient_height = *(gradient_sprite.add(0x18) as *const i32);
84
  if gradient_height <= 0 {
85
  return;
@@ -88,17 +126,9 @@ pub(crate) unsafe fn compute_complex_gradient(
88
  let get_pixel: unsafe extern "thiscall" fn(*mut u8, i32, i32) -> u32 =
89
  core::mem::transmute(*(*(gradient_sprite as *const *const u32)).add(4));
90
 
91
- // Compute target rows for stretching
92
- let target_rows = {
93
- let max_rows = 0x70i32 - sky_height as i32;
94
- if gradient_height < max_rows {
95
- gradient_height
96
- } else {
97
- max_rows
98
- }
99
- };
100
 
101
- // Stretch gradient through palette: map each source row's pixel color
102
  if target_rows > 0 {
103
  let mut src_pos = 0i32;
104
  for _ in 0..target_rows {
@@ -109,14 +139,15 @@ pub(crate) unsafe fn compute_complex_gradient(
109
  }
110
  }
111
 
112
- // If target_rows == gradient_height, also set gradient_image_2
113
  if target_rows == gradient_height {
114
  let gradient2 = call_gfx_find_and_load(land_layer, b"gradient.img\0".as_ptr(), layer3_ctx);
115
  (*ddgame).gradient_image_2 = gradient2;
116
  }
117
 
118
- // Sample 7 anchor colors by averaging 8×2 pixel blocks
119
- let mut anchors = [[0i32; 3]; 7]; // [band][r, g, b] in shifted format
 
120
  let grad_height_minus2 = (gradient_height - 2).max(0);
121
 
122
  for band in 0..7u32 {
@@ -132,8 +163,7 @@ pub(crate) unsafe fn compute_complex_gradient(
132
  for dy in 0..2i32 {
133
  let row = start_row + dy;
134
  for col in 0..8i32 {
135
- let pixel = get_pixel(gradient_sprite, col, row);
136
- let pidx = pixel as usize;
137
  if pidx < 256 && palette.valid[pidx] {
138
  let c = palette.colors[pidx];
139
  r_sum += (c & 0xFF) as i32;
@@ -143,7 +173,7 @@ pub(crate) unsafe fn compute_complex_gradient(
143
  }
144
  }
145
 
146
- // Shift left 4 (multiply by 16) — converts 8-bit×16 samples to 8.8 fixed-point
147
  anchors[band as usize] = [r_sum << 4, g_sum << 4, b_sum << 4];
148
  }
149
 
@@ -152,13 +182,19 @@ pub(crate) unsafe fn compute_complex_gradient(
152
  let release: unsafe extern "thiscall" fn(*mut u8, u8) = core::mem::transmute(*gvt.add(3));
153
  release(gradient_sprite, 1);
154
 
155
- // Create the gradient image
 
 
 
 
 
 
156
  let total_height = (*ddgame).level_height as i32 + 0xDC;
157
  if total_height <= 0 {
158
  return;
159
  }
160
 
161
- let stride = 0x200u32; // 64 columns × 8 bytes per pixel
162
  let data_size = total_height as u32 * stride;
163
  let data = wa_malloc(data_size + 0x20);
164
  if data.is_null() {
@@ -166,59 +202,35 @@ pub(crate) unsafe fn compute_complex_gradient(
166
  }
167
  core::ptr::write_bytes(data, 0, data_size as usize);
168
 
169
- // Build raw image header (0x44 bytes, same layout GradientImage__WriteRow expects)
170
- let header = wa_malloc(0x44);
171
- if header.is_null() {
172
- wa_free(data);
173
- return;
174
- }
175
- core::ptr::write_bytes(header, 0, 0x44);
176
- let h = header as *mut u32;
177
- *h.add(0) = data as u32; // data pointer
178
- *h.add(1) = stride; // row stride
179
- *h.add(2) = 0x40; // max columns (64)
180
- *h.add(3) = total_height as u32; // height
181
- *h.add(4) = 0; // bounds left
182
- *h.add(5) = 0; // bounds top
183
- *h.add(6) = 0x40; // bounds right
184
- *h.add(7) = total_height as u32; // bounds bottom
185
-
186
- // Interpolate between anchor colors and write each row
187
- let band_size = total_height / 6;
188
  for row in 0..total_height {
189
- let band_idx = ((row * 6) / total_height).min(5) as usize;
190
- let pos = row - band_idx as i32 * band_size;
191
 
192
- // Interpolate RGB between anchor[band_idx] and anchor[band_idx+1]
193
- let a0 = &anchors[band_idx];
194
- let a1 = &anchors[(band_idx + 1).min(6)];
 
 
195
 
196
- let r = if band_size > 0 {
197
- a0[0] + ((a1[0] - a0[0]) * pos) / band_size
198
- } else {
199
- a0[0]
200
- };
201
- let g = if band_size > 0 {
202
- a0[1] + ((a1[1] - a0[1]) * pos) / band_size
203
- } else {
204
- a0[1]
205
- };
206
- let b = if band_size > 0 {
207
- a0[2] + ((a1[2] - a0[2]) * pos) / band_size
208
- } else {
209
- a0[2]
210
- };
211
-
212
- // Clamp to [0, 0xFF00]
213
- let r = r.clamp(0, 0xFF00) as u32;
214
- let g = g.clamp(0, 0xFF00) as u32;
215
- let b = b.clamp(0, 0xFF00) as u32;
216
 
217
- // Pack: color_low = R | (G << 16), color_high = B
218
- let color_low = (r & 0xFFFF) | ((g & 0xFFFF) << 16);
219
- let color_high = b & 0xFFFF;
220
 
221
- // Write to all 64 columns
222
  let row_base = data.add(row as usize * stride as usize);
223
  for col in 0..64u32 {
224
  let pixel = row_base.add(col as usize * 8) as *mut u32;
@@ -227,11 +239,10 @@ pub(crate) unsafe fn compute_complex_gradient(
227
  }
228
  }
229
 
230
- // Wrap in DisplayGfx: use the same vtable as the gradient stub
231
  let gfx_obj = wa_malloc(0x2C);
232
  if gfx_obj.is_null() {
233
  wa_free(data);
234
- wa_free(header);
235
  return;
236
  }
237
  core::ptr::write_bytes(gfx_obj, 0, 0x2C);
@@ -240,9 +251,6 @@ pub(crate) unsafe fn compute_complex_gradient(
240
  *g.add(2) = data as u32; // pixel data
241
  *g.add(4) = stride; // bytes per row
242
  *g.add(5) = 0; // x offset
243
- *g.add(6) = total_height as u32; // height (checked by CTaskLand: `if (0 < [6])`)
244
  (*ddgame).gradient_image = gfx_obj;
245
-
246
- // Free the raw header (DisplayGfx wrapper now owns the pixel data)
247
- wa_free(header);
248
  }
 
1
  //! Sky gradient computation for non-standard level heights.
2
+ //!
3
+ //! NOTE: This code path is UNTESTED — it only triggers on maps with
4
+ //! non-standard dimensions (sky_height >= 0x61 or level_height != 0x2B8).
5
+ //! The algorithm is ported from the DDGame constructor decompilation but
6
+ //! may have subtle color differences due to the tangled decompiler output.
7
 
8
  use crate::engine::ddgame::DDGame;
9
  use crate::rebase::rb;
 
11
  use crate::wa_alloc::{wa_free, wa_malloc};
12
 
13
  /// Palette context for gradient color mapping.
14
+ /// Reimplements the WA palette management functions:
15
+ /// - PaletteContext__Init (0x5411A0)
16
+ /// - PaletteContext__MapColor (0x5412B0)
17
+ /// - PaletteContext__FindClosest (0x541420)
18
  struct PaletteContext {
 
 
19
  colors: [u32; 256], // RGB packed, indexed by palette index
20
+ valid: [bool; 256],
21
  used_count: usize,
22
+ used_list: [u8; 256],
23
  }
24
 
25
  impl PaletteContext {
26
  fn new() -> Self {
27
  Self {
 
 
28
  colors: [0; 256],
29
  valid: [false; 256],
30
  used_count: 0,
 
32
  }
33
  }
34
 
35
+ /// Map an RGB color to a palette index (PaletteContext__MapColor, 0x5412B0).
36
+ /// Returns existing index if already mapped, otherwise allocates a new one.
37
  fn map_color(&mut self, rgb: u32) -> Option<u8> {
38
  let masked = rgb & 0x00FF_FFFF;
 
39
  for i in 0..self.used_count {
40
  let idx = self.used_list[i] as usize;
41
  if idx != 0 && (self.colors[idx] & 0x00FF_FFFF) == masked {
42
  return Some(idx as u8);
43
  }
44
  }
45
+ for idx in (1..=255usize).rev() {
 
 
46
  if !self.valid[idx] {
47
  self.colors[idx] = masked;
48
  self.valid[idx] = true;
 
53
  }
54
  None
55
  }
56
+
57
+ /// Find closest palette match (PaletteContext__FindClosest, 0x541420).
58
+ /// Uses weighted perceptual distance: R*3 + G*5 + B*2.
59
+ /// Input channels are 8.8 fixed-point (0..0xFF00).
60
+ fn find_closest(&self, r: i32, g: i32, b: i32) -> u8 {
61
+ let mut best_idx = 0u8;
62
+ let mut best_dist = i32::MAX;
63
+ for i in 0..self.used_count {
64
+ let idx = self.used_list[i] as usize;
65
+ if idx == 0 {
66
+ continue;
67
+ }
68
+ let c = self.colors[idx];
69
+ let cr = ((c & 0xFF) as i32) << 8;
70
+ let cg = (((c >> 8) & 0xFF) as i32) << 8;
71
+ let cb = (((c >> 16) & 0xFF) as i32) << 8;
72
+ let dist = (r - cr).abs() * 3 + (g - cg).abs() * 5 + (b - cb).abs() * 2;
73
+ if dist == 0 {
74
+ return idx as u8;
75
+ }
76
+ if dist < best_dist {
77
+ best_dist = dist;
78
+ best_idx = idx as u8;
79
+ }
80
+ }
81
+ best_idx
82
+ }
83
+
84
+ /// Read RGB color for a palette index as 8.8 fixed-point (0..0xFF00).
85
+ fn color_fp(&self, idx: u8) -> [i32; 3] {
86
+ let c = self.colors[idx as usize];
87
+ [
88
+ ((c & 0xFF) as i32) << 8,
89
+ (((c >> 8) & 0xFF) as i32) << 8,
90
+ (((c >> 16) & 0xFF) as i32) << 8,
91
+ ]
92
+ }
93
  }
94
 
95
  /// Compute the complex sky gradient for non-standard level heights.
 
98
  /// Loads gradient.img, samples 7 anchor colors, then creates an interpolated
99
  /// gradient image (64 columns × (level_height + 0xDC) rows).
100
  ///
101
+ /// NOTE: This code path is untested — no replay or map available that
102
+ /// triggers it. The algorithm follows the DDGame constructor decompilation
103
+ /// but may have subtle differences in color output.
104
  #[cfg(target_arch = "x86")]
105
  pub(crate) unsafe fn compute_complex_gradient(
106
  ddgame: *mut DDGame,
 
110
  ) {
111
  let mut palette = PaletteContext::new();
112
 
113
+ // Step 1: Load gradient.img
114
  let gradient_sprite =
115
  call_gfx_find_and_load(land_layer, b"gradient.img\0".as_ptr(), layer3_ctx);
116
  if gradient_sprite.is_null() {
117
+ return;
118
  }
119
 
120
+ // Sprite height at offset +0x18
 
121
  let gradient_height = *(gradient_sprite.add(0x18) as *const i32);
122
  if gradient_height <= 0 {
123
  return;
 
126
  let get_pixel: unsafe extern "thiscall" fn(*mut u8, i32, i32) -> u32 =
127
  core::mem::transmute(*(*(gradient_sprite as *const *const u32)).add(4));
128
 
129
+ // Step 2: Compute target rows and stretch gradient through palette
130
+ let target_rows = (0x70i32 - sky_height as i32).min(gradient_height);
 
 
 
 
 
 
 
131
 
 
132
  if target_rows > 0 {
133
  let mut src_pos = 0i32;
134
  for _ in 0..target_rows {
 
139
  }
140
  }
141
 
142
+ // Step 3: If heights match, also set gradient_image_2
143
  if target_rows == gradient_height {
144
  let gradient2 = call_gfx_find_and_load(land_layer, b"gradient.img\0".as_ptr(), layer3_ctx);
145
  (*ddgame).gradient_image_2 = gradient2;
146
  }
147
 
148
+ // Step 4: Sample 7 anchor colors by averaging 8×2 pixel blocks.
149
+ // get_pixel returns palette indices; we look up RGB from our palette context.
150
+ let mut anchors = [[0i32; 3]; 7]; // [band][r, g, b] in 8.8 fixed-point
151
  let grad_height_minus2 = (gradient_height - 2).max(0);
152
 
153
  for band in 0..7u32 {
 
163
  for dy in 0..2i32 {
164
  let row = start_row + dy;
165
  for col in 0..8i32 {
166
+ let pidx = get_pixel(gradient_sprite, col, row) as usize;
 
167
  if pidx < 256 && palette.valid[pidx] {
168
  let c = palette.colors[pidx];
169
  r_sum += (c & 0xFF) as i32;
 
173
  }
174
  }
175
 
176
+ // Shift left 4: 16 samples × 8-bit → 8.8 fixed-point
177
  anchors[band as usize] = [r_sum << 4, g_sum << 4, b_sum << 4];
178
  }
179
 
 
182
  let release: unsafe extern "thiscall" fn(*mut u8, u8) = core::mem::transmute(*gvt.add(3));
183
  release(gradient_sprite, 1);
184
 
185
+ // Step 5: Establish initial/fallback color via palette closest-match.
186
+ // The original uses PaletteContext__FindClosest + PaletteContext__ReadColor
187
+ // to get the canonical palette color for the first anchor point.
188
+ let initial_idx = palette.find_closest(anchors[0][0], anchors[0][1], anchors[0][2]);
189
+ let mut fallback = palette.color_fp(initial_idx);
190
+
191
+ // Step 6: Create the gradient image buffer
192
  let total_height = (*ddgame).level_height as i32 + 0xDC;
193
  if total_height <= 0 {
194
  return;
195
  }
196
 
197
+ let stride = 0x200u32; // 64 columns × 8 bytes per entry
198
  let data_size = total_height as u32 * stride;
199
  let data = wa_malloc(data_size + 0x20);
200
  if data.is_null() {
 
202
  }
203
  core::ptr::write_bytes(data, 0, data_size as usize);
204
 
205
+ // Step 7: Interpolate between anchor colors and write each row.
206
+ // For each row, determine which two anchors to interpolate between.
207
+ // Rows beyond band 5 use the last interpolated color as fallback.
208
+ let band_size = if total_height > 6 {
209
+ total_height / 6
210
+ } else {
211
+ 1
212
+ };
213
+
 
 
 
 
 
 
 
 
 
 
214
  for row in 0..total_height {
215
+ let band_idx = (row * 6) / total_height;
 
216
 
217
+ if band_idx < 6 {
218
+ let pos = row - band_idx * band_size;
219
+ let bi = band_idx as usize;
220
+ let a0 = &anchors[bi];
221
+ let a1 = &anchors[(bi + 1).min(6)];
222
 
223
+ for ch in 0..3 {
224
+ fallback[ch] = a0[ch] + ((a1[ch] - a0[ch]) * pos) / band_size;
225
+ fallback[ch] = fallback[ch].clamp(0, 0xFF00);
226
+ }
227
+ }
228
+ // else: rows beyond band 5 keep the last fallback color
 
 
 
 
 
 
 
 
 
 
 
 
 
 
229
 
230
+ let color_low = (fallback[0] as u32 & 0xFFFF) | ((fallback[1] as u32 & 0xFFFF) << 16);
231
+ let color_high = fallback[2] as u32 & 0xFFFF;
 
232
 
233
+ // Write to all 64 columns (matches GradientImage__WriteRow at 0x4F91C0)
234
  let row_base = data.add(row as usize * stride as usize);
235
  for col in 0..64u32 {
236
  let pixel = row_base.add(col as usize * 8) as *mut u32;
 
239
  }
240
  }
241
 
242
+ // Step 8: Wrap pixel data in a DisplayGfx-compatible object
243
  let gfx_obj = wa_malloc(0x2C);
244
  if gfx_obj.is_null() {
245
  wa_free(data);
 
246
  return;
247
  }
248
  core::ptr::write_bytes(gfx_obj, 0, 0x2C);
 
251
  *g.add(2) = data as u32; // pixel data
252
  *g.add(4) = stride; // bytes per row
253
  *g.add(5) = 0; // x offset
254
+ *g.add(6) = total_height as u32; // height (CTaskLand checks `if (0 < [6])`)
255
  (*ddgame).gradient_image = gfx_obj;
 
 
 
256
  }