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