diff --git a/benches/benches/wgpu-benchmark/shader.rs b/benches/benches/wgpu-benchmark/shader.rs index e85fd75e4a1..548b1fdfac5 100644 --- a/benches/benches/wgpu-benchmark/shader.rs +++ b/benches/benches/wgpu-benchmark/shader.rs @@ -471,6 +471,7 @@ pub fn backends(ctx: BenchmarkContext) -> anyhow::Result> { version: naga::back::glsl::Version::new_gles(320), writer_flags: naga::back::glsl::WriterFlags::empty(), binding_map: Default::default(), + external_texture_binding_map: Default::default(), zero_initialize_workgroup_memory: true, }; for input in &inputs.inner { diff --git a/naga/src/back/glsl/features.rs b/naga/src/back/glsl/features.rs index bd155b25552..b6c2214ec9c 100644 --- a/naga/src/back/glsl/features.rs +++ b/naga/src/back/glsl/features.rs @@ -59,6 +59,8 @@ bitflags::bitflags! { const SHADER_BARYCENTRICS = 1 << 26; /// Primitive index builtin const PRIMITIVE_INDEX = 1 << 27; + /// External texture (using samplerExternalOES) + const EXTERNAL_TEXTURE = 1 << 28; } } @@ -139,6 +141,7 @@ impl FeaturesManager { check_feature!(TEXTURE_LEVELS, 130); check_feature!(IMAGE_SIZE, 430, 310); check_feature!(TEXTURE_SHADOW_LOD, 200, 300); + check_feature!(EXTERNAL_TEXTURE, 9999, 300); // Return an error if there are missing features if missing.is_empty() { @@ -312,6 +315,11 @@ impl FeaturesManager { } } + if self.0.contains(Features::EXTERNAL_TEXTURE) && options.version.is_es() { + // https://www.khronos.org/registry/OpenGL/extensions/OES/OES_EGL_image_external_essl3.txt + writeln!(out, "#extension GL_OES_EGL_image_external_essl3 : require")?; + } + Ok(()) } } @@ -444,9 +452,11 @@ impl Writer<'_, W> { } _ => {} }, + ImageClass::External => { + self.features.request(Features::EXTERNAL_TEXTURE); + } ImageClass::Sampled { multi: false, .. } - | ImageClass::Depth { multi: false } - | ImageClass::External => {} + | ImageClass::Depth { multi: false } => {} } } _ => {} diff --git a/naga/src/back/glsl/keywords.rs b/naga/src/back/glsl/keywords.rs index aeaec5b03c0..2be699a2607 100644 --- a/naga/src/back/glsl/keywords.rs +++ b/naga/src/back/glsl/keywords.rs @@ -492,6 +492,9 @@ pub const RESERVED_KEYWORDS: &[&str] = &[ super::MODF_FUNCTION, super::FREXP_FUNCTION, super::FIRST_INSTANCE_BINDING, + super::SAMPLE_EXTERNAL_TEXTURE_FUNCTION, + super::IMAGE_LOAD_EXTERNAL_FUNCTION, + super::IMAGE_SIZE_EXTERNAL_FUNCTION, ]; /// The above set of reserved keywords, turned into a cached HashSet. This saves diff --git a/naga/src/back/glsl/mod.rs b/naga/src/back/glsl/mod.rs index d9890edb345..4e827c287d6 100644 --- a/naga/src/back/glsl/mod.rs +++ b/naga/src/back/glsl/mod.rs @@ -91,6 +91,9 @@ const CLAMPED_LOD_SUFFIX: &str = "_clamped_lod"; pub(crate) const MODF_FUNCTION: &str = "naga_modf"; pub(crate) const FREXP_FUNCTION: &str = "naga_frexp"; +pub(crate) const SAMPLE_EXTERNAL_TEXTURE_FUNCTION: &str = "nagaSampleExternalTexture"; +pub(crate) const IMAGE_LOAD_EXTERNAL_FUNCTION: &str = "nagaTextureLoadExternal"; +pub(crate) const IMAGE_SIZE_EXTERNAL_FUNCTION: &str = "nagaTextureDimensionsExternal"; // Must match code in glsl_built_in pub const FIRST_INSTANCE_BINDING: &str = "naga_vs_first_instance"; @@ -120,6 +123,48 @@ where /// Mapping between resources and bindings. pub type BindingMap = alloc::collections::BTreeMap; +/// Binding targets for a single external texture. +/// +/// Holds both the texture unit for the `samplerExternalOES` and the uniform +/// buffer binding point for the `NagaExternalTextureParams` UBO. +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize))] +#[cfg_attr(feature = "deserialize", derive(serde::Deserialize))] +pub struct ExternalTextureBindTarget { + /// The texture unit to bind the `samplerExternalOES` to. + pub texture: u8, + /// The uniform buffer binding point for `NagaExternalTextureParams`. + pub params: u8, +} + +/// Mapping between resources and external texture bind targets. +pub type ExternalTextureBindingMap = + alloc::collections::BTreeMap; + +#[cfg(feature = "deserialize")] +#[derive(serde::Deserialize)] +struct ExternalTextureBindingMapSerialization { + resource_binding: crate::ResourceBinding, + bind_target: ExternalTextureBindTarget, +} + +#[cfg(feature = "deserialize")] +fn deserialize_external_texture_binding_map<'de, D>( + deserializer: D, +) -> Result +where + D: serde::Deserializer<'de>, +{ + use serde::Deserialize; + + let vec = Vec::::deserialize(deserializer)?; + let mut map = ExternalTextureBindingMap::default(); + for item in vec { + map.insert(item.resource_binding, item.bind_target); + } + Ok(map) +} + impl crate::AtomicFunction { const fn to_glsl(self) -> &'static str { match self { @@ -336,6 +381,16 @@ pub struct Options { serde(deserialize_with = "deserialize_binding_map") )] pub binding_map: BindingMap, + /// Map of external texture resources to their bind targets. + /// + /// For each external texture, specifies the texture unit for the + /// `samplerExternalOES` and the uniform buffer binding point for the + /// `NagaExternalTextureParams` UBO used for color space conversion. + #[cfg_attr( + feature = "deserialize", + serde(deserialize_with = "deserialize_external_texture_binding_map") + )] + pub external_texture_binding_map: ExternalTextureBindingMap, /// Should workgroup variables be zero initialized (by polyfilling)? pub zero_initialize_workgroup_memory: bool, } @@ -346,6 +401,7 @@ impl Default for Options { version: Version::new_gles(310), writer_flags: WriterFlags::ADJUST_COORDINATE_SPACE, binding_map: BindingMap::default(), + external_texture_binding_map: ExternalTextureBindingMap::default(), zero_initialize_workgroup_memory: true, } } @@ -645,4 +701,5 @@ pub fn supported_capabilities() -> valid::Capabilities { | Caps::DRAW_INDEX | Caps::MEMORY_DECORATION_COHERENT | Caps::MEMORY_DECORATION_VOLATILE + | Caps::TEXTURE_EXTERNAL } diff --git a/naga/src/back/glsl/writer.rs b/naga/src/back/glsl/writer.rs index ea30d705585..4ec79cf5d24 100644 --- a/naga/src/back/glsl/writer.rs +++ b/naga/src/back/glsl/writer.rs @@ -391,7 +391,40 @@ impl<'a, W: Write> Writer<'a, W> { writeln!(self.out, " {global_name};")?; writeln!(self.out)?; - self.reflection_names_globals.insert(handle, global_name); + self.reflection_names_globals + .insert(handle, global_name.clone()); + + // For external textures, also emit a UBO for the params + if class == crate::ImageClass::External { + if let Some(ref br) = global.binding { + if let Some(ext_target) = + self.options.external_texture_binding_map.get(br) + { + let params_ty = + self.module.special_types.external_texture_params.unwrap(); + let params_ty_name = &self.names[&NameKey::Type(params_ty)].clone(); + let block_name = format!( + "{}_block_{}{:?}", + params_ty_name.trim_end_matches('_'), + self.block_id.generate(), + self.entry_point.stage, + ); + if self.options.version.supports_explicit_locations() { + write!( + self.out, + "layout(std430, binding = {}) ", + ext_target.params + )?; + } else { + write!(self.out, "layout(std430) ")?; + } + write!(self.out, "readonly buffer {block_name} {{ ")?; + write!(self.out, "{params_ty_name} _member; ")?; + writeln!(self.out, "}} {global_name}_params;")?; + writeln!(self.out)?; + } + } + } } // glsl has no concept of samplers so we just ignore it TypeInner::Sampler { .. } => continue, @@ -412,6 +445,12 @@ impl<'a, W: Write> Writer<'a, W> { } writeln!(self.out)?; + // Write external texture helpers when requested + if self.features.contains(Features::EXTERNAL_TEXTURE) { + self.write_external_texture_helpers()?; + writeln!(self.out)?; + } + // Write all regular functions for (handle, function) in self.module.functions.iter() { // Check that the function doesn't use globals that aren't supported @@ -597,7 +636,10 @@ impl<'a, W: Write> Writer<'a, W> { Ic::Depth { multi: true } => ("sampler", float, "MS", ""), Ic::Depth { multi: false } => ("sampler", float, "", "Shadow"), Ic::Storage { format, .. } => ("image", format.into(), "", ""), - Ic::External => unimplemented!(), + Ic::External => { + write!(self.out, "highp samplerExternalOES")?; + return Ok(()); + } }; let precision = if self.options.version.is_es() { @@ -1313,6 +1355,18 @@ impl<'a, W: Write> Writer<'a, W> { _ => {} } + // For external texture arguments, emit an additional params parameter + if let TypeInner::Image { + class: crate::ImageClass::External, + .. + } = this.module.types[arg.ty].inner + { + let params_ty = this.module.special_types.external_texture_params.unwrap(); + let params_ty_name = this.names[&NameKey::Type(params_ty)].clone(); + let arg_name = this.names[&ctx.argument_key(i as u32)].clone(); + write!(this.out, ", {params_ty_name} {arg_name}_params")?; + } + Ok(()) })?; @@ -1485,6 +1539,85 @@ impl<'a, W: Write> Writer<'a, W> { Ok(()) } + #[rustfmt::skip] + fn write_external_texture_helpers(&mut self) -> BackendResult { + let params_ty = self.module.special_types.external_texture_params.unwrap(); + let params_ty_name = self.names[&NameKey::Type(params_ty)].clone(); + + // External sampler + writeln!(self.out, "vec4 {SAMPLE_EXTERNAL_TEXTURE_FUNCTION}(highp samplerExternalOES tex, {params_ty_name} params, vec2 coords) {{")?; + writeln!(self.out, " coords = (params.sample_transform * vec3(coords, 1.0));")?; + writeln!(self.out, " vec2 bounds_min = (params.sample_transform * vec3(0.0, 0.0, 1.0));")?; + writeln!(self.out, " vec2 bounds_max = (params.sample_transform * vec3(1.0, 1.0, 1.0));")?; + writeln!(self.out, " vec4 bounds = vec4(min(bounds_min, bounds_max), max(bounds_min, bounds_max));")?; + writeln!(self.out, " vec2 size = vec2(textureSize(tex, 0));")?; + writeln!(self.out, " vec2 half_texel = vec2(0.5) / size;")?; + writeln!(self.out, " vec2 clamped = clamp(coords, bounds.xy + half_texel, bounds.zw - half_texel);")?; + writeln!(self.out, " vec4 srcColor = texture(tex, clamped);")?; + writeln!(self.out, " vec3 srcGammaRgb = srcColor.rgb;")?; + writeln!(self.out, " vec3 srcLinearRgb = mix(")?; + writeln!(self.out, " pow((srcGammaRgb + params.src_tf.a - 1.0) / params.src_tf.a, vec3(params.src_tf.g)),")?; + writeln!(self.out, " srcGammaRgb / params.src_tf.k,")?; + writeln!(self.out, " lessThan(srcGammaRgb, vec3(params.src_tf.k * params.src_tf.b)));")?; + writeln!(self.out, " vec3 dstLinearRgb = params.gamut_conversion_matrix * srcLinearRgb;")?; + writeln!(self.out, " vec3 dstGammaRgb = mix(")?; + writeln!(self.out, " params.dst_tf.a * pow(dstLinearRgb, vec3(1.0 / params.dst_tf.g)) - (params.dst_tf.a - 1.0),")?; + writeln!(self.out, " params.dst_tf.k * dstLinearRgb,")?; + writeln!(self.out, " lessThan(dstLinearRgb, vec3(params.dst_tf.b)));")?; + writeln!(self.out, " return vec4(dstGammaRgb, srcColor.a);")?; + writeln!(self.out, "}}")?; + + // External load + writeln!(self.out)?; + writeln!(self.out, "vec4 {IMAGE_LOAD_EXTERNAL_FUNCTION}(highp samplerExternalOES tex, {params_ty_name} params, ivec2 coords) {{")?; + writeln!(self.out, " uvec2 tex_size = uvec2(textureSize(tex, 0));")?; + writeln!(self.out, " uvec2 cropped_size = (params.size != uvec2(0u)) ? params.size : tex_size;")?; + writeln!(self.out, " coords = min(coords, ivec2(cropped_size - uvec2(1u)));")?; + writeln!(self.out, " ivec2 transformed = ivec2(round(params.load_transform * vec3(vec2(coords), 1.0)));")?; + writeln!(self.out, " vec4 srcColor = texelFetch(tex, transformed, 0);")?; + writeln!(self.out, " vec3 srcGammaRgb = srcColor.rgb;")?; + writeln!(self.out, " vec3 srcLinearRgb = mix(")?; + writeln!(self.out, " pow((srcGammaRgb + params.src_tf.a - 1.0) / params.src_tf.a, vec3(params.src_tf.g)),")?; + writeln!(self.out, " srcGammaRgb / params.src_tf.k,")?; + writeln!(self.out, " lessThan(srcGammaRgb, vec3(params.src_tf.k * params.src_tf.b)));")?; + writeln!(self.out, " vec3 dstLinearRgb = params.gamut_conversion_matrix * srcLinearRgb;")?; + writeln!(self.out, " vec3 dstGammaRgb = mix(")?; + writeln!(self.out, " params.dst_tf.a * pow(dstLinearRgb, vec3(1.0 / params.dst_tf.g)) - (params.dst_tf.a - 1.0),")?; + writeln!(self.out, " params.dst_tf.k * dstLinearRgb,")?; + writeln!(self.out, " lessThan(dstLinearRgb, vec3(params.dst_tf.b)));")?; + writeln!(self.out, " return vec4(dstGammaRgb, srcColor.a);")?; + writeln!(self.out, "}}")?; + + // External dimension + writeln!(self.out)?; + writeln!(self.out, "uvec2 {IMAGE_SIZE_EXTERNAL_FUNCTION}(highp samplerExternalOES tex, {params_ty_name} params) {{")?; + writeln!(self.out, " uvec2 s = params.size;")?; + writeln!(self.out, " return (s != uvec2(0u)) ? s : uvec2(textureSize(tex, 0));")?; + writeln!(self.out, "}}")?; + + Ok(()) + } + + fn write_external_texture_params( + &mut self, + image: Handle, + ctx: &back::FunctionCtx, + ) -> BackendResult { + match ctx.expressions[image] { + crate::Expression::GlobalVariable(handle) => { + let global = &self.module.global_variables[handle]; + let name = self.get_global_name(handle, global); + write!(self.out, "{name}_params._member")?; + } + crate::Expression::FunctionArgument(index) => { + let name = self.names[&ctx.argument_key(index)].clone(); + write!(self.out, "{name}_params")?; + } + _ => unreachable!("External texture must be a global variable or function argument"), + } + Ok(()) + } + /// Helper method used to output a dot product as an arithmetic expression /// fn write_dot_product( @@ -2056,11 +2189,24 @@ impl<'a, W: Write> Writer<'a, W> { let arg_ty = self.module.functions[function].arguments[i].ty; match self.module.types[arg_ty].inner { TypeInner::Sampler { .. } => None, - _ => Some(*arg), + _ => Some((i, *arg)), } }) .collect(); - self.write_slice(&arguments, |this, _, arg| this.write_expr(*arg, ctx))?; + self.write_slice(&arguments, |this, _, &(orig_idx, arg)| { + this.write_expr(arg, ctx)?; + // For external texture arguments, also pass the params + let arg_ty = this.module.functions[function].arguments[orig_idx].ty; + if let TypeInner::Image { + class: crate::ImageClass::External, + .. + } = this.module.types[arg_ty].inner + { + write!(this.out, ", ")?; + this.write_external_texture_params(arg, ctx)?; + } + Ok(()) + })?; writeln!(self.out, ");")? } Statement::Atomic { @@ -2550,6 +2696,18 @@ impl<'a, W: Write> Writer<'a, W> { } => (dim, class, arrayed), _ => unreachable!(), }; + + if class == crate::ImageClass::External { + write!(self.out, "{SAMPLE_EXTERNAL_TEXTURE_FUNCTION}(")?; + self.write_expr(image, ctx)?; + write!(self.out, ", ")?; + self.write_external_texture_params(image, ctx)?; + write!(self.out, ", ")?; + self.write_expr(coordinate, ctx)?; + write!(self.out, ")")?; + return Ok(()); + } + let mut err = None; if dim == crate::ImageDimension::Cube { if offset.is_some() { @@ -2785,7 +2943,12 @@ impl<'a, W: Write> Writer<'a, W> { write!(self.out, "imageSize(")?; self.write_expr(image, ctx)?; } - ImageClass::External => unimplemented!(), + ImageClass::External => { + write!(self.out, "{IMAGE_SIZE_EXTERNAL_FUNCTION}(")?; + self.write_expr(image, ctx)?; + write!(self.out, ", ")?; + self.write_external_texture_params(image, ctx)?; + } } write!(self.out, ")")?; if components != 1 || self.options.version.is_es() { @@ -2801,7 +2964,7 @@ impl<'a, W: Write> Writer<'a, W> { let fun_name = match class { ImageClass::Sampled { .. } | ImageClass::Depth { .. } => "textureSize", ImageClass::Storage { .. } => "imageSize", - ImageClass::External => unimplemented!(), + ImageClass::External => unreachable!(), }; write!(self.out, "{fun_name}(")?; self.write_expr(image, ctx)?; @@ -2821,7 +2984,7 @@ impl<'a, W: Write> Writer<'a, W> { "textureSamples" } ImageClass::Storage { .. } => "imageSamples", - ImageClass::External => unimplemented!(), + ImageClass::External => unreachable!(), }; write!(self.out, "{fun_name}(")?; self.write_expr(image, ctx)?; @@ -4079,6 +4242,20 @@ impl<'a, W: Write> Writer<'a, W> { _ => unreachable!(), }; + // For external textures, use the dedicated load helper + if class == crate::ImageClass::External { + write!(self.out, "{IMAGE_LOAD_EXTERNAL_FUNCTION}(")?; + self.write_expr(image, ctx)?; + write!(self.out, ", ")?; + self.write_external_texture_params(image, ctx)?; + write!(self.out, ", ")?; + // Coordinates need to be ivec2; the WGSL coord may be vec2 or vec2 + write!(self.out, "ivec2(")?; + self.write_expr(coordinate, ctx)?; + write!(self.out, "))")?; + return Ok(()); + } + // Get the name of the function to be used for the load operation // and the policy to be used with it. let (fun_name, policy) = match class { @@ -4106,7 +4283,7 @@ impl<'a, W: Write> Writer<'a, W> { "WGSL `textureLoad` from depth textures is not supported in GLSL".to_string(), )) } - crate::ImageClass::External => unimplemented!(), + crate::ImageClass::External => unreachable!(), }; // openGL es doesn't have 1D images so we need workaround it diff --git a/naga/tests/in/wgsl/texture-external.toml b/naga/tests/in/wgsl/texture-external.toml index 6e12c515eec..0ad5955b63a 100644 --- a/naga/tests/in/wgsl/texture-external.toml +++ b/naga/tests/in/wgsl/texture-external.toml @@ -1,5 +1,5 @@ capabilities = "TEXTURE_EXTERNAL" -targets = "HLSL | IR | METAL | WGSL" +targets = "HLSL | IR | METAL | WGSL | GLSL" [msl.per_entry_point_map.compute_main] resources = [ @@ -43,3 +43,15 @@ bind_target.planes = [ { space = 0, register = 2 }, ] bind_target.params = { space = 0, register = 3 } + +[glsl] +version.Embedded = { is_webgl = false, version = 310 } +writer_flags = "" +zero_initialize_workgroup_memory = true +binding_map = [ + { resource_binding = { group = 0, binding = 0 }, bind_target = 0 }, + { resource_binding = { group = 0, binding = 1 }, bind_target = 1 }, +] +external_texture_binding_map = [ + { resource_binding = { group = 0, binding = 0 }, bind_target = { texture = 0, params = 0 } }, +] diff --git a/naga/tests/out/glsl/wgsl-texture-external.compute_main.Compute.glsl b/naga/tests/out/glsl/wgsl-texture-external.compute_main.Compute.glsl new file mode 100644 index 00000000000..83bba76b5f2 --- /dev/null +++ b/naga/tests/out/glsl/wgsl-texture-external.compute_main.Compute.glsl @@ -0,0 +1,99 @@ +#version 310 es +#extension GL_OES_EGL_image_external_essl3 : require + +precision highp float; +precision highp int; + +layout(local_size_x = 1, local_size_y = 1, local_size_z = 1) in; + +struct NagaExternalTextureTransferFn { + float a; + float b; + float g; + float k; +}; +struct NagaExternalTextureParams { + mat4x4 yuv_conversion_matrix; + mat3x3 gamut_conversion_matrix; + NagaExternalTextureTransferFn src_tf; + NagaExternalTextureTransferFn dst_tf; + mat3x2 sample_transform; + mat3x2 load_transform; + uvec2 size; + uint num_planes; +}; +layout(binding = 0) uniform highp samplerExternalOES _group_0_binding_0_cs; + +layout(std430, binding = 0) readonly buffer NagaExternalTextureParams_block_0Compute { NagaExternalTextureParams _member; } _group_0_binding_0_cs_params; + + +vec4 nagaSampleExternalTexture(highp samplerExternalOES tex, NagaExternalTextureParams params, vec2 coords) { + coords = (params.sample_transform * vec3(coords, 1.0)); + vec2 bounds_min = (params.sample_transform * vec3(0.0, 0.0, 1.0)); + vec2 bounds_max = (params.sample_transform * vec3(1.0, 1.0, 1.0)); + vec4 bounds = vec4(min(bounds_min, bounds_max), max(bounds_min, bounds_max)); + vec2 size = vec2(textureSize(tex, 0)); + vec2 half_texel = vec2(0.5) / size; + vec2 clamped = clamp(coords, bounds.xy + half_texel, bounds.zw - half_texel); + vec4 srcColor = texture(tex, clamped); + vec3 srcGammaRgb = srcColor.rgb; + vec3 srcLinearRgb = mix( + pow((srcGammaRgb + params.src_tf.a - 1.0) / params.src_tf.a, vec3(params.src_tf.g)), + srcGammaRgb / params.src_tf.k, + lessThan(srcGammaRgb, vec3(params.src_tf.k * params.src_tf.b))); + vec3 dstLinearRgb = params.gamut_conversion_matrix * srcLinearRgb; + vec3 dstGammaRgb = mix( + params.dst_tf.a * pow(dstLinearRgb, vec3(1.0 / params.dst_tf.g)) - (params.dst_tf.a - 1.0), + params.dst_tf.k * dstLinearRgb, + lessThan(dstLinearRgb, vec3(params.dst_tf.b))); + return vec4(dstGammaRgb, srcColor.a); +} + +vec4 nagaTextureLoadExternal(highp samplerExternalOES tex, NagaExternalTextureParams params, ivec2 coords) { + uvec2 tex_size = uvec2(textureSize(tex, 0)); + uvec2 cropped_size = (params.size != uvec2(0u)) ? params.size : tex_size; + coords = min(coords, ivec2(cropped_size - uvec2(1u))); + ivec2 transformed = ivec2(round(params.load_transform * vec3(vec2(coords), 1.0))); + vec4 srcColor = texelFetch(tex, transformed, 0); + vec3 srcGammaRgb = srcColor.rgb; + vec3 srcLinearRgb = mix( + pow((srcGammaRgb + params.src_tf.a - 1.0) / params.src_tf.a, vec3(params.src_tf.g)), + srcGammaRgb / params.src_tf.k, + lessThan(srcGammaRgb, vec3(params.src_tf.k * params.src_tf.b))); + vec3 dstLinearRgb = params.gamut_conversion_matrix * srcLinearRgb; + vec3 dstGammaRgb = mix( + params.dst_tf.a * pow(dstLinearRgb, vec3(1.0 / params.dst_tf.g)) - (params.dst_tf.a - 1.0), + params.dst_tf.k * dstLinearRgb, + lessThan(dstLinearRgb, vec3(params.dst_tf.b))); + return vec4(dstGammaRgb, srcColor.a); +} + +uvec2 nagaTextureDimensionsExternal(highp samplerExternalOES tex, NagaExternalTextureParams params) { + uvec2 s = params.size; + return (s != uvec2(0u)) ? s : uvec2(textureSize(tex, 0)); +} + +vec4 test(highp samplerExternalOES t, NagaExternalTextureParams t_params) { + vec4 a = vec4(0.0); + vec4 b = vec4(0.0); + vec4 c = vec4(0.0); + uvec2 d = uvec2(0u); + vec4 _e4 = nagaSampleExternalTexture(t, t_params, vec2(0.0)); + a = _e4; + vec4 _e8 = nagaTextureLoadExternal(t, t_params, ivec2(ivec2(0))); + b = _e8; + vec4 _e12 = nagaTextureLoadExternal(t, t_params, ivec2(uvec2(0u))); + c = _e12; + d = uvec2(nagaTextureDimensionsExternal(t, t_params).xy); + vec4 _e16 = a; + vec4 _e17 = b; + vec4 _e19 = c; + uvec2 _e21 = d; + return (((_e16 + _e17) + _e19) + vec2(_e21).xyxy); +} + +void main() { + vec4 _e1 = test(_group_0_binding_0_cs, _group_0_binding_0_cs_params._member); + return; +} + diff --git a/naga/tests/out/glsl/wgsl-texture-external.fragment_main.Fragment.glsl b/naga/tests/out/glsl/wgsl-texture-external.fragment_main.Fragment.glsl new file mode 100644 index 00000000000..0e9f926f43f --- /dev/null +++ b/naga/tests/out/glsl/wgsl-texture-external.fragment_main.Fragment.glsl @@ -0,0 +1,99 @@ +#version 310 es +#extension GL_OES_EGL_image_external_essl3 : require + +precision highp float; +precision highp int; + +struct NagaExternalTextureTransferFn { + float a; + float b; + float g; + float k; +}; +struct NagaExternalTextureParams { + mat4x4 yuv_conversion_matrix; + mat3x3 gamut_conversion_matrix; + NagaExternalTextureTransferFn src_tf; + NagaExternalTextureTransferFn dst_tf; + mat3x2 sample_transform; + mat3x2 load_transform; + uvec2 size; + uint num_planes; +}; +layout(binding = 0) uniform highp samplerExternalOES _group_0_binding_0_fs; + +layout(std430, binding = 0) readonly buffer NagaExternalTextureParams_block_0Fragment { NagaExternalTextureParams _member; } _group_0_binding_0_fs_params; + +layout(location = 0) out vec4 _fs2p_location0; + +vec4 nagaSampleExternalTexture(highp samplerExternalOES tex, NagaExternalTextureParams params, vec2 coords) { + coords = (params.sample_transform * vec3(coords, 1.0)); + vec2 bounds_min = (params.sample_transform * vec3(0.0, 0.0, 1.0)); + vec2 bounds_max = (params.sample_transform * vec3(1.0, 1.0, 1.0)); + vec4 bounds = vec4(min(bounds_min, bounds_max), max(bounds_min, bounds_max)); + vec2 size = vec2(textureSize(tex, 0)); + vec2 half_texel = vec2(0.5) / size; + vec2 clamped = clamp(coords, bounds.xy + half_texel, bounds.zw - half_texel); + vec4 srcColor = texture(tex, clamped); + vec3 srcGammaRgb = srcColor.rgb; + vec3 srcLinearRgb = mix( + pow((srcGammaRgb + params.src_tf.a - 1.0) / params.src_tf.a, vec3(params.src_tf.g)), + srcGammaRgb / params.src_tf.k, + lessThan(srcGammaRgb, vec3(params.src_tf.k * params.src_tf.b))); + vec3 dstLinearRgb = params.gamut_conversion_matrix * srcLinearRgb; + vec3 dstGammaRgb = mix( + params.dst_tf.a * pow(dstLinearRgb, vec3(1.0 / params.dst_tf.g)) - (params.dst_tf.a - 1.0), + params.dst_tf.k * dstLinearRgb, + lessThan(dstLinearRgb, vec3(params.dst_tf.b))); + return vec4(dstGammaRgb, srcColor.a); +} + +vec4 nagaTextureLoadExternal(highp samplerExternalOES tex, NagaExternalTextureParams params, ivec2 coords) { + uvec2 tex_size = uvec2(textureSize(tex, 0)); + uvec2 cropped_size = (params.size != uvec2(0u)) ? params.size : tex_size; + coords = min(coords, ivec2(cropped_size - uvec2(1u))); + ivec2 transformed = ivec2(round(params.load_transform * vec3(vec2(coords), 1.0))); + vec4 srcColor = texelFetch(tex, transformed, 0); + vec3 srcGammaRgb = srcColor.rgb; + vec3 srcLinearRgb = mix( + pow((srcGammaRgb + params.src_tf.a - 1.0) / params.src_tf.a, vec3(params.src_tf.g)), + srcGammaRgb / params.src_tf.k, + lessThan(srcGammaRgb, vec3(params.src_tf.k * params.src_tf.b))); + vec3 dstLinearRgb = params.gamut_conversion_matrix * srcLinearRgb; + vec3 dstGammaRgb = mix( + params.dst_tf.a * pow(dstLinearRgb, vec3(1.0 / params.dst_tf.g)) - (params.dst_tf.a - 1.0), + params.dst_tf.k * dstLinearRgb, + lessThan(dstLinearRgb, vec3(params.dst_tf.b))); + return vec4(dstGammaRgb, srcColor.a); +} + +uvec2 nagaTextureDimensionsExternal(highp samplerExternalOES tex, NagaExternalTextureParams params) { + uvec2 s = params.size; + return (s != uvec2(0u)) ? s : uvec2(textureSize(tex, 0)); +} + +vec4 test(highp samplerExternalOES t, NagaExternalTextureParams t_params) { + vec4 a = vec4(0.0); + vec4 b = vec4(0.0); + vec4 c = vec4(0.0); + uvec2 d = uvec2(0u); + vec4 _e4 = nagaSampleExternalTexture(t, t_params, vec2(0.0)); + a = _e4; + vec4 _e8 = nagaTextureLoadExternal(t, t_params, ivec2(ivec2(0))); + b = _e8; + vec4 _e12 = nagaTextureLoadExternal(t, t_params, ivec2(uvec2(0u))); + c = _e12; + d = uvec2(nagaTextureDimensionsExternal(t, t_params).xy); + vec4 _e16 = a; + vec4 _e17 = b; + vec4 _e19 = c; + uvec2 _e21 = d; + return (((_e16 + _e17) + _e19) + vec2(_e21).xyxy); +} + +void main() { + vec4 _e1 = test(_group_0_binding_0_fs, _group_0_binding_0_fs_params._member); + _fs2p_location0 = _e1; + return; +} + diff --git a/naga/tests/out/glsl/wgsl-texture-external.vertex_main.Vertex.glsl b/naga/tests/out/glsl/wgsl-texture-external.vertex_main.Vertex.glsl new file mode 100644 index 00000000000..98184304303 --- /dev/null +++ b/naga/tests/out/glsl/wgsl-texture-external.vertex_main.Vertex.glsl @@ -0,0 +1,98 @@ +#version 310 es +#extension GL_OES_EGL_image_external_essl3 : require + +precision highp float; +precision highp int; + +struct NagaExternalTextureTransferFn { + float a; + float b; + float g; + float k; +}; +struct NagaExternalTextureParams { + mat4x4 yuv_conversion_matrix; + mat3x3 gamut_conversion_matrix; + NagaExternalTextureTransferFn src_tf; + NagaExternalTextureTransferFn dst_tf; + mat3x2 sample_transform; + mat3x2 load_transform; + uvec2 size; + uint num_planes; +}; +layout(binding = 0) uniform highp samplerExternalOES _group_0_binding_0_vs; + +layout(std430, binding = 0) readonly buffer NagaExternalTextureParams_block_0Vertex { NagaExternalTextureParams _member; } _group_0_binding_0_vs_params; + + +vec4 nagaSampleExternalTexture(highp samplerExternalOES tex, NagaExternalTextureParams params, vec2 coords) { + coords = (params.sample_transform * vec3(coords, 1.0)); + vec2 bounds_min = (params.sample_transform * vec3(0.0, 0.0, 1.0)); + vec2 bounds_max = (params.sample_transform * vec3(1.0, 1.0, 1.0)); + vec4 bounds = vec4(min(bounds_min, bounds_max), max(bounds_min, bounds_max)); + vec2 size = vec2(textureSize(tex, 0)); + vec2 half_texel = vec2(0.5) / size; + vec2 clamped = clamp(coords, bounds.xy + half_texel, bounds.zw - half_texel); + vec4 srcColor = texture(tex, clamped); + vec3 srcGammaRgb = srcColor.rgb; + vec3 srcLinearRgb = mix( + pow((srcGammaRgb + params.src_tf.a - 1.0) / params.src_tf.a, vec3(params.src_tf.g)), + srcGammaRgb / params.src_tf.k, + lessThan(srcGammaRgb, vec3(params.src_tf.k * params.src_tf.b))); + vec3 dstLinearRgb = params.gamut_conversion_matrix * srcLinearRgb; + vec3 dstGammaRgb = mix( + params.dst_tf.a * pow(dstLinearRgb, vec3(1.0 / params.dst_tf.g)) - (params.dst_tf.a - 1.0), + params.dst_tf.k * dstLinearRgb, + lessThan(dstLinearRgb, vec3(params.dst_tf.b))); + return vec4(dstGammaRgb, srcColor.a); +} + +vec4 nagaTextureLoadExternal(highp samplerExternalOES tex, NagaExternalTextureParams params, ivec2 coords) { + uvec2 tex_size = uvec2(textureSize(tex, 0)); + uvec2 cropped_size = (params.size != uvec2(0u)) ? params.size : tex_size; + coords = min(coords, ivec2(cropped_size - uvec2(1u))); + ivec2 transformed = ivec2(round(params.load_transform * vec3(vec2(coords), 1.0))); + vec4 srcColor = texelFetch(tex, transformed, 0); + vec3 srcGammaRgb = srcColor.rgb; + vec3 srcLinearRgb = mix( + pow((srcGammaRgb + params.src_tf.a - 1.0) / params.src_tf.a, vec3(params.src_tf.g)), + srcGammaRgb / params.src_tf.k, + lessThan(srcGammaRgb, vec3(params.src_tf.k * params.src_tf.b))); + vec3 dstLinearRgb = params.gamut_conversion_matrix * srcLinearRgb; + vec3 dstGammaRgb = mix( + params.dst_tf.a * pow(dstLinearRgb, vec3(1.0 / params.dst_tf.g)) - (params.dst_tf.a - 1.0), + params.dst_tf.k * dstLinearRgb, + lessThan(dstLinearRgb, vec3(params.dst_tf.b))); + return vec4(dstGammaRgb, srcColor.a); +} + +uvec2 nagaTextureDimensionsExternal(highp samplerExternalOES tex, NagaExternalTextureParams params) { + uvec2 s = params.size; + return (s != uvec2(0u)) ? s : uvec2(textureSize(tex, 0)); +} + +vec4 test(highp samplerExternalOES t, NagaExternalTextureParams t_params) { + vec4 a = vec4(0.0); + vec4 b = vec4(0.0); + vec4 c = vec4(0.0); + uvec2 d = uvec2(0u); + vec4 _e4 = nagaSampleExternalTexture(t, t_params, vec2(0.0)); + a = _e4; + vec4 _e8 = nagaTextureLoadExternal(t, t_params, ivec2(ivec2(0))); + b = _e8; + vec4 _e12 = nagaTextureLoadExternal(t, t_params, ivec2(uvec2(0u))); + c = _e12; + d = uvec2(nagaTextureDimensionsExternal(t, t_params).xy); + vec4 _e16 = a; + vec4 _e17 = b; + vec4 _e19 = c; + uvec2 _e21 = d; + return (((_e16 + _e17) + _e19) + vec2(_e21).xyxy); +} + +void main() { + vec4 _e1 = test(_group_0_binding_0_vs, _group_0_binding_0_vs_params._member); + gl_Position = _e1; + return; +} + diff --git a/wgpu-hal/src/gles/adapter.rs b/wgpu-hal/src/gles/adapter.rs index 246384b804a..015ff92f38a 100644 --- a/wgpu-hal/src/gles/adapter.rs +++ b/wgpu-hal/src/gles/adapter.rs @@ -488,6 +488,11 @@ impl super::Adapter { wgt::Features::SHADER_EARLY_DEPTH_TEST, supported((3, 1), (4, 2)) || extensions.contains("GL_ARB_shader_image_load_store"), ); + features.set( + wgt::Features::EXTERNAL_TEXTURE, + es_ver.is_some_and(|v| v >= (3, 0)) + && extensions.contains("GL_OES_EGL_image_external_essl3"), + ); if extensions.contains("GL_ARB_timer_query") { features.set(wgt::Features::TIMESTAMP_QUERY, true); features.set(wgt::Features::TIMESTAMP_QUERY_INSIDE_ENCODERS, true); diff --git a/wgpu-hal/src/gles/command.rs b/wgpu-hal/src/gles/command.rs index 409481b682a..628e46ac2cd 100644 --- a/wgpu-hal/src/gles/command.rs +++ b/wgpu-hal/src/gles/command.rs @@ -805,6 +805,41 @@ impl crate::CommandEncoder for super::CommandEncoder { binding: binding.clone(), }); } + super::RawBinding::ExternalTexture { + raw, + target, + params_raw, + params_offset, + params_size, + } => { + // Bind the texture to its texture unit slot + dirty_textures |= 1 << slot; + self.state.texture_slots[slot as usize].tex_target = target; + self.cmd_buffer.commands.push(C::BindTexture { + slot, + texture: raw, + target, + aspects: crate::FormatAspects::COLOR, + mip_levels: 0..1, + }); + // Bind the params SSBO to its shader storage buffer binding point + let params_slot = layout + .naga_options + .external_texture_binding_map + .get(&naga::ResourceBinding { + group: index, + binding: binding_layout.binding, + }) + .map(|t| t.params as u32) + .unwrap(); + self.cmd_buffer.commands.push(C::BindBuffer { + target: glow::SHADER_STORAGE_BUFFER, + slot: params_slot, + buffer: params_raw, + offset: params_offset, + size: params_size, + }); + } } } diff --git a/wgpu-hal/src/gles/device.rs b/wgpu-hal/src/gles/device.rs index 8a5bae7bedd..534ea1be6f3 100644 --- a/wgpu-hal/src/gles/device.rs +++ b/wgpu-hal/src/gles/device.rs @@ -1240,6 +1240,7 @@ impl crate::Device for super::Device { // https://github.com/gfx-rs/wgpu/pull/3440/files#r1095726950 writer_flags.set(glsl::WriterFlags::FORCE_POINT_SIZE, true); let mut binding_map = glsl::BindingMap::default(); + let mut external_texture_binding_map = glsl::ExternalTextureBindingMap::default(); for (group_index, bg_layout) in desc.bind_group_layouts.iter().enumerate() { let Some(bg_layout) = bg_layout else { @@ -1273,7 +1274,28 @@ impl crate::Device for super::Device { .. } => &mut num_storage_buffers, wgt::BindingType::AccelerationStructure { .. } => unimplemented!(), - wgt::BindingType::ExternalTexture => unimplemented!(), + wgt::BindingType::ExternalTexture => { + let texture_slot = num_textures; + let params_slot = num_storage_buffers; + let count = entry.count.map_or(1, |c| c.get() as u8); + + binding_to_slot[entry.binding as usize] = texture_slot; + let br = naga::ResourceBinding { + group: group_index as u32, + binding: entry.binding, + }; + binding_map.insert(br, texture_slot); + external_texture_binding_map.insert( + br, + glsl::ExternalTextureBindTarget { + texture: texture_slot, + params: params_slot, + }, + ); + num_textures += count; + num_storage_buffers += count; + continue; + } }; binding_to_slot[entry.binding as usize] = *counter; @@ -1299,6 +1321,7 @@ impl crate::Device for super::Device { version: self.shared.shading_language_version, writer_flags, binding_map, + external_texture_binding_map, zero_initialize_workgroup_memory: true, }, }) @@ -1384,7 +1407,22 @@ impl crate::Device for super::Device { }) } wgt::BindingType::AccelerationStructure { .. } => unimplemented!(), - wgt::BindingType::ExternalTexture => unimplemented!(), + wgt::BindingType::ExternalTexture => { + let ext_tex = &desc.external_textures[entry.resource_index as usize]; + let view = ext_tex.planes[0].view; + let (raw, target) = view.inner.as_native(); + let params = &ext_tex.params; + super::RawBinding::ExternalTexture { + raw, + target, + params_raw: params.buffer.raw.unwrap(), + params_offset: params.offset as i32, + params_size: match params.size { + Some(s) => s.get() as i32, + None => (params.buffer.size - params.offset) as i32, + }, + } + } }; contents.push(binding); } diff --git a/wgpu-hal/src/gles/egl.rs b/wgpu-hal/src/gles/egl.rs index 6323316517b..a0b0bb843aa 100644 --- a/wgpu-hal/src/gles/egl.rs +++ b/wgpu-hal/src/gles/egl.rs @@ -395,6 +395,7 @@ impl Inner { flags: wgt::InstanceFlags, egl: Arc, display: khronos_egl::Display, + client_api: wgt::GlClientApi, force_gles_minor_version: wgt::Gles3MinorVersion, ) -> Result { let version = initialize_display(&egl, display) @@ -456,6 +457,14 @@ impl Inner { false }; + let use_opengl = match client_api { + wgt::GlClientApi::Auto | wgt::GlClientApi::OpenGl => supports_opengl, + wgt::GlClientApi::OpenGlEs => { + log::debug!("GlClientApi::OpenGlEs: skipping desktop OpenGL, forcing GLES"); + false + } + }; + let mut khr_context_flags = 0; let supports_khr_context = display_extensions.contains("EGL_KHR_create_context"); @@ -466,7 +475,7 @@ impl Inner { gl_context_attributes.push(3); gl_context_attributes.push(khronos_egl::CONTEXT_MINOR_VERSION); gl_context_attributes.push(3); - if supports_opengl && force_gles_minor_version != wgt::Gles3MinorVersion::Automatic { + if use_opengl && force_gles_minor_version != wgt::Gles3MinorVersion::Automatic { log::warn!("Ignoring specified GLES minor version as OpenGL is used"); } gles_context_attributes.push(khronos_egl::CONTEXT_MAJOR_VERSION); @@ -582,7 +591,7 @@ impl Inner { } }; - let result = if supports_opengl { + let result = if use_opengl { create_context(khronos_egl::OPENGL_API, &gl_context_attributes).or_else( |gl_error| { log::debug!("Failed to create desktop OpenGL context: {gl_error}, falling back to OpenGL ES"); @@ -899,6 +908,7 @@ impl crate::Instance for Instance { desc.flags, egl, display, + desc.backend_options.gl.client_api, desc.backend_options.gl.gles_minor_version, )?; diff --git a/wgpu-hal/src/gles/mod.rs b/wgpu-hal/src/gles/mod.rs index fc090e177a0..5bf3a2cce1f 100644 --- a/wgpu-hal/src/gles/mod.rs +++ b/wgpu-hal/src/gles/mod.rs @@ -599,6 +599,13 @@ enum RawBinding { }, Image(ImageBinding), Sampler(glow::Sampler), + ExternalTexture { + raw: glow::Texture, + target: BindTarget, + params_raw: glow::Buffer, + params_offset: i32, + params_size: i32, + }, } #[derive(Debug)] diff --git a/wgpu-types/src/backend.rs b/wgpu-types/src/backend.rs index b71cabaa58e..9eaf238ea12 100644 --- a/wgpu-types/src/backend.rs +++ b/wgpu-types/src/backend.rs @@ -248,6 +248,14 @@ impl BackendOptions { /// Part of [`BackendOptions`]. #[derive(Clone, Debug, Default)] pub struct GlBackendOptions { + /// Which EGL client API to use when creating the context. + /// + /// Defaults to [`GlClientApi::Auto`], which prefers desktop OpenGL when + /// the EGL display advertises it and falls back to OpenGL ES otherwise. + /// Set to [`GlClientApi::OpenGlEs`] to force OpenGL ES even on machines + /// that also support desktop OpenGL (required for GLES-only features such + /// as `EXTERNAL_TEXTURE`). + pub client_api: GlClientApi, /// Which OpenGL ES 3 minor version to request, if using OpenGL ES. pub gles_minor_version: Gles3MinorVersion, /// Behavior of OpenGL fences. Affects how `on_completed_work_done` and `device.poll` behave. @@ -277,6 +285,7 @@ impl GlBackendOptions { let gles_minor_version = Gles3MinorVersion::from_env().unwrap_or_default(); let debug_fns = GlDebugFns::from_env().unwrap_or_default(); Self { + client_api: GlClientApi::Auto, gles_minor_version, fence_behavior: GlFenceBehavior::Normal, debug_fns, @@ -292,6 +301,7 @@ impl GlBackendOptions { let fence_behavior = self.fence_behavior.with_env(); let debug_fns = self.debug_fns.with_env(); Self { + client_api: self.client_api, gles_minor_version, fence_behavior, debug_fns, @@ -1018,3 +1028,21 @@ impl GlFenceBehavior { } } } + +/// Selects which EGL client API is used when creating the OpenGL context. +/// +/// Used in [`GlBackendOptions::client_api`]. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub enum GlClientApi { + /// Prefer desktop OpenGL when the EGL display advertises it; fall back to + /// OpenGL ES otherwise. This is the default behaviour. + #[default] + Auto, + /// Always request a desktop OpenGL context. + /// + /// Falls back to OpenGL ES if the EGL display does not support desktop GL. + OpenGl, + /// Always request an OpenGL ES context, even when desktop OpenGL is + /// available. + OpenGlEs, +} diff --git a/wgpu/src/lib.rs b/wgpu/src/lib.rs index e17be8f48da..b54127a13e9 100644 --- a/wgpu/src/lib.rs +++ b/wgpu/src/lib.rs @@ -138,9 +138,9 @@ pub use wgt::{ DownlevelLimits, Dx12BackendOptions, Dx12Compiler, Dx12SwapchainKind, Dx12UseFrameLatencyWaitableObject, DxcShaderModel, DynamicOffset, ExperimentalFeatures, Extent3d, ExternalTextureFormat, ExternalTextureTransferFunction, Face, Features, FeaturesWGPU, - FeaturesWebGPU, FilterMode, ForceShaderModelToken, FrontFace, GlBackendOptions, GlDebugFns, - GlFenceBehavior, Gles3MinorVersion, HalCounters, ImageSubresourceRange, IndexFormat, - InstanceDescriptor, InstanceFlags, InternalCounters, Limits, LoadOpDontCare, + FeaturesWebGPU, FilterMode, ForceShaderModelToken, FrontFace, GlBackendOptions, GlClientApi, + GlDebugFns, GlFenceBehavior, Gles3MinorVersion, HalCounters, ImageSubresourceRange, + IndexFormat, InstanceDescriptor, InstanceFlags, InternalCounters, Limits, LoadOpDontCare, MemoryBudgetThresholds, MemoryHints, MipmapFilterMode, MultisampleState, NoopBackendOptions, Origin2d, Origin3d, PassthroughShaderEntryPoint, PipelineStatisticsTypes, PollError, PollStatus, PolygonMode, PowerPreference, PredefinedColorSpace, PresentMode,