Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improved UI render batching #8793

Merged
merged 21 commits into from
Jun 22, 2023
Merged

Conversation

ickshonpe
Copy link
Contributor

@ickshonpe ickshonpe commented Jun 8, 2023

Objective

prepare_uinodes creates a new UiBatch whenever the texture changes, when most often it's just queuing untextured quads. Instead of switching textures, we can reduce the number of batches generated significantly by adding a condition to the fragment shader so that it only multiplies by the textureSample value when drawing a textured quad.

Solution

Add a mode field to UiVertex.
In prepare_uinodes set mode to 0 if the quad is textured or 1 if untextured.
Add a condition to the fragment shader that only multiplies by the color value from textureSample if mode is set to 1.


Changelog

  • Added a mode field to UiVertex, and added an extra u32 vertex attribute to the shader and vertex buffer layout.
  • In prepare_uinodes mode is set to 0 for the vertices of textured quads, and 1 if untextured.
  • Added a condition to the fragment shader in ui.wgsl that only multiplies by the color value from textureSample if the mode is equal to 0.

…extured rather than switching textures and creating a new `UiBatch`. Add a condition to the fragment shader that only multiplies by the color value from `textureSample` if the quad is actually textured.
@alice-i-cecile alice-i-cecile added A-Rendering Drawing game state to the screen C-Performance A change motivated by improving speed, memory usage or compile times A-UI Graphical user interfaces, styles, layouts, and widgets labels Jun 8, 2023
@ickshonpe
Copy link
Contributor Author

cargo run --profile stress-test --features trace_tracy --example many_buttons

ui_batch_capture

Comment on lines 410 to 424
let mode = if extracted_uinode.image.id() != default_id {
if stored_batch_handle.id() != default_id {
if stored_batch_handle.id() != extracted_uinode.image.id() {
if start != end {
commands.spawn(UiBatch {
range: start..end,
image: stored_batch_handle,
z: last_z,
});
start = end;
}
stored_batch_handle = extracted_uinode.image.clone_weak();
}
} else {
stored_batch_handle = extracted_uinode.image.clone_weak();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code could be dramatically improved. It's very hard to parse and understand what is going on.

First, I would name the predicates:

    let is_extracted_default = extracted_uinode.image.id() == default_id;
    let is_batch_default     = stored_batch_handle.id() == default_id;
    let is_batch_extracted   = stored_batch_handle.id() == extracted_uinode.image.id() ;

Then, I would extract mode values outside of all that nasty logic:

    let mode = if !is_extracted_default { TEXTURED_QUAD } else { UNTEXTURED_QUAD };

Then, I would invert some comparisons so that the single lines are not separated from their predicate, it makes it easier to see when each line are triggered:

    let mode = if is_extracted_default { UNTEXTURED_QUAD } else { TEXTURED_QUAD };

    if !is_extracted_default {
        if is_batch_default {
            stored_batch_handle = extracted_uinode.image.clone_weak();
        } else {
            if !is_batch_extracted {
                stored_batch_handle = extracted_uinode.image.clone_weak();
                if start != end {
                    commands.spawn(UiBatch {
                        range: start..end,
                        image: stored_batch_handle,
                        z: last_z,
                    });
                    start = end;
                }
            }
        }

Finally, I notice a bit of redundant logic here, so we end up with

        if is_batch_default {
            stored_batch_handle = extracted_uinode.image.clone_weak();
        } else if !is_batch_extracted {
            stored_batch_handle = extracted_uinode.image.clone_weak();
            if start != end {
                commands.spawn(UiBatch {
                    range: start..end,
                    image: stored_batch_handle,
                    z: last_z,
                });
                start = end;
            }
        }

^^^ This is the code as I would submit it, but if it was a personal project, I would use the following:

    match () {
        () if is_extracted_default => {},
        () if is_batch_default || !is_batch_extracted && start == end => {
            stored_batch_handle = extracted_uinode.image.clone_weak();
        }
        () if !is_batch_extracted => {
            stored_batch_handle = extracted_uinode.image.clone_weak();
            commands.spawn(UiBatch {
                range: start..end,
                image: stored_batch_handle,
                z: last_z,
            });
            start = end;
        }
    }

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AbleHeathen on discord mentioned another possible improvement:

if is_batch_default || !is_batch_extracted {
    stored_batch_handle = extracted_uinode.image.clone_weak();
}

if !is_batch_extracted && start != end {
    commands.spawn(UiBatch { 
      range: start..end,
      image: stored_batch_handle,
      z: last_z,
    });
    start = end;
}

Didn't check the logic, but seems even better.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The conditionals are all necessary I think. It's more complicated than it looks at first when there are multiple different textures being queued.

This is the result we want:
prepare_uinodes

But with your changes we get:
prepare_uinodes_2

You are right that it can be cleaned up though, I'll see what I can do.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I triple checked my code. It wasn't easy to do (hence the remark about it being difficult to reason about) but I've done it slowly and step by step. I'm not 100% sure, but I'm fairly confident the logic is equivalent, at least for the following code:

    let mode = if is_extracted_default { UNTEXTURED_QUAD } else { TEXTURED_QUAD };

    if !is_extracted_default {
        if is_batch_default {
            stored_batch_handle = extracted_uinode.image.clone_weak();
        } else if !is_batch_extracted {
            stored_batch_handle = extracted_uinode.image.clone_weak();
            if start != end {
                commands.spawn(UiBatch {
                    range: start..end,
                    image: stored_batch_handle,
                    z: last_z,
                });
                start = end;
            }
        }
    }

I made a gist so the changes can be visualized: https://gist.github.com/nicopap/2511c8761eccb059757c88e4d6d69689/revisions

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No this is definitely incorrect.
stored_batch_handle holds the handle of the texture for the next UiBatch.
Then after that UiBatch is spawned, we set stored_batch_handle to the texture for the next batch from extracted_uinode.

Your change overwrites stored_batch_handle first before the UiBatch is created so the batch will be spawned with the wrong texture. Also, it doesn't look like it will compile because UiBatch takes ownership of stored_batch_handle, so the value can't be used in the next iteration of the loop.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the lastest version I just pushed is looking ok now. You were right that stored_batch_handle could be set at the end of the code paths, instead of in both of the different if blocks.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed that this section of code is still hard to understand. I don't feel confident reviewing it atm.

@@ -514,6 +529,7 @@ pub fn prepare_uinodes(
position: positions_clipped[i].into(),
uv: uvs[i].into(),
color,
mode,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For other reviewers, this is like the most important line of this PR

Comment on lines +386 to +387
const TEXTURED_QUAD: u32 = 0;
const UNTEXTURED_QUAD: u32 = 1;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the futures, this could be turned into a bitflags, (for example if you want to cram more metadata into the vertex attributes) but I think it should be kept at two u32 consts for now.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I kept it this basic because the shader will be replaced soon for one that does rounded corners etc.

Copy link
Contributor

@nicopap nicopap left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me. The logic of that nested if is much more apparent with the is_textured function. Just one question regarding the sort_key.

crates/bevy_ui/src/render/mod.rs Outdated Show resolved Hide resolved
z: last_z,
});
start = end;
let mode = if is_textured(&extracted_uinode.image) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: I still would separate the logic from the value setting, in the imperative style (I say that as someone who has a strong bias in favor of functional programming)

Suggested change
let mode = if is_textured(&extracted_uinode.image) {
let mode = if is_textured(&extracted_uinode.image) {
TEXTURED_QUAD
} else {
UNTEXTURED_QUAD
};
if is_textured(&extracted_uinode.image) {

crates/bevy_ui/src/render/mod.rs Outdated Show resolved Hide resolved
@ickshonpe
Copy link
Contributor Author

Removed the z ordering changes, they aren't important right now. Better to keep this simple.

@james7132 james7132 self-requested a review June 12, 2023 17:55
crates/bevy_ui/src/render/ui.wgsl Outdated Show resolved Hide resolved
crates/bevy_ui/src/render/ui.wgsl Show resolved Hide resolved
@alice-i-cecile alice-i-cecile added this to the 0.11 milestone Jun 19, 2023
Replace mode value with a const.

Added comment to the fragment shader explaining `textureSample` that cannot be called inside an if branch, only in uniform flow control.
@nicopap
Copy link
Contributor

nicopap commented Jun 21, 2023

We should consider merging this before 0.11. It's a very small change with a major positive performance impact.

@cart cart added this pull request to the merge queue Jun 21, 2023
Merged via the queue into bevyengine:main with commit c39e02c Jun 22, 2023
github-merge-queue bot pushed a commit that referenced this pull request Jun 25, 2023
…ute (#8933)

# Objective

- Fix this error to be able to run UI examples in WebGPU
```
1 error(s) generated while compiling the shader:
:31:18 error: integral user-defined vertex outputs must have a flat interpolation attribute
    @location(3) mode: u32,
                 ^^^^

:36:1 note: while analyzing entry point 'vertex'
fn vertex(
^^
```

It was introduce in #8793

## Solution

- Add `@interpolate(flat)` to the `mode` field
@ickshonpe ickshonpe mentioned this pull request Aug 28, 2023
github-merge-queue bot pushed a commit that referenced this pull request Nov 3, 2023
# Objective

Reimplement #8793 on top of the recent rendering changes.

## Solution

The batch creation logic is quite convoluted, but I tested it on enough
examples to convince myself that it works.

The initial value of `batch_image_handle` is changed from
`HandleId::Id(Uuid::nil(), u64::MAX)` to `DEFAULT_IMAGE_HANDLE.id()`,
which allowed me to make the if-block simpler I think.

The default image from `DEFAULT_IMAGE_HANDLE` is always inserted into
`UiImageBindGroups` even if it's not used. I tried to add a check so
that it would be only inserted when there is only one batch using the
default image but this crashed.

---

## Changelog
`prepare_uinodes`
* Changed the initial value of `batch_image_handle` to
`DEFAULT_IMAGE_HANDLE.id()`.
* The default image is added to the UI image bind groups before
assembling the batches.
* A new `UiBatch` isn't created when the next `ExtractedUiNode`s image
is set to `DEFAULT_IMAGE_HANDLE` (unless it is the first item in the UI
phase items list).
aevyrie added a commit to aevyrie/bevy that referenced this pull request Nov 4, 2023
Fix branding inconsistencies

don't Implement `Display` for `Val` (bevyengine#10345)

- Revert bevyengine#10296

- Avoid implementing `Display` without a justification
- `Display` implementation is a guarantee without a direct use, takes
additional time to compile and require work to maintain
- `Debug`, `Reflect` or `Serialize` should cover all needs

Combine visibility queries in check_visibility_system (bevyengine#10196)

Alternative to bevyengine#7310

Implemented the suggestion from
bevyengine#7310 (comment)

I am guessing that these were originally split as an optimization, but I
am not sure since I believe the original author of the code is the one
speculating about combining them up there.

I ran three benchmarks to compare main, this PR, and the approach from
([updated](https://github.com/rparrett/bevy/commits/rebased-parallel-check-visibility)
to the same commit on main).

This seems to perform slightly better than main in scenarios where most
entities have AABBs, and a bit worse when they don't (`many_lights`).
That seems to make sense to me.

Either way, the difference is ~-20 microseconds in the more common
scenarios or ~+100 microseconds in the less common scenario. I would
speculate that this might perform **very slightly** worse in
single-threaded scenarios.

Benches were run in release mode for 2000 frames while capturing a trace
with tracy.

| bench | commit | check_visibility_system mean μs |
| -- | -- | -- |
| many_cubes | main | 929.5 |
| many_cubes | this | 914.0 |
| many_cubes | 7310 | 1003.5 |
| | |
| many_foxes | main | 191.6 |
| many_foxes | this | 173.2 |
| many_foxes | 7310 | 167.9 |
| | |
| many_lights | main | 619.3 |
| many_lights | this | 703.7 |
| many_lights | 7310 | 842.5 |

Technically this behaves slightly differently -- prior to this PR, view
visibility was determined even for entities without `GlobalTransform`. I
don't think this has any practical impact though.

IMO, I don't think we need to do this. But I opened a PR because it
seemed like the handiest way to share the code / benchmarks.

I have done some rudimentary testing with the examples above, but I can
do some screenshot diffing if it seems like we want to do this.

Make VERTEX_COLORS usable in prepass shader, if available (bevyengine#10341)

I was working with forward rendering prepass fragment shaders and ran
into an issue of not being able to access vertex colors in the prepass.
I was able to access vertex colors in regular fragment shaders as well
as in deferred shaders.

It seems like this `if` was nested unintentionally as moving it outside
of the `deferred` block works.

---

Enable vertex colors in forward rendering prepass fragment shaders

allow DeferredPrepass to work without other prepass markers (bevyengine#10223)

fix crash / misbehaviour when `DeferredPrepass` is used without
`DepthPrepass`.

- Deferred lighting requires the depth prepass texture to be present, so
that the depth texture is available for binding. without it the deferred
lighting pass will use 0 for depth of all meshes.
- When `DeferredPrepass` is used without other prepass markers, and with
any materials that use `OpaqueRenderMode::Forward`, those entities will
try to queue to the `Opaque3dPrepass` render phase, which doesn't exist,
causing a crash.

- check if the prepass phases exist before queueing
- generate prepass textures if `Opaque3dDeferred` is present
- add a note to the DeferredPrepass marker to note that DepthPrepass is
also required by the default deferred lighting pass
- also changed some `With<T>.is_some()`s to `Has<T>`s

UI batching Fix (bevyengine#9610)

Reimplement bevyengine#8793 on top of the recent rendering changes.

The batch creation logic is quite convoluted, but I tested it on enough
examples to convince myself that it works.

The initial value of `batch_image_handle` is changed from
`HandleId::Id(Uuid::nil(), u64::MAX)` to `DEFAULT_IMAGE_HANDLE.id()`,
which allowed me to make the if-block simpler I think.

The default image from `DEFAULT_IMAGE_HANDLE` is always inserted into
`UiImageBindGroups` even if it's not used. I tried to add a check so
that it would be only inserted when there is only one batch using the
default image but this crashed.

---

`prepare_uinodes`
* Changed the initial value of `batch_image_handle` to
`DEFAULT_IMAGE_HANDLE.id()`.
* The default image is added to the UI image bind groups before
assembling the batches.
* A new `UiBatch` isn't created when the next `ExtractedUiNode`s image
is set to `DEFAULT_IMAGE_HANDLE` (unless it is the first item in the UI
phase items list).

Increase default normal bias to avoid common artifacts (bevyengine#10346)

Bevy's default bias values for directional and spot lights currently
cause significant artifacts. We should fix that so shadows look good by
default!

This is a less controversial/invasive alternative to bevyengine#10188, which might
enable us to keep the default bias value low, but also has its own sets
of concerns and caveats that make it a risky choice for Bevy 0.12.

Bump the default normal bias from `0.6` to `1.8`. There is precedent for
values in this general area as Godot has a default normal bias of `2.0`.

![image](https://github.com/superdump/bevy/assets/2694663/a5828011-33fc-4427-90ed-f093d7389053)

![image](https://github.com/bevyengine/bevy/assets/2694663/0f2b16b0-c116-41ab-9886-1ace9e00efd6)

The default `shadow_normal_bias` value for `DirectionalLight` and
`SpotLight` has changed to accommodate artifacts introduced with the new
shadow PCF changes. It is unlikely (especially given the new PCF shadow
behaviors with these values), but you might need to manually tweak this
value if your scene requires a lower bias and it relied on the previous
default value.

Make `DirectionalLight` `Cascades` computation generic over `CameraProjection` (bevyengine#9226)

Fixes bevyengine#9077 (see this issue for
motivations)

Implement 1 and 2 of the "How to fix it" section of
bevyengine#9077

`update_directional_light_cascades` is split into
`clear_directional_light_cascades` and a generic
`build_directional_light_cascades`, to clear once and potentially insert
many times.

---

`DirectionalLight`'s computation is now generic over `CameraProjection`
and can work with custom camera projections.

If you have a component `MyCustomProjection` that implements
`CameraProjection`:
- You need to implement a new required associated method,
`get_frustum_corners`, returning an array of the corners of a subset of
the frustum with given `z_near` and `z_far`, in local camera space.
- You can now add the
`build_directional_light_cascades::<MyCustomProjection>` system in
`SimulationLightSystems::UpdateDirectionalLightCascades` after
`clear_directional_light_cascades` for your projection to work with
directional lights.

---------

Co-authored-by: Carter Anderson <mcanders1@gmail.com>

Update default `ClearColor` to better match Bevy's branding (bevyengine#10339)

- Changes the default clear color to match the code block color on
Bevy's website.

- Changed the clear color, updated text in examples to ensure adequate
contrast. Inconsistent usage of white text color set to use the default
color instead, which is already white.
- Additionally, updated the `3d_scene` example to make it look a bit
better, and use bevy's branding colors.

![image](https://github.com/bevyengine/bevy/assets/2632925/540a22c0-826c-4c33-89aa-34905e3e313a)

Corrected incorrect doc comment on read_asset_bytes (bevyengine#10352)

Fixes bevyengine#10302

- Removed the incorrect comment.

Allow AccessKit to react to WindowEvents before they reach the engine (bevyengine#10356)

- Adopt bevyengine#10239 to get it in time for the release
- Fix accessibility on macOS and linux

- call `on_event` from AcccessKit adapter on winit events

---------

Co-authored-by: Nolan Darilek <nolan@thewordnerd.info>
Co-authored-by: Alice Cecile <alice.i.cecil@gmail.com>
Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com>

Fix typo in window.rs (bevyengine#10358)

Fixes a small typo in `bevy_window/src/window.rs`

Change `Should be used instead 'scale_factor' when set.` to `Should be
used instead of 'scale_factor' when set.`

Add UI Materials (bevyengine#9506)

- Add Ui Materials so that UI can render more complex and animated
widgets.
- Fixes bevyengine#5607

- Create a UiMaterial trait for specifying a Shader Asset and Bind Group
Layout/Data.
- Create a pipeline for rendering these Materials inside the Ui
layout/tree.
- Create a MaterialNodeBundle for simple spawning.

- Created a `UiMaterial` trait for specifying a Shader asset and Bind
Group.
- Created a `UiMaterialPipeline` for rendering said Materials.
- Added Example [`ui_material`
](https://github.com/MarkusTheOrt/bevy/blob/ui_material/examples/ui/ui_material.rs)
for example usage.
- Created
[`UiVertexOutput`](https://github.com/MarkusTheOrt/bevy/blob/ui_material/crates/bevy_ui/src/render/ui_vertex_output.wgsl)
export as VertexData for shaders.
- Created
[`material_ui`](https://github.com/MarkusTheOrt/bevy/blob/ui_material/crates/bevy_ui/src/render/ui_material.wgsl)
shader as default for both Vertex and Fragment shaders.

---------

Co-authored-by: ickshonpe <david.curthoys@googlemail.com>
Co-authored-by: François <mockersf@gmail.com>

support file operations in single threaded context (bevyengine#10312)

- Fixes bevyengine#10209
- Assets should work in single threaded

- In single threaded mode, don't use `async_fs` but fallback on
`std::fs` with a thin layer to mimic the async API
- file `file_asset.rs` is the async imps from `mod.rs`
- file `sync_file_asset.rs` is the same with `async_fs` APIs replaced by
`std::fs`
- which module is used depends on the `multi-threaded` feature

---------

Co-authored-by: Carter Anderson <mcanders1@gmail.com>

Fix gizmo crash when prepass enabled (bevyengine#10360)

- Fix gizmo crash when prepass enabled

- Add the prepass to the view key

Fixes: bevyengine#10347
ameknite pushed a commit to ameknite/bevy that referenced this pull request Nov 6, 2023
# Objective

Reimplement bevyengine#8793 on top of the recent rendering changes.

## Solution

The batch creation logic is quite convoluted, but I tested it on enough
examples to convince myself that it works.

The initial value of `batch_image_handle` is changed from
`HandleId::Id(Uuid::nil(), u64::MAX)` to `DEFAULT_IMAGE_HANDLE.id()`,
which allowed me to make the if-block simpler I think.

The default image from `DEFAULT_IMAGE_HANDLE` is always inserted into
`UiImageBindGroups` even if it's not used. I tried to add a check so
that it would be only inserted when there is only one batch using the
default image but this crashed.

---

## Changelog
`prepare_uinodes`
* Changed the initial value of `batch_image_handle` to
`DEFAULT_IMAGE_HANDLE.id()`.
* The default image is added to the UI image bind groups before
assembling the batches.
* A new `UiBatch` isn't created when the next `ExtractedUiNode`s image
is set to `DEFAULT_IMAGE_HANDLE` (unless it is the first item in the UI
phase items list).
rdrpenguin04 pushed a commit to rdrpenguin04/bevy that referenced this pull request Jan 9, 2024
# Objective

Reimplement bevyengine#8793 on top of the recent rendering changes.

## Solution

The batch creation logic is quite convoluted, but I tested it on enough
examples to convince myself that it works.

The initial value of `batch_image_handle` is changed from
`HandleId::Id(Uuid::nil(), u64::MAX)` to `DEFAULT_IMAGE_HANDLE.id()`,
which allowed me to make the if-block simpler I think.

The default image from `DEFAULT_IMAGE_HANDLE` is always inserted into
`UiImageBindGroups` even if it's not used. I tried to add a check so
that it would be only inserted when there is only one batch using the
default image but this crashed.

---

## Changelog
`prepare_uinodes`
* Changed the initial value of `batch_image_handle` to
`DEFAULT_IMAGE_HANDLE.id()`.
* The default image is added to the UI image bind groups before
assembling the batches.
* A new `UiBatch` isn't created when the next `ExtractedUiNode`s image
is set to `DEFAULT_IMAGE_HANDLE` (unless it is the first item in the UI
phase items list).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-Rendering Drawing game state to the screen A-UI Graphical user interfaces, styles, layouts, and widgets C-Performance A change motivated by improving speed, memory usage or compile times
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants