1use serde::{Deserialize, Serialize};
19
20use crate::noise::NoiseEngine;
21
22#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
30pub struct ShaderPrecisionProfile {
31 pub high_float_range_min: i32,
33 pub high_float_range_max: i32,
35 pub high_float_precision: i32,
37 pub medium_float_precision: i32,
39 pub low_float_precision: i32,
41 pub high_int_precision: i32,
43}
44
45impl Default for ShaderPrecisionProfile {
46 fn default() -> Self {
47 Self {
48 high_float_range_min: 127,
49 high_float_range_max: 127,
50 high_float_precision: 23,
51 medium_float_precision: 23,
52 low_float_precision: 23,
53 high_int_precision: 31,
54 }
55 }
56}
57
58#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
64pub struct ContextAttributes {
65 pub alpha: bool,
67 pub antialias: bool,
69 pub depth: bool,
71 pub fail_if_major_performance_caveat: bool,
73 pub power_preference: String,
75 pub premultiplied_alpha: bool,
77 pub preserve_drawing_buffer: bool,
79 pub stencil: bool,
81 pub desynchronized: bool,
83}
84
85impl Default for ContextAttributes {
86 fn default() -> Self {
87 Self {
88 alpha: true,
89 antialias: true,
90 depth: true,
91 fail_if_major_performance_caveat: false,
92 power_preference: "default".to_string(),
93 premultiplied_alpha: true,
94 preserve_drawing_buffer: false,
95 stencil: false,
96 desynchronized: false,
97 }
98 }
99}
100
101#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
119pub struct WebGlProfile {
120 pub vendor: String,
122 pub renderer: String,
124 pub max_texture_size: u32,
126 pub max_viewport_dims: (u32, u32),
128 pub max_renderbuffer_size: u32,
130 pub max_vertex_attribs: u32,
132 pub max_varying_vectors: u32,
134 pub max_fragment_uniform_vectors: u32,
136 pub max_vertex_uniform_vectors: u32,
138 pub extensions: Vec<String>,
140 pub shader_precision: ShaderPrecisionProfile,
142 pub context_attributes: ContextAttributes,
144}
145
146impl WebGlProfile {
147 #[must_use]
158 pub fn nvidia_rtx_3060() -> Self {
159 Self {
160 vendor: "Google Inc. (NVIDIA)".to_string(),
161 renderer: "ANGLE (NVIDIA, NVIDIA GeForce RTX 3060 Direct3D11 vs_5_0 ps_5_0, D3D11)"
162 .to_string(),
163 max_texture_size: 16384,
164 max_viewport_dims: (32768, 32768),
165 max_renderbuffer_size: 16384,
166 max_vertex_attribs: 16,
167 max_varying_vectors: 15,
168 max_fragment_uniform_vectors: 1024,
169 max_vertex_uniform_vectors: 4096,
170 extensions: default_extensions(),
171 shader_precision: ShaderPrecisionProfile::default(),
172 context_attributes: ContextAttributes::default(),
173 }
174 }
175
176 #[must_use]
186 pub fn nvidia_gtx_1660() -> Self {
187 Self {
188 vendor: "Google Inc. (NVIDIA)".to_string(),
189 renderer:
190 "ANGLE (NVIDIA, NVIDIA GeForce GTX 1660 SUPER Direct3D11 vs_5_0 ps_5_0, D3D11)"
191 .to_string(),
192 max_texture_size: 16384,
193 max_viewport_dims: (32768, 32768),
194 max_renderbuffer_size: 16384,
195 max_vertex_attribs: 16,
196 max_varying_vectors: 15,
197 max_fragment_uniform_vectors: 1024,
198 max_vertex_uniform_vectors: 4096,
199 extensions: default_extensions(),
200 shader_precision: ShaderPrecisionProfile::default(),
201 context_attributes: ContextAttributes::default(),
202 }
203 }
204
205 #[must_use]
215 pub fn amd_rx_6700() -> Self {
216 Self {
217 vendor: "Google Inc. (AMD)".to_string(),
218 renderer: "ANGLE (AMD, AMD Radeon RX 6700 XT Direct3D11 vs_5_0 ps_5_0, D3D11)"
219 .to_string(),
220 max_texture_size: 16384,
221 max_viewport_dims: (32768, 32768),
222 max_renderbuffer_size: 16384,
223 max_vertex_attribs: 16,
224 max_varying_vectors: 15,
225 max_fragment_uniform_vectors: 1024,
226 max_vertex_uniform_vectors: 4096,
227 extensions: default_extensions(),
228 shader_precision: ShaderPrecisionProfile::default(),
229 context_attributes: ContextAttributes::default(),
230 }
231 }
232
233 #[must_use]
243 pub fn intel_uhd_630() -> Self {
244 Self {
245 vendor: "Google Inc. (Intel)".to_string(),
246 renderer: "ANGLE (Intel, Intel(R) UHD Graphics 630 Direct3D11 vs_5_0 ps_5_0, D3D11)"
247 .to_string(),
248 max_texture_size: 8192,
249 max_viewport_dims: (16384, 16384),
250 max_renderbuffer_size: 8192,
251 max_vertex_attribs: 16,
252 max_varying_vectors: 15,
253 max_fragment_uniform_vectors: 1024,
254 max_vertex_uniform_vectors: 4096,
255 extensions: default_extensions(),
256 shader_precision: ShaderPrecisionProfile::default(),
257 context_attributes: ContextAttributes::default(),
258 }
259 }
260
261 pub fn assert_consistent(&self) {
271 assert!(
272 self.max_texture_size <= self.max_viewport_dims.0,
273 "max_texture_size must be <= max_viewport_dims.0"
274 );
275 assert!(
276 self.max_texture_size <= self.max_viewport_dims.1,
277 "max_texture_size must be <= max_viewport_dims.1"
278 );
279 assert!(
280 self.max_renderbuffer_size <= self.max_texture_size,
281 "max_renderbuffer_size must be <= max_texture_size"
282 );
283 }
284}
285
286fn default_extensions() -> Vec<String> {
287 [
288 "ANGLE_instanced_arrays",
289 "EXT_blend_minmax",
290 "EXT_clip_control",
291 "EXT_color_buffer_half_float",
292 "EXT_depth_clamp",
293 "EXT_disjoint_timer_query",
294 "EXT_float_blend",
295 "EXT_frag_depth",
296 "EXT_shader_texture_lod",
297 "EXT_texture_compression_bptc",
298 "EXT_texture_compression_rgtc",
299 "EXT_texture_filter_anisotropic",
300 "EXT_sRGB",
301 "KHR_parallel_shader_compile",
302 "OES_element_index_uint",
303 "OES_fbo_render_mipmap",
304 "OES_standard_derivatives",
305 "OES_texture_float",
306 "OES_texture_float_linear",
307 "OES_texture_half_float",
308 "OES_texture_half_float_linear",
309 "OES_vertex_array_object",
310 "WEBGL_color_buffer_float",
311 "WEBGL_compressed_texture_s3tc",
312 "WEBGL_compressed_texture_s3tc_srgb",
313 "WEBGL_debug_renderer_info",
314 "WEBGL_debug_shaders",
315 "WEBGL_depth_texture",
316 "WEBGL_draw_buffers",
317 "WEBGL_lose_context",
318 "WEBGL_multi_draw",
319 "WEBGL_polygon_mode",
320 ]
321 .iter()
322 .map(|s| (*s).to_string())
323 .collect()
324}
325
326#[must_use]
343#[allow(clippy::too_many_lines)]
344pub fn webgl_noise_script(profile: &WebGlProfile, engine: &NoiseEngine) -> String {
345 let noise_fn = engine.js_noise_fn();
346 let vendor = &profile.vendor;
347 let renderer = &profile.renderer;
348 let max_tex = profile.max_texture_size;
349 let vp_w = profile.max_viewport_dims.0;
350 let vp_h = profile.max_viewport_dims.1;
351 let max_rb = profile.max_renderbuffer_size;
352 let max_va = profile.max_vertex_attribs;
353 let max_varying_vectors = profile.max_varying_vectors;
354 let max_fragment_uniform_vectors = profile.max_fragment_uniform_vectors;
355 let max_vertex_uniform_vectors = profile.max_vertex_uniform_vectors;
356
357 let exts_json = {
358 let items: Vec<String> = profile
359 .extensions
360 .iter()
361 .map(|e| format!("{e:?}"))
362 .collect();
363 format!("[{}]", items.join(", "))
364 };
365
366 let sp = &profile.shader_precision;
367 let ca = &profile.context_attributes;
368 let ca_power = &ca.power_preference;
369
370 format!(
371 r"(function() {{
372 'use strict';
373
374 // ── Noise helpers ──────────────────────────────────────────────────────
375 {noise_fn}
376
377 // ── WebGL constants ────────────────────────────────────────────────────
378 const _VENDOR = 0x1F00;
379 const _RENDERER = 0x1F01;
380 const _UNMASKED_VENDOR = 0x9245;
381 const _UNMASKED_RENDERER = 0x9246;
382 const _MAX_TEXTURE_SIZE = 0x0D33;
383 const _MAX_VIEWPORT_DIMS = 0x0D3A;
384 const _MAX_RENDERBUFFER_SIZE = 0x84E8;
385 const _MAX_VERTEX_ATTRIBS = 0x8869;
386 const _MAX_VARYING_VECTORS = 0x8DFC;
387 const _MAX_FRAGMENT_UNIFORM_VECTORS = 0x8DFD;
388 const _MAX_VERTEX_UNIFORM_VECTORS = 0x8DFB;
389
390 const _PROFILE_VENDOR = {vendor:?};
391 const _PROFILE_RENDERER = {renderer:?};
392 const _EXTENSIONS = {exts_json};
393
394 // ── Spoof toString ─────────────────────────────────────────────────────
395 function _nts(name) {{ return function toString() {{ return 'function ' + name + '() {{ [native code] }}'; }}; }}
396 function _def(obj, prop, fn) {{
397 fn.toString = _nts(prop);
398 Object.defineProperty(obj, prop, {{ value: fn, writable: false, configurable: false, enumerable: false }});
399 }}
400
401 // ── Patch both WebGL1 and WebGL2 ────────────────────────────────────────
402 [WebGLRenderingContext, (typeof WebGL2RenderingContext !== 'undefined' ? WebGL2RenderingContext : null)]
403 .filter(Boolean)
404 .forEach(function(Ctx) {{
405 const proto = Ctx.prototype;
406
407 // getParameter
408 const _origGP = proto.getParameter;
409 _def(proto, 'getParameter', function getParameter(pname) {{
410 switch (pname) {{
411 case _VENDOR: return _PROFILE_VENDOR;
412 case _RENDERER: return _PROFILE_RENDERER;
413 case _UNMASKED_VENDOR: return _PROFILE_VENDOR;
414 case _UNMASKED_RENDERER: return _PROFILE_RENDERER;
415 case _MAX_TEXTURE_SIZE: return {max_tex};
416 case _MAX_VIEWPORT_DIMS: return new Int32Array([{vp_w}, {vp_h}]);
417 case _MAX_RENDERBUFFER_SIZE: return {max_rb};
418 case _MAX_VERTEX_ATTRIBS: return {max_va};
419 case _MAX_VARYING_VECTORS: return {max_varying_vectors};
420 case _MAX_FRAGMENT_UNIFORM_VECTORS: return {max_fragment_uniform_vectors};
421 case _MAX_VERTEX_UNIFORM_VECTORS: return {max_vertex_uniform_vectors};
422 default: return _origGP.call(this, pname);
423 }}
424 }});
425
426 // getSupportedExtensions
427 _def(proto, 'getSupportedExtensions', function getSupportedExtensions() {{
428 return _EXTENSIONS.slice();
429 }});
430
431 // getExtension
432 const _origGE = proto.getExtension;
433 _def(proto, 'getExtension', function getExtension(name) {{
434 if (!_EXTENSIONS.includes(name)) return null;
435 return _origGE.call(this, name);
436 }});
437
438 // getShaderPrecisionFormat
439 _def(proto, 'getShaderPrecisionFormat', function getShaderPrecisionFormat(shaderType, precisionType) {{
440 // HIGH_FLOAT = 0x8DF2, MEDIUM_FLOAT = 0x8DF1, LOW_FLOAT = 0x8DF0
441 // HIGH_INT = 0x8DF5, MEDIUM_INT = 0x8DF4, LOW_INT = 0x8DF3
442 const HIGH_FLOAT = 0x8DF2, MEDIUM_FLOAT = 0x8DF1, HIGH_INT = 0x8DF5;
443 if (precisionType === HIGH_FLOAT) {{
444 return {{ rangeMin: {sp_hfrm}, rangeMax: {sp_hfrx}, precision: {sp_hfp} }};
445 }} else if (precisionType === MEDIUM_FLOAT) {{
446 return {{ rangeMin: 127, rangeMax: 127, precision: {sp_mfp} }};
447 }} else if (precisionType === HIGH_INT) {{
448 return {{ rangeMin: 31, rangeMax: 30, precision: {sp_hip} }};
449 }}
450 return {{ rangeMin: 1, rangeMax: 1, precision: 8 }};
451 }});
452
453 // getContextAttributes
454 _def(proto, 'getContextAttributes', function getContextAttributes() {{
455 return {{
456 alpha: {ca_alpha},
457 antialias: {ca_antialias},
458 depth: {ca_depth},
459 failIfMajorPerformanceCaveat: {ca_fail},
460 powerPreference: {ca_power:?},
461 premultipliedAlpha: {ca_pma},
462 preserveDrawingBuffer: {ca_pdb},
463 stencil: {ca_stencil},
464 desynchronized: {ca_desync},
465 }};
466 }});
467
468 // readPixels — apply webgl noise to output
469 const _origRP = proto.readPixels;
470 _def(proto, 'readPixels', function readPixels(x, y, width, height, format, type, pixels) {{
471 _origRP.call(this, x, y, width, height, format, type, pixels);
472 if (pixels instanceof Uint8Array || pixels instanceof Uint8ClampedArray) {{
473 for (let i = 0; i < pixels.length; i += 4) {{
474 const px = (x + ((i / 4) % width)) >>> 0;
475 const py = (y + (((i / 4) / width) | 0)) >>> 0;
476 if (pixels[i] === 0 && pixels[i+1] === 0 && pixels[i+2] === 0 && pixels[i+3] === 0) continue;
477 const [dr, dg, db, da] = __stygian_webgl_noise('readPixels', px, py);
478 pixels[i] = Math.max(0, Math.min(255, pixels[i] + dr));
479 pixels[i+1] = Math.max(0, Math.min(255, pixels[i+1] + dg));
480 pixels[i+2] = Math.max(0, Math.min(255, pixels[i+2] + db));
481 pixels[i+3] = Math.max(0, Math.min(255, pixels[i+3] + da));
482 }}
483 }}
484 }});
485 }});
486}})();
487",
488 noise_fn = noise_fn,
489 vendor = vendor,
490 renderer = renderer,
491 exts_json = exts_json,
492 max_tex = max_tex,
493 vp_w = vp_w,
494 vp_h = vp_h,
495 max_rb = max_rb,
496 max_va = max_va,
497 max_varying_vectors = max_varying_vectors,
498 max_fragment_uniform_vectors = max_fragment_uniform_vectors,
499 max_vertex_uniform_vectors = max_vertex_uniform_vectors,
500 sp_hfrm = sp.high_float_range_min,
501 sp_hfrx = sp.high_float_range_max,
502 sp_hfp = sp.high_float_precision,
503 sp_mfp = sp.medium_float_precision,
504 sp_hip = sp.high_int_precision,
505 ca_alpha = ca.alpha,
506 ca_antialias = ca.antialias,
507 ca_depth = ca.depth,
508 ca_fail = ca.fail_if_major_performance_caveat,
509 ca_power = ca_power,
510 ca_pma = ca.premultiplied_alpha,
511 ca_pdb = ca.preserve_drawing_buffer,
512 ca_stencil = ca.stencil,
513 ca_desync = ca.desynchronized,
514 )
515}
516
517#[cfg(test)]
522mod tests {
523 use super::*;
524 use crate::noise::{NoiseEngine, NoiseSeed};
525
526 fn eng() -> NoiseEngine {
527 NoiseEngine::new(NoiseSeed::from(1_u64))
528 }
529
530 #[test]
531 fn all_profiles_consistent() {
532 WebGlProfile::nvidia_rtx_3060().assert_consistent();
533 WebGlProfile::nvidia_gtx_1660().assert_consistent();
534 WebGlProfile::amd_rx_6700().assert_consistent();
535 WebGlProfile::intel_uhd_630().assert_consistent();
536 }
537
538 #[test]
539 fn script_contains_webgl_overrides() {
540 let js = webgl_noise_script(&WebGlProfile::nvidia_rtx_3060(), &eng());
541 assert!(js.contains("getParameter"), "missing getParameter");
542 assert!(
543 js.contains("getSupportedExtensions"),
544 "missing getSupportedExtensions"
545 );
546 assert!(js.contains("getExtension"), "missing getExtension");
547 assert!(
548 js.contains("getShaderPrecisionFormat"),
549 "missing getShaderPrecisionFormat"
550 );
551 assert!(
552 js.contains("getContextAttributes"),
553 "missing getContextAttributes"
554 );
555 assert!(js.contains("readPixels"), "missing readPixels");
556 }
557
558 #[test]
559 fn script_contains_noise_reference() {
560 let js = webgl_noise_script(&WebGlProfile::nvidia_rtx_3060(), &eng());
561 assert!(
562 js.contains("__stygian_webgl_noise"),
563 "missing webgl noise fn"
564 );
565 }
566
567 #[test]
568 fn script_contains_native_tostring() {
569 let js = webgl_noise_script(&WebGlProfile::nvidia_rtx_3060(), &eng());
570 assert!(js.contains("[native code]"), "missing toString spoof");
571 }
572
573 #[test]
574 fn profile_serde_round_trip() {
575 let p = WebGlProfile::nvidia_rtx_3060();
576 let json_result = serde_json::to_string(&p);
577 assert!(json_result.is_ok(), "serialize failed: {json_result:?}");
578 let Ok(json) = json_result else {
579 return;
580 };
581 let back_result: Result<WebGlProfile, _> = serde_json::from_str(&json);
582 assert!(back_result.is_ok(), "deserialize failed: {back_result:?}");
583 let Ok(back) = back_result else {
584 return;
585 };
586 assert_eq!(back.vendor, p.vendor);
587 assert_eq!(back.renderer, p.renderer);
588 assert_eq!(back.max_texture_size, p.max_texture_size);
589 assert_eq!(back.extensions.len(), p.extensions.len());
590 }
591
592 #[test]
593 fn script_contains_renderer_string() {
594 let p = WebGlProfile::nvidia_rtx_3060();
595 let js = webgl_noise_script(&p, &eng());
596 assert!(js.contains("RTX 3060"), "renderer not in script");
597 }
598}