From ff2ff308ed92095cc684c11385e5d0104bc7e489 Mon Sep 17 00:00:00 2001 From: Hirek193 Date: Mon, 9 Nov 2026 20:58:02 +0100 Subject: [PATCH] Some optimalisations --- CMakeLists.txt | 1 + model/Texture.cpp | 31 +++- model/Texture.h | 4 + rendering/common/scenewalk.cpp | 33 +++- rendering/common/scenewalk.h | 7 +- rendering/common/skyocclusion.cpp | 39 ++++- rendering/common/skyocclusion.h | 16 +- rendering/common/texturealpha.cpp | 219 ++++++++++++++++++++++++ rendering/common/texturealpha.h | 38 ++++ rendering/vulkan14/vulkan14context.h | 6 + rendering/vulkan14/vulkan14debug.cpp | 2 + rendering/vulkan14/vulkan14frame.cpp | 11 +- rendering/vulkan14/vulkan14indirect.cpp | 10 +- rendering/vulkan14/vulkan14renderer.h | 17 +- rendering/vulkan14/vulkan14scene.cpp | 51 +++++- rendering/vulkan14/vulkan14texture.cpp | 75 ++++++++ rendering/vulkan14/vulkan14texture.h | 7 + scene/scene.h | 6 + tests/unit/CMakeLists.txt | 2 + tests/unit/test_texture_alpha.cpp | 101 +++++++++++ 20 files changed, 645 insertions(+), 31 deletions(-) create mode 100644 rendering/common/texturealpha.cpp create mode 100644 rendering/common/texturealpha.h create mode 100644 tests/unit/test_texture_alpha.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 3e3c7bcf..df7270a4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -342,6 +342,7 @@ set(SOURCES "rendering/common/localshadows.cpp" "rendering/common/cabprobe.cpp" "rendering/common/skyocclusion.cpp" +"rendering/common/texturealpha.cpp" "rendering/common/terrainmaterials.cpp" "rendering/common/grassfield.cpp" "rendering/common/gpuparticles.cpp" diff --git a/model/Texture.cpp b/model/Texture.cpp index ec351afc..14f78661 100644 --- a/model/Texture.cpp +++ b/model/Texture.cpp @@ -23,6 +23,7 @@ http://mozilla.org/MPL/2.0/. #include "utilities/utilities.h" #include "scene/sn_utils.h" #include "rendering/flip-s3tc.h" +#include "rendering/common/texturealpha.h" #include "stb/stb_image.h" //#include #include "dds-ktx/dds-ktx.h" @@ -54,12 +55,25 @@ struct texture_request { glm::dvec3 origin { 0.0 }; // results, written by the thread reading the data std::vector data; + // every texel passes the alpha test of the shadow maps: checked by the worker threads, not by a thread waiting for the data + std::optional alphakept; bool failed { false }; bool done { false }; // guarded by the mutex of the loader }; namespace { +// the alpha test of the shadow maps checked on the data of a dds file, block by block of its format +std::optional dds_alpha_kept( texture_request const &Request ) { + switch( Request.fourcc ) { + case FOURCC_DXT1: { return gfx::texels_pass_alpha_test( gfx::alpha_layout::bc1, Request.data ); } + case FOURCC_DXT3: { return gfx::texels_pass_alpha_test( gfx::alpha_layout::bc2, Request.data ); } + case FOURCC_DXT5: { return gfx::texels_pass_alpha_test( gfx::alpha_layout::bc3, Request.data ); } + // the only one load_DDS() takes of the dx10 formats + case FOURCC_DX10: { return gfx::texels_pass_alpha_test( gfx::alpha_layout::bc7, Request.data ); } + default: { return std::nullopt; } + } } + // worker threads reading texture data from disk class texture_loader { @@ -92,7 +106,8 @@ public: if( lookup != std::end( m_queue ) ) { m_queue.erase( lookup ); lock.unlock(); - read( *Request ); + // the thread waiting for the data doesn't check its alpha: the one which makes the texture does it, if it can wait + read( *Request, false ); lock.lock(); Request->done = true; return; @@ -111,6 +126,7 @@ public: return false; } Request.data = std::vector(); + Request.alphakept.reset(); Request.done = false; Request.failed = false; return true; } @@ -136,14 +152,14 @@ private: auto request { m_queue.front() }; m_queue.pop_front(); lock.unlock(); - read( *request ); + read( *request, true ); lock.lock(); request->done = true; m_read.notify_all(); } } - // same as the reading part of texture_source::load_DDS() - static void read( texture_request &Request ) { + // same as the reading part of texture_source::load_DDS(). Check: the alpha of the texels checked too (on a worker thread) + static void read( texture_request &Request, bool const Check ) { std::ifstream file( Request.file, std::ios::binary ); if( false == file.is_open() ) { Request.failed = true; @@ -171,6 +187,10 @@ private: height = std::max( height / 2, 4 ); --mapcount; } + } + Request.alphakept.reset(); + if( Check && file.good() ) { + Request.alphakept = dds_alpha_kept( Request ); } } std::mutex m_mutex; @@ -438,6 +458,8 @@ texture_source::load() { WriteLog( "Loading texture data from \"" + name + "\"", logtype::texture ); data_state = resource_state::loading; + // known only of the data a worker thread reads (complete_data()) + data_alphakept.reset(); if( type == ".dds" ) { load_DDS(); } else if( type == ".tga" ) { load_TGA(); } @@ -1343,6 +1365,7 @@ texture_source::complete_data() { return; } data.swap( completed->data ); + data_alphakept = completed->alphakept; data_state = resource_state::good; size = data.size() / 1024; } diff --git a/model/Texture.h b/model/Texture.h index a5dd7ee4..3f24a13c 100644 --- a/model/Texture.h +++ b/model/Texture.h @@ -12,6 +12,7 @@ http://mozilla.org/MPL/2.0/. #include #include #include +#include #include "winheaders.h" #include "utilities/memorystats.h" #include @@ -133,6 +134,9 @@ public: bool is_rendertarget = false; // is used as postfx rendertarget, without loaded data bool is_texstub = false; // for make_from_memory internal_src: functionality std::vector data; // texture data (stored GL-style, bottom-left origin) + // whether every texel of the data passes the alpha test of the shadow maps (gfx::texels_pass_alpha_test()), when the worker + // thread which read it checked that; unknown otherwise + std::optional data_alphakept; resource_state data_state{ resource_state::none }; // current state of texture data unsigned int data_width{ 0 }, data_height{ 0 }, diff --git a/rendering/common/scenewalk.cpp b/rendering/common/scenewalk.cpp index 8718ae45..63aa3ce4 100644 --- a/rendering/common/scenewalk.cpp +++ b/rendering/common/scenewalk.cpp @@ -643,9 +643,9 @@ void scene_walk::Render(cell_sequence::iterator First, cell_sequence::iterator L } // remaining (non-instanceable) opaque instance nodes go through the per-node path, or with the instances // to the gpu culling, each on its own - for (auto *instance : cell->m_instancesopaque) + // (the instanceable ones are handled via bucket) + for (auto *instance : Single_Instances(*cell)) { - if( instance->m_instanceable ) { continue; } // already handled via bucket // the model of a lamp would shadow everything in the map of the lamp if( instance == m_cubeskipinstance ) { continue; } if( walk_queue_model( instance, cellinrange ) ) { continue; } @@ -680,8 +680,7 @@ void scene_walk::Render(cell_sequence::iterator First, cell_sequence::iterator L // opaque parts of instanced models -- accumulate into the // frame-level map; flushed once per unique model after the loop. Queue_Instances( cell ); - for( auto *instance : cell->m_instancesopaque ) { - if( instance->m_instanceable ) { continue; } + for( auto *instance : Single_Instances( *cell ) ) { if( walk_queue_model( instance, true ) ) { continue; } Render( instance ); m_modelcomparekey = nullptr; @@ -1074,6 +1073,32 @@ void scene_walk::Update_Instance_Records( TModel3d const *Model, scene::basic_ce Bucket.gpustale = true; } +// the opaque instances of a scene cell drawn each on its own. an instance stops being drawn with the others of its model by an event +// (TAnimModel::AddContainer()), which changes scene::node_state_version and with it the version of the instance records; the editor +// makes the records anew in every frame. the list is made once for a version, before any walk of the frame reads it, so a walk going +// through it doesn't see it change +scene::basic_cell::instance_sequence const &scene_walk::Single_Instances( scene::basic_cell const &Cell ) const +{ + auto const stale = [ this, &Cell ]() { + return ( Cell.m_instancessingleversion != m_instancerecordsversion ) + || ( Cell.m_instancessinglesource != Cell.m_instancesopaque.size() ); }; + if( stale() ) { + // the lists the passes share are made by one walk at a time (walk_sync) + gfx::walk_sync::change change; + if( stale() ) { + Cell.m_instancessingle.clear(); + for( auto *instance : Cell.m_instancesopaque ) { + if( false == instance->m_instanceable ) { + Cell.m_instancessingle.emplace_back( instance ); + } + } + Cell.m_instancessinglesource = Cell.m_instancesopaque.size(); + Cell.m_instancessingleversion = m_instancerecordsversion; + } + } + return Cell.m_instancessingle; +} + // whether none of the instances of a bucket can pass the tests of the culling of the pass (Render_Instanced(), on the gpu // instance_cull.comp): by the bounds of their records the visible ones lie wholly out of the frustum of the pass, beyond their // range or the draw range, or are too small on the screen. the tests are those of the culling with the nearest location and diff --git a/rendering/common/scenewalk.h b/rendering/common/scenewalk.h index b6f3c3de..91a708c8 100644 --- a/rendering/common/scenewalk.h +++ b/rendering/common/scenewalk.h @@ -153,6 +153,8 @@ class scene_walk void Queue_Instances(scene::basic_cell const *Cell); // makes the records of the instances held by a bucket of a scene cell, with locations relative to Origin void Update_Instance_Records(TModel3d const *Model, scene::basic_cell::instance_bucket const &Bucket, glm::dvec3 const &Origin); + // the opaque instances of a scene cell which aren't drawn with the others of their model, made anew with the instance records + scene::basic_cell::instance_sequence const &Single_Instances(scene::basic_cell const &Cell) const; // whether none of the instances of a bucket can pass the culling of the current pass, by the bounds of their records bool Bucket_Culled(scene::basic_cell::instance_bucket const &Bucket) const; // the sphere around the locations of the visible instances of a bucket, with a margin for the precision of the records @@ -344,8 +346,9 @@ class scene_walk // whether two recorded draws can go in one multi draw of the colour pass: the same material and texture variant, geometry // bank, primitive and blending bool Indirect_Mergeable(std::size_t const First, std::size_t const Other) const; - // the texture the shadow material binds for the alpha test of a material, or null_handle (no test) - texture_handle Indirect_Shadow_Texture(material_handle const Material) const; + // the texture the shadow material binds for the alpha test of a material, or null_handle (no test). a renderer which knows a + // texture passes the test everywhere draws its shadows without one + virtual texture_handle Indirect_Shadow_Texture(material_handle const Material) const; // the records of the sleepers and of the models drawn on their own not queued for a while are let go void Expire_Indirect_Records(); // a scenery is being loaded: the tracks and the models the records were made for may be gone diff --git a/rendering/common/skyocclusion.cpp b/rendering/common/skyocclusion.cpp index f0b648e3..4aef4cce 100644 --- a/rendering/common/skyocclusion.cpp +++ b/rendering/common/skyocclusion.cpp @@ -314,6 +314,8 @@ void sky_occlusion::stop() void sky_occlusion::clear() { + // the cells go with the scenery + m_cellsignatures.clear(); // once, until a scenery is there again if ((m_side <= 0) || (m_region == nullptr)) return; @@ -628,20 +630,39 @@ std::uint64_t sky_occlusion::signature(scene::basic_region *Region, glm::ivec2 c if (overlaps(data.area, shape_reach(shape))) hash = hash_mix(hash, static_cast(data.geometry) ^ static_cast(data.visible ? 1 : 0)); } - for (auto const *instance : cell.m_instancesopaque) - { - hash = hash_mix(hash, static_cast(reinterpret_cast(instance)) ^ static_cast(instance->visible() ? 1 : 0)); - auto const location{instance->location()}; - hash = hash_mix_double(hash, location.x); - hash = hash_mix_double(hash, location.y); - hash = hash_mix_double(hash, location.z); - hash = hash_mix_double(hash, static_cast(instance->vAngle.y)); - } + hash = hash_mix(hash, instances_signature(cell)); } } return hash; } +std::uint64_t sky_occlusion::instances_signature(scene::basic_cell const &Cell) +{ + auto const make = [&Cell]() { + std::uint64_t hash{0x1a57a2ce}; + for (auto const *instance : Cell.m_instancesopaque) + { + hash = hash_mix(hash, static_cast(reinterpret_cast(instance)) ^ static_cast(instance->visible() ? 1 : 0)); + auto const location{instance->location()}; + hash = hash_mix_double(hash, location.x); + hash = hash_mix_double(hash, location.y); + hash = hash_mix_double(hash, location.z); + hash = hash_mix_double(hash, static_cast(instance->vAngle.y)); + } + return hash; + }; + // the editor moves the instances and hides them with their layers without the version changing: read anew each time there, and + // kept for when it's left + auto &kept{m_cellsignatures[&Cell]}; + if (EditorModeFlag || (kept.version != scene::node_state_version) || (kept.count != Cell.m_instancesopaque.size())) + { + kept.hash = make(); + kept.version = scene::node_state_version; + kept.count = Cell.m_instancesopaque.size(); + } + return kept.hash; +} + void sky_occlusion::gather(scene::basic_region *Region, glm::ivec2 const &Tile, std::vector &Triangles) { auto const tilesize{static_cast(m_layout.tile)}; diff --git a/rendering/common/skyocclusion.h b/rendering/common/skyocclusion.h index 2a9de973..80c862b6 100644 --- a/rendering/common/skyocclusion.h +++ b/rendering/common/skyocclusion.h @@ -18,6 +18,7 @@ http://mozilla.org/MPL/2.0/. #include #include #include +#include #include #include "gl/ubo.h" @@ -29,7 +30,8 @@ class TSubModel; namespace scene { class basic_region; -} +class basic_cell; +} // namespace scene namespace gfx { @@ -181,6 +183,8 @@ class sky_occlusion void gather_tunnels(scene::basic_region *Region, glm::ivec2 const &Tile, std::vector &Points); // what the scenery over a tile is made of, cheap to work out: a change of it means the tile is gathered again std::uint64_t signature(scene::basic_region *Region, glm::ivec2 const &Tile); + // the part of the signature made of the opaque instances of a cell: their places and whether they're shown + std::uint64_t instances_signature(scene::basic_cell const &Cell); // the values of a tile nothing is known of yet, in the textures and in the copy of the cpu void neutral(int const Slot); void apply(tile_result &Result); @@ -208,6 +212,16 @@ class sky_occlusion std::vector m_neutralheights; std::vector m_neutralocclusion; std::size_t m_checkslot{0}; // the slot whose signature is checked next + // the part of the signatures made of the instances of a cell, worked out for a scene::node_state_version and a count of its + // instances. outside the editor an instance is shown or hidden only with the version changing, and isn't moved; reading every + // instance of the cells around for each check took a large part of the frame in the forests + struct cell_signature + { + unsigned int version{0}; + std::size_t count{0}; + std::uint64_t hash{0}; + }; + std::unordered_map m_cellsignatures; scene::basic_region const *m_region{nullptr}; // shared with the thread, under m_mutex diff --git a/rendering/common/texturealpha.cpp b/rendering/common/texturealpha.cpp new file mode 100644 index 00000000..80b7a2ca --- /dev/null +++ b/rendering/common/texturealpha.cpp @@ -0,0 +1,219 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#include "stdafx.h" +#include "rendering/common/texturealpha.h" + +#include +#include + +namespace gfx +{ + +namespace +{ + +using block_check = bool (*)(std::uint8_t const *Block); + +bool rgba8_kept(std::span const Data) +{ + for (std::size_t alpha = 3; alpha < Data.size(); alpha += 4) + { + if (Data[alpha] < alpha_test_kept) + { + return false; + } + } + return true; +} + +// bc1: with the first colour above the second the block has four colours and no alpha; otherwise the index 3 is transparent black +bool bc1_kept(std::uint8_t const *Block) +{ + auto const colour0{static_cast(Block[0]) | (static_cast(Block[1]) << 8)}; + auto const colour1{static_cast(Block[2]) | (static_cast(Block[3]) << 8)}; + if (colour0 > colour1) + { + return true; + } + for (std::size_t row = 4; row < 8; ++row) + { + // both bits of a 2 bit index set: the index 3 + if ((Block[row] & (Block[row] >> 1) & 0x55u) != 0) + { + return false; + } + } + return true; +} + +// bc2: 4 bits of alpha per texel, kept from 8 up (the top bit of each half of a byte) +bool bc2_kept(std::uint8_t const *Block) +{ + for (std::size_t byte = 0; byte < 8; ++byte) + { + if ((Block[byte] & 0x88u) != 0x88u) + { + return false; + } + } + return true; +} + +// bc3: two alpha endpoints and 3 bit indices into the palette they make +bool bc3_kept(std::uint8_t const *Block) +{ + auto const alpha0{static_cast(Block[0])}; + auto const alpha1{static_cast(Block[1])}; + std::array palette{alpha0, alpha1, 0u, 0u, 0u, 0u, 0u, 0u}; + if (alpha0 > alpha1) + { + for (unsigned index = 2; index < 8; ++index) + { + palette[index] = ((8 - index) * alpha0 + (index - 1) * alpha1) / 7; + } + } + else + { + for (unsigned index = 2; index < 6; ++index) + { + palette[index] = ((6 - index) * alpha0 + (index - 1) * alpha1) / 5; + } + palette[6] = 0; + palette[7] = 255; + } + std::uint64_t indices{0}; + for (std::size_t byte = 0; byte < 6; ++byte) + { + indices |= static_cast(Block[2 + byte]) << (8 * byte); + } + for (unsigned texel = 0; texel < 16; ++texel) + { + if (palette[(indices >> (3 * texel)) & 7u] < alpha_test_kept) + { + return false; + } + } + return true; +} + +// Count bits of a bc7 block from bit Offset, the lowest first +unsigned bc7_bits(std::uint8_t const *Block, unsigned const Offset, unsigned const Count) +{ + unsigned result{0}; + for (unsigned bit = 0; bit < Count; ++bit) + { + auto const index{Offset + bit}; + result |= ((static_cast(Block[index >> 3]) >> (index & 7u)) & 1u) << bit; + } + return result; +} + +// an endpoint of Bits bits as the decoder makes it 8 bits wide +unsigned bc7_expand(unsigned const Value, unsigned const Bits) +{ + return (Value << (8 - Bits)) | (Value >> (2 * Bits - 8)); +} + +// modes 4 and 5: one subset with alpha endpoints of their own, which the rotation swaps with one of the colour channels +bool bc7_rotated_kept(std::uint8_t const *Block, unsigned const Mode) +{ + auto const rotation{bc7_bits(Block, Mode + 1, 2)}; + auto const colourbits{Mode == 4 ? 5u : 7u}; + auto const alphabits{Mode == 4 ? 6u : 8u}; + auto const bits{rotation == 0 ? alphabits : colourbits}; + auto const start{rotation == 0 ? 8 + 6 * colourbits : 8 + 2 * colourbits * (rotation - 1)}; + for (unsigned endpoint = 0; endpoint < 2; ++endpoint) + { + if (bc7_expand(bc7_bits(Block, start + endpoint * bits, bits), bits) < alpha_test_kept) + { + return false; + } + } + return true; +} + +// bc7: the alpha of a texel lies between the alpha endpoints of its subset, so the endpoints decide +bool bc7_kept(std::uint8_t const *Block) +{ + if (Block[0] == 0) + { + // the reserved mode decodes to transparent black + return false; + } + unsigned mode{0}; + while ((Block[0] & (1u << mode)) == 0) + { + ++mode; + } + switch (mode) + { + case 4: + case 5: + return bc7_rotated_kept(Block, mode); + case 6: + // 7 bit alpha endpoints, each with its own p-bit + for (unsigned endpoint = 0; endpoint < 2; ++endpoint) + { + if (((bc7_bits(Block, 49 + endpoint * 7, 7) << 1) | bc7_bits(Block, 63 + endpoint, 1)) < alpha_test_kept) + { + return false; + } + } + return true; + case 7: + // two subsets, 5 bit alpha endpoints made 6 bits by their p-bits + for (unsigned endpoint = 0; endpoint < 4; ++endpoint) + { + if (bc7_expand((bc7_bits(Block, 74 + endpoint * 5, 5) << 1) | bc7_bits(Block, 94 + endpoint, 1), 6) < alpha_test_kept) + { + return false; + } + } + return true; + default: + // modes 0-3 hold no alpha: opaque + return true; + } +} + +bool blocks_kept(std::span const Data, std::size_t const Size, block_check const Check) +{ + for (std::size_t offset = 0; offset + Size <= Data.size(); offset += Size) + { + if (false == Check(Data.data() + offset)) + { + return false; + } + } + return true; +} + +} // namespace + +bool texels_pass_alpha_test(alpha_layout const Layout, std::span const Data) +{ + switch (Layout) + { + case alpha_layout::rgba8: + return rgba8_kept(Data); + case alpha_layout::bc1: + return blocks_kept(Data, 8, bc1_kept); + case alpha_layout::bc2: + return blocks_kept(Data, 16, bc2_kept); + case alpha_layout::bc3: + return blocks_kept(Data, 16, bc3_kept); + case alpha_layout::bc7: + return blocks_kept(Data, 16, bc7_kept); + default: + return false; + } +} + +} // namespace gfx diff --git a/rendering/common/texturealpha.h b/rendering/common/texturealpha.h new file mode 100644 index 00000000..d439199d --- /dev/null +++ b/rendering/common/texturealpha.h @@ -0,0 +1,38 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#pragma once + +#include +#include + +namespace gfx +{ + +// how the texels of a texture's data are laid out: rgba (or bgra) bytes, or the 4x4 blocks of the compressed formats +enum class alpha_layout +{ + rgba8, + bc1, + bc2, + bc3, + bc7 +}; + +// the alpha the alpha test of the shadow maps (alphashadowmap.frag) keeps for sure: it discards below a half, and this leaves a +// margin for the decoders and the filtering of the gpus (136/255, the 8/15 of the 4 bit alpha of bc2) +constexpr std::uint8_t alpha_test_kept{136}; + +// whether every texel of Data (the levels of the texture one after another, whole blocks) decodes to an alpha of at least +// alpha_test_kept. the filtering only mixes texels, so the alpha test then keeps every fragment, and a program without the test +// draws the same depth. the compressed blocks are checked by what they can decode to, so the answer errs on the side of a +// translucent texture +bool texels_pass_alpha_test(alpha_layout Layout, std::span Data); + +} // namespace gfx diff --git a/rendering/vulkan14/vulkan14context.h b/rendering/vulkan14/vulkan14context.h index e77f2db2..bdc57dc7 100644 --- a/rendering/vulkan14/vulkan14context.h +++ b/rendering/vulkan14/vulkan14context.h @@ -730,6 +730,12 @@ struct vulkan14_renderer::draw_state bool cullface{true}; bool glare{false}; bool depthmask_off{false}; // the editor overlay: no depth written whatever the blending + // the colour pass of the opaque scene after its depth (render_scene_depth()), keepdepth: the draws whose depth the prepass drew with + // the alpha test (prepassed, set by walk_material()) find it in the depth buffer and don't write it again. without the write the + // depth is tested before their fragment shaders, which discard, and the hidden leaves aren't lit. the solid surfaces write theirs + // as they did, so what's in front of what stays as it was + bool keepdepth{false}; + bool prepassed{false}; // the pipelines and the textures a draw needs first may be made by the background threads, the draw left out till then (the walks // of the scene, gfx.vulkan.asyncresources); the draws outside of them (the cab, the sky, the particles) wait for what they need bool async{false}; diff --git a/rendering/vulkan14/vulkan14debug.cpp b/rendering/vulkan14/vulkan14debug.cpp index c609def2..7dc118c2 100644 --- a/rendering/vulkan14/vulkan14debug.cpp +++ b/rendering/vulkan14/vulkan14debug.cpp @@ -243,6 +243,7 @@ void vulkan14_renderer::draw_debug_ui() if (ImGui::Begin("Sun shadows")) { ImGui::Checkbox("cascades fitted to the depth of the view (gfx.shadow.sdsm)", &Global.gfx_shadow_sdsm); + ImGui::Checkbox("the textures without translucent texels drawn without the alpha test", &m_shadowskipalphatest); ImGui::SliderFloat("splits, 0 equal lengths .. 1 equal ratios (gfx.shadow.sdsm.split)", &Global.gfx_shadow_sdsm_split, 0.f, 1.f, "%.2f"); auto const &fit{m_shadowfitstate}; if (Global.gfx_shadow_sdsm) @@ -386,6 +387,7 @@ void vulkan14_renderer::draw_debug_ui() ImGui::Checkbox("keep the walks of the submodel trees of the instances from frame to frame (gfx.gpucull.walkcache)", &Global.gfx_gpucull_walkcache); } ImGui::Checkbox("draw the depth of the whole scene before its colour (gfx.depthprepass.scene)", &Global.gfx_depthprepass_scene); + ImGui::Checkbox("the colour pass leaves the depth of the alpha tested surfaces as the depth of the scene drew it", &m_keepprepassdepth); if (Global.gfx_contactshadows > 0) { ImGui::TextUnformatted("(drawn anyway for the contact shadows, gfx.contactshadows)"); diff --git a/rendering/vulkan14/vulkan14frame.cpp b/rendering/vulkan14/vulkan14frame.cpp index f0a0050e..e4d08ca1 100644 --- a/rendering/vulkan14/vulkan14frame.cpp +++ b/rendering/vulkan14/vulkan14frame.cpp @@ -1680,7 +1680,8 @@ void vulkan14_renderer::render_viewport(std::size_t const Index) auto const reflections{m_ssrframe && (false == Global.bWireFrame)}; // and the ambient occlusion in the light of the surfaces auto const aolighting{ao_lighting_wanted(vp)}; - if ((Global.gfx_depthprepass_scene || contactshadows || reflections || aolighting) && vp.main && (false == Global.bWireFrame)) + auto const scenedepth{(Global.gfx_depthprepass_scene || contactshadows || reflections || aolighting) && vp.main && (false == Global.bWireFrame)}; + if (scenedepth) { render_scene_depth(Index, future); if (contactshadows) @@ -1697,8 +1698,9 @@ void vulkan14_renderer::render_viewport(std::size_t const Index) } } - // opaque parts... + // opaque parts... what the prepass drew with the alpha test keeps its depth (draw_state::keepdepth) ctx.passes.begin_pass("colour, opaque"); + m_draw->keepdepth = scenedepth && m_keepprepassdepth && (false == m_prepassincomplete); walk_blending(false); // without rain/snow we can render the cab early to limit the overdraw if (!FreeFlyModeFlag && Global.Overcast <= 1.0f && Global.render_cab && simulation::Train) @@ -1716,6 +1718,8 @@ void vulkan14_renderer::render_viewport(std::size_t const Index) model_ubs.future = future; walk_region(simulation::Region, false); m_indirectcolourpending = false; + m_draw->keepdepth = false; + m_draw->prepassed = false; // the grass of the terrain, opaque, over the ground drawn if (vp.main && (false == Global.bWireFrame)) @@ -1860,6 +1864,9 @@ void vulkan14_renderer::render_scene_depth(std::size_t const Index, glm::mat4 co draw.weather_offset = offsets[3]; draw.depthpass = true; draw.depthskip = true; + // what the prepass of this frame leaves out (keepdepth) + m_prepassincomplete = false; + m_prepassunready.clear(); commands->set_depth_bias_enable(VK_TRUE); commands->set_depth_bias(-1.f, 0.f, -1.f); auto const colourstats{m_renderpass.draw_stats}; diff --git a/rendering/vulkan14/vulkan14indirect.cpp b/rendering/vulkan14/vulkan14indirect.cpp index f88a3336..d6883c42 100644 --- a/rendering/vulkan14/vulkan14indirect.cpp +++ b/rendering/vulkan14/vulkan14indirect.cpp @@ -326,17 +326,22 @@ bool vulkan14_renderer::draw_indirect(vk14::instance_cull &Cull, command_set con { return false; } - // the buffers of the bank of the chunks, with their data on the gpu (the chunks of a multi draw are of one bank) + // the buffers of the bank of the chunks, with their data on the gpu (the chunks of a multi draw are of one bank). what the prepass + // of the scene leaves out has its depth written by the colour pass (keepdepth) vk14::chunk_source chunk; if (false == chunk_of(geometry, chunk)) { + m_prepassincomplete = (m_prepassincomplete || draw.depthpass); return false; } if (false == bind_scene_pipeline(Program, *draw.targets, Blend, static_cast(chunk.topology))) { + m_prepassincomplete = (m_prepassincomplete || draw.depthpass); return false; } - apply_draw_state(); + // the prepass draws the triangles of the draws (draw_indirect_depth()) + auto const triangles{(first.type == GL_TRIANGLES) || (first.type == GL_TRIANGLE_STRIP) || (first.type == GL_TRIANGLE_FAN)}; + apply_draw_state(triangles); // the blocks of the draws, one after another in the window of the blocks: the multi draw picks them by the number of the draw std::uint32_t block{~0u}; if (Count == 1) @@ -377,6 +382,7 @@ bool vulkan14_renderer::draw_indirect(vk14::instance_cull &Cull, command_set con draw.block = ~0u; if (block == ~0u) { + m_prepassincomplete = (m_prepassincomplete || draw.depthpass); return false; } if (draw.set_dirty) diff --git a/rendering/vulkan14/vulkan14renderer.h b/rendering/vulkan14/vulkan14renderer.h index 484f06c4..5f269b7f 100644 --- a/rendering/vulkan14/vulkan14renderer.h +++ b/rendering/vulkan14/vulkan14renderer.h @@ -173,6 +173,9 @@ class vulkan14_renderer : public gfx_renderer, public gfx::scene_walk // the draws of the shared walk of the scene, recorded void walk_material(material_handle const Material, TSubModel const *Submodel = nullptr, lighting_data const *Lighting = nullptr) override; void walk_shadow_material(material_handle const Material) override; + // none for a texture made from a file of which every texel passes the alpha test (vk14::vulkan14_texture::passes_alpha_test()): + // the program without the test draws the same depth, without reading the texture and with the early depth test + texture_handle Indirect_Shadow_Texture(material_handle const Material) const override; opengl_material const &walk_material_data(material_handle const Material) const override; opengl_material const &walk_material_data(TSubModel const *Submodel) const override; void walk_program(walk_shader const Program) override; @@ -410,6 +413,17 @@ class vulkan14_renderer : public gfx_renderer, public gfx::scene_walk // the depth of the whole opaque scene of the colour pass of the main viewport before its colour, for the contact shadows // (Contactonly: with them alone) or for itself (gfx.depthprepass.scene), and the contact shadows set up to read it void render_scene_depth(std::size_t const Viewport, glm::mat4 const &Future); + // the colour pass doesn't write the depth the prepass of the scene drew with the alpha test (draw_state::keepdepth); off in the debug + // window, to compare. what the prepass of the frame left out: a draw it had no pipeline or geometry for yet (the colour pass writes + // all of its depth then), the textures of the alpha test it found not loaded yet (their draws write it) + bool m_keepprepassdepth{true}; + // the shadows of the textures which pass the alpha test everywhere drawn without it (Indirect_Shadow_Texture()), read from the + // main renderer by the walkers + bool m_shadowskipalphatest{true}; + bool m_prepassincomplete{false}; + std::vector m_prepassunready; + // whether the prepass drew the depth of the draw of the material of the colour pass with the alpha test (walk_material()) + bool prepassed_alpha(material_handle const Material, texture_handle const Texture); bool contact_shadows_wanted(viewport_config const &Viewport) const; void setup_contact_shadows(std::size_t const Viewport); bool m_contactshadows{false}; @@ -493,7 +507,8 @@ class vulkan14_renderer : public gfx_renderer, public gfx::scene_walk // its own, for the gpu times). Repeated: a bit for each input read texel by texel and repeated (the noise of the ssao) void post_pass(char const *Fragment, vk14::image &Target, std::vector const &Inputs, vk14::draw_constants const &Constants, char const *Pass = nullptr, unsigned const Repeated = 0); bool bind_scene_pipeline(vk14::scene_program &Program, vk14::target_formats const &Targets, vk14::blend_mode const Blend, int const Topology); - void apply_draw_state(); + // Triangles: the draw is of triangles, which the prepass draws (keepdepth) + void apply_draw_state(bool const Triangles = false); void push_textures(); bool texture_ready(texture_source &Texture); texture_source &used_texture(texture_handle const Texture); diff --git a/rendering/vulkan14/vulkan14scene.cpp b/rendering/vulkan14/vulkan14scene.cpp index dccfc32d..d4cfe3e2 100644 --- a/rendering/vulkan14/vulkan14scene.cpp +++ b/rendering/vulkan14/vulkan14scene.cpp @@ -25,6 +25,7 @@ http://mozilla.org/MPL/2.0/. #include "utilities/color.h" #include "utilities/utilities.h" +#include #include #include #include @@ -1456,6 +1457,7 @@ void vulkan14_renderer::walk_material(material_handle const Material, TSubModel { return; } + m_draw->prepassed = false; if (Material == null_handle) { if (Material != m_invalid_material) @@ -1600,6 +1602,11 @@ void vulkan14_renderer::walk_material(material_handle const Material, TSubModel auto &texture{m_context->textures->mark_as_used(materialtextures[0])}; if ((false == texture_ready(texture)) || (m_context->images.find(texture.get_id()) == nullptr)) { + // the colour pass writes the depth of the draws of the texture itself (prepassed_alpha()) + if (std::ranges::find(m_prepassunready, materialtextures[0]) == m_prepassunready.end()) + { + m_prepassunready.emplace_back(materialtextures[0]); + } return; } walk_texture(0, materialtextures[0]); @@ -1612,6 +1619,7 @@ void vulkan14_renderer::walk_material(material_handle const Material, TSubModel draw.depthskip = false; return; } + draw.prepassed = draw.keepdepth && prepassed_alpha(Material, materialtextures[0]); for (std::size_t unit = 0; unit < gl::MAX_TEXTURES; ++unit) { if (draw.textures[unit] != materialtextures[unit]) @@ -1635,9 +1643,9 @@ void vulkan14_renderer::walk_shadow_material(material_handle const Material) auto &draw{*m_draw}; auto &scene{m_context->scene}; // the cube maps of the lamps with programs of their own (the faces drawn by multiview) - if ((Material != null_handle) && (m_materials.material(Material).textures[0] != null_handle)) + if (auto const texture{Indirect_Shadow_Texture(Material)}; texture != null_handle) { - walk_texture(0, m_materials.material(Material).textures[0]); + walk_texture(0, texture); draw.program = (m_cubeshadows ? scene.cube_alpha_shadow.get() : scene.alpha_shadow.get()); } else @@ -1646,6 +1654,19 @@ void vulkan14_renderer::walk_shadow_material(material_handle const Material) } } +texture_handle vulkan14_renderer::Indirect_Shadow_Texture(material_handle const Material) const +{ + auto const texture{gfx::scene_walk::Indirect_Shadow_Texture(Material)}; + auto const &main{m_main != nullptr ? *m_main : *this}; + if ((texture == null_handle) || (false == main.m_shadowskipalphatest)) + { + return texture; + } + // a texture not made yet keeps the test: what it holds isn't known + auto const &source{static_cast(m_context->textures->texture(texture))}; + return ((source.resident() && source.passes_alpha_test()) ? null_handle : texture); +} + opengl_material const &vulkan14_renderer::walk_material_data(material_handle const Material) const { return m_materials.material(Material); @@ -2003,10 +2024,13 @@ void vulkan14_renderer::draw_chunk(gfx::geometry_handle const &Geometry, bool co vk14::chunk_source chunk; if (false == chunk_of(Geometry, chunk)) { + // what the prepass of the scene leaves out has its depth written by the colour pass (keepdepth) + m_prepassincomplete = (m_prepassincomplete || draw.depthpass); return; } - if ((draw.depthpass || draw.probepass) && (draw.depthskip || (topology_class(chunk.topology) != VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST))) + auto const triangles{topology_class(chunk.topology) == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST}; + if ((draw.depthpass || draw.probepass) && (draw.depthskip || (false == triangles))) { // the depth of the scene: what the programs of the prepass don't draw, and no points or lines return; @@ -2018,14 +2042,16 @@ void vulkan14_renderer::draw_chunk(gfx::geometry_handle const &Geometry, bool co auto const blend{draw.glare ? vk14::blend_mode::scene_additive : (draw.blending ? vk14::blend_mode::scene_alpha : vk14::blend_mode::none)}; if (false == bind_scene_pipeline(*draw.program, *draw.targets, blend, static_cast(chunk.topology))) { + m_prepassincomplete = (m_prepassincomplete || draw.depthpass); return; } - apply_draw_state(); + apply_draw_state(triangles); if (Newblock || (draw.block == ~0u)) { draw.block = push_draw_block(); if (draw.block == ~0u) { + m_prepassincomplete = (m_prepassincomplete || draw.depthpass); return; } } @@ -2507,7 +2533,7 @@ void vulkan14_renderer::context::save_scene_pipelines() } // the dynamic state of the draws for the state of the walk: the faces culled, the depth tested and written -void vulkan14_renderer::apply_draw_state() +void vulkan14_renderer::apply_draw_state(bool const Triangles) { auto &draw{*m_draw}; auto const cull{draw.cullface ? (draw.shadows ? VK_CULL_MODE_FRONT_BIT : VK_CULL_MODE_BACK_BIT) : VK_CULL_MODE_NONE}; @@ -2522,7 +2548,9 @@ void vulkan14_renderer::apply_draw_state() draw.commands->set_depth_test_enable(depthtest ? VK_TRUE : VK_FALSE); draw.depthtest = depthtest; } - auto const depthwrite{(draw.blending || draw.depthmask_off) ? 0 : 1}; + // the depth the prepass of the scene drew already isn't written again (draw_state::keepdepth) + auto const kept{draw.keepdepth && draw.prepassed && Triangles}; + auto const depthwrite{(draw.blending || draw.depthmask_off || kept) ? 0 : 1}; if (depthwrite != draw.depthwrite) { draw.commands->set_depth_write_enable(depthwrite ? VK_TRUE : VK_FALSE); @@ -2609,6 +2637,17 @@ int vulkan14_renderer::prepass_class(material_handle const Material) return prepassclass; } +// what the prepass draws with the alpha test, as walk_material() and draw_indirect_depth() choose it: not blended, the alpha test of +// the opaque pass, the program of the prepass with the test, the diffuse texture there and found loaded by the prepass +bool vulkan14_renderer::prepassed_alpha(material_handle const Material, texture_handle const Texture) +{ + if (m_draw->blending || (model_ubs.opacity < 0.f) || (Texture == null_handle) || (prepass_class(Material) != 2)) + { + return false; + } + return std::ranges::find(m_prepassunready, Texture) == m_prepassunready.end(); +} + // textures: the files read and decoded as for the opengl renderer, made into images when they are first used texture_handle vulkan14_renderer::Fetch_Texture(std::string const &Filename, bool const Loadnow, GLint format_hint) diff --git a/rendering/vulkan14/vulkan14texture.cpp b/rendering/vulkan14/vulkan14texture.cpp index 7e18eb9a..d369f0d3 100644 --- a/rendering/vulkan14/vulkan14texture.cpp +++ b/rendering/vulkan14/vulkan14texture.cpp @@ -9,6 +9,7 @@ http://mozilla.org/MPL/2.0/. #include "stdafx.h" #include "rendering/vulkan14/vulkan14texture.h" +#include "rendering/common/texturealpha.h" #include "rendering/vulkan14/vulkan14commands.h" #include "rendering/vulkan14/vulkan14device.h" #include "rendering/vulkan14/vulkan14tasks.h" @@ -16,10 +17,74 @@ http://mozilla.org/MPL/2.0/. #include "utilities/Logs.h" #include +#include +#include namespace vk14 { +namespace +{ + +// the data a texture is made of, for the alpha test of the shadow maps +struct alpha_source +{ + std::uint8_t const *data{nullptr}; + VkDeviceSize size{0}; + // what the worker thread which read the data found (texture_source::data_alphakept) + std::optional known; + // the texels may be checked here: a background thread makes the texture. the thread of the renderer doesn't wait for that, the + // texture keeps the test + bool scan{false}; +}; + +// whether the alpha test of the shadow maps keeps every texel of the data of the levels: the alpha the material reads is one, or +// that of each texel passes the test (the mipmaps the gpu makes of a single level only mix its texels) +bool texels_pass_alpha_test(texture_format const &Format, alpha_source const &Source) +{ + if ((Format.components.a == VK_COMPONENT_SWIZZLE_ONE) || Format.expand_rgb) + { + return true; + } + if (Format.components.a != VK_COMPONENT_SWIZZLE_IDENTITY) + { + return false; + } + if (Source.known.has_value()) + { + return *Source.known; + } + if (false == Source.scan) + { + return false; + } + std::span const data{Source.data, static_cast(Source.size)}; + switch (Format.format) + { + case VK_FORMAT_BC1_RGBA_SRGB_BLOCK: + case VK_FORMAT_BC1_RGBA_UNORM_BLOCK: + return gfx::texels_pass_alpha_test(gfx::alpha_layout::bc1, data); + case VK_FORMAT_BC2_SRGB_BLOCK: + case VK_FORMAT_BC2_UNORM_BLOCK: + return gfx::texels_pass_alpha_test(gfx::alpha_layout::bc2, data); + case VK_FORMAT_BC3_SRGB_BLOCK: + case VK_FORMAT_BC3_UNORM_BLOCK: + return gfx::texels_pass_alpha_test(gfx::alpha_layout::bc3, data); + case VK_FORMAT_BC7_SRGB_BLOCK: + case VK_FORMAT_BC7_UNORM_BLOCK: + return gfx::texels_pass_alpha_test(gfx::alpha_layout::bc7, data); + case VK_FORMAT_R8G8B8A8_SRGB: + case VK_FORMAT_R8G8B8A8_UNORM: + case VK_FORMAT_B8G8R8A8_SRGB: + case VK_FORMAT_B8G8R8A8_UNORM: + return gfx::texels_pass_alpha_test(gfx::alpha_layout::rgba8, data); + default: + return false; + } +} + +} // namespace + // uploader void uploader::init(allocator const &Allocator, deletion_queue &Deletions, VkPhysicalDevice Physical) @@ -751,6 +816,9 @@ vulkan14_texture::make_result vulkan14_texture::make(bool const Background) return failed(); } + // published by m_made (made now, or by publish() of the thread of the renderer) + m_passesalphatest = texels_pass_alpha_test(format, alpha_source{source, size, data_alphakept, Background}); + // a single level is made into the whole chain on the gpu, as glGenerateMipmap() does for the opengl renderer auto const generate{(levels.size() == 1) && (false == format.compressed) && m_resources.uploads->can_blit(format.format)}; // the levels the file gives are filled by the host where it can, by a background thread (what the gpu makes goes through the @@ -878,6 +946,7 @@ void vulkan14_texture::release() m_image.reset(); m_id = 0; m_made = false; + m_passesalphatest = false; is_ready = false; } @@ -904,6 +973,7 @@ void vulkan14_texture::destroy() m_image.reset(); m_id = 0; m_made = false; + m_passesalphatest = false; } void vulkan14_texture::on_failed() @@ -915,6 +985,7 @@ void vulkan14_texture::on_failed() } m_image.reset(); m_id = 0; + m_passesalphatest = false; m_made = true; } @@ -972,6 +1043,8 @@ void vulkan14_texture::adopt(std::shared_ptr const &Image, VkSampler cons m_id = m_resources.images->add(Image, Sampler); } m_image = Image; + // filled by the renderer, not from a file + m_passesalphatest = false; m_made = true; if (Image) { @@ -1027,6 +1100,8 @@ void vulkan14_texture::upload_rgba(std::uint8_t const *Data, std::size_t Width, data_height = height; data_format = GL_SRGB8_ALPHA8; data_components = GL_RGBA; + // content given from memory, not checked + m_passesalphatest = false; m_made = true; // the content given straight to the gpu can't be read again: released by the garbage sweep, the texture would come back as // the grey stub diff --git a/rendering/vulkan14/vulkan14texture.h b/rendering/vulkan14/vulkan14texture.h index 6e82598e..21d6eab1 100644 --- a/rendering/vulkan14/vulkan14texture.h +++ b/rendering/vulkan14/vulkan14texture.h @@ -226,6 +226,12 @@ struct vulkan14_texture : public texture_source return m_job != job_none; } void destroy() override; + // made from the data of a file of which every texel passes the alpha test of the shadow maps (gfx::texels_pass_alpha_test()): + // its shadows need no test. read after resident() + bool passes_alpha_test() const + { + return m_passesalphatest; + } // the content given by update_from_memory() from another thread, uploaded by the thread of the renderer void apply_pending(); @@ -284,6 +290,7 @@ struct vulkan14_texture : public texture_source std::vector m_pending; std::size_t m_pendingwidth{0}; std::size_t m_pendingheight{0}; + std::atomic m_passesalphatest{false}; }; } // namespace vk14 diff --git a/scene/scene.h b/scene/scene.h index 9b72f000..2d289c0f 100644 --- a/scene/scene.h +++ b/scene/scene.h @@ -305,6 +305,12 @@ public: linesnode_sequence m_lines; path_sequence m_paths; // path pieces instance_sequence m_instancesopaque; + // the instances of m_instancesopaque drawn each on its own (TAnimModel::m_instanceable false), in the same order: the walks + // go through these instead of reading every instance of the cell in each pass. made by the walks for the version of the + // instance records they go by and the count of the instances they were made from (gfx::scene_walk::Single_Instances()) + mutable instance_sequence m_instancessingle; + mutable unsigned int m_instancessingleversion { 0 }; + mutable std::size_t m_instancessinglesource { 0 }; instance_sequence m_instancetranslucent; // batched instance buckets keyed by shared TModel3d*; populated alongside // m_instancesopaque for nodes whose TAnimModel::m_instanceable == true. diff --git a/tests/unit/CMakeLists.txt b/tests/unit/CMakeLists.txt index e5097977..3db9e45d 100644 --- a/tests/unit/CMakeLists.txt +++ b/tests/unit/CMakeLists.txt @@ -29,6 +29,8 @@ add_executable(${UNIT_TESTS_NAME} "${CMAKE_SOURCE_DIR}/editor/editorProfile.cpp" # and the switches laid into the curves "${CMAKE_SOURCE_DIR}/editor/editorSwitchCurve.cpp" + # which textures the shadow maps draw without the alpha test + "${CMAKE_SOURCE_DIR}/rendering/common/texturealpha.cpp" ${UNIT_TEST_CASES}) # the core library brings the game's build settings along diff --git a/tests/unit/test_texture_alpha.cpp b/tests/unit/test_texture_alpha.cpp new file mode 100644 index 00000000..6c0a0f3d --- /dev/null +++ b/tests/unit/test_texture_alpha.cpp @@ -0,0 +1,101 @@ +#include "stdafx.h" +#include "doctest.h" +#include "rendering/common/texturealpha.h" + +#include +#include + +// which textures the shadow maps draw without the alpha test: every texel has to pass it, so a block which can decode to a +// translucent texel makes the whole texture keep the test +namespace +{ +using gfx::alpha_layout; +using gfx::texels_pass_alpha_test; + +bool passes(alpha_layout const Layout, std::vector const &Data) +{ + return texels_pass_alpha_test(Layout, Data); +} + +// a bc7 block of mode 6 (7 bit colour and alpha endpoints, a p-bit each) with the given alpha endpoints before the p-bits +std::vector bc7_mode6(unsigned const Alpha0, unsigned const Alpha1, unsigned const Pbits) +{ + std::vector block(16, 0); + auto const put = [&block](unsigned const Offset, unsigned const Count, unsigned const Value) { + for (unsigned bit = 0; bit < Count; ++bit) + { + if (((Value >> bit) & 1u) != 0) + { + block[(Offset + bit) >> 3] |= static_cast(1u << ((Offset + bit) & 7u)); + } + } + }; + put(6, 1, 1); + put(49, 7, Alpha0); + put(56, 7, Alpha1); + put(63, 2, Pbits); + return block; +} +} // namespace + +TEST_CASE("texture alpha: rgba texels pass when every alpha is from 136 up") +{ + CHECK(passes(alpha_layout::rgba8, {10, 20, 30, 255, 0, 0, 0, 136})); + CHECK_FALSE(passes(alpha_layout::rgba8, {10, 20, 30, 255, 0, 0, 0, 135})); + CHECK_FALSE(passes(alpha_layout::rgba8, {255, 255, 255, 0})); +} + +TEST_CASE("texture alpha: bc1 is opaque unless a three colour block uses the index 3") +{ + // the first colour above the second: four colours, the index 3 opaque + CHECK(passes(alpha_layout::bc1, {0x00, 0xf8, 0x1f, 0x00, 0xff, 0xff, 0xff, 0xff})); + // three colours, no index 3 + CHECK(passes(alpha_layout::bc1, {0x1f, 0x00, 0x00, 0xf8, 0x00, 0x55, 0xaa, 0x66})); + // three colours, one texel of the index 3: transparent black + CHECK_FALSE(passes(alpha_layout::bc1, {0x1f, 0x00, 0x00, 0xf8, 0x00, 0x00, 0x30, 0x00})); + // equal colours count as three + CHECK_FALSE(passes(alpha_layout::bc1, {0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00})); + // one translucent block among opaque ones + CHECK_FALSE(passes(alpha_layout::bc1, {0x00, 0xf8, 0x1f, 0x00, 0xff, 0xff, 0xff, 0xff, 0x1f, 0x00, 0x00, 0xf8, 0xc0, 0x00, 0x00, 0x00})); +} + +TEST_CASE("texture alpha: bc2 passes when every 4 bit alpha is from 8 up") +{ + std::vector block(16, 0); + std::fill_n(block.begin(), 8, std::uint8_t{0x88}); + CHECK(passes(alpha_layout::bc2, block)); + block[5] = 0x87; + CHECK_FALSE(passes(alpha_layout::bc2, block)); +} + +TEST_CASE("texture alpha: bc3 is checked by the palette entries its texels use") +{ + // both endpoints opaque + std::vector block{255, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; + CHECK(passes(alpha_layout::bc3, block)); + // a transparent second endpoint which no texel uses (all indices 0) + block[1] = 0; + CHECK(passes(alpha_layout::bc3, block)); + // one texel on the index 1, the transparent endpoint + block[2] = 0x01; + CHECK_FALSE(passes(alpha_layout::bc3, block)); + // six interpolated values: the index 6 is 0, the index 7 is 255 + std::vector six{200, 220, 0x07, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; + CHECK(passes(alpha_layout::bc3, six)); + six[2] = 0x06; + CHECK_FALSE(passes(alpha_layout::bc3, six)); +} + +TEST_CASE("texture alpha: bc7 is checked by its alpha endpoints") +{ + // mode 6, alpha endpoints 254 and 255 + CHECK(passes(alpha_layout::bc7, bc7_mode6(127, 127, 2))); + // one endpoint at 100 + CHECK_FALSE(passes(alpha_layout::bc7, bc7_mode6(50, 127, 3))); + // mode 1 holds no alpha + std::vector mode1(16, 0); + mode1[0] = 0x02; + CHECK(passes(alpha_layout::bc7, mode1)); + // the reserved mode decodes to transparent black + CHECK_FALSE(passes(alpha_layout::bc7, std::vector(16, 0))); +}