paavohtl Claude Opus 4.6 (1M context) commited on
Commit
2d732a2
·
1 Parent(s): a2be614

refactor: extract gradient computation to display/gradient.rs

Browse files

Move PaletteContext struct/impl and compute_complex_gradient function.
PaletteContext is private (implementation detail of gradient math).

ddgame.rs: 2250 → 1965 lines (-285)
New: display/gradient.rs (296 lines)

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

crates/openwa-core/src/display/gradient.rs ADDED
@@ -0,0 +1,290 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ //! Sky gradient computation for non-standard level heights.
2
+
3
+ use crate::engine::ddgame::DDGame;
4
+ use crate::rebase::rb;
5
+ 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,
27
+ used_list: [0; 256],
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;
48
+ self.used_list[self.used_count] = idx as u8;
49
+ self.used_count += 1;
50
+ return Some(idx as u8);
51
+ }
52
+ }
53
+ None
54
+ }
55
+
56
+ /// Find the closest palette match for an RGB color (components in 8.8 fixed-point).
57
+ /// Returns (palette_index, error_percentage).
58
+ fn find_closest(&self, r: i32, g: i32, b: i32) -> (u8, i32) {
59
+ let mut best_idx = 0u8;
60
+ let mut best_dist = i32::MAX;
61
+ for i in 0..self.used_count {
62
+ let idx = self.used_list[i] as usize;
63
+ if idx == 0 {
64
+ continue;
65
+ }
66
+ let c = self.colors[idx];
67
+ let cr = ((c & 0xFF) as i32) * 0x100;
68
+ let cg = (((c >> 8) & 0xFF) as i32) * 0x100;
69
+ let cb = (((c >> 16) & 0xFF) as i32) * 0x100;
70
+ // Weighted perceptual distance: R*3 + G*5 + B*2
71
+ let dist = (r - cr).abs() * 3 + (g - cg).abs() * 5 + (b - cb).abs() * 2;
72
+ if dist == 0 {
73
+ return (idx as u8, 0);
74
+ }
75
+ if dist < best_dist {
76
+ best_dist = dist;
77
+ best_idx = idx as u8;
78
+ }
79
+ }
80
+ let error = if best_dist < i32::MAX {
81
+ (best_dist * 100) / 0x2FD00
82
+ } else {
83
+ 100
84
+ };
85
+ (best_idx, error)
86
+ }
87
+
88
+ /// Read RGB color for a palette index, in 8.8 fixed-point format.
89
+ /// Returns (r, g, b) each in range 0..0xFF00.
90
+ fn read_color_fp(&self, idx: u8) -> (i32, i32, i32) {
91
+ let c = self.colors[idx as usize];
92
+ let r = ((c & 0xFF) as i32) << 8;
93
+ let g = (((c >> 8) & 0xFF) as i32) << 8;
94
+ let b = (((c >> 16) & 0xFF) as i32) << 8;
95
+ (r, g, b)
96
+ }
97
+ }
98
+
99
+ /// Compute the complex sky gradient for non-standard level heights.
100
+ ///
101
+ /// Called when `sky_height >= 0x61 OR level_height != 0x2B8`.
102
+ /// Loads gradient.img, samples 7 anchor colors, then creates an interpolated
103
+ /// gradient image (64 columns × (level_height + 0xDC) rows).
104
+ ///
105
+ /// The result is stored at `DDGame+0x30` (gradient_image) and optionally
106
+ /// `DDGame+0x34` (gradient_image_2).
107
+ #[cfg(target_arch = "x86")]
108
+ pub(crate) unsafe fn compute_complex_gradient(
109
+ ddgame: *mut DDGame,
110
+ land_layer: *mut u8,
111
+ layer3_ctx: *mut u8,
112
+ sky_height: i16,
113
+ ) {
114
+ let mut palette = PaletteContext::new();
115
+
116
+ // Load gradient.img
117
+ let gradient_sprite =
118
+ call_gfx_find_and_load(land_layer, b"gradient.img\0".as_ptr(), layer3_ctx);
119
+ if gradient_sprite.is_null() {
120
+ return; // No gradient available — stub fallback handles this
121
+ }
122
+
123
+ // Read gradient sprite dimensions via vtable[4] (get_pixel)
124
+ // Sprite height is at sprite+0x18 (field [6] in u32 layout)
125
+ let gradient_height = *(gradient_sprite.add(0x18) as *const i32);
126
+ if gradient_height <= 0 {
127
+ return;
128
+ }
129
+
130
+ let get_pixel: unsafe extern "thiscall" fn(*mut u8, i32, i32) -> u32 =
131
+ core::mem::transmute(*(*(gradient_sprite as *const *const u32)).add(4));
132
+
133
+ // Compute target rows for stretching
134
+ let target_rows = {
135
+ let max_rows = 0x70i32 - sky_height as i32;
136
+ if gradient_height < max_rows {
137
+ gradient_height
138
+ } else {
139
+ max_rows
140
+ }
141
+ };
142
+
143
+ // Stretch gradient through palette: map each source row's pixel color
144
+ if target_rows > 0 {
145
+ let mut src_pos = 0i32;
146
+ for _ in 0..target_rows {
147
+ let src_row = src_pos / target_rows;
148
+ let color = get_pixel(gradient_sprite, 0, src_row);
149
+ palette.map_color(color);
150
+ src_pos += gradient_height;
151
+ }
152
+ }
153
+
154
+ // If target_rows == gradient_height, also set gradient_image_2
155
+ if target_rows == gradient_height {
156
+ let gradient2 = call_gfx_find_and_load(land_layer, b"gradient.img\0".as_ptr(), layer3_ctx);
157
+ (*ddgame).gradient_image_2 = gradient2;
158
+ }
159
+
160
+ // Sample 7 anchor colors by averaging 8×2 pixel blocks
161
+ let mut anchors = [[0i32; 3]; 7]; // [band][r, g, b] in shifted format
162
+ let grad_height_minus2 = (gradient_height - 2).max(0);
163
+
164
+ for band in 0..7u32 {
165
+ let start_row = if grad_height_minus2 > 0 {
166
+ (band as i32 * grad_height_minus2) / 6
167
+ } else {
168
+ 0
169
+ };
170
+ let mut r_sum = 0i32;
171
+ let mut g_sum = 0i32;
172
+ let mut b_sum = 0i32;
173
+
174
+ for dy in 0..2i32 {
175
+ let row = start_row + dy;
176
+ for col in 0..8i32 {
177
+ let pixel = get_pixel(gradient_sprite, col, row);
178
+ let pidx = pixel as usize;
179
+ if pidx < 256 && palette.valid[pidx] {
180
+ let c = palette.colors[pidx];
181
+ r_sum += (c & 0xFF) as i32;
182
+ g_sum += ((c >> 8) & 0xFF) as i32;
183
+ b_sum += ((c >> 16) & 0xFF) as i32;
184
+ }
185
+ }
186
+ }
187
+
188
+ // Shift left 4 (multiply by 16) — converts 8-bit×16 samples to 8.8 fixed-point
189
+ anchors[band as usize] = [r_sum << 4, g_sum << 4, b_sum << 4];
190
+ }
191
+
192
+ // Release the gradient sprite
193
+ let gvt = *(gradient_sprite as *const *const u32);
194
+ let release: unsafe extern "thiscall" fn(*mut u8, u8) = core::mem::transmute(*gvt.add(3));
195
+ release(gradient_sprite, 1);
196
+
197
+ // Create the gradient image
198
+ let total_height = (*ddgame).level_height as i32 + 0xDC;
199
+ if total_height <= 0 {
200
+ return;
201
+ }
202
+
203
+ let stride = 0x200u32; // 64 columns × 8 bytes per pixel
204
+ let data_size = total_height as u32 * stride;
205
+ let data = wa_malloc(data_size + 0x20);
206
+ if data.is_null() {
207
+ return;
208
+ }
209
+ core::ptr::write_bytes(data, 0, data_size as usize);
210
+
211
+ // Build raw image header (0x44 bytes, same layout GradientImage__WriteRow expects)
212
+ let header = wa_malloc(0x44);
213
+ if header.is_null() {
214
+ wa_free(data);
215
+ return;
216
+ }
217
+ core::ptr::write_bytes(header, 0, 0x44);
218
+ let h = header as *mut u32;
219
+ *h.add(0) = data as u32; // data pointer
220
+ *h.add(1) = stride; // row stride
221
+ *h.add(2) = 0x40; // max columns (64)
222
+ *h.add(3) = total_height as u32; // height
223
+ *h.add(4) = 0; // bounds left
224
+ *h.add(5) = 0; // bounds top
225
+ *h.add(6) = 0x40; // bounds right
226
+ *h.add(7) = total_height as u32; // bounds bottom
227
+
228
+ // Interpolate between anchor colors and write each row
229
+ let band_size = total_height / 6;
230
+ for row in 0..total_height {
231
+ let band_idx = ((row * 6) / total_height).min(5) as usize;
232
+ let pos = row - band_idx as i32 * band_size;
233
+
234
+ // Interpolate RGB between anchor[band_idx] and anchor[band_idx+1]
235
+ let a0 = &anchors[band_idx];
236
+ let a1 = &anchors[(band_idx + 1).min(6)];
237
+
238
+ let r = if band_size > 0 {
239
+ a0[0] + ((a1[0] - a0[0]) * pos) / band_size
240
+ } else {
241
+ a0[0]
242
+ };
243
+ let g = if band_size > 0 {
244
+ a0[1] + ((a1[1] - a0[1]) * pos) / band_size
245
+ } else {
246
+ a0[1]
247
+ };
248
+ let b = if band_size > 0 {
249
+ a0[2] + ((a1[2] - a0[2]) * pos) / band_size
250
+ } else {
251
+ a0[2]
252
+ };
253
+
254
+ // Clamp to [0, 0xFF00]
255
+ let r = r.clamp(0, 0xFF00) as u32;
256
+ let g = g.clamp(0, 0xFF00) as u32;
257
+ let b = b.clamp(0, 0xFF00) as u32;
258
+
259
+ // Pack: color_low = R | (G << 16), color_high = B
260
+ let color_low = (r & 0xFFFF) | ((g & 0xFFFF) << 16);
261
+ let color_high = b & 0xFFFF;
262
+
263
+ // Write to all 64 columns
264
+ let row_base = data.add(row as usize * stride as usize);
265
+ for col in 0..64u32 {
266
+ let pixel = row_base.add(col as usize * 8) as *mut u32;
267
+ *pixel = color_low;
268
+ *pixel.add(1) = color_high;
269
+ }
270
+ }
271
+
272
+ // Wrap in DisplayGfx: use the same vtable as the gradient stub
273
+ let gfx_obj = wa_malloc(0x2C);
274
+ if gfx_obj.is_null() {
275
+ wa_free(data);
276
+ wa_free(header);
277
+ return;
278
+ }
279
+ core::ptr::write_bytes(gfx_obj, 0, 0x2C);
280
+ let g = gfx_obj as *mut u32;
281
+ *g = rb(0x6640EC); // vtable
282
+ *g.add(2) = data as u32; // pixel data
283
+ *g.add(4) = stride; // bytes per row
284
+ *g.add(5) = 0; // x offset
285
+ *g.add(6) = total_height as u32; // height (checked by CTaskLand: `if (0 < [6])`)
286
+ (*ddgame).gradient_image = gfx_obj;
287
+
288
+ // Free the raw header (DisplayGfx wrapper now owns the pixel data)
289
+ wa_free(header);
290
+ }
crates/openwa-core/src/display/mod.rs CHANGED
@@ -1,6 +1,7 @@
1
  pub mod base;
2
  pub mod dd_display;
3
  pub mod gfx;
 
4
  pub mod opengl;
5
  pub mod palette;
6
 
 
1
  pub mod base;
2
  pub mod dd_display;
3
  pub mod gfx;
4
+ pub mod gradient;
5
  pub mod opengl;
6
  pub mod palette;
7
 
crates/openwa-core/src/engine/ddgame.rs CHANGED
@@ -4,6 +4,7 @@ use crate::audio::dssound::DSSound;
4
  use crate::audio::music::Music;
5
  use crate::audio::speech::SpeechSlotTable;
6
  use crate::display::dd_display::DDDisplay;
 
7
  use crate::display::palette::Palette;
8
  use crate::engine::ddgame_wrapper::DDGameWrapper;
9
  use crate::engine::game_info::GameInfo;
@@ -19,7 +20,7 @@ pub use crate::render::gfx_handler::{
19
  use crate::render::landscape::PCLandscape;
20
  use crate::render::queue::RenderQueue;
21
  use crate::render::turn_order::TurnOrderWidget;
22
- use crate::wa_alloc::{wa_free, wa_malloc};
23
 
24
  /// DDGame — the main game engine object.
25
  ///
@@ -1365,292 +1366,6 @@ unsafe fn init_graphics_and_resources(
1365
  let _ = crate::log::log_line("[DDGame] init_graphics_and_resources DONE");
1366
  }
1367
 
1368
- // ── Complex gradient computation ──────────────────────────────────────────────
1369
-
1370
- /// Palette context for gradient color mapping.
1371
- /// Matches the WA stack-allocated structure used by PaletteContext__Init (0x5411A0).
1372
- struct PaletteContext {
1373
- min_index: u8,
1374
- max_index: u8,
1375
- colors: [u32; 256], // RGB packed, indexed by palette index
1376
- valid: [bool; 256], // whether index has a color assigned
1377
- used_count: usize,
1378
- used_list: [u8; 256], // palette indices currently in use
1379
- }
1380
-
1381
- impl PaletteContext {
1382
- fn new() -> Self {
1383
- Self {
1384
- min_index: 0,
1385
- max_index: 0xFF,
1386
- colors: [0; 256],
1387
- valid: [false; 256],
1388
- used_count: 0,
1389
- used_list: [0; 256],
1390
- }
1391
- }
1392
-
1393
- /// Map an RGB color to a palette index. Returns existing index if color
1394
- /// already mapped, otherwise allocates a new one. Returns None if full.
1395
- fn map_color(&mut self, rgb: u32) -> Option<u8> {
1396
- let masked = rgb & 0x00FF_FFFF;
1397
- // Search existing entries
1398
- for i in 0..self.used_count {
1399
- let idx = self.used_list[i] as usize;
1400
- if idx != 0 && (self.colors[idx] & 0x00FF_FFFF) == masked {
1401
- return Some(idx as u8);
1402
- }
1403
- }
1404
- // Allocate new: use indices from max_index downward
1405
- // Find first unused index
1406
- for idx in (self.min_index as usize..=self.max_index as usize).rev() {
1407
- if !self.valid[idx] {
1408
- self.colors[idx] = masked;
1409
- self.valid[idx] = true;
1410
- self.used_list[self.used_count] = idx as u8;
1411
- self.used_count += 1;
1412
- return Some(idx as u8);
1413
- }
1414
- }
1415
- None
1416
- }
1417
-
1418
- /// Find the closest palette match for an RGB color (components in 8.8 fixed-point).
1419
- /// Returns (palette_index, error_percentage).
1420
- fn find_closest(&self, r: i32, g: i32, b: i32) -> (u8, i32) {
1421
- let mut best_idx = 0u8;
1422
- let mut best_dist = i32::MAX;
1423
- for i in 0..self.used_count {
1424
- let idx = self.used_list[i] as usize;
1425
- if idx == 0 {
1426
- continue;
1427
- }
1428
- let c = self.colors[idx];
1429
- let cr = ((c & 0xFF) as i32) * 0x100;
1430
- let cg = (((c >> 8) & 0xFF) as i32) * 0x100;
1431
- let cb = (((c >> 16) & 0xFF) as i32) * 0x100;
1432
- // Weighted perceptual distance: R*3 + G*5 + B*2
1433
- let dist = (r - cr).abs() * 3 + (g - cg).abs() * 5 + (b - cb).abs() * 2;
1434
- if dist == 0 {
1435
- return (idx as u8, 0);
1436
- }
1437
- if dist < best_dist {
1438
- best_dist = dist;
1439
- best_idx = idx as u8;
1440
- }
1441
- }
1442
- let error = if best_dist < i32::MAX {
1443
- (best_dist * 100) / 0x2FD00
1444
- } else {
1445
- 100
1446
- };
1447
- (best_idx, error)
1448
- }
1449
-
1450
- /// Read RGB color for a palette index, in 8.8 fixed-point format.
1451
- /// Returns (r, g, b) each in range 0..0xFF00.
1452
- fn read_color_fp(&self, idx: u8) -> (i32, i32, i32) {
1453
- let c = self.colors[idx as usize];
1454
- let r = ((c & 0xFF) as i32) << 8;
1455
- let g = (((c >> 8) & 0xFF) as i32) << 8;
1456
- let b = (((c >> 16) & 0xFF) as i32) << 8;
1457
- (r, g, b)
1458
- }
1459
- }
1460
-
1461
- /// Compute the complex sky gradient for non-standard level heights.
1462
- ///
1463
- /// Called when `sky_height >= 0x61 OR level_height != 0x2B8`.
1464
- /// Loads gradient.img, samples 7 anchor colors, then creates an interpolated
1465
- /// gradient image (64 columns × (level_height + 0xDC) rows).
1466
- ///
1467
- /// The result is stored at `DDGame+0x30` (gradient_image) and optionally
1468
- /// `DDGame+0x34` (gradient_image_2).
1469
- #[cfg(target_arch = "x86")]
1470
- unsafe fn compute_complex_gradient(
1471
- ddgame: *mut DDGame,
1472
- land_layer: *mut u8,
1473
- layer3_ctx: *mut u8,
1474
- sky_height: i16,
1475
- ) {
1476
- let mut palette = PaletteContext::new();
1477
-
1478
- // Load gradient.img
1479
- let gradient_sprite =
1480
- call_gfx_find_and_load(land_layer, b"gradient.img\0".as_ptr(), layer3_ctx);
1481
- if gradient_sprite.is_null() {
1482
- return; // No gradient available — stub fallback handles this
1483
- }
1484
-
1485
- // Read gradient sprite dimensions via vtable[4] (get_pixel)
1486
- // Sprite height is at sprite+0x18 (field [6] in u32 layout)
1487
- let gradient_height = *(gradient_sprite.add(0x18) as *const i32);
1488
- if gradient_height <= 0 {
1489
- return;
1490
- }
1491
-
1492
- let get_pixel: unsafe extern "thiscall" fn(*mut u8, i32, i32) -> u32 =
1493
- core::mem::transmute(*(*(gradient_sprite as *const *const u32)).add(4));
1494
-
1495
- // Compute target rows for stretching
1496
- let target_rows = {
1497
- let max_rows = 0x70i32 - sky_height as i32;
1498
- if gradient_height < max_rows {
1499
- gradient_height
1500
- } else {
1501
- max_rows
1502
- }
1503
- };
1504
-
1505
- // Stretch gradient through palette: map each source row's pixel color
1506
- if target_rows > 0 {
1507
- let mut src_pos = 0i32;
1508
- for _ in 0..target_rows {
1509
- let src_row = src_pos / target_rows;
1510
- let color = get_pixel(gradient_sprite, 0, src_row);
1511
- palette.map_color(color);
1512
- src_pos += gradient_height;
1513
- }
1514
- }
1515
-
1516
- // If target_rows == gradient_height, also set gradient_image_2
1517
- if target_rows == gradient_height {
1518
- let gradient2 = call_gfx_find_and_load(land_layer, b"gradient.img\0".as_ptr(), layer3_ctx);
1519
- (*ddgame).gradient_image_2 = gradient2;
1520
- }
1521
-
1522
- // Sample 7 anchor colors by averaging 8×2 pixel blocks
1523
- let mut anchors = [[0i32; 3]; 7]; // [band][r, g, b] in shifted format
1524
- let grad_height_minus2 = (gradient_height - 2).max(0);
1525
-
1526
- for band in 0..7u32 {
1527
- let start_row = if grad_height_minus2 > 0 {
1528
- (band as i32 * grad_height_minus2) / 6
1529
- } else {
1530
- 0
1531
- };
1532
- let mut r_sum = 0i32;
1533
- let mut g_sum = 0i32;
1534
- let mut b_sum = 0i32;
1535
-
1536
- for dy in 0..2i32 {
1537
- let row = start_row + dy;
1538
- for col in 0..8i32 {
1539
- let pixel = get_pixel(gradient_sprite, col, row);
1540
- let pidx = pixel as usize;
1541
- if pidx < 256 && palette.valid[pidx] {
1542
- let c = palette.colors[pidx];
1543
- r_sum += (c & 0xFF) as i32;
1544
- g_sum += ((c >> 8) & 0xFF) as i32;
1545
- b_sum += ((c >> 16) & 0xFF) as i32;
1546
- }
1547
- }
1548
- }
1549
-
1550
- // Shift left 4 (multiply by 16) — converts 8-bit×16 samples to 8.8 fixed-point
1551
- anchors[band as usize] = [r_sum << 4, g_sum << 4, b_sum << 4];
1552
- }
1553
-
1554
- // Release the gradient sprite
1555
- let gvt = *(gradient_sprite as *const *const u32);
1556
- let release: unsafe extern "thiscall" fn(*mut u8, u8) = core::mem::transmute(*gvt.add(3));
1557
- release(gradient_sprite, 1);
1558
-
1559
- // Create the gradient image
1560
- let total_height = (*ddgame).level_height as i32 + 0xDC;
1561
- if total_height <= 0 {
1562
- return;
1563
- }
1564
-
1565
- let stride = 0x200u32; // 64 columns × 8 bytes per pixel
1566
- let data_size = total_height as u32 * stride;
1567
- let data = wa_malloc(data_size + 0x20);
1568
- if data.is_null() {
1569
- return;
1570
- }
1571
- core::ptr::write_bytes(data, 0, data_size as usize);
1572
-
1573
- // Build raw image header (0x44 bytes, same layout GradientImage__WriteRow expects)
1574
- let header = wa_malloc(0x44);
1575
- if header.is_null() {
1576
- wa_free(data);
1577
- return;
1578
- }
1579
- core::ptr::write_bytes(header, 0, 0x44);
1580
- let h = header as *mut u32;
1581
- *h.add(0) = data as u32; // data pointer
1582
- *h.add(1) = stride; // row stride
1583
- *h.add(2) = 0x40; // max columns (64)
1584
- *h.add(3) = total_height as u32; // height
1585
- *h.add(4) = 0; // bounds left
1586
- *h.add(5) = 0; // bounds top
1587
- *h.add(6) = 0x40; // bounds right
1588
- *h.add(7) = total_height as u32; // bounds bottom
1589
-
1590
- // Interpolate between anchor colors and write each row
1591
- let band_size = total_height / 6;
1592
- for row in 0..total_height {
1593
- let band_idx = ((row * 6) / total_height).min(5) as usize;
1594
- let pos = row - band_idx as i32 * band_size;
1595
-
1596
- // Interpolate RGB between anchor[band_idx] and anchor[band_idx+1]
1597
- let a0 = &anchors[band_idx];
1598
- let a1 = &anchors[(band_idx + 1).min(6)];
1599
-
1600
- let r = if band_size > 0 {
1601
- a0[0] + ((a1[0] - a0[0]) * pos) / band_size
1602
- } else {
1603
- a0[0]
1604
- };
1605
- let g = if band_size > 0 {
1606
- a0[1] + ((a1[1] - a0[1]) * pos) / band_size
1607
- } else {
1608
- a0[1]
1609
- };
1610
- let b = if band_size > 0 {
1611
- a0[2] + ((a1[2] - a0[2]) * pos) / band_size
1612
- } else {
1613
- a0[2]
1614
- };
1615
-
1616
- // Clamp to [0, 0xFF00]
1617
- let r = r.clamp(0, 0xFF00) as u32;
1618
- let g = g.clamp(0, 0xFF00) as u32;
1619
- let b = b.clamp(0, 0xFF00) as u32;
1620
-
1621
- // Pack: color_low = R | (G << 16), color_high = B
1622
- let color_low = (r & 0xFFFF) | ((g & 0xFFFF) << 16);
1623
- let color_high = b & 0xFFFF;
1624
-
1625
- // Write to all 64 columns
1626
- let row_base = data.add(row as usize * stride as usize);
1627
- for col in 0..64u32 {
1628
- let pixel = row_base.add(col as usize * 8) as *mut u32;
1629
- *pixel = color_low;
1630
- *pixel.add(1) = color_high;
1631
- }
1632
- }
1633
-
1634
- // Wrap in DisplayGfx: use the same vtable as the gradient stub
1635
- let gfx_obj = wa_malloc(0x2C);
1636
- if gfx_obj.is_null() {
1637
- wa_free(data);
1638
- wa_free(header);
1639
- return;
1640
- }
1641
- core::ptr::write_bytes(gfx_obj, 0, 0x2C);
1642
- let g = gfx_obj as *mut u32;
1643
- *g = rb(0x6640EC); // vtable
1644
- *g.add(2) = data as u32; // pixel data
1645
- *g.add(4) = stride; // bytes per row
1646
- *g.add(5) = 0; // x offset
1647
- *g.add(6) = total_height as u32; // height (checked by CTaskLand: `if (0 < [6])`)
1648
- (*ddgame).gradient_image = gfx_obj;
1649
-
1650
- // Free the raw header (DisplayGfx wrapper now owns the pixel data)
1651
- wa_free(header);
1652
- }
1653
-
1654
  // Statics for usercall bridge addresses
1655
  static mut FUN_570A90_ADDR: u32 = 0;
1656
  static mut FUN_570F30_ADDR: u32 = 0;
 
4
  use crate::audio::music::Music;
5
  use crate::audio::speech::SpeechSlotTable;
6
  use crate::display::dd_display::DDDisplay;
7
+ use crate::display::gradient::compute_complex_gradient;
8
  use crate::display::palette::Palette;
9
  use crate::engine::ddgame_wrapper::DDGameWrapper;
10
  use crate::engine::game_info::GameInfo;
 
20
  use crate::render::landscape::PCLandscape;
21
  use crate::render::queue::RenderQueue;
22
  use crate::render::turn_order::TurnOrderWidget;
23
+ use crate::wa_alloc::wa_malloc;
24
 
25
  /// DDGame — the main game engine object.
26
  ///
 
1366
  let _ = crate::log::log_line("[DDGame] init_graphics_and_resources DONE");
1367
  }
1368
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1369
  // Statics for usercall bridge addresses
1370
  static mut FUN_570A90_ADDR: u32 = 0;
1371
  static mut FUN_570F30_ADDR: u32 = 0;