siglus_scene_vm/runtime/
tonecurve.rs1use std::path::{Path, PathBuf};
2
3use anyhow::Result;
4use siglus_assets::gameexe::{GameexeConfig, decode_gameexe_dat_bytes};
5
6use crate::assets::RgbaImage;
7use crate::image_manager::{ImageHandle, ImageManager};
8
9#[derive(Debug, Clone)]
10struct ToneCurveRow {
11 lut_r: [u8; 256],
12 lut_g: [u8; 256],
13 lut_b: [u8; 256],
14 sat: i32,
15}
16
17#[derive(Debug, Default)]
18pub struct ToneCurveRuntime {
19 rows: Option<Vec<Option<ToneCurveRow>>>,
20 source_path: Option<PathBuf>,
21 lut_image_id: Option<ImageHandle>,
22}
23
24impl ToneCurveRuntime {
25 pub fn new(project_dir: &Path) -> Self {
26 let mut out = Self::default();
27 if let Some(path) = find_tonecurve_path(project_dir) {
28 out.source_path = Some(path.clone());
29 out.rows = load_tonecurve_rows(&path).ok();
30 }
31 out
32 }
33
34 pub fn has_table(&self) -> bool {
35 self.rows.is_some()
36 }
37
38 pub fn shader_binding(
39 &mut self,
40 images: &mut ImageManager,
41 tonecurve_no: i32,
42 ) -> Option<(ImageHandle, f32, f32)> {
43 let idx = tonecurve_no.max(0) as usize;
44 let row = self.rows.as_ref()?.get(idx)?.as_ref()?.clone();
45 let lut_id = self.ensure_lut_image(images)?;
46 let row_y = ((idx.min(255) as f32) + 0.5) / 256.0;
47 let sat = if row.sat < 0 {
48 ((-row.sat) as f32 / 100.0).clamp(0.0, 1.0)
49 } else {
50 0.0
51 };
52 Some((lut_id, row_y, sat))
53 }
54
55 fn ensure_lut_image(&mut self, images: &mut ImageManager) -> Option<ImageHandle> {
56 if let Some(ref id) = self.lut_image_id {
57 return Some(id.clone());
58 }
59 let rows = self.rows.as_ref()?;
60 let mut rgba = vec![0u8; 256 * 256 * 4];
61 for y in 0..256usize {
62 for x in 0..256usize {
63 let idx = (y * 256 + x) * 4;
64 if let Some(Some(row)) = rows.get(y) {
65 rgba[idx] = row.lut_r[x];
66 rgba[idx + 1] = row.lut_g[x];
67 rgba[idx + 2] = row.lut_b[x];
68 } else {
69 let v = x as u8;
70 rgba[idx] = v;
71 rgba[idx + 1] = v;
72 rgba[idx + 2] = v;
73 }
74 rgba[idx + 3] = 255;
75 }
76 }
77 let id = images.insert_image(RgbaImage {
78 width: 256,
79 height: 256,
80 center_x: 0,
81 center_y: 0,
82 rgba,
83 });
84 self.lut_image_id = Some(id.clone());
85 Some(id)
86 }
87}
88
89fn find_tonecurve_path(project_dir: &Path) -> Option<PathBuf> {
90 let cfg = load_gameexe_config(project_dir)?;
91 let rel = cfg.get_unquoted("TONECURVE_FILE")?;
92 if rel.trim().is_empty() {
93 return None;
94 }
95 let p = PathBuf::from(rel.trim());
96 if p.is_absolute() {
97 return crate::resource::resolve_game_file(&p).ok().flatten();
98 }
99 let direct = project_dir.join(&p);
100 if let Some(path) = crate::resource::resolve_game_file(&direct).ok().flatten() {
101 return Some(path);
102 }
103 let dat = project_dir.join("dat").join(&p);
104 crate::resource::resolve_game_file(&dat).ok().flatten()
105}
106
107fn load_gameexe_config(project_dir: &Path) -> Option<GameexeConfig> {
108 let path = find_gameexe_path(project_dir)?;
109 let raw = crate::resource::read_file_bytes(&path).ok()?;
110 if path
111 .extension()
112 .and_then(|s| s.to_str())
113 .is_some_and(|ext| ext.eq_ignore_ascii_case("ini"))
114 {
115 let text = String::from_utf8(raw).ok()?;
116 return Some(GameexeConfig::from_text(&text));
117 }
118 let opt = crate::resource::load_gameexe_decode_options(project_dir).ok()?;
119 let (text, _report) = decode_gameexe_dat_bytes(&raw, &opt).ok()?;
120 Some(GameexeConfig::from_text(&text))
121}
122
123fn find_gameexe_path(project_dir: &Path) -> Option<PathBuf> {
124 const CANDIDATES: &[&str] = &[
125 "Gameexe.dat",
126 "Gameexe.ini",
127 "gameexe.dat",
128 "gameexe.ini",
129 "GameexeEN.dat",
130 "GameexeEN.ini",
131 "GameexeZH.dat",
132 "GameexeZH.ini",
133 "GameexeZHTW.dat",
134 "GameexeZHTW.ini",
135 "GameexeDE.dat",
136 "GameexeDE.ini",
137 "GameexeES.dat",
138 "GameexeES.ini",
139 "GameexeFR.dat",
140 "GameexeFR.ini",
141 "GameexeID.dat",
142 "GameexeID.ini",
143 ];
144 for name in CANDIDATES {
145 let p = project_dir.join(name);
146 if let Some(path) = crate::resource::resolve_game_file(&p).ok().flatten() {
147 return Some(path);
148 }
149 }
150 None
151}
152
153fn read_i32_le(data: &[u8], off: usize) -> Option<i32> {
154 let bytes: [u8; 4] = data.get(off..off + 4)?.try_into().ok()?;
155 Some(i32::from_le_bytes(bytes))
156}
157
158fn load_tonecurve_rows(path: &Path) -> Result<Vec<Option<ToneCurveRow>>> {
159 let data = crate::resource::read_file_bytes(path)?;
160 if data.len() < 8 {
161 anyhow::bail!("tonecurve file too small");
162 }
163 let cnt = read_i32_le(&data, 4).unwrap_or(0).clamp(0, 256) as usize;
164 let mut out = vec![None; 256];
165 for i in 0..cnt {
166 let off = read_i32_le(&data, 8 + i * 4).unwrap_or(0);
167 if off <= 0 {
168 continue;
169 }
170 let off = off as usize;
171 if off + 16 * 4 + 768 > data.len() {
172 continue;
173 }
174 let typ = read_i32_le(&data, off).unwrap_or(0);
175 let base = off + 16 * 4;
176 let mut lut_r = [0u8; 256];
177 let mut lut_g = [0u8; 256];
178 let mut lut_b = [0u8; 256];
179 lut_r.copy_from_slice(&data[base..base + 256]);
180 lut_g.copy_from_slice(&data[base + 256..base + 512]);
181 lut_b.copy_from_slice(&data[base + 512..base + 768]);
182 let sat = if typ == 1 {
183 read_i32_le(&data, base + 768).unwrap_or(0)
184 } else {
185 0
186 };
187 out[i] = Some(ToneCurveRow {
188 lut_r,
189 lut_g,
190 lut_b,
191 sat,
192 });
193 }
194 Ok(out)
195}