Further improved WebGL spoofing beta.12

- Added ability to spoof webgl2 supported extensions
- Added ability to block parameters that aren't defined in config
- Passing null in config will block the value
- Added more parameters to the demo site
This commit is contained in:
daijro 2024-10-14 20:31:58 -05:00
parent 1532d7bb31
commit 5bfc3ee026
5 changed files with 100 additions and 65 deletions

View file

@ -348,7 +348,7 @@ Because I don't have a dataset of WebGL fingerprints to rotate against, WebGL fi
This repository includes a demo site (see [here](https://github.com/daijro/camoufox/blob/main/scripts/examples/webgl.html)) that prints your browser's WebGL parameters. You can use this site to generate WebGL fingerprints for Camoufox from other devices. This repository includes a demo site (see [here](https://github.com/daijro/camoufox/blob/main/scripts/examples/webgl.html)) that prints your browser's WebGL parameters. You can use this site to generate WebGL fingerprints for Camoufox from other devices.
<img src="https://i.imgur.com/jwT5VqG.png" width="80%"> <img src="https://i.imgur.com/jwT5VqG.png">
### Properties ### Properties
@ -357,18 +357,21 @@ Camoufox supports spoofing WebGL parameters, supported extensions, context attri
**Note**: Do NOT randomly assign values to these properties. WAFs hash your WebGL fingerprint and compare it against a dataset. Randomly assigning values will lead to detection as an unknown device. **Note**: Do NOT randomly assign values to these properties. WAFs hash your WebGL fingerprint and compare it against a dataset. Randomly assigning values will lead to detection as an unknown device.
| Property | Description | Example | | Property | Description | Example |
| ----------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- | | ----------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- |
| webgl:renderer | Spoofs the name of the unmasked WebGL renderer. | `"NVIDIA GeForce GTX 980, or similar"` | | webgl:renderer | Spoofs the name of the unmasked WebGL renderer. | `"NVIDIA GeForce GTX 980, or similar"` |
| webgl:vendor | Spoofs the name of the unmasked WebGL vendor. | `"NVIDIA Corporation"` | | webgl:vendor | Spoofs the name of the unmasked WebGL vendor. | `"NVIDIA Corporation"` |
| webgl:supportedExtensions | An array of supported WebGL extensions ([full list](https://registry.khronos.org/webgl/extensions/)). | `["ANGLE_instanced_arrays", "EXT_color_buffer_float", "EXT_disjoint_timer_query", ...]` | | webgl:supportedExtensions | An array of supported WebGL extensions ([full list](https://registry.khronos.org/webgl/extensions/)). | `["ANGLE_instanced_arrays", "EXT_color_buffer_float", "EXT_disjoint_timer_query", ...]` |
| webgl2:supportedExtensions | The same as `webgl:supportedExtensions`, but for WebGL2. | `["ANGLE_instanced_arrays", "EXT_color_buffer_float", "EXT_disjoint_timer_query", ...]` |
| webgl:contextAttributes | A dictionary of WebGL context attributes. | `{"alpha": true, "antialias": true, "depth": true, ...}` | | webgl:contextAttributes | A dictionary of WebGL context attributes. | `{"alpha": true, "antialias": true, "depth": true, ...}` |
| webgl2:contextAttributes | The same as `webgl:contextAttributes`, but for WebGL2. | `{"alpha": true, "antialias": true, "depth": true, ...}` | | webgl2:contextAttributes | The same as `webgl:contextAttributes`, but for WebGL2. | `{"alpha": true, "antialias": true, "depth": true, ...}` |
| webgl:parameters | A dictionary of WebGL parameters. Keys must be GL enums, and values are the values to spoof them as. | `{"2849": 1, "2884": false, "2928": [0, 1], ...}` | | webgl:parameters | A dictionary of WebGL parameters. Keys must be GL enums, and values are the values to spoof them as. | `{"2849": 1, "2884": false, "2928": [0, 1], ...}` |
| webgl2:parameters | The same as `webgl:parameters`, but for WebGL2. | `{"2849": 1, "2884": false, "2928": [0, 1], ...}` | | webgl2:parameters | The same as `webgl:parameters`, but for WebGL2. | `{"2849": 1, "2884": false, "2928": [0, 1], ...}` |
| webgl:parameters:blockIfNotDefined | If set to `true`, only the parameters in `webgl:parameters` will be allowed. Can be dangerous if not used correctly. | `true`/`false` |
| webgl2:parameters:blockIfNotDefined | If set to `true`, only the parameters in `webgl2:parameters` will be allowed. Can be dangerous if not used correctly. | `true`/`false` |
| webgl:shaderPrecisionFormats | A dictionary of WebGL shader precision formats. Keys are formatted as `"<shaderType>,<precisionType>"`. | `{"35633,36336": {"rangeMin": 127, "rangeMax": 127, "precision": 23}, ...}` | | webgl:shaderPrecisionFormats | A dictionary of WebGL shader precision formats. Keys are formatted as `"<shaderType>,<precisionType>"`. | `{"35633,36336": {"rangeMin": 127, "rangeMax": 127, "precision": 23}, ...}` |
| webgl2:shaderPrecisionFormats | The same as `webGL:shaderPrecisionFormats`, but for WebGL2. | `{"35633,36336": {"rangeMin": 127, "rangeMax": 127, "precision": 23}, ...}` | | webgl2:shaderPrecisionFormats | The same as `webGL:shaderPrecisionFormats`, but for WebGL2. | `{"35633,36336": {"rangeMin": 127, "rangeMax": 127, "precision": 23}, ...}` |
| webgl:shaderPrecisionFormats:blockIfNotDefined | If set to `true`, only the shader percisions in `webgl:shaderPrecisionFormats` will be passed. Everything else will be blocked. | `true` | | webgl:shaderPrecisionFormats:blockIfNotDefined | If set to `true`, only the shader percisions in `webgl:shaderPrecisionFormats` will be allowed. | `true`/`false` |
| webgl2:shaderPrecisionFormats:blockIfNotDefined | If set to `true`, only the shader percisions in `webgl2:shaderPrecisionFormats` will be passed. Everything else will be blocked. | `true` | | webgl2:shaderPrecisionFormats:blockIfNotDefined | If set to `true`, only the shader percisions in `webgl2:shaderPrecisionFormats` will be allowed. | `true`/`false` |
</details> </details>

View file

@ -15,6 +15,7 @@ Written by daijro.
#include <stdlib.h> #include <stdlib.h>
#include <stdio.h> #include <stdio.h>
#include <variant> #include <variant>
#include <cstddef>
#ifdef _WIN32 #ifdef _WIN32
# include <windows.h> # include <windows.h>
@ -217,14 +218,15 @@ inline std::optional<T> GetAttribute(const std::string attrib, bool isWebGL2) {
return value.value().get<T>(); return value.value().get<T>();
} }
inline std::optional<std::variant<int64_t, bool, double, std::string>> GLParam( inline std::optional<
uint32_t pname, bool isWebGL2) { std::variant<int64_t, bool, double, std::string, std::nullptr_t>>
GLParam(uint32_t pname, bool isWebGL2) {
auto value = auto value =
MaskConfig::GetNested(isWebGL2 ? "webgl2:parameters" : "webgl:parameters", MaskConfig::GetNested(isWebGL2 ? "webgl2:parameters" : "webgl:parameters",
std::to_string(pname)); std::to_string(pname));
if (!value) return std::nullopt; if (!value) return std::nullopt;
auto data = value.value(); auto data = value.value();
// cast the data and return if (data.is_null()) return std::nullptr_t();
if (data.is_number_integer()) return data.get<int64_t>(); if (data.is_number_integer()) return data.get<int64_t>();
if (data.is_boolean()) return data.get<bool>(); if (data.is_boolean()) return data.get<bool>();
if (data.is_number_float()) return data.get<double>(); if (data.is_number_float()) return data.get<double>();
@ -249,7 +251,8 @@ inline std::vector<T> MParamGLVector(uint32_t pname,
bool isWebGL2) { bool isWebGL2) {
if (auto value = MaskConfig::GetNested( if (auto value = MaskConfig::GetNested(
isWebGL2 ? "webgl2:parameters" : "webgl:parameters", isWebGL2 ? "webgl2:parameters" : "webgl:parameters",
std::to_string(pname)); value.has_value()) { std::to_string(pname));
value.has_value()) {
if (value.value().is_array()) { if (value.value().is_array()) {
std::array<T, 4UL> result = value.value().get<std::array<T, 4UL>>(); std::array<T, 4UL> result = value.value().get<std::array<T, 4UL>>();
return std::vector<T>(result.begin(), result.end()); return std::vector<T>(result.begin(), result.end());

View file

@ -1,17 +1,18 @@
diff --git a/dom/canvas/ClientWebGLContext.cpp b/dom/canvas/ClientWebGLContext.cpp diff --git a/dom/canvas/ClientWebGLContext.cpp b/dom/canvas/ClientWebGLContext.cpp
index db60868f65..afed6eeb7c 100644 index db60868f65..7361f0fc9c 100644
--- a/dom/canvas/ClientWebGLContext.cpp --- a/dom/canvas/ClientWebGLContext.cpp
+++ b/dom/canvas/ClientWebGLContext.cpp +++ b/dom/canvas/ClientWebGLContext.cpp
@@ -4,6 +4,8 @@ @@ -4,6 +4,9 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "ClientWebGLContext.h" #include "ClientWebGLContext.h"
+#include "MaskConfig.hpp" +#include "MaskConfig.hpp"
+#include <algorithm> +#include <algorithm>
+#include <cstddef>
#include <bitset> #include <bitset>
@@ -744,6 +746,13 @@ void ClientWebGLContext::SetDrawingBufferColorSpace( @@ -744,6 +747,13 @@ void ClientWebGLContext::SetDrawingBufferColorSpace(
Run<RPROC(SetDrawingBufferColorSpace)>(*mDrawingBufferColorSpace); Run<RPROC(SetDrawingBufferColorSpace)>(*mDrawingBufferColorSpace);
} }
@ -25,7 +26,7 @@ index db60868f65..afed6eeb7c 100644
void ClientWebGLContext::GetContextAttributes( void ClientWebGLContext::GetContextAttributes(
dom::Nullable<dom::WebGLContextAttributes>& retval) { dom::Nullable<dom::WebGLContextAttributes>& retval) {
retval.SetNull(); retval.SetNull();
@@ -754,14 +763,38 @@ void ClientWebGLContext::GetContextAttributes( @@ -754,14 +764,38 @@ void ClientWebGLContext::GetContextAttributes(
const auto& options = mNotLost->info.options; const auto& options = mNotLost->info.options;
@ -72,7 +73,7 @@ index db60868f65..afed6eeb7c 100644
} }
// ----------------------- // -----------------------
@@ -979,18 +1012,28 @@ bool ClientWebGLContext::CreateHostContext(const uvec2& requestedSize) { @@ -979,18 +1013,28 @@ bool ClientWebGLContext::CreateHostContext(const uvec2& requestedSize) {
std::unordered_map<GLenum, bool> webgl::MakeIsEnabledMap(const bool webgl2) { std::unordered_map<GLenum, bool> webgl::MakeIsEnabledMap(const bool webgl2) {
auto ret = std::unordered_map<GLenum, bool>{}; auto ret = std::unordered_map<GLenum, bool>{};
@ -111,11 +112,13 @@ index db60868f65..afed6eeb7c 100644
} }
return ret; return ret;
@@ -2058,6 +2101,28 @@ void ClientWebGLContext::GetParameter(JSContext* cx, GLenum pname, @@ -2058,6 +2102,57 @@ void ClientWebGLContext::GetParameter(JSContext* cx, GLenum pname,
const auto& state = State(); const auto& state = State();
// - // -
+ std::optional<std::variant<int64_t, bool, double, std::string>> data; + std::optional<
+ std::variant<int64_t, bool, double, std::string, std::nullptr_t>>
+ data;
+ data = MaskConfig::GLParam(pname, mIsWebGL2); + data = MaskConfig::GLParam(pname, mIsWebGL2);
+ +
+ if (data.has_value()) { + if (data.has_value()) {
@ -136,11 +139,38 @@ index db60868f65..afed6eeb7c 100644
+ retval.set(StringValue(cx, std::get<std::string>(value), rv)); + retval.set(StringValue(cx, std::get<std::string>(value), rv));
+ return; + return;
+ } + }
+ if (std::holds_alternative<std::nullptr_t>(value)) {
+ retval.set(JS::NullValue());
+ return;
+ }
+ }
+ // If the value is not array (we will handle those later),
+ // then check if it should be blocked.
+ switch (pname) {
+ case LOCAL_GL_DEPTH_RANGE:
+ case LOCAL_GL_ALIASED_POINT_SIZE_RANGE:
+ case LOCAL_GL_ALIASED_LINE_WIDTH_RANGE:
+ case LOCAL_GL_COLOR_CLEAR_VALUE:
+ case LOCAL_GL_BLEND_COLOR:
+ case LOCAL_GL_MAX_VIEWPORT_DIMS:
+ case LOCAL_GL_SCISSOR_BOX:
+ case LOCAL_GL_VIEWPORT:
+ case LOCAL_GL_COMPRESSED_TEXTURE_FORMATS:
+ case LOCAL_GL_COLOR_WRITEMASK:
+ case dom::WEBGL_debug_renderer_info_Binding::UNMASKED_RENDERER_WEBGL:
+ case dom::WEBGL_debug_renderer_info_Binding::UNMASKED_VENDOR_WEBGL:
+ break;
+ default:
+ if (MaskConfig::GetBool(mIsWebGL2 ? "webgl2:parameters:blockIfNotDefined"
+ : "webgl:parameters:blockIfNotDefined")) {
+ retval.set(JS::NullValue());
+ return;
+ }
+ } + }
const auto fnSetRetval_Buffer = [&](const GLenum target) { const auto fnSetRetval_Buffer = [&](const GLenum target) {
const auto buffer = *MaybeFind(state.mBoundBufferByTarget, target); const auto buffer = *MaybeFind(state.mBoundBufferByTarget, target);
@@ -2163,49 +2228,84 @@ void ClientWebGLContext::GetParameter(JSContext* cx, GLenum pname, @@ -2163,49 +2258,84 @@ void ClientWebGLContext::GetParameter(JSContext* cx, GLenum pname,
// 2 floats // 2 floats
case LOCAL_GL_DEPTH_RANGE: case LOCAL_GL_DEPTH_RANGE:
@ -238,7 +268,7 @@ index db60868f65..afed6eeb7c 100644
return; return;
} }
@@ -2385,6 +2485,10 @@ void ClientWebGLContext::GetParameter(JSContext* cx, GLenum pname, @@ -2385,6 +2515,10 @@ void ClientWebGLContext::GetParameter(JSContext* cx, GLenum pname,
switch (pname) { switch (pname) {
case dom::WEBGL_debug_renderer_info_Binding::UNMASKED_RENDERER_WEBGL: case dom::WEBGL_debug_renderer_info_Binding::UNMASKED_RENDERER_WEBGL:
@ -249,7 +279,7 @@ index db60868f65..afed6eeb7c 100644
ret = GetUnmaskedRenderer(); ret = GetUnmaskedRenderer();
if (ret && StaticPrefs::webgl_sanitize_unmasked_renderer()) { if (ret && StaticPrefs::webgl_sanitize_unmasked_renderer()) {
*ret = webgl::SanitizeRenderer(*ret); *ret = webgl::SanitizeRenderer(*ret);
@@ -2392,6 +2496,10 @@ void ClientWebGLContext::GetParameter(JSContext* cx, GLenum pname, @@ -2392,6 +2526,10 @@ void ClientWebGLContext::GetParameter(JSContext* cx, GLenum pname,
break; break;
case dom::WEBGL_debug_renderer_info_Binding::UNMASKED_VENDOR_WEBGL: case dom::WEBGL_debug_renderer_info_Binding::UNMASKED_VENDOR_WEBGL:
@ -260,7 +290,7 @@ index db60868f65..afed6eeb7c 100644
ret = GetUnmaskedVendor(); ret = GetUnmaskedVendor();
break; break;
@@ -2482,7 +2590,9 @@ void ClientWebGLContext::GetParameter(JSContext* cx, GLenum pname, @@ -2482,7 +2620,9 @@ void ClientWebGLContext::GetParameter(JSContext* cx, GLenum pname,
case LOCAL_GL_COLOR_WRITEMASK: { case LOCAL_GL_COLOR_WRITEMASK: {
const auto mask = uint8_t(*maybe); const auto mask = uint8_t(*maybe);
const auto bs = std::bitset<4>(mask); const auto bs = std::bitset<4>(mask);
@ -271,7 +301,7 @@ index db60868f65..afed6eeb7c 100644
JS::Rooted<JS::Value> arr(cx); JS::Rooted<JS::Value> arr(cx);
if (!dom::ToJSValue(cx, src.data(), src.size(), &arr)) { if (!dom::ToJSValue(cx, src.data(), src.size(), &arr)) {
rv = NS_ERROR_OUT_OF_MEMORY; rv = NS_ERROR_OUT_OF_MEMORY;
@@ -2865,6 +2975,24 @@ ClientWebGLContext::GetShaderPrecisionFormat(const GLenum shadertype, @@ -2865,6 +3005,24 @@ ClientWebGLContext::GetShaderPrecisionFormat(const GLenum shadertype,
const GLenum precisiontype) { const GLenum precisiontype) {
if (IsContextLost()) return nullptr; if (IsContextLost()) return nullptr;
const auto info = [&]() { const auto info = [&]() {
@ -296,7 +326,7 @@ index db60868f65..afed6eeb7c 100644
const auto& inProcess = mNotLost->inProcess; const auto& inProcess = mNotLost->inProcess;
if (inProcess) { if (inProcess) {
return inProcess->GetShaderPrecisionFormat(shadertype, precisiontype); return inProcess->GetShaderPrecisionFormat(shadertype, precisiontype);
@@ -5822,6 +5950,17 @@ bool ClientWebGLContext::IsSupported(const WebGLExtensionID ext, @@ -5822,6 +5980,17 @@ bool ClientWebGLContext::IsSupported(const WebGLExtensionID ext,
return false; return false;
} }
@ -314,14 +344,15 @@ index db60868f65..afed6eeb7c 100644
const auto& limits = Limits(); const auto& limits = Limits();
return limits.supportedExtensions[ext]; return limits.supportedExtensions[ext];
} }
@@ -5833,6 +5972,17 @@ void ClientWebGLContext::GetSupportedExtensions( @@ -5833,6 +6002,18 @@ void ClientWebGLContext::GetSupportedExtensions(
if (!mNotLost) return; if (!mNotLost) return;
auto& retarr = retval.SetValue(); auto& retarr = retval.SetValue();
+ +
+ // Implement separately to prevent O(n^2) timing + // Implement separately to prevent O(n^2) timing
+ if (std::vector<std::string> maskValues = + if (std::vector<std::string> maskValues =
+ MaskConfig::GetStringList("webgl:supportedExtensions"); + MaskConfig::GetStringList(mIsWebGL2 ? "webgl2:supportedExtensions"
+ : "webgl:supportedExtensions");
+ !maskValues.empty()) { + !maskValues.empty()) {
+ for (const auto& ext : maskValues) { + for (const auto& ext : maskValues) {
+ retarr.AppendElement(NS_ConvertUTF8toUTF16(ext)); + retarr.AppendElement(NS_ConvertUTF8toUTF16(ext));

View file

@ -87,7 +87,8 @@
border-radius: 4px; border-radius: 4px;
display: none; display: none;
position: relative; position: relative;
transition: opacity 0.3s ease-out, max-height 0.3s ease-out, padding 0.3s ease-out; transition: opacity 0.3s ease-out, max-height 0.3s ease-out,
padding 0.3s ease-out;
opacity: 1; opacity: 1;
max-height: 100px; max-height: 100px;
overflow: hidden; overflow: hidden;
@ -114,7 +115,10 @@
<body> <body>
<div class="container"> <div class="container">
<h1>Your WebGL Information (in Camoufox format)</h1> <h1>Your WebGL Information (in Camoufox format)</h1>
<div id="warning">Warning: This browser is not Firefox. The fingerprint below will not run properly on Camoufox.<button id="close-warning">&times;</button></div> <div id="warning">
Warning: This browser is not Firefox. The fingerprint below will not run
properly on Camoufox.<button id="close-warning">&times;</button>
</div>
<div id="hash"></div> <div id="hash"></div>
<pre id="output"></pre> <pre id="output"></pre>
<div class="button-container"> <div class="button-container">
@ -152,33 +156,21 @@
return {}; return {};
} }
function getMaxAnisotropy(ctx) {
if (ctx) {
const ext =
ctx.getExtension("EXT_texture_filter_anisotropic") ||
ctx.getExtension("WEBKIT_EXT_texture_filter_anisotropic") ||
ctx.getExtension("MOZ_EXT_texture_filter_anisotropic");
if (ext) {
return ctx.getParameter(ext.MAX_TEXTURE_MAX_ANISOTROPY_EXT);
}
}
return null;
}
const glEnums = [ const glEnums = [
2849, 2884, 2885, 2886, 2928, 2929, 2930, 2931, 2932, 2960, 2961, 2849, 2884, 2885, 2886, 2928, 2929, 2930, 2931, 2932, 2960, 2961,
2962, 2963, 2964, 2965, 2966, 2967, 2968, 2978, 3024, 3042, 3088, 2962, 2963, 2964, 2965, 2966, 2967, 2968, 2978, 3024, 3042, 3074,
3089, 3106, 3107, 3317, 3333, 3379, 3386, 3408, 3410, 3411, 3412, 3088, 3089, 3106, 3107, 3314, 3315, 3316, 3317, 3330, 3331, 3332,
3413, 3414, 3415, 7936, 7937, 7938, 10752, 32773, 32777, 32823, 32824, 3333, 3379, 3386, 3408, 3410, 3411, 3412, 3413, 3414, 3415, 7936,
32873, 32883, 32936, 32937, 32938, 32939, 32968, 32969, 32970, 32971, 7937, 7938, 10752, 32773, 32777, 32823, 32824, 32873, 32877, 32878,
33170, 33901, 33902, 34016, 34024, 34045, 34047, 34068, 34076, 34467, 32883, 32926, 32928, 32936, 32937, 32938, 32939, 32968, 32969, 32970,
34816, 34817, 34818, 34819, 34852, 34877, 34921, 34930, 34964, 34965, 32971, 33000, 33001, 33170, 33901, 33902, 34016, 34024, 34045, 34047,
35071, 35076, 35077, 35371, 35373, 35374, 35375, 35376, 35377, 35379, 34068, 34076, 34467, 34816, 34817, 34818, 34819, 34852, 34853, 34854,
35380, 35657, 35658, 35659, 35660, 35661, 35724, 35725, 35968, 35978, 34855, 34856, 34857, 34858, 34859, 34860, 34877, 34921, 34930, 34964,
35979, 36003, 36004, 36005, 36006, 36007, 36063, 36183, 36347, 36348, 34965, 35071, 35076, 35077, 35371, 35373, 35374, 35375, 35376, 35377,
36349, 37154, 37157, 37440, 37441, 37443, 37444, 37445, 37446, 35379, 35380, 35657, 35658, 35659, 35660, 35661, 35723, 35724, 35725,
35738, 35739, 35968, 35977, 35978, 35979, 36003, 36004, 36005, 36006,
36007, 36063, 36183, 36203, 36345, 36347, 36348, 36349, 36387, 36388,
37137, 37154, 37157, 37440, 37441, 37443, 37444, 37445, 37446, 37447,
]; ];
let rendererInfo = null; let rendererInfo = null;
@ -300,14 +292,17 @@
"DOMContentLoaded", "DOMContentLoaded",
async () => { async () => {
// Check if the user agent is Firefox // Check if the user agent is Firefox
const isFirefox = navigator.userAgent.toLowerCase().indexOf('firefox') > -1; const isFirefox =
navigator.userAgent.toLowerCase().indexOf("firefox") > -1;
if (!isFirefox) { if (!isFirefox) {
document.getElementById('warning').style.display = 'block'; document.getElementById("warning").style.display = "block";
} }
// Add event listener for close button // Add event listener for close button
document.getElementById('close-warning').addEventListener('click', function() { document
document.getElementById('warning').classList.add('hide'); .getElementById("close-warning")
.addEventListener("click", function () {
document.getElementById("warning").classList.add("hide");
}); });
const [webglDetail, webgl2Detail] = await Promise.all([ const [webglDetail, webgl2Detail] = await Promise.all([

View file

@ -70,8 +70,11 @@
{ "property": "webgl:renderer", "type": "str" }, { "property": "webgl:renderer", "type": "str" },
{ "property": "webgl:vendor", "type": "str" }, { "property": "webgl:vendor", "type": "str" },
{ "property": "webgl:supportedExtensions", "type": "array" }, { "property": "webgl:supportedExtensions", "type": "array" },
{ "property": "webgl2:supportedExtensions", "type": "array" },
{ "property": "webgl:parameters", "type": "dict" }, { "property": "webgl:parameters", "type": "dict" },
{ "property": "webgl:parameters:blockIfNotDefined", "type": "bool" },
{ "property": "webgl2:parameters", "type": "dict" }, { "property": "webgl2:parameters", "type": "dict" },
{ "property": "webgl2:parameters:blockIfNotDefined", "type": "bool" },
{ "property": "webgl:shaderPrecisionFormats", "type": "dict" }, { "property": "webgl:shaderPrecisionFormats", "type": "dict" },
{ "property": "webgl:shaderPrecisionFormats:blockIfNotDefined", "type": "bool" }, { "property": "webgl:shaderPrecisionFormats:blockIfNotDefined", "type": "bool" },
{ "property": "webgl2:shaderPrecisionFormats", "type": "dict" }, { "property": "webgl2:shaderPrecisionFormats", "type": "dict" },