Skip to content

Commit

Permalink
Support for Multisampled Anti-Aliasing (#213)
Browse files Browse the repository at this point in the history
closes #198 

### Changes
This PR introduces a `sample_count: u32` to `TextureDesc` as well as a
`multisample_state: MultisampleState` to the `RenderPipelineDesc`. Also
added `alpha_to_coverage` while at it.

```rust
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub struct MultisampleState {
    pub sample_count: u32,
    pub sample_mask: u64,
    pub alpha_to_coverage: bool,
}

impl Default for MultisampleState {
    fn default() -> Self {
        Self {
            sample_count: 1,
            sample_mask: !0,
            alpha_to_coverage: false,
        }
    }
}
```

Together with the existing `FinishOp::ResolveTo(TextureView)`, a
multisampled renderpass can now be described and executed


### Implementations
<details>
  <summary>Vulkan</summary>


The rendering attachment needs to be told to resolve
```rust
if let crate::FinishOp::ResolveTo(resolve_view) = rt.finish_op {
    vk_info = vk_info
        .resolve_image_view(resolve_view.raw)
        .resolve_image_layout(vk::ImageLayout::GENERAL)
        .resolve_mode(vk::ResolveModeFlags::AVERAGE);
}

```

The store_op is currently always set to `DONT_CARE` as in many cases for
msaa rendering the resolved texture is the only one required. Would be
nice to be able to specify this behaviour in blade as well though:

```rust
vk_info.store_op = match rt.finish_op {
    crate::FinishOp::ResolveTo(..) => {
        /*
            TODO: DONT_CARE is most optimal in many cases where the msaa texture itself is never read afterwards but only the resolved,
                  but how can the user specify this in blade?
                  https://docs.vulkan.org/samples/latest/samples/performance/msaa/README.html#_best_practice_summary
        */

        // vk::AttachmentStoreOp::STORE
        vk::AttachmentStoreOp::DONT_CARE
    }
};

```

For the texture creation, a `SampleCountFlags` is constructed for the
`vk::ImageCreateInfo`:
```rust
/// in vulkan/resource.rs:268
samples: vk::SampleCountFlags::from_raw(desc.sample_count),
```

and for the pipeline, I looked at how wgpu does it and came to the
following code

```rust
let vk_sample_mask = [
    desc.multisample_state.sample_mask as u32,
    (desc.multisample_state.sample_mask >> 32) as u32,
];

let vk_multisample = vk::PipelineMultisampleStateCreateInfo::default()
    .rasterization_samples(vk::SampleCountFlags::from_raw(
        desc.multisample_state.sample_count,
    ))
    .alpha_to_coverage_enable(desc.multisample_state.alpha_to_coverage)
    .sample_mask(&vk_sample_mask);

```
  
</details>

<details>
  <summary>Metal</summary>
The metal backend already had support for the `ResolveTo` finishop so no
changes needed for the renderpipeline. The creation of the
renderpipeline required description though:

```rust
descriptor.set_raster_sample_count(desc.multisample_state.sample_count);
descriptor.set_alpha_to_coverage_enabled(desc.multisample_state.alpha_to_coverage);
```

For the texture, only specifying the sample_count was required

```rust
/// in metal/resource.rs:205
descriptor.set_sample_count(desc.sample_count as u64);
```
</details>

<details>
  <summary>OpenGL ES</summary>

This was the hardest one as I am very unfamiliar with msaa in opengl,
but in summary the texture needs to be created with
`renderbuffer_storage_multisample` if it is a rendertarget and with a
`glow::TEXTURE_2D_MULTISAMPLE` target type if it is a normal texture.

The sample count is then set with `tex_storage_2d_multisample`, but
there is now no-longer a way to specify `mip_count` so don't know if
that is even possible.
```rust
/*
    TODO(ErikWDev): How to set mip count and sample count? Not possible in gles?
*/
gl.tex_storage_2d_multisample(
    target,
    desc.sample_count as i32,
    format_desc.internal,
    desc.size.width as i32,
    desc.size.height as i32,
    true,
);
```


For the rendering, I use `blit_framebuffer` to blit the msaa texture
onto the resolve target which required turning the renderbuffers into
FBO:s. Currently, the FBO:s are created and deleted ad-hoc but could be
saved as done in wgpu-hal. See #198

```rust
/// in gles/command.rs:814
Self::BlitFramebuffer { from, to } => {
    /*
        TODO(ErikWDev): Validate
    */

    let target_from = match from.inner {
        super::TextureInner::Renderbuffer { raw } => raw,
        _ => panic!("Unsupported resolve between non-renderbuffers"),
    };
    let target_to = match to.inner {
        super::TextureInner::Renderbuffer { raw } => raw,
        _ => panic!("Unsupported resolve between non-renderbuffers"),
    };

    let framebuf_from = gl.create_framebuffer().unwrap();
    let framebuf_to = gl.create_framebuffer().unwrap();

    gl.bind_framebuffer(glow::READ_FRAMEBUFFER, Some(framebuf_from));
    gl.framebuffer_renderbuffer(
        glow::READ_FRAMEBUFFER,
        glow::COLOR_ATTACHMENT0, // NOTE: Assuming color attachment
        glow::RENDERBUFFER,
        Some(target_from),
    );

    gl.bind_framebuffer(glow::DRAW_FRAMEBUFFER, Some(framebuf_to));
    gl.framebuffer_renderbuffer(
        glow::DRAW_FRAMEBUFFER,
        glow::COLOR_ATTACHMENT0, // NOTE: Assuming color attachment
        glow::RENDERBUFFER,
        Some(target_to),
    );
    assert_eq!(
        gl.check_framebuffer_status(glow::DRAW_FRAMEBUFFER),
        glow::FRAMEBUFFER_COMPLETE,
        "DRAW_FRAMEBUFFER is not complete"
    );

    gl.blit_framebuffer(
        0,
        0,
        from.target_size[0] as _,
        from.target_size[1] as _,
        0,
        0,
        to.target_size[0] as _,
        to.target_size[1] as _,
        glow::COLOR_BUFFER_BIT, // NOTE: Assuming color
        glow::NEAREST,
    );

    gl.bind_framebuffer(glow::READ_FRAMEBUFFER, None);
    gl.bind_framebuffer(glow::DRAW_FRAMEBUFFER, None);

    gl.delete_framebuffer(framebuf_from);
    gl.delete_framebuffer(framebuf_to);
}


``` 


</details>

### Testing
The vulkan implementation has been validated on an AMD RX 6600 on
debian, an integrated intel GPU on Fedora as well as a GTX 1050 on
Windows and it all seems to work (the particle sample). After inspection
in renderdoc the result is as expected.

The metal implementation has been tested on a Mac Mini M4 and works.
However, particle example seems to break after the sample count is
adjusted in runtime for some reason.

The Opengl ES implementation has **not been tested**!!!

### Particle Example
I changed the particle example to now utilize msaa which requires it to
keep a msaa texture with the desired `sample_count` available and
recreated upon resizing as well as a FinishOp::ResolveTo to resolve the
msaa texture to the acquired frame texture as such:

```rust
if let mut pass = self.command_encoder.render(
    "draw",
    gpu::RenderTargetSet {
        colors: &[gpu::RenderTarget {
            view: self.msaa_view,
            init_op: gpu::InitOp::Clear(gpu::TextureColor::OpaqueBlack),
            finish_op: gpu::FinishOp::ResolveTo(frame.texture_view()),
        }],
        depth_stencil: None,
    },
) {
    self.particle_system
        .draw(&mut pass, screen_desc.physical_size);
    self.gui_painter
        .paint(&mut pass, gui_primitives, screen_desc, &self.context);
}
``` 

### Egui
Furthermore, the `blade_egui` renderer also requires information about
the multisample state since it has a pipeline that now needs information
about the texture it's going to render to so it's initializer now takes
a sample_count as well:

```rust
pub fn new(
    info: blade_graphics::SurfaceInfo,
    context: &blade_graphics::Context,
    sample_count: u32,
) -> GuiPainter {
// ...
}
```

---

Let me know what needs changing or if testing fails somewhere.
Especially the Opengl ES implementation as I didn't know how to run
using it
  • Loading branch information
EriKWDev authored Dec 10, 2024
1 parent ea25b77 commit 450bb7d
Show file tree
Hide file tree
Showing 25 changed files with 475 additions and 62 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
/blade-asset/cooked
/_failure.wgsl

libEGL.dylib
libGLESv2.dylib

.DS_Store
/.vs
/.vscode
6 changes: 4 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ profiling = "1"
slab = "0.4"
strum = { version = "0.25", features = ["derive"] }
web-sys = "0.3.60"
winit = "0.30"
winit = { version = "0.30" }

[lib]

Expand Down Expand Up @@ -90,9 +90,11 @@ del-geo = "=0.1.29"

[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies]
# see https://github.com/emilk/egui/issues/4270
egui-winit = "0.29"
egui-winit = { version="0.29", default-features=false, features=["links"] }

[target.'cfg(target_arch = "wasm32")'.dev-dependencies]
# see https://github.com/emilk/egui/issues/4270
egui-winit = { version="0.29", default-features=false, features=["links"] }
console_error_panic_hook = "0.1.7"
console_log = "1"
web-sys = { workspace = true, features = ["Window"] }
Expand Down
2 changes: 2 additions & 0 deletions blade-egui/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ impl GuiTexture {
mip_level_count: 1,
dimension: blade_graphics::TextureDimension::D2,
usage: blade_graphics::TextureUsage::COPY | blade_graphics::TextureUsage::RESOURCE,
sample_count: 1,
});
let view = context.create_texture_view(
allocation,
Expand Down Expand Up @@ -199,6 +200,7 @@ impl GuiPainter {
}),
write_mask: blade_graphics::ColorWrites::all(),
}],
multisample_state: Default::default(),
});

let belt = BufferBelt::new(BufferBeltDescriptor {
Expand Down
81 changes: 81 additions & 0 deletions blade-graphics/src/gles/command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,10 @@ impl super::CommandEncoder {
if let crate::FinishOp::Discard = rt.finish_op {
invalidate_attachments.push(attachment);
}
if let crate::FinishOp::ResolveTo(to) = rt.finish_op {
self.commands
.push(super::Command::BlitFramebuffer { from: rt.view, to });
}
}
if let Some(ref rt) = targets.depth_stencil {
let attachment = match rt.view.aspects {
Expand Down Expand Up @@ -806,6 +810,83 @@ impl super::Command {
None,
);
}

Self::BlitFramebuffer { from, to } => {
/*
TODO: Framebuffers could be re-used instead of being created on the fly.
Currently deleted down below
*/
let framebuf_from = gl.create_framebuffer().unwrap();
let framebuf_to = gl.create_framebuffer().unwrap();

gl.bind_framebuffer(glow::READ_FRAMEBUFFER, Some(framebuf_from));
match from.inner {
super::TextureInner::Renderbuffer { raw } => {
gl.framebuffer_renderbuffer(
glow::READ_FRAMEBUFFER,
glow::COLOR_ATTACHMENT0, // NOTE: Assuming color attachment
glow::RENDERBUFFER,
Some(raw),
);
}
super::TextureInner::Texture { raw, target } => {
gl.framebuffer_texture_2d(
glow::READ_FRAMEBUFFER,
glow::COLOR_ATTACHMENT0,
target,
Some(raw),
0,
);
}
}

gl.bind_framebuffer(glow::DRAW_FRAMEBUFFER, Some(framebuf_to));
match to.inner {
super::TextureInner::Renderbuffer { raw } => {
gl.framebuffer_renderbuffer(
glow::DRAW_FRAMEBUFFER,
glow::COLOR_ATTACHMENT0, // NOTE: Assuming color attachment
glow::RENDERBUFFER,
Some(raw),
);
}
super::TextureInner::Texture { raw, target } => {
gl.framebuffer_texture_2d(
glow::DRAW_FRAMEBUFFER,
glow::COLOR_ATTACHMENT0,
target,
Some(raw),
0,
);
}
}

debug_assert_eq!(
gl.check_framebuffer_status(glow::DRAW_FRAMEBUFFER),
glow::FRAMEBUFFER_COMPLETE,
"DRAW_FRAMEBUFFER is not complete"
);

gl.blit_framebuffer(
0,
0,
from.target_size[0] as _,
from.target_size[1] as _,
0,
0,
to.target_size[0] as _,
to.target_size[1] as _,
glow::COLOR_BUFFER_BIT, // NOTE: Assuming color
glow::NEAREST,
);

gl.bind_framebuffer(glow::READ_FRAMEBUFFER, None);
gl.bind_framebuffer(glow::DRAW_FRAMEBUFFER, None);

gl.delete_framebuffer(framebuf_from);
gl.delete_framebuffer(framebuf_to);
}

Self::BindAttachment {
attachment,
ref view,
Expand Down
3 changes: 2 additions & 1 deletion blade-graphics/src/gles/egl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -357,7 +357,8 @@ impl super::Context {
let window_ptr = unsafe {
use objc::{msg_send, runtime::Object, sel, sel_impl};
// ns_view always have a layer and don't need to verify that it exists.
let layer: *mut Object = msg_send![handle.ns_view.as_ptr(), layer];
let layer: *mut Object =
msg_send![handle.ns_view.as_ptr() as *mut Object, layer];
layer as *mut ffi::c_void
};
window_ptr
Expand Down
4 changes: 4 additions & 0 deletions blade-graphics/src/gles/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,10 @@ enum Command {
size: crate::Extent,
},
ResetFramebuffer,
BlitFramebuffer {
from: TextureView,
to: TextureView,
},
BindAttachment {
attachment: u32,
view: TextureView,
Expand Down
74 changes: 58 additions & 16 deletions blade-graphics/src/gles/resource.rs
Original file line number Diff line number Diff line change
Expand Up @@ -128,12 +128,24 @@ impl crate::traits::ResourceDevice for super::Context {
let raw = unsafe { gl.create_renderbuffer().unwrap() };
unsafe {
gl.bind_renderbuffer(glow::RENDERBUFFER, Some(raw));
gl.renderbuffer_storage(
glow::RENDERBUFFER,
format_desc.internal,
desc.size.width as i32,
desc.size.height as i32,
);

if desc.sample_count <= 1 {
gl.renderbuffer_storage(
glow::RENDERBUFFER,
format_desc.internal,
desc.size.width as i32,
desc.size.height as i32,
);
} else {
gl.renderbuffer_storage_multisample(
glow::RENDERBUFFER,
desc.sample_count as i32,
format_desc.internal,
desc.size.width as i32,
desc.size.height as i32,
);
}

gl.bind_renderbuffer(glow::RENDERBUFFER, None);
#[cfg(not(target_arch = "wasm32"))]
if !desc.name.is_empty() && gl.supports_debug() {
Expand All @@ -144,11 +156,16 @@ impl crate::traits::ResourceDevice for super::Context {
);
}
}

super::TextureInner::Renderbuffer { raw }
} else {
let raw = unsafe { gl.create_texture().unwrap() };

let target = match desc.dimension {
crate::TextureDimension::D1 => {
if desc.sample_count > 1 {
log::warn!("Sample count is ignored: not supported for 1D textures",);
}
if desc.array_layer_count > 1 {
glow::TEXTURE_1D_ARRAY
} else {
Expand All @@ -157,12 +174,25 @@ impl crate::traits::ResourceDevice for super::Context {
}
crate::TextureDimension::D2 => {
if desc.array_layer_count > 1 {
glow::TEXTURE_2D_ARRAY
if desc.sample_count <= 1 {
glow::TEXTURE_2D_ARRAY
} else {
glow::TEXTURE_2D_MULTISAMPLE_ARRAY
}
} else {
glow::TEXTURE_2D
if desc.sample_count <= 1 {
glow::TEXTURE_2D
} else {
glow::TEXTURE_2D_MULTISAMPLE
}
}
}
crate::TextureDimension::D3 => {
if desc.sample_count > 1 {
log::warn!("Sample count is ignored: not supported for 3D textures",);
}
glow::TEXTURE_3D
}
crate::TextureDimension::D3 => glow::TEXTURE_3D,
};

unsafe {
Expand All @@ -184,13 +214,25 @@ impl crate::traits::ResourceDevice for super::Context {
);
}
crate::TextureDimension::D2 => {
gl.tex_storage_2d(
target,
desc.mip_level_count as i32,
format_desc.internal,
desc.size.width as i32,
desc.size.height as i32,
);
if desc.sample_count <= 1 {
gl.tex_storage_2d(
target,
desc.mip_level_count as i32,
format_desc.internal,
desc.size.width as i32,
desc.size.height as i32,
);
} else {
assert_eq!(desc.mip_level_count, 1);
gl.tex_storage_2d_multisample(
target,
desc.sample_count as i32,
format_desc.internal,
desc.size.width as i32,
desc.size.height as i32,
true,
);
}
}
crate::TextureDimension::D1 => {
gl.tex_storage_1d(
Expand Down
22 changes: 22 additions & 0 deletions blade-graphics/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -402,6 +402,7 @@ pub struct TextureDesc<'a> {
pub size: Extent,
pub array_layer_count: u32,
pub mip_level_count: u32,
pub sample_count: u32,
pub dimension: TextureDimension,
pub usage: TextureUsage,
}
Expand Down Expand Up @@ -1018,18 +1019,39 @@ pub struct RenderPipelineDesc<'a> {
pub depth_stencil: Option<DepthStencilState>,
pub fragment: ShaderFunction<'a>,
pub color_targets: &'a [ColorTargetState],
pub multisample_state: MultisampleState,
}

#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub struct MultisampleState {
pub sample_count: u32,
pub sample_mask: u64,
pub alpha_to_coverage: bool,
}

impl Default for MultisampleState {
fn default() -> Self {
Self {
sample_count: 1,
sample_mask: !0,
alpha_to_coverage: false,
}
}
}

#[derive(Clone, Copy, Debug)]
pub enum InitOp {
Load,
Clear(TextureColor),
DontCare,
}

#[derive(Clone, Copy, Debug)]
pub enum FinishOp {
Store,
Discard,
/// The texture specified here will be stored but it is undefined what
/// happens to the original render target
ResolveTo(TextureView),
Ignore,
}
Expand Down
2 changes: 2 additions & 0 deletions blade-graphics/src/metal/command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,7 @@ impl super::CommandEncoder {
at_descriptor.set_clear_color(clear_color);
metal::MTLLoadAction::Clear
}
crate::InitOp::DontCare => metal::MTLLoadAction::DontCare,
};
at_descriptor.set_load_action(load_action);

Expand Down Expand Up @@ -247,6 +248,7 @@ impl super::CommandEncoder {
at_descriptor.set_clear_depth(clear_depth);
metal::MTLLoadAction::Clear
}
crate::InitOp::DontCare => metal::MTLLoadAction::DontCare,
};
let store_action = match rt.finish_op {
crate::FinishOp::Store | crate::FinishOp::Ignore => {
Expand Down
2 changes: 2 additions & 0 deletions blade-graphics/src/metal/pipeline.rs
Original file line number Diff line number Diff line change
Expand Up @@ -420,6 +420,8 @@ impl crate::traits::ShaderDevice for super::Context {
},
);
descriptor.set_vertex_function(Some(&vs.function));
descriptor.set_raster_sample_count(desc.multisample_state.sample_count as _);
descriptor.set_alpha_to_coverage_enabled(desc.multisample_state.alpha_to_coverage);

// Fragment shader
let fs = self.load_shader(
Expand Down
Loading

0 comments on commit 450bb7d

Please sign in to comment.