Allow prepass in webgl (#7537)
# Objective - Use the prepass textures in webgl ## Solution - Bind the prepass textures even when using webgl, but only if msaa is disabled - Also did some refactors to centralize how textures are bound, similar to the EnvironmentMapLight PR - ~~Also did some refactors of the example to make it work in webgl~~ - ~~To make the example work in webgl, I needed to use a sampler for the depth texture, the resulting code looks a bit weird, but it's simple enough and I think it's worth it to show how it works when using webgl~~
This commit is contained in:
parent
8d1f6ff7fa
commit
71cf35ce42
@ -1399,7 +1399,7 @@ path = "examples/shader/shader_prepass.rs"
|
|||||||
|
|
||||||
[package.metadata.example.shader_prepass]
|
[package.metadata.example.shader_prepass]
|
||||||
name = "Material Prepass"
|
name = "Material Prepass"
|
||||||
description = "A shader that uses the depth texture generated in a prepass"
|
description = "A shader that uses the various textures generated by the prepass"
|
||||||
category = "Shaders"
|
category = "Shaders"
|
||||||
wasm = false
|
wasm = false
|
||||||
|
|
||||||
|
@ -2,10 +2,14 @@
|
|||||||
#import bevy_pbr::mesh_view_bindings
|
#import bevy_pbr::mesh_view_bindings
|
||||||
#import bevy_pbr::prepass_utils
|
#import bevy_pbr::prepass_utils
|
||||||
|
|
||||||
|
struct ShowPrepassSettings {
|
||||||
|
show_depth: u32,
|
||||||
|
show_normals: u32,
|
||||||
|
padding_1: u32,
|
||||||
|
padding_2: u32,
|
||||||
|
}
|
||||||
@group(1) @binding(0)
|
@group(1) @binding(0)
|
||||||
var<uniform> show_depth: f32;
|
var<uniform> settings: ShowPrepassSettings;
|
||||||
@group(1) @binding(1)
|
|
||||||
var<uniform> show_normal: f32;
|
|
||||||
|
|
||||||
@fragment
|
@fragment
|
||||||
fn fragment(
|
fn fragment(
|
||||||
@ -13,14 +17,13 @@ fn fragment(
|
|||||||
@builtin(sample_index) sample_index: u32,
|
@builtin(sample_index) sample_index: u32,
|
||||||
#import bevy_pbr::mesh_vertex_output
|
#import bevy_pbr::mesh_vertex_output
|
||||||
) -> @location(0) vec4<f32> {
|
) -> @location(0) vec4<f32> {
|
||||||
if show_depth == 1.0 {
|
if settings.show_depth == 1u {
|
||||||
let depth = prepass_depth(frag_coord, sample_index);
|
let depth = prepass_depth(frag_coord, sample_index);
|
||||||
return vec4(depth, depth, depth, 1.0);
|
return vec4(depth, depth, depth, 1.0);
|
||||||
} else if show_normal == 1.0 {
|
} else if settings.show_normals == 1u {
|
||||||
let normal = prepass_normal(frag_coord, sample_index);
|
let normal = prepass_normal(frag_coord, sample_index);
|
||||||
return vec4(normal, 1.0);
|
return vec4(normal, 1.0);
|
||||||
} else {
|
}
|
||||||
// transparent
|
|
||||||
return vec4(0.0);
|
return vec4(0.0);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
@ -26,16 +26,17 @@ use bevy_render::{
|
|||||||
},
|
},
|
||||||
render_resource::{
|
render_resource::{
|
||||||
BindGroup, BindGroupDescriptor, BindGroupEntry, BindGroupLayout, BindGroupLayoutDescriptor,
|
BindGroup, BindGroupDescriptor, BindGroupEntry, BindGroupLayout, BindGroupLayoutDescriptor,
|
||||||
BindGroupLayoutEntry, BindingType, BlendState, BufferBindingType, ColorTargetState,
|
BindGroupLayoutEntry, BindingResource, BindingType, BlendState, BufferBindingType,
|
||||||
ColorWrites, CompareFunction, DepthBiasState, DepthStencilState, Extent3d, FragmentState,
|
ColorTargetState, ColorWrites, CompareFunction, DepthBiasState, DepthStencilState,
|
||||||
FrontFace, MultisampleState, PipelineCache, PolygonMode, PrimitiveState,
|
Extent3d, FragmentState, FrontFace, MultisampleState, PipelineCache, PolygonMode,
|
||||||
RenderPipelineDescriptor, Shader, ShaderDefVal, ShaderRef, ShaderStages, ShaderType,
|
PrimitiveState, RenderPipelineDescriptor, Shader, ShaderDefVal, ShaderRef, ShaderStages,
|
||||||
SpecializedMeshPipeline, SpecializedMeshPipelineError, SpecializedMeshPipelines,
|
ShaderType, SpecializedMeshPipeline, SpecializedMeshPipelineError,
|
||||||
StencilFaceState, StencilState, TextureDescriptor, TextureDimension, TextureFormat,
|
SpecializedMeshPipelines, StencilFaceState, StencilState, TextureDescriptor,
|
||||||
TextureUsages, VertexState,
|
TextureDimension, TextureFormat, TextureSampleType, TextureUsages, TextureViewDimension,
|
||||||
|
VertexState,
|
||||||
},
|
},
|
||||||
renderer::RenderDevice,
|
renderer::RenderDevice,
|
||||||
texture::TextureCache,
|
texture::{FallbackImagesDepth, FallbackImagesMsaa, TextureCache},
|
||||||
view::{ExtractedView, Msaa, ViewUniform, ViewUniformOffset, ViewUniforms, VisibleEntities},
|
view::{ExtractedView, Msaa, ViewUniform, ViewUniformOffset, ViewUniforms, VisibleEntities},
|
||||||
Extract, ExtractSchedule, RenderApp, RenderSet,
|
Extract, ExtractSchedule, RenderApp, RenderSet,
|
||||||
};
|
};
|
||||||
@ -333,6 +334,73 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_bind_group_layout_entries(
|
||||||
|
bindings: [u32; 2],
|
||||||
|
multisampled: bool,
|
||||||
|
) -> [BindGroupLayoutEntry; 2] {
|
||||||
|
[
|
||||||
|
// Depth texture
|
||||||
|
BindGroupLayoutEntry {
|
||||||
|
binding: bindings[0],
|
||||||
|
visibility: ShaderStages::FRAGMENT,
|
||||||
|
ty: BindingType::Texture {
|
||||||
|
multisampled,
|
||||||
|
sample_type: TextureSampleType::Depth,
|
||||||
|
view_dimension: TextureViewDimension::D2,
|
||||||
|
},
|
||||||
|
count: None,
|
||||||
|
},
|
||||||
|
// Normal texture
|
||||||
|
BindGroupLayoutEntry {
|
||||||
|
binding: bindings[1],
|
||||||
|
visibility: ShaderStages::FRAGMENT,
|
||||||
|
ty: BindingType::Texture {
|
||||||
|
multisampled,
|
||||||
|
sample_type: TextureSampleType::Float { filterable: true },
|
||||||
|
view_dimension: TextureViewDimension::D2,
|
||||||
|
},
|
||||||
|
count: None,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_bindings<'a>(
|
||||||
|
prepass_textures: Option<&'a ViewPrepassTextures>,
|
||||||
|
fallback_images: &'a mut FallbackImagesMsaa,
|
||||||
|
fallback_depths: &'a mut FallbackImagesDepth,
|
||||||
|
msaa: &'a Msaa,
|
||||||
|
bindings: [u32; 2],
|
||||||
|
) -> [BindGroupEntry<'a>; 2] {
|
||||||
|
let depth_view = match prepass_textures.and_then(|x| x.depth.as_ref()) {
|
||||||
|
Some(texture) => &texture.default_view,
|
||||||
|
None => {
|
||||||
|
&fallback_depths
|
||||||
|
.image_for_samplecount(msaa.samples())
|
||||||
|
.texture_view
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let normal_view = match prepass_textures.and_then(|x| x.normal.as_ref()) {
|
||||||
|
Some(texture) => &texture.default_view,
|
||||||
|
None => {
|
||||||
|
&fallback_images
|
||||||
|
.image_for_samplecount(msaa.samples())
|
||||||
|
.texture_view
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
[
|
||||||
|
BindGroupEntry {
|
||||||
|
binding: bindings[0],
|
||||||
|
resource: BindingResource::TextureView(depth_view),
|
||||||
|
},
|
||||||
|
BindGroupEntry {
|
||||||
|
binding: bindings[1],
|
||||||
|
resource: BindingResource::TextureView(normal_view),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
// Extract the render phases for the prepass
|
// Extract the render phases for the prepass
|
||||||
pub fn extract_camera_prepass_phase(
|
pub fn extract_camera_prepass_phase(
|
||||||
mut commands: Commands,
|
mut commands: Commands,
|
||||||
|
@ -6,7 +6,7 @@ fn prepass_normal(frag_coord: vec4<f32>, sample_index: u32) -> vec3<f32> {
|
|||||||
let normal_sample = textureLoad(normal_prepass_texture, vec2<i32>(frag_coord.xy), i32(sample_index));
|
let normal_sample = textureLoad(normal_prepass_texture, vec2<i32>(frag_coord.xy), i32(sample_index));
|
||||||
#else
|
#else
|
||||||
let normal_sample = textureLoad(normal_prepass_texture, vec2<i32>(frag_coord.xy), 0);
|
let normal_sample = textureLoad(normal_prepass_texture, vec2<i32>(frag_coord.xy), 0);
|
||||||
#endif
|
#endif // MULTISAMPLED
|
||||||
return normal_sample.xyz * 2.0 - vec3(1.0);
|
return normal_sample.xyz * 2.0 - vec3(1.0);
|
||||||
}
|
}
|
||||||
#endif // NORMAL_PREPASS
|
#endif // NORMAL_PREPASS
|
||||||
@ -17,7 +17,7 @@ fn prepass_depth(frag_coord: vec4<f32>, sample_index: u32) -> f32 {
|
|||||||
let depth_sample = textureLoad(depth_prepass_texture, vec2<i32>(frag_coord.xy), i32(sample_index));
|
let depth_sample = textureLoad(depth_prepass_texture, vec2<i32>(frag_coord.xy), i32(sample_index));
|
||||||
#else
|
#else
|
||||||
let depth_sample = textureLoad(depth_prepass_texture, vec2<i32>(frag_coord.xy), 0);
|
let depth_sample = textureLoad(depth_prepass_texture, vec2<i32>(frag_coord.xy), 0);
|
||||||
#endif
|
#endif // MULTISAMPLED
|
||||||
return depth_sample;
|
return depth_sample;
|
||||||
}
|
}
|
||||||
#endif // DEPTH_PREPASS
|
#endif // DEPTH_PREPASS
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
environment_map, queue_shadow_view_bind_group, EnvironmentMapLight, FogMeta, GlobalLightMeta,
|
environment_map, prepass, queue_shadow_view_bind_group, EnvironmentMapLight, FogMeta,
|
||||||
GpuFog, GpuLights, GpuPointLights, LightMeta, NotShadowCaster, NotShadowReceiver,
|
GlobalLightMeta, GpuFog, GpuLights, GpuPointLights, LightMeta, NotShadowCaster,
|
||||||
ShadowPipeline, ViewClusterBindings, ViewFogUniformOffset, ViewLightsUniformOffset,
|
NotShadowReceiver, ShadowPipeline, ViewClusterBindings, ViewFogUniformOffset,
|
||||||
ViewShadowBindings, CLUSTERED_FORWARD_STORAGE_BUFFER_COUNT, MAX_CASCADES_PER_LIGHT,
|
ViewLightsUniformOffset, ViewShadowBindings, CLUSTERED_FORWARD_STORAGE_BUFFER_COUNT,
|
||||||
MAX_DIRECTIONAL_LIGHTS,
|
MAX_CASCADES_PER_LIGHT, MAX_DIRECTIONAL_LIGHTS,
|
||||||
};
|
};
|
||||||
use bevy_app::{IntoSystemAppConfigs, Plugin};
|
use bevy_app::{IntoSystemAppConfigs, Plugin};
|
||||||
use bevy_asset::{load_internal_asset, Assets, Handle, HandleUntyped};
|
use bevy_asset::{load_internal_asset, Assets, Handle, HandleUntyped};
|
||||||
@ -432,29 +432,11 @@ impl FromWorld for MeshPipeline {
|
|||||||
let tonemapping_lut_entries = get_lut_bind_group_layout_entries([14, 15]);
|
let tonemapping_lut_entries = get_lut_bind_group_layout_entries([14, 15]);
|
||||||
entries.extend_from_slice(&tonemapping_lut_entries);
|
entries.extend_from_slice(&tonemapping_lut_entries);
|
||||||
|
|
||||||
if cfg!(not(feature = "webgl")) {
|
if cfg!(not(feature = "webgl")) || (cfg!(feature = "webgl") && !multisampled) {
|
||||||
// Depth texture
|
entries.extend_from_slice(&prepass::get_bind_group_layout_entries(
|
||||||
entries.push(BindGroupLayoutEntry {
|
[16, 17],
|
||||||
binding: 16,
|
|
||||||
visibility: ShaderStages::FRAGMENT,
|
|
||||||
ty: BindingType::Texture {
|
|
||||||
multisampled,
|
multisampled,
|
||||||
sample_type: TextureSampleType::Depth,
|
));
|
||||||
view_dimension: TextureViewDimension::D2,
|
|
||||||
},
|
|
||||||
count: None,
|
|
||||||
});
|
|
||||||
// Normal texture
|
|
||||||
entries.push(BindGroupLayoutEntry {
|
|
||||||
binding: 17,
|
|
||||||
visibility: ShaderStages::FRAGMENT,
|
|
||||||
ty: BindingType::Texture {
|
|
||||||
multisampled,
|
|
||||||
sample_type: TextureSampleType::Float { filterable: true },
|
|
||||||
view_dimension: TextureViewDimension::D2,
|
|
||||||
},
|
|
||||||
count: None,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
entries
|
entries
|
||||||
@ -1067,34 +1049,15 @@ pub fn queue_mesh_view_bind_groups(
|
|||||||
get_lut_bindings(&images, &tonemapping_luts, tonemapping, [14, 15]);
|
get_lut_bindings(&images, &tonemapping_luts, tonemapping, [14, 15]);
|
||||||
entries.extend_from_slice(&tonemapping_luts);
|
entries.extend_from_slice(&tonemapping_luts);
|
||||||
|
|
||||||
// When using WebGL with MSAA, we can't create the fallback textures required by the prepass
|
// When using WebGL, we can't have a depth texture with multisampling
|
||||||
// When using WebGL, and MSAA is disabled, we can't bind the textures either
|
if cfg!(not(feature = "webgl")) || (cfg!(feature = "webgl") && msaa.samples() == 1) {
|
||||||
if cfg!(not(feature = "webgl")) {
|
entries.extend_from_slice(&prepass::get_bindings(
|
||||||
let depth_view = match prepass_textures.and_then(|x| x.depth.as_ref()) {
|
prepass_textures,
|
||||||
Some(texture) => &texture.default_view,
|
&mut fallback_images,
|
||||||
None => {
|
&mut fallback_depths,
|
||||||
&fallback_depths
|
&msaa,
|
||||||
.image_for_samplecount(msaa.samples())
|
[16, 17],
|
||||||
.texture_view
|
));
|
||||||
}
|
|
||||||
};
|
|
||||||
entries.push(BindGroupEntry {
|
|
||||||
binding: 16,
|
|
||||||
resource: BindingResource::TextureView(depth_view),
|
|
||||||
});
|
|
||||||
|
|
||||||
let normal_view = match prepass_textures.and_then(|x| x.normal.as_ref()) {
|
|
||||||
Some(texture) => &texture.default_view,
|
|
||||||
None => {
|
|
||||||
&fallback_images
|
|
||||||
.image_for_samplecount(msaa.samples())
|
|
||||||
.texture_view
|
|
||||||
}
|
|
||||||
};
|
|
||||||
entries.push(BindGroupEntry {
|
|
||||||
binding: 17,
|
|
||||||
resource: BindingResource::TextureView(normal_view),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let view_bind_group = render_device.create_bind_group(&BindGroupDescriptor {
|
let view_bind_group = render_device.create_bind_group(&BindGroupDescriptor {
|
||||||
|
@ -276,7 +276,7 @@ Example | Description
|
|||||||
[Material](../examples/shader/shader_material.rs) | A shader and a material that uses it
|
[Material](../examples/shader/shader_material.rs) | A shader and a material that uses it
|
||||||
[Material - GLSL](../examples/shader/shader_material_glsl.rs) | A shader that uses the GLSL shading language
|
[Material - GLSL](../examples/shader/shader_material_glsl.rs) | A shader that uses the GLSL shading language
|
||||||
[Material - Screenspace Texture](../examples/shader/shader_material_screenspace_texture.rs) | A shader that samples a texture with view-independent UV coordinates
|
[Material - Screenspace Texture](../examples/shader/shader_material_screenspace_texture.rs) | A shader that samples a texture with view-independent UV coordinates
|
||||||
[Material Prepass](../examples/shader/shader_prepass.rs) | A shader that uses the depth texture generated in a prepass
|
[Material Prepass](../examples/shader/shader_prepass.rs) | A shader that uses the various textures generated by the prepass
|
||||||
[Post Processing](../examples/shader/post_processing.rs) | A custom post processing effect, using two cameras, with one reusing the render texture of the first one
|
[Post Processing](../examples/shader/post_processing.rs) | A custom post processing effect, using two cameras, with one reusing the render texture of the first one
|
||||||
[Shader Defs](../examples/shader/shader_defs.rs) | A shader that uses "shaders defs" (a bevy tool to selectively toggle parts of a shader)
|
[Shader Defs](../examples/shader/shader_defs.rs) | A shader that uses "shaders defs" (a bevy tool to selectively toggle parts of a shader)
|
||||||
[Texture Binding Array (Bindless Textures)](../examples/shader/texture_binding_array.rs) | A shader that shows how to bind and sample multiple textures as a binding array (a.k.a. bindless textures).
|
[Texture Binding Array (Bindless Textures)](../examples/shader/texture_binding_array.rs) | A shader that shows how to bind and sample multiple textures as a binding array (a.k.a. bindless textures).
|
||||||
|
@ -10,7 +10,7 @@ use bevy::{
|
|||||||
pbr::{NotShadowCaster, PbrPlugin},
|
pbr::{NotShadowCaster, PbrPlugin},
|
||||||
prelude::*,
|
prelude::*,
|
||||||
reflect::TypeUuid,
|
reflect::TypeUuid,
|
||||||
render::render_resource::{AsBindGroup, ShaderRef},
|
render::render_resource::{AsBindGroup, ShaderRef, ShaderType},
|
||||||
};
|
};
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
@ -30,7 +30,7 @@ fn main() {
|
|||||||
})
|
})
|
||||||
.add_startup_system(setup)
|
.add_startup_system(setup)
|
||||||
.add_system(rotate)
|
.add_system(rotate)
|
||||||
.add_system(update)
|
.add_system(toggle_prepass_view)
|
||||||
.run();
|
.run();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -69,8 +69,7 @@ fn setup(
|
|||||||
MaterialMeshBundle {
|
MaterialMeshBundle {
|
||||||
mesh: meshes.add(shape::Quad::new(Vec2::new(20.0, 20.0)).into()),
|
mesh: meshes.add(shape::Quad::new(Vec2::new(20.0, 20.0)).into()),
|
||||||
material: depth_materials.add(PrepassOutputMaterial {
|
material: depth_materials.add(PrepassOutputMaterial {
|
||||||
show_depth: 0.0,
|
settings: ShowPrepassSettings::default(),
|
||||||
show_normal: 0.0,
|
|
||||||
}),
|
}),
|
||||||
transform: Transform::from_xyz(-0.75, 1.25, 3.0)
|
transform: Transform::from_xyz(-0.75, 1.25, 3.0)
|
||||||
.looking_at(Vec3::new(2.0, -2.5, -5.0), Vec3::Y),
|
.looking_at(Vec3::new(2.0, -2.5, -5.0), Vec3::Y),
|
||||||
@ -193,14 +192,20 @@ fn rotate(mut q: Query<&mut Transform, With<Rotates>>, time: Res<Time>) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default, ShaderType)]
|
||||||
|
struct ShowPrepassSettings {
|
||||||
|
show_depth: u32,
|
||||||
|
show_normals: u32,
|
||||||
|
padding_1: u32,
|
||||||
|
padding_2: u32,
|
||||||
|
}
|
||||||
|
|
||||||
// This shader simply loads the prepass texture and outputs it directly
|
// This shader simply loads the prepass texture and outputs it directly
|
||||||
#[derive(AsBindGroup, TypeUuid, Debug, Clone)]
|
#[derive(AsBindGroup, TypeUuid, Debug, Clone)]
|
||||||
#[uuid = "0af99895-b96e-4451-bc12-c6b1c1c52750"]
|
#[uuid = "0af99895-b96e-4451-bc12-c6b1c1c52750"]
|
||||||
pub struct PrepassOutputMaterial {
|
pub struct PrepassOutputMaterial {
|
||||||
#[uniform(0)]
|
#[uniform(0)]
|
||||||
show_depth: f32,
|
settings: ShowPrepassSettings,
|
||||||
#[uniform(1)]
|
|
||||||
show_normal: f32,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Material for PrepassOutputMaterial {
|
impl Material for PrepassOutputMaterial {
|
||||||
@ -214,7 +219,8 @@ impl Material for PrepassOutputMaterial {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update(
|
/// Every time you press space, it will cycle between transparent, depth and normals view
|
||||||
|
fn toggle_prepass_view(
|
||||||
keycode: Res<Input<KeyCode>>,
|
keycode: Res<Input<KeyCode>>,
|
||||||
material_handle: Query<&Handle<PrepassOutputMaterial>>,
|
material_handle: Query<&Handle<PrepassOutputMaterial>>,
|
||||||
mut materials: ResMut<Assets<PrepassOutputMaterial>>,
|
mut materials: ResMut<Assets<PrepassOutputMaterial>>,
|
||||||
@ -222,20 +228,20 @@ fn update(
|
|||||||
) {
|
) {
|
||||||
if keycode.just_pressed(KeyCode::Space) {
|
if keycode.just_pressed(KeyCode::Space) {
|
||||||
let handle = material_handle.single();
|
let handle = material_handle.single();
|
||||||
let mut mat = materials.get_mut(handle).unwrap();
|
let mat = materials.get_mut(handle).unwrap();
|
||||||
let out_text;
|
let out_text;
|
||||||
if mat.show_depth == 1.0 {
|
if mat.settings.show_depth == 1 {
|
||||||
out_text = "normal";
|
out_text = "normal";
|
||||||
mat.show_depth = 0.0;
|
mat.settings.show_depth = 0;
|
||||||
mat.show_normal = 1.0;
|
mat.settings.show_normals = 1;
|
||||||
} else if mat.show_normal == 1.0 {
|
} else if mat.settings.show_normals == 1 {
|
||||||
out_text = "transparent";
|
out_text = "transparent";
|
||||||
mat.show_depth = 0.0;
|
mat.settings.show_depth = 0;
|
||||||
mat.show_normal = 0.0;
|
mat.settings.show_normals = 0;
|
||||||
} else {
|
} else {
|
||||||
out_text = "depth";
|
out_text = "depth";
|
||||||
mat.show_depth = 1.0;
|
mat.settings.show_depth = 1;
|
||||||
mat.show_normal = 0.0;
|
mat.settings.show_normals = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut text = text.single_mut();
|
let mut text = text.single_mut();
|
||||||
|
Loading…
Reference in New Issue
Block a user