1use optic_core::consts::{OPTIC_CACHE_VERSION, OPTIC_MAGIC, SHADER_COMPUTE, SHADER_PIPELINE};
2use optic_core::{OpticError, OpticErrorKind, OpticResult};
3
4use crate::handles::shader::{link_compute_program, link_program, Shader};
5
6pub enum ShaderType {
8 Pipeline,
10 Compute,
12}
13
14impl ShaderType {
15 pub fn is_compute(&self) -> bool {
17 matches!(self, ShaderType::Compute)
18 }
19}
20
21enum GLSL {
23 ParsedCompute(String),
24 ParsedPipeline { v_src: String, f_src: String },
25 FailedPipeline { v_missing: bool, _f_missing: bool },
26}
27
28impl GLSL {
29 fn parse(src: &str, typ: &ShaderType) -> Self {
37 if typ.is_compute() {
38 return GLSL::ParsedCompute(src.to_string());
39 }
40
41 let mut v_src = String::new();
42 let mut f_src = String::new();
43 let mut v_found = false;
44 let mut f_found = false;
45 let mut cur = &mut v_src;
46
47 for line in src.lines() {
48 let line = line.trim();
49 match line {
50 "//v" | "//V" | "//vert" | "//VERT" | "//vertex" | "//VERTEX"
51 | "// v" | "// V" | "// vert" | "// VERT" | "// vertex" | "// VERTEX" => {
52 cur = &mut v_src;
53 v_found = true;
54 }
55 "//f" | "//F" | "//frag" | "//FRAG" | "//fragment" | "//FRAGMENT"
56 | "// f" | "// F" | "// frag" | "// FRAG" | "// fragment" | "// FRAGMENT" => {
57 cur = &mut f_src;
58 f_found = true;
59 }
60 _ => {
61 cur.push_str(line);
62 cur.push('\n');
63 }
64 }
65 }
66
67 let v_missing = v_src.is_empty() || !v_found;
68 let f_missing = f_src.is_empty() || !f_found;
69
70 if v_missing || f_missing {
71 GLSL::FailedPipeline { v_missing, _f_missing: f_missing }
72 } else {
73 GLSL::ParsedPipeline { v_src, f_src }
74 }
75 }
76}
77
78pub struct ShaderFile {
105 pub v_src: String,
106 pub f_src: String,
107 pub is_compute: bool,
108}
109
110impl ShaderFile {
111 pub fn from_src(src: &str, typ: ShaderType) -> OpticResult<Self> {
113 match GLSL::parse(src, &typ) {
114 GLSL::ParsedCompute(src) => Ok(Self {
115 v_src: src.clone(),
116 f_src: String::new(),
117 is_compute: true,
118 }),
119 GLSL::ParsedPipeline { v_src, f_src } => Ok(Self {
120 v_src,
121 f_src,
122 is_compute: false,
123 }),
124 GLSL::FailedPipeline { v_missing, _f_missing: _ } => {
125 if v_missing {
126 Err(OpticError::new(OpticErrorKind::Shader, "vertex shader section missing"))
127 } else {
128 Err(OpticError::new(OpticErrorKind::Shader, "fragment shader section missing"))
129 }
130 }
131 }
132 }
133
134 pub fn from_vert_frag(v_src: &str, f_src: &str) -> Self {
136 Self {
137 v_src: v_src.to_string(),
138 f_src: f_src.to_string(),
139 is_compute: false,
140 }
141 }
142
143 pub fn compile(&self) -> OpticResult<Shader> {
145 if self.is_compute {
146 let id = link_compute_program(&self.v_src)?;
147 Ok(Shader::new(id, true))
148 } else {
149 let id = link_program(&self.v_src, &self.f_src)?;
150 Ok(Shader::new(id, false))
151 }
152 }
153}
154
155#[cfg(debug_assertions)]
157impl ShaderFile {
158 pub fn from_disk(path: &str, typ: ShaderType) -> OpticResult<Self> {
160 let src = optic_file::read_string(path)?;
161 let shader = Self::from_src(&src, typ)?;
162 let cache = optic_file::cached_path(path, "oshdr");
163 shader.save_cached(&cache)?;
164 Ok(shader)
165 }
166}
167
168#[cfg(not(debug_assertions))]
169impl ShaderFile {
170 pub fn from_disk(path: &str, _typ: ShaderType) -> OpticResult<Self> {
172 let cache = optic_file::cached_path(path, "oshdr");
173 Self::from_cached(&cache)
174 }
175}
176
177impl ShaderFile {
179 pub fn save_cached(&self, path: &str) -> OpticResult<()> {
181 let typ_byte = if self.is_compute { SHADER_COMPUTE } else { SHADER_PIPELINE };
182 let v_bytes = self.v_src.as_bytes();
183 let f_bytes = self.f_src.as_bytes();
184 let mut data = Vec::with_capacity(13 + v_bytes.len() + f_bytes.len());
185 data.extend_from_slice(&OPTIC_MAGIC);
186 data.extend_from_slice(&OPTIC_CACHE_VERSION.to_le_bytes());
187 data.push(typ_byte);
188 data.extend_from_slice(&(v_bytes.len() as u32).to_le_bytes());
189 data.extend_from_slice(v_bytes);
190 data.extend_from_slice(&(f_bytes.len() as u32).to_le_bytes());
191 data.extend_from_slice(f_bytes);
192 optic_file::write_bytes(path, &data)
193 }
194
195 #[cfg_attr(debug_assertions, allow(dead_code))]
197 fn from_cached(path: &str) -> OpticResult<Self> {
198 let data = optic_file::read_bytes(path)?;
199 if data.len() < 15 {
200 return Err(OpticError::new(OpticErrorKind::Asset, &format!("cached shader too short: {path}")));
201 }
202 if data[0..8] != OPTIC_MAGIC {
203 return Err(OpticError::new(OpticErrorKind::Asset, &format!("not a valid Optic cache file (bad magic): {path}")));
204 }
205 let version = u16::from_le_bytes([data[8], data[9]]);
206 if version != OPTIC_CACHE_VERSION {
207 return Err(OpticError::new(OpticErrorKind::Asset, &format!(
208 "cache file version {version} is not supported (expected {OPTIC_CACHE_VERSION}): {path}"
209 )));
210 }
211 let is_compute = data[10] == SHADER_COMPUTE;
212
213 let mut off = 11usize;
214 let v_len = u32::from_le_bytes([data[off], data[off + 1], data[off + 2], data[off + 3]]) as usize;
215 off += 4;
216 if off + v_len > data.len() {
217 return Err(OpticError::new(OpticErrorKind::Asset, &format!("truncated cached shader (vertex section): {path}")));
218 }
219 let v_src = String::from_utf8(data[off..off + v_len].to_vec())
220 .map_err(|_| OpticError::new(OpticErrorKind::Asset, &format!("invalid UTF-8 in cached shader: {path}")))?;
221 off += v_len;
222
223 if off + 4 > data.len() {
224 return Err(OpticError::new(OpticErrorKind::Asset, &format!("truncated cached shader (fragment length): {path}")));
225 }
226 let f_len = u32::from_le_bytes([data[off], data[off + 1], data[off + 2], data[off + 3]]) as usize;
227 off += 4;
228 if off + f_len > data.len() {
229 return Err(OpticError::new(OpticErrorKind::Asset, &format!("truncated cached shader (fragment section): {path}")));
230 }
231 let f_src = if f_len > 0 {
232 String::from_utf8(data[off..off + f_len].to_vec())
233 .map_err(|_| OpticError::new(OpticErrorKind::Asset, &format!("invalid UTF-8 in cached shader: {path}")))?
234 } else {
235 String::new()
236 };
237
238 Ok(Self { v_src, f_src, is_compute })
239 }
240}
241
242impl ShaderFile {
243 pub fn default_3d() -> OpticResult<Self> {
245 Self::from_disk("optic/assets/shdr/fallback3d.glsl", ShaderType::Pipeline)
246 }
247
248 pub fn default_2d() -> OpticResult<Self> {
250 Self::from_disk("optic/assets/shdr/fallback2d.glsl", ShaderType::Pipeline)
251 }
252}
253
254#[cfg(test)]
255mod tests {
256 use super::*;
257
258 #[test]
259 fn parse_compute_shader() {
260 let src = "#version 430\nvoid main() {}";
261 let asset = ShaderFile::from_src(src, ShaderType::Compute).unwrap();
262 assert!(asset.is_compute);
263 assert_eq!(asset.v_src, src);
264 assert!(asset.f_src.is_empty());
265 }
266
267 #[test]
268 fn parse_pipeline_shader() {
269 let src = "// v\nvoid vertex_main() {}\n// f\nvoid fragment_main() {}";
270 let asset = ShaderFile::from_src(src, ShaderType::Pipeline).unwrap();
271 assert!(!asset.is_compute);
272 assert!(asset.v_src.contains("vertex_main"));
273 assert!(asset.f_src.contains("fragment_main"));
274 }
275
276 #[test]
277 fn parse_pipeline_missing_vertex() {
278 let src = "// f\nvoid fragment_main() {}";
279 let result = ShaderFile::from_src(src, ShaderType::Pipeline);
280 assert!(result.is_err());
281 }
282
283 #[test]
284 fn parse_pipeline_missing_fragment() {
285 let src = "// v\nvoid vertex_main() {}";
286 let result = ShaderFile::from_src(src, ShaderType::Pipeline);
287 assert!(result.is_err());
288 }
289
290 #[test]
291 fn parse_pipeline_empty_source() {
292 let result = ShaderFile::from_src("", ShaderType::Pipeline);
293 assert!(result.is_err());
294 }
295
296 #[test]
297 fn parse_pipeline_various_markers() {
298 let cases = vec![
299 ("//VERT\nv\n//FRAG\nf", "v", "f"),
300 ("//vertex\nv\n//fragment\nf", "v", "f"),
301 ("// V\nv\n// F\nf", "v", "f"),
302 ];
303 for (src, v_exp, f_exp) in cases {
304 let asset = ShaderFile::from_src(src, ShaderType::Pipeline).unwrap();
305 assert!(asset.v_src.trim().contains(v_exp));
306 assert!(asset.f_src.trim().contains(f_exp));
307 }
308 }
309
310 #[test]
311 fn shader_cached_roundtrip_pipeline() {
312 let src = "// VERTEX\nvoid main() {}\n// FRAGMENT\nvoid main() {}";
313 let asset = ShaderFile::from_src(src, ShaderType::Pipeline).unwrap();
314 let path = "/tmp/optic_test_shdr_pipe.oshdr";
315 asset.save_cached(path).unwrap();
316 let loaded = ShaderFile::from_cached(path).unwrap();
317 assert!(!loaded.is_compute);
318 assert_eq!(loaded.v_src, asset.v_src);
319 assert_eq!(loaded.f_src, asset.f_src);
320 let _ = std::fs::remove_file(path);
321 }
322
323 #[test]
324 fn shader_cached_roundtrip_compute() {
325 let src = "#version 430\nvoid main() {}";
326 let asset = ShaderFile::from_src(src, ShaderType::Compute).unwrap();
327 let path = "/tmp/optic_test_shdr_comp.oshdr";
328 asset.save_cached(path).unwrap();
329 let loaded = ShaderFile::from_cached(path).unwrap();
330 assert!(loaded.is_compute);
331 assert_eq!(loaded.v_src, src);
332 let _ = std::fs::remove_file(path);
333 }
334
335 #[test]
336 fn shader_type_is_compute() {
337 assert!(ShaderType::Compute.is_compute());
338 assert!(!ShaderType::Pipeline.is_compute());
339 }
340
341 #[test]
342 fn from_vert_frag() {
343 let asset = ShaderFile::from_vert_frag("v_src", "f_src");
344 assert!(!asset.is_compute);
345 assert_eq!(asset.v_src, "v_src");
346 assert_eq!(asset.f_src, "f_src");
347 }
348}