paavohtl Claude Opus 4.6 (1M context) commited on
Commit
177fc4e
·
1 Parent(s): 3c72efb

feat: implement complex gradient computation for non-standard levels

Browse files

Port the ~200-line gradient interpolation algorithm from the DDGame
constructor. Creates sky gradient images for levels where the simple
gradient.img load isn't sufficient (sky_height >= 0x61 or level height
!= 0x2B8).

Algorithm:
1. Load gradient.img and build a PaletteContext (color mapping)
2. Stretch gradient to target height, mapping colors through palette
3. Sample 7 anchor colors by averaging 8x2 pixel blocks
4. Create 64-column gradient image by interpolating between anchors
5. Store at DDGame+0x30, optionally DDGame+0x34

PaletteContext is a pure Rust reimplementation of the WA palette
management functions (0x5411A0, 0x5412B0, 0x541420, 0x541230) that
use complex usercall conventions. No FFI bridges needed.

Named 5 functions in Ghidra:
- PaletteContext__Init (0x5411A0)
- PaletteContext__MapColor (0x5412B0)
- PaletteContext__FindClosest (0x541420)
- PaletteContext__ReadColor (0x541230)
- GradientImage__WriteRow (0x4F91C0)

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

crates/openwa-core/src/engine/ddgame.rs CHANGED
@@ -12,7 +12,7 @@ use crate::rebase::rb;
12
  use crate::render::landscape::PCLandscape;
13
  use crate::render::queue::RenderQueue;
14
  use crate::render::turn_order::TurnOrderWidget;
15
- use crate::wa_alloc::wa_malloc;
16
 
17
  /// DDGame — the main game engine object.
18
  ///
@@ -1583,8 +1583,7 @@ unsafe fn init_graphics_and_resources(
1583
  call_gfx_find_and_load(land_layer, b"gradient.img\0".as_ptr(), layer3_ctx);
1584
  (*ddgame).gradient_image = gradient;
1585
  } else {
1586
- // TODO: port complex gradient computation (~200 lines of color interpolation).
1587
- // Affects sky background appearance. Not yet implemented.
1588
  }
1589
 
1590
  // ── Fill image → fill_pixel (0x7338) ──
@@ -1698,6 +1697,292 @@ unsafe fn call_gfx_find_and_load(
1698
  call_gfx_load_and_wrap(gfx_dir, name, display_ctx)
1699
  }
1700
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1701
  // Statics for usercall bridge addresses
1702
  static mut FUN_570A90_ADDR: u32 = 0;
1703
  static mut FUN_570F30_ADDR: u32 = 0;
 
12
  use crate::render::landscape::PCLandscape;
13
  use crate::render::queue::RenderQueue;
14
  use crate::render::turn_order::TurnOrderWidget;
15
+ use crate::wa_alloc::{wa_free, wa_malloc};
16
 
17
  /// DDGame — the main game engine object.
18
  ///
 
1583
  call_gfx_find_and_load(land_layer, b"gradient.img\0".as_ptr(), layer3_ctx);
1584
  (*ddgame).gradient_image = gradient;
1585
  } else {
1586
+ compute_complex_gradient(ddgame, land_layer, layer3_ctx, s_var1);
 
1587
  }
1588
 
1589
  // ── Fill image → fill_pixel (0x7338) ──
 
1697
  call_gfx_load_and_wrap(gfx_dir, name, display_ctx)
1698
  }
1699
 
1700
+ // ── Complex gradient computation ──────────────────────────────────────────────
1701
+
1702
+ /// Palette context for gradient color mapping.
1703
+ /// Matches the WA stack-allocated structure used by PaletteContext__Init (0x5411A0).
1704
+ struct PaletteContext {
1705
+ min_index: u8,
1706
+ max_index: u8,
1707
+ colors: [u32; 256], // RGB packed, indexed by palette index
1708
+ valid: [bool; 256], // whether index has a color assigned
1709
+ used_count: usize,
1710
+ used_list: [u8; 256], // palette indices currently in use
1711
+ }
1712
+
1713
+ impl PaletteContext {
1714
+ fn new() -> Self {
1715
+ Self {
1716
+ min_index: 0,
1717
+ max_index: 0xFF,
1718
+ colors: [0; 256],
1719
+ valid: [false; 256],
1720
+ used_count: 0,
1721
+ used_list: [0; 256],
1722
+ }
1723
+ }
1724
+
1725
+ /// Map an RGB color to a palette index. Returns existing index if color
1726
+ /// already mapped, otherwise allocates a new one. Returns None if full.
1727
+ fn map_color(&mut self, rgb: u32) -> Option<u8> {
1728
+ let masked = rgb & 0x00FF_FFFF;
1729
+ // Search existing entries
1730
+ for i in 0..self.used_count {
1731
+ let idx = self.used_list[i] as usize;
1732
+ if idx != 0 && (self.colors[idx] & 0x00FF_FFFF) == masked {
1733
+ return Some(idx as u8);
1734
+ }
1735
+ }
1736
+ // Allocate new: use indices from max_index downward
1737
+ // Find first unused index
1738
+ for idx in (self.min_index as usize..=self.max_index as usize).rev() {
1739
+ if !self.valid[idx] {
1740
+ self.colors[idx] = masked;
1741
+ self.valid[idx] = true;
1742
+ self.used_list[self.used_count] = idx as u8;
1743
+ self.used_count += 1;
1744
+ return Some(idx as u8);
1745
+ }
1746
+ }
1747
+ None
1748
+ }
1749
+
1750
+ /// Find the closest palette match for an RGB color (components in 8.8 fixed-point).
1751
+ /// Returns (palette_index, error_percentage).
1752
+ fn find_closest(&self, r: i32, g: i32, b: i32) -> (u8, i32) {
1753
+ let mut best_idx = 0u8;
1754
+ let mut best_dist = i32::MAX;
1755
+ for i in 0..self.used_count {
1756
+ let idx = self.used_list[i] as usize;
1757
+ if idx == 0 {
1758
+ continue;
1759
+ }
1760
+ let c = self.colors[idx];
1761
+ let cr = ((c & 0xFF) as i32) * 0x100;
1762
+ let cg = (((c >> 8) & 0xFF) as i32) * 0x100;
1763
+ let cb = (((c >> 16) & 0xFF) as i32) * 0x100;
1764
+ // Weighted perceptual distance: R*3 + G*5 + B*2
1765
+ let dist = (r - cr).abs() * 3 + (g - cg).abs() * 5 + (b - cb).abs() * 2;
1766
+ if dist == 0 {
1767
+ return (idx as u8, 0);
1768
+ }
1769
+ if dist < best_dist {
1770
+ best_dist = dist;
1771
+ best_idx = idx as u8;
1772
+ }
1773
+ }
1774
+ let error = if best_dist < i32::MAX {
1775
+ (best_dist * 100) / 0x2FD00
1776
+ } else {
1777
+ 100
1778
+ };
1779
+ (best_idx, error)
1780
+ }
1781
+
1782
+ /// Read RGB color for a palette index, in 8.8 fixed-point format.
1783
+ /// Returns (r, g, b) each in range 0..0xFF00.
1784
+ fn read_color_fp(&self, idx: u8) -> (i32, i32, i32) {
1785
+ let c = self.colors[idx as usize];
1786
+ let r = ((c & 0xFF) as i32) << 8;
1787
+ let g = (((c >> 8) & 0xFF) as i32) << 8;
1788
+ let b = (((c >> 16) & 0xFF) as i32) << 8;
1789
+ (r, g, b)
1790
+ }
1791
+ }
1792
+
1793
+ /// Compute the complex sky gradient for non-standard level heights.
1794
+ ///
1795
+ /// Called when `sky_height >= 0x61 OR level_height != 0x2B8`.
1796
+ /// Loads gradient.img, samples 7 anchor colors, then creates an interpolated
1797
+ /// gradient image (64 columns × (level_height + 0xDC) rows).
1798
+ ///
1799
+ /// The result is stored at `DDGame+0x30` (gradient_image) and optionally
1800
+ /// `DDGame+0x34` (gradient_image_2).
1801
+ #[cfg(target_arch = "x86")]
1802
+ unsafe fn compute_complex_gradient(
1803
+ ddgame: *mut DDGame,
1804
+ land_layer: *mut u8,
1805
+ layer3_ctx: *mut u8,
1806
+ sky_height: i16,
1807
+ ) {
1808
+ let mut palette = PaletteContext::new();
1809
+
1810
+ // Load gradient.img
1811
+ let gradient_sprite =
1812
+ call_gfx_find_and_load(land_layer, b"gradient.img\0".as_ptr(), layer3_ctx);
1813
+ if gradient_sprite.is_null() {
1814
+ return; // No gradient available — stub fallback handles this
1815
+ }
1816
+
1817
+ // Read gradient sprite dimensions via vtable[4] (get_pixel)
1818
+ // Sprite height is at sprite+0x18 (field [6] in u32 layout)
1819
+ let gradient_height = *(gradient_sprite.add(0x18) as *const i32);
1820
+ if gradient_height <= 0 {
1821
+ return;
1822
+ }
1823
+
1824
+ let get_pixel: unsafe extern "thiscall" fn(*mut u8, i32, i32) -> u32 =
1825
+ core::mem::transmute(*(*(gradient_sprite as *const *const u32)).add(4));
1826
+
1827
+ // Compute target rows for stretching
1828
+ let target_rows = {
1829
+ let max_rows = 0x70i32 - sky_height as i32;
1830
+ if gradient_height < max_rows {
1831
+ gradient_height
1832
+ } else {
1833
+ max_rows
1834
+ }
1835
+ };
1836
+
1837
+ // Stretch gradient through palette: map each source row's pixel color
1838
+ if target_rows > 0 {
1839
+ let mut src_pos = 0i32;
1840
+ for _ in 0..target_rows {
1841
+ let src_row = src_pos / target_rows;
1842
+ let color = get_pixel(gradient_sprite, 0, src_row);
1843
+ palette.map_color(color);
1844
+ src_pos += gradient_height;
1845
+ }
1846
+ }
1847
+
1848
+ // If target_rows == gradient_height, also set gradient_image_2
1849
+ if target_rows == gradient_height {
1850
+ let gradient2 = call_gfx_find_and_load(land_layer, b"gradient.img\0".as_ptr(), layer3_ctx);
1851
+ (*ddgame).gradient_image_2 = gradient2;
1852
+ }
1853
+
1854
+ // Sample 7 anchor colors by averaging 8×2 pixel blocks
1855
+ let mut anchors = [[0i32; 3]; 7]; // [band][r, g, b] in shifted format
1856
+ let grad_height_minus2 = (gradient_height - 2).max(0);
1857
+
1858
+ for band in 0..7u32 {
1859
+ let start_row = if grad_height_minus2 > 0 {
1860
+ (band as i32 * grad_height_minus2) / 6
1861
+ } else {
1862
+ 0
1863
+ };
1864
+ let mut r_sum = 0i32;
1865
+ let mut g_sum = 0i32;
1866
+ let mut b_sum = 0i32;
1867
+
1868
+ for dy in 0..2i32 {
1869
+ let row = start_row + dy;
1870
+ for col in 0..8i32 {
1871
+ let pixel = get_pixel(gradient_sprite, col, row);
1872
+ let pidx = pixel as usize;
1873
+ if pidx < 256 && palette.valid[pidx] {
1874
+ let c = palette.colors[pidx];
1875
+ r_sum += (c & 0xFF) as i32;
1876
+ g_sum += ((c >> 8) & 0xFF) as i32;
1877
+ b_sum += ((c >> 16) & 0xFF) as i32;
1878
+ }
1879
+ }
1880
+ }
1881
+
1882
+ // Shift left 4 (multiply by 16) — converts 8-bit×16 samples to 8.8 fixed-point
1883
+ anchors[band as usize] = [r_sum << 4, g_sum << 4, b_sum << 4];
1884
+ }
1885
+
1886
+ // Release the gradient sprite
1887
+ let gvt = *(gradient_sprite as *const *const u32);
1888
+ let release: unsafe extern "thiscall" fn(*mut u8, u8) = core::mem::transmute(*gvt.add(3));
1889
+ release(gradient_sprite, 1);
1890
+
1891
+ // Create the gradient image
1892
+ let total_height = (*ddgame).level_height as i32 + 0xDC;
1893
+ if total_height <= 0 {
1894
+ return;
1895
+ }
1896
+
1897
+ let stride = 0x200u32; // 64 columns × 8 bytes per pixel
1898
+ let data_size = total_height as u32 * stride;
1899
+ let data = wa_malloc(data_size + 0x20);
1900
+ if data.is_null() {
1901
+ return;
1902
+ }
1903
+ core::ptr::write_bytes(data, 0, data_size as usize);
1904
+
1905
+ // Build raw image header (0x44 bytes, same layout GradientImage__WriteRow expects)
1906
+ let header = wa_malloc(0x44);
1907
+ if header.is_null() {
1908
+ wa_free(data);
1909
+ return;
1910
+ }
1911
+ core::ptr::write_bytes(header, 0, 0x44);
1912
+ let h = header as *mut u32;
1913
+ *h.add(0) = data as u32; // data pointer
1914
+ *h.add(1) = stride; // row stride
1915
+ *h.add(2) = 0x40; // max columns (64)
1916
+ *h.add(3) = total_height as u32; // height
1917
+ *h.add(4) = 0; // bounds left
1918
+ *h.add(5) = 0; // bounds top
1919
+ *h.add(6) = 0x40; // bounds right
1920
+ *h.add(7) = total_height as u32; // bounds bottom
1921
+
1922
+ // Interpolate between anchor colors and write each row
1923
+ let band_size = total_height / 6;
1924
+ for row in 0..total_height {
1925
+ let band_idx = ((row * 6) / total_height).min(5) as usize;
1926
+ let pos = row - band_idx as i32 * band_size;
1927
+
1928
+ // Interpolate RGB between anchor[band_idx] and anchor[band_idx+1]
1929
+ let a0 = &anchors[band_idx];
1930
+ let a1 = &anchors[(band_idx + 1).min(6)];
1931
+
1932
+ let r = if band_size > 0 {
1933
+ a0[0] + ((a1[0] - a0[0]) * pos) / band_size
1934
+ } else {
1935
+ a0[0]
1936
+ };
1937
+ let g = if band_size > 0 {
1938
+ a0[1] + ((a1[1] - a0[1]) * pos) / band_size
1939
+ } else {
1940
+ a0[1]
1941
+ };
1942
+ let b = if band_size > 0 {
1943
+ a0[2] + ((a1[2] - a0[2]) * pos) / band_size
1944
+ } else {
1945
+ a0[2]
1946
+ };
1947
+
1948
+ // Clamp to [0, 0xFF00]
1949
+ let r = r.clamp(0, 0xFF00) as u32;
1950
+ let g = g.clamp(0, 0xFF00) as u32;
1951
+ let b = b.clamp(0, 0xFF00) as u32;
1952
+
1953
+ // Pack: color_low = R | (G << 16), color_high = B
1954
+ let color_low = (r & 0xFFFF) | ((g & 0xFFFF) << 16);
1955
+ let color_high = b & 0xFFFF;
1956
+
1957
+ // Write to all 64 columns
1958
+ let row_base = data.add(row as usize * stride as usize);
1959
+ for col in 0..64u32 {
1960
+ let pixel = row_base.add(col as usize * 8) as *mut u32;
1961
+ *pixel = color_low;
1962
+ *pixel.add(1) = color_high;
1963
+ }
1964
+ }
1965
+
1966
+ // Wrap in DisplayGfx: use the same vtable as the gradient stub
1967
+ let gfx_obj = wa_malloc(0x2C);
1968
+ if gfx_obj.is_null() {
1969
+ wa_free(data);
1970
+ wa_free(header);
1971
+ return;
1972
+ }
1973
+ core::ptr::write_bytes(gfx_obj, 0, 0x2C);
1974
+ let g = gfx_obj as *mut u32;
1975
+ *g = rb(0x6640EC); // vtable
1976
+ *g.add(2) = data as u32; // pixel data
1977
+ *g.add(4) = stride; // bytes per row
1978
+ *g.add(5) = 0; // x offset
1979
+ *g.add(6) = total_height as u32; // height (checked by CTaskLand: `if (0 < [6])`)
1980
+ (*ddgame).gradient_image = gfx_obj;
1981
+
1982
+ // Free the raw header (DisplayGfx wrapper now owns the pixel data)
1983
+ wa_free(header);
1984
+ }
1985
+
1986
  // Statics for usercall bridge addresses
1987
  static mut FUN_570A90_ADDR: u32 = 0;
1988
  static mut FUN_570F30_ADDR: u32 = 0;