bevyengine / bevy

A refreshingly simple data-driven game engine built in Rust
https://bevyengine.org
Apache License 2.0
36.51k stars 3.6k forks source link

Load only mip levels up to a specific size #16056

Open inodentry opened 1 month ago

inodentry commented 1 month ago

What problem does this solve or what need does it fill?

It is common for games to offer multiple levels of graphics settings, like: "Low", "High", "Ultra". When it comes to Texture Quality, this typically means the size of textures used. For example, a 2048x2048 texture will use 4x the GPU memory than a 1024x1024 texture and add more overhead during rendering, but look a lot crisper.

In 3D games, mipmaps are essentially required for high quality and performant rendering. If the game's assets are stored as KTX2 or DDS files with mipmaps, the smaller texture sizes are already available on-disk (and will, in fact, be loaded by Bevy and into GPU memory) anyway. In order to limit the max texture size at lower graphics settings, it could be implemented simply by just skipping the first mip level or two.

For example, if we have a 2048x2048 texture as a KTX2 file with a full set of mipmaps down to 1x1 (12 levels total), our hypothetical game could behave as follows:

What solution would you like?

If Bevy supports specifying which mip levels to load, this could be done trivially, and reduce loading times by loading less data from disk.

What alternative(s) have you considered?

1) Load everything anyway, and then simply not use it. The range of mip levels used by the GPU can be restricted using the lod_min_clamp and lod_max_clamp fields in the SamplerDescriptor. Disadvantage: we don't get any savings in GPU memory or loading times, so it kinda defeats the point.

2) Implement a two-step process, by discarding the unwanted data from the Image asset after it is loaded. We get GPU memory savings, but no loading time savings. Also, the full resolution data is stored intermittently in CPU memory, before we get a chance to delete it. Inefficient: loading data only to immediately drop it.

3) Create separate asset files with a lower base resolution. Works, but is redundant, wastes disk space. The lower resolution images are already available as mip levels in the larger asset.

Additional context

This, of course, only applies to assets stored in file formats that have mipmaps (KTX2 and DDS). When loading images from, say, PNGs, this is irrelevant. In that case, if you want multiple different sizes, you need multiple files, or to downscale the image at runtime.

JMS55 commented 1 month ago

Yeah we should have something like this. Faster load times, faster runtime (more cache efficient to sample a lower-res texture), and less memory usage (the main draw).

mrec commented 1 month ago

"This, of course, only applies to assets stored in file formats that have mipmaps (KTX2 and DDS)" - I think .basis does too, doesn't it?

JMS55 commented 1 month ago

It does iirc, but basisu is generally not worth it https://github.com/bevyengine/bevy/issues/14671.

inodentry commented 1 month ago

I personally really dislike Basis Universal and I completely forgot about it when I was writing the OP. 😆

It aims to solve a problem (targeting both mobile GPUs using ASTC and desktop GPUs using BCn using the same asset files) which is largely non-existent in practice (you ship different builds for desktop and mobile anyway; just ship your assets in the appropriate format). And the cost is worse texture quality (it has to make compromises with the lossy compression) and slower loading times (you have to decode it on the CPU; sure it is fast, but with the native GPU formats you can copy directly to GPU memory and no decoding is needed).

Anyway, let's not derail the thread.