use serde::{Deserialize, Serialize};
use crate::noise::NoiseEngine;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct ShaderPrecisionProfile {
pub high_float_range_min: i32,
pub high_float_range_max: i32,
pub high_float_precision: i32,
pub medium_float_precision: i32,
pub low_float_precision: i32,
pub high_int_precision: i32,
}
impl Default for ShaderPrecisionProfile {
fn default() -> Self {
Self {
high_float_range_min: 127,
high_float_range_max: 127,
high_float_precision: 23,
medium_float_precision: 23,
low_float_precision: 23,
high_int_precision: 31,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct ContextAttributes {
pub alpha: bool,
pub antialias: bool,
pub depth: bool,
pub fail_if_major_performance_caveat: bool,
pub power_preference: String,
pub premultiplied_alpha: bool,
pub preserve_drawing_buffer: bool,
pub stencil: bool,
pub desynchronized: bool,
}
impl Default for ContextAttributes {
fn default() -> Self {
Self {
alpha: true,
antialias: true,
depth: true,
fail_if_major_performance_caveat: false,
power_preference: "default".to_string(),
premultiplied_alpha: true,
preserve_drawing_buffer: false,
stencil: false,
desynchronized: false,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct WebGlProfile {
pub vendor: String,
pub renderer: String,
pub max_texture_size: u32,
pub max_viewport_dims: (u32, u32),
pub max_renderbuffer_size: u32,
pub max_vertex_attribs: u32,
pub max_varying_vectors: u32,
pub max_fragment_uniform_vectors: u32,
pub max_vertex_uniform_vectors: u32,
pub extensions: Vec<String>,
pub shader_precision: ShaderPrecisionProfile,
pub context_attributes: ContextAttributes,
}
impl WebGlProfile {
#[must_use]
pub fn nvidia_rtx_3060() -> Self {
Self {
vendor: "Google Inc. (NVIDIA)".to_string(),
renderer: "ANGLE (NVIDIA, NVIDIA GeForce RTX 3060 Direct3D11 vs_5_0 ps_5_0, D3D11)"
.to_string(),
max_texture_size: 16384,
max_viewport_dims: (32768, 32768),
max_renderbuffer_size: 16384,
max_vertex_attribs: 16,
max_varying_vectors: 15,
max_fragment_uniform_vectors: 1024,
max_vertex_uniform_vectors: 4096,
extensions: default_extensions(),
shader_precision: ShaderPrecisionProfile::default(),
context_attributes: ContextAttributes::default(),
}
}
#[must_use]
pub fn nvidia_gtx_1660() -> Self {
Self {
vendor: "Google Inc. (NVIDIA)".to_string(),
renderer:
"ANGLE (NVIDIA, NVIDIA GeForce GTX 1660 SUPER Direct3D11 vs_5_0 ps_5_0, D3D11)"
.to_string(),
max_texture_size: 16384,
max_viewport_dims: (32768, 32768),
max_renderbuffer_size: 16384,
max_vertex_attribs: 16,
max_varying_vectors: 15,
max_fragment_uniform_vectors: 1024,
max_vertex_uniform_vectors: 4096,
extensions: default_extensions(),
shader_precision: ShaderPrecisionProfile::default(),
context_attributes: ContextAttributes::default(),
}
}
#[must_use]
pub fn amd_rx_6700() -> Self {
Self {
vendor: "Google Inc. (AMD)".to_string(),
renderer: "ANGLE (AMD, AMD Radeon RX 6700 XT Direct3D11 vs_5_0 ps_5_0, D3D11)"
.to_string(),
max_texture_size: 16384,
max_viewport_dims: (32768, 32768),
max_renderbuffer_size: 16384,
max_vertex_attribs: 16,
max_varying_vectors: 15,
max_fragment_uniform_vectors: 1024,
max_vertex_uniform_vectors: 4096,
extensions: default_extensions(),
shader_precision: ShaderPrecisionProfile::default(),
context_attributes: ContextAttributes::default(),
}
}
#[must_use]
pub fn intel_uhd_630() -> Self {
Self {
vendor: "Google Inc. (Intel)".to_string(),
renderer: "ANGLE (Intel, Intel(R) UHD Graphics 630 Direct3D11 vs_5_0 ps_5_0, D3D11)"
.to_string(),
max_texture_size: 8192,
max_viewport_dims: (16384, 16384),
max_renderbuffer_size: 8192,
max_vertex_attribs: 16,
max_varying_vectors: 15,
max_fragment_uniform_vectors: 1024,
max_vertex_uniform_vectors: 4096,
extensions: default_extensions(),
shader_precision: ShaderPrecisionProfile::default(),
context_attributes: ContextAttributes::default(),
}
}
pub fn assert_consistent(&self) {
assert!(
self.max_texture_size <= self.max_viewport_dims.0,
"max_texture_size must be <= max_viewport_dims.0"
);
assert!(
self.max_texture_size <= self.max_viewport_dims.1,
"max_texture_size must be <= max_viewport_dims.1"
);
assert!(
self.max_renderbuffer_size <= self.max_texture_size,
"max_renderbuffer_size must be <= max_texture_size"
);
}
}
fn default_extensions() -> Vec<String> {
[
"ANGLE_instanced_arrays",
"EXT_blend_minmax",
"EXT_clip_control",
"EXT_color_buffer_half_float",
"EXT_depth_clamp",
"EXT_disjoint_timer_query",
"EXT_float_blend",
"EXT_frag_depth",
"EXT_shader_texture_lod",
"EXT_texture_compression_bptc",
"EXT_texture_compression_rgtc",
"EXT_texture_filter_anisotropic",
"EXT_sRGB",
"KHR_parallel_shader_compile",
"OES_element_index_uint",
"OES_fbo_render_mipmap",
"OES_standard_derivatives",
"OES_texture_float",
"OES_texture_float_linear",
"OES_texture_half_float",
"OES_texture_half_float_linear",
"OES_vertex_array_object",
"WEBGL_color_buffer_float",
"WEBGL_compressed_texture_s3tc",
"WEBGL_compressed_texture_s3tc_srgb",
"WEBGL_debug_renderer_info",
"WEBGL_debug_shaders",
"WEBGL_depth_texture",
"WEBGL_draw_buffers",
"WEBGL_lose_context",
"WEBGL_multi_draw",
"WEBGL_polygon_mode",
]
.iter()
.map(|s| (*s).to_string())
.collect()
}
#[must_use]
#[allow(clippy::too_many_lines)]
pub fn webgl_noise_script(profile: &WebGlProfile, engine: &NoiseEngine) -> String {
let noise_fn = engine.js_noise_fn();
let vendor = &profile.vendor;
let renderer = &profile.renderer;
let max_tex = profile.max_texture_size;
let vp_w = profile.max_viewport_dims.0;
let vp_h = profile.max_viewport_dims.1;
let max_rb = profile.max_renderbuffer_size;
let max_va = profile.max_vertex_attribs;
let max_varying_vectors = profile.max_varying_vectors;
let max_fragment_uniform_vectors = profile.max_fragment_uniform_vectors;
let max_vertex_uniform_vectors = profile.max_vertex_uniform_vectors;
let exts_json = {
let items: Vec<String> = profile
.extensions
.iter()
.map(|e| format!("{e:?}"))
.collect();
format!("[{}]", items.join(", "))
};
let sp = &profile.shader_precision;
let ca = &profile.context_attributes;
let ca_power = &ca.power_preference;
format!(
r"(function() {{
'use strict';
// ── Noise helpers ──────────────────────────────────────────────────────
{noise_fn}
// ── WebGL constants ────────────────────────────────────────────────────
const _VENDOR = 0x1F00;
const _RENDERER = 0x1F01;
const _UNMASKED_VENDOR = 0x9245;
const _UNMASKED_RENDERER = 0x9246;
const _MAX_TEXTURE_SIZE = 0x0D33;
const _MAX_VIEWPORT_DIMS = 0x0D3A;
const _MAX_RENDERBUFFER_SIZE = 0x84E8;
const _MAX_VERTEX_ATTRIBS = 0x8869;
const _MAX_VARYING_VECTORS = 0x8DFC;
const _MAX_FRAGMENT_UNIFORM_VECTORS = 0x8DFD;
const _MAX_VERTEX_UNIFORM_VECTORS = 0x8DFB;
const _PROFILE_VENDOR = {vendor:?};
const _PROFILE_RENDERER = {renderer:?};
const _EXTENSIONS = {exts_json};
// ── Spoof toString ─────────────────────────────────────────────────────
function _nts(name) {{ return function toString() {{ return 'function ' + name + '() {{ [native code] }}'; }}; }}
function _def(obj, prop, fn) {{
fn.toString = _nts(prop);
Object.defineProperty(obj, prop, {{ value: fn, writable: false, configurable: false, enumerable: false }});
}}
// ── Patch both WebGL1 and WebGL2 ────────────────────────────────────────
[WebGLRenderingContext, (typeof WebGL2RenderingContext !== 'undefined' ? WebGL2RenderingContext : null)]
.filter(Boolean)
.forEach(function(Ctx) {{
const proto = Ctx.prototype;
// getParameter
const _origGP = proto.getParameter;
_def(proto, 'getParameter', function getParameter(pname) {{
switch (pname) {{
case _VENDOR: return _PROFILE_VENDOR;
case _RENDERER: return _PROFILE_RENDERER;
case _UNMASKED_VENDOR: return _PROFILE_VENDOR;
case _UNMASKED_RENDERER: return _PROFILE_RENDERER;
case _MAX_TEXTURE_SIZE: return {max_tex};
case _MAX_VIEWPORT_DIMS: return new Int32Array([{vp_w}, {vp_h}]);
case _MAX_RENDERBUFFER_SIZE: return {max_rb};
case _MAX_VERTEX_ATTRIBS: return {max_va};
case _MAX_VARYING_VECTORS: return {max_varying_vectors};
case _MAX_FRAGMENT_UNIFORM_VECTORS: return {max_fragment_uniform_vectors};
case _MAX_VERTEX_UNIFORM_VECTORS: return {max_vertex_uniform_vectors};
default: return _origGP.call(this, pname);
}}
}});
// getSupportedExtensions
_def(proto, 'getSupportedExtensions', function getSupportedExtensions() {{
return _EXTENSIONS.slice();
}});
// getExtension
const _origGE = proto.getExtension;
_def(proto, 'getExtension', function getExtension(name) {{
if (!_EXTENSIONS.includes(name)) return null;
return _origGE.call(this, name);
}});
// getShaderPrecisionFormat
_def(proto, 'getShaderPrecisionFormat', function getShaderPrecisionFormat(shaderType, precisionType) {{
// HIGH_FLOAT = 0x8DF2, MEDIUM_FLOAT = 0x8DF1, LOW_FLOAT = 0x8DF0
// HIGH_INT = 0x8DF5, MEDIUM_INT = 0x8DF4, LOW_INT = 0x8DF3
const HIGH_FLOAT = 0x8DF2, MEDIUM_FLOAT = 0x8DF1, HIGH_INT = 0x8DF5;
if (precisionType === HIGH_FLOAT) {{
return {{ rangeMin: {sp_hfrm}, rangeMax: {sp_hfrx}, precision: {sp_hfp} }};
}} else if (precisionType === MEDIUM_FLOAT) {{
return {{ rangeMin: 127, rangeMax: 127, precision: {sp_mfp} }};
}} else if (precisionType === HIGH_INT) {{
return {{ rangeMin: 31, rangeMax: 30, precision: {sp_hip} }};
}}
return {{ rangeMin: 1, rangeMax: 1, precision: 8 }};
}});
// getContextAttributes
_def(proto, 'getContextAttributes', function getContextAttributes() {{
return {{
alpha: {ca_alpha},
antialias: {ca_antialias},
depth: {ca_depth},
failIfMajorPerformanceCaveat: {ca_fail},
powerPreference: {ca_power:?},
premultipliedAlpha: {ca_pma},
preserveDrawingBuffer: {ca_pdb},
stencil: {ca_stencil},
desynchronized: {ca_desync},
}};
}});
// readPixels — apply webgl noise to output
const _origRP = proto.readPixels;
_def(proto, 'readPixels', function readPixels(x, y, width, height, format, type, pixels) {{
_origRP.call(this, x, y, width, height, format, type, pixels);
if (pixels instanceof Uint8Array || pixels instanceof Uint8ClampedArray) {{
for (let i = 0; i < pixels.length; i += 4) {{
const px = (x + ((i / 4) % width)) >>> 0;
const py = (y + (((i / 4) / width) | 0)) >>> 0;
if (pixels[i] === 0 && pixels[i+1] === 0 && pixels[i+2] === 0 && pixels[i+3] === 0) continue;
const [dr, dg, db, da] = __stygian_webgl_noise('readPixels', px, py);
pixels[i] = Math.max(0, Math.min(255, pixels[i] + dr));
pixels[i+1] = Math.max(0, Math.min(255, pixels[i+1] + dg));
pixels[i+2] = Math.max(0, Math.min(255, pixels[i+2] + db));
pixels[i+3] = Math.max(0, Math.min(255, pixels[i+3] + da));
}}
}}
}});
}});
}})();
",
noise_fn = noise_fn,
vendor = vendor,
renderer = renderer,
exts_json = exts_json,
max_tex = max_tex,
vp_w = vp_w,
vp_h = vp_h,
max_rb = max_rb,
max_va = max_va,
max_varying_vectors = max_varying_vectors,
max_fragment_uniform_vectors = max_fragment_uniform_vectors,
max_vertex_uniform_vectors = max_vertex_uniform_vectors,
sp_hfrm = sp.high_float_range_min,
sp_hfrx = sp.high_float_range_max,
sp_hfp = sp.high_float_precision,
sp_mfp = sp.medium_float_precision,
sp_hip = sp.high_int_precision,
ca_alpha = ca.alpha,
ca_antialias = ca.antialias,
ca_depth = ca.depth,
ca_fail = ca.fail_if_major_performance_caveat,
ca_power = ca_power,
ca_pma = ca.premultiplied_alpha,
ca_pdb = ca.preserve_drawing_buffer,
ca_stencil = ca.stencil,
ca_desync = ca.desynchronized,
)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::noise::{NoiseEngine, NoiseSeed};
fn eng() -> NoiseEngine {
NoiseEngine::new(NoiseSeed::from(1_u64))
}
#[test]
fn all_profiles_consistent() {
WebGlProfile::nvidia_rtx_3060().assert_consistent();
WebGlProfile::nvidia_gtx_1660().assert_consistent();
WebGlProfile::amd_rx_6700().assert_consistent();
WebGlProfile::intel_uhd_630().assert_consistent();
}
#[test]
fn script_contains_webgl_overrides() {
let js = webgl_noise_script(&WebGlProfile::nvidia_rtx_3060(), &eng());
assert!(js.contains("getParameter"), "missing getParameter");
assert!(
js.contains("getSupportedExtensions"),
"missing getSupportedExtensions"
);
assert!(js.contains("getExtension"), "missing getExtension");
assert!(
js.contains("getShaderPrecisionFormat"),
"missing getShaderPrecisionFormat"
);
assert!(
js.contains("getContextAttributes"),
"missing getContextAttributes"
);
assert!(js.contains("readPixels"), "missing readPixels");
}
#[test]
fn script_contains_noise_reference() {
let js = webgl_noise_script(&WebGlProfile::nvidia_rtx_3060(), &eng());
assert!(
js.contains("__stygian_webgl_noise"),
"missing webgl noise fn"
);
}
#[test]
fn script_contains_native_tostring() {
let js = webgl_noise_script(&WebGlProfile::nvidia_rtx_3060(), &eng());
assert!(js.contains("[native code]"), "missing toString spoof");
}
#[test]
fn profile_serde_round_trip() {
let p = WebGlProfile::nvidia_rtx_3060();
let json_result = serde_json::to_string(&p);
assert!(json_result.is_ok(), "serialize failed: {json_result:?}");
let Ok(json) = json_result else {
return;
};
let back_result: Result<WebGlProfile, _> = serde_json::from_str(&json);
assert!(back_result.is_ok(), "deserialize failed: {back_result:?}");
let Ok(back) = back_result else {
return;
};
assert_eq!(back.vendor, p.vendor);
assert_eq!(back.renderer, p.renderer);
assert_eq!(back.max_texture_size, p.max_texture_size);
assert_eq!(back.extensions.len(), p.extensions.len());
}
#[test]
fn script_contains_renderer_string() {
let p = WebGlProfile::nvidia_rtx_3060();
let js = webgl_noise_script(&p, &eng());
assert!(js.contains("RTX 3060"), "renderer not in script");
}
}