From 3ad3dd5cd9935e2397dbeadf86304543686c804d Mon Sep 17 00:00:00 2001 From: Eidolon Date: Sat, 15 Apr 2023 21:04:07 -0500 Subject: [PATCH] rhi: Extract patch atlas cache to own class --- src/hwr2/CMakeLists.txt | 2 + src/hwr2/pass_twodee.cpp | 325 +++------------------------------ src/hwr2/pass_twodee.hpp | 7 +- src/hwr2/patch_atlas.cpp | 379 +++++++++++++++++++++++++++++++++++++++ src/hwr2/patch_atlas.hpp | 145 +++++++++++++++ src/i_video_common.cpp | 4 + 6 files changed, 555 insertions(+), 307 deletions(-) create mode 100644 src/hwr2/patch_atlas.cpp create mode 100644 src/hwr2/patch_atlas.hpp diff --git a/src/hwr2/CMakeLists.txt b/src/hwr2/CMakeLists.txt index cef776505..8e525cad9 100644 --- a/src/hwr2/CMakeLists.txt +++ b/src/hwr2/CMakeLists.txt @@ -19,6 +19,8 @@ target_sources(SRB2SDL2 PRIVATE pass_twodee.hpp pass.cpp pass.hpp + patch_atlas.cpp + patch_atlas.hpp twodee.cpp twodee.hpp ) diff --git a/src/hwr2/pass_twodee.cpp b/src/hwr2/pass_twodee.cpp index 8b0f22b71..326caab0c 100644 --- a/src/hwr2/pass_twodee.cpp +++ b/src/hwr2/pass_twodee.cpp @@ -22,47 +22,10 @@ using namespace srb2; using namespace srb2::hwr2; using namespace srb2::rhi; -namespace -{ - -struct AtlasEntry -{ - uint32_t x; - uint32_t y; - uint32_t w; - uint32_t h; - - uint32_t trim_x; - uint32_t trim_y; - uint32_t orig_w; - uint32_t orig_h; -}; - -struct Atlas -{ - Atlas() = default; - Atlas(Atlas&&) = default; - - Handle tex; - uint32_t tex_width; - uint32_t tex_height; - std::unordered_map entries; - - std::unique_ptr rp_ctx {nullptr}; - std::unique_ptr rp_nodes {nullptr}; - - Atlas& operator=(Atlas&&) = default; -}; - -} // namespace - struct srb2::hwr2::TwodeePassData { Handle default_tex; Handle default_colormap_tex; - std::vector patch_atlases; - std::unordered_map patch_lookup; - std::vector patches_to_upload; std::unordered_map> colormaps; std::vector colormaps_to_upload; std::unordered_map> pipelines; @@ -83,202 +46,6 @@ TwodeePass::~TwodeePass() = default; static constexpr const uint32_t kVboInitSize = 32768; static constexpr const uint32_t kIboInitSize = 4096; -static Rect trimmed_patch_dim(const patch_t* patch); - -static void create_atlas(Rhi& rhi, TwodeePassData& pass_data) -{ - Atlas new_atlas; - new_atlas.tex = rhi.create_texture({ - TextureFormat::kLuminanceAlpha, - 2048, - 2048, - TextureWrapMode::kClamp, - TextureWrapMode::kClamp - }); - new_atlas.tex_width = 2048; - new_atlas.tex_height = 2048; - new_atlas.rp_ctx = std::make_unique(); - new_atlas.rp_nodes = std::make_unique(4096); - for (size_t i = 0; i < 4096; i++) - { - new_atlas.rp_nodes[i] = {}; - } - stbrp_init_target(new_atlas.rp_ctx.get(), 2048, 2048, new_atlas.rp_nodes.get(), 4096); - // it is CRITICALLY important that the atlas is MOVED, not COPIED, otherwise the node ptrs will be broken - pass_data.patch_atlases.push_back(std::move(new_atlas)); -} - -static void pack_patches(Rhi& rhi, TwodeePassData& pass_data, tcb::span patches) -{ - // Prepare stbrp rects for patches to be loaded. - std::vector rects; - for (size_t i = 0; i < patches.size(); i++) - { - const patch_t* patch = patches[i]; - Rect trimmed_rect = trimmed_patch_dim(patch); - stbrp_rect rect {}; - rect.id = i; - rect.w = trimmed_rect.w; - rect.h = trimmed_rect.h; - rects.push_back(std::move(rect)); - } - - while (rects.size() > 0) - { - if (pass_data.patch_atlases.size() == 0) - { - create_atlas(rhi, pass_data); - } - - for (size_t atlas_index = 0; atlas_index < pass_data.patch_atlases.size(); atlas_index++) - { - auto& atlas = pass_data.patch_atlases[atlas_index]; - - stbrp_pack_rects(atlas.rp_ctx.get(), rects.data(), rects.size()); - for (auto itr = rects.begin(); itr != rects.end();) - { - auto& rect = *itr; - if (rect.was_packed) - { - AtlasEntry entry; - const patch_t* patch = patches[rect.id]; - // TODO prevent unnecessary recalculation of trim? - Rect trimmed_rect = trimmed_patch_dim(patch); - entry.x = static_cast(rect.x); - entry.y = static_cast(rect.y); - entry.w = static_cast(rect.w); - entry.h = static_cast(rect.h); - entry.trim_x = static_cast(trimmed_rect.x); - entry.trim_y = static_cast(trimmed_rect.y); - entry.orig_w = static_cast(patch->width); - entry.orig_h = static_cast(patch->height); - atlas.entries.insert_or_assign(patch, std::move(entry)); - pass_data.patch_lookup.insert_or_assign(patch, atlas_index); - pass_data.patches_to_upload.push_back(patch); - rects.erase(itr); - continue; - } - ++itr; - } - - // If we still have rects to pack, and we're at the last atlas, create another atlas. - // TODO This could end up in an infinite loop if the patches are bigger than an atlas. Such patches need to - // be loaded as individual RHI textures instead. - if (atlas_index == pass_data.patch_atlases.size() - 1 && rects.size() > 0) - { - create_atlas(rhi, pass_data); - } - } - } -} - -/// @brief Derive the subrect of the given patch with empty columns and rows excluded. -static Rect trimmed_patch_dim(const patch_t* patch) -{ - bool minx_found = false; - int32_t minx = 0; - int32_t maxx = 0; - int32_t miny = patch->height; - int32_t maxy = 0; - for (int32_t x = 0; x < patch->width; x++) - { - const int32_t columnofs = patch->columnofs[x]; - const column_t* column = reinterpret_cast(patch->columns + columnofs); - - // If the first pole is empty (topdelta = 255), there are no pixels in this column - if (!minx_found && column->topdelta == 0xFF) - { - // Thus, the minx is at least one higher than the current column. - minx = x + 1; - continue; - } - minx_found = true; - - if (minx_found && column->topdelta != 0xFF) - { - maxx = x; - } - - miny = std::min(static_cast(column->topdelta), miny); - - int32_t prevdelta = 0; - int32_t topdelta = 0; - while (column->topdelta != 0xFF) - { - topdelta = column->topdelta; - - // Tall patches hack - if (topdelta <= prevdelta) - { - topdelta += prevdelta; - } - prevdelta = topdelta; - - maxy = std::max(topdelta + column->length, maxy); - - column = reinterpret_cast(reinterpret_cast(column) + column->length + 4); - } - } - - maxx += 1; - maxx = std::max(minx, maxx); - maxy = std::max(miny, maxy); - - return {minx, miny, static_cast(maxx - minx), static_cast(maxy - miny)}; -} - -static void convert_patch_to_trimmed_rg8_pixels(const patch_t* patch, std::vector& out) -{ - Rect trimmed_rect = trimmed_patch_dim(patch); - if (trimmed_rect.w % 2 > 0) - { - // In order to force 4-byte row alignment, an extra column is added to the image data. - // Look up GL_UNPACK_ALIGNMENT (which defaults to 4 bytes) - trimmed_rect.w += 1; - } - out.clear(); - // 2 bytes per pixel; 1 for the color index, 1 for the alpha. (RG8) - out.resize(trimmed_rect.w * trimmed_rect.h * 2, 0); - for (int32_t x = 0; x < static_cast(trimmed_rect.w) && x < (patch->width - trimmed_rect.x); x++) - { - const int32_t columnofs = patch->columnofs[x + trimmed_rect.x]; - const column_t* column = reinterpret_cast(patch->columns + columnofs); - - int32_t prevdelta = 0; - int32_t topdelta = 0; - while (column->topdelta != 0xFF) - { - topdelta = column->topdelta; - // prevdelta is used to implement tall patches hack - if (topdelta <= prevdelta) - { - topdelta += prevdelta; - } - - prevdelta = topdelta; - const uint8_t* source = reinterpret_cast(column) + 3; - int32_t count = column->length; // is this byte order safe...? - - for (int32_t i = 0; i < count; i++) - { - int32_t output_y = topdelta + i - trimmed_rect.y; - if (output_y < 0) - { - continue; - } - if (output_y >= static_cast(trimmed_rect.h)) - { - break; - } - size_t pixel_index = (output_y * trimmed_rect.w + x) * 2; - out[pixel_index + 0] = source[i]; // index in luminance/red channel - out[pixel_index + 1] = 0xFF; // alpha/green value of 1 - } - column = reinterpret_cast(reinterpret_cast(column) + column->length + 4); - } - } -} - static TwodeePipelineKey pipeline_key_for_cmd(const Draw2dCmd& cmd) { return {hwr2::get_blend_mode(cmd), hwr2::is_draw_lines(cmd)}; @@ -358,24 +125,26 @@ static PipelineDesc make_pipeline_desc(TwodeePipelineKey key) {0.f, 0.f, 0.f, 1.f}}; } -static void rewrite_patch_quad_vertices(Draw2dList& list, const Draw2dPatchQuad& cmd, TwodeePassData* data) +void TwodeePass::rewrite_patch_quad_vertices(Draw2dList& list, const Draw2dPatchQuad& cmd) const { // Patch quads are clipped according to the patch's atlas entry - if (cmd.patch == nullptr) + const patch_t* patch = cmd.patch; + if (patch == nullptr) { return; } - std::size_t atlas_index = data->patch_lookup[cmd.patch]; - auto& atlas = data->patch_atlases[atlas_index]; - auto& entry = atlas.entries[cmd.patch]; + srb2::NotNull atlas = patch_atlas_cache_->find_patch(patch); + std::optional entry_optional = atlas->find_patch(patch); + SRB2_ASSERT(entry_optional.has_value()); + PatchAtlas::Entry entry = *entry_optional; // Rewrite the vertex data completely. // The UVs of the trimmed patch in atlas UV space. - const float atlas_umin = static_cast(entry.x) / atlas.tex_width; - const float atlas_umax = static_cast(entry.x + entry.w) / atlas.tex_width; - const float atlas_vmin = static_cast(entry.y) / atlas.tex_height; - const float atlas_vmax = static_cast(entry.y + entry.h) / atlas.tex_height; + const float atlas_umin = static_cast(entry.x) / atlas->texture_size(); + const float atlas_umax = static_cast(entry.x + entry.w) / atlas->texture_size(); + const float atlas_vmin = static_cast(entry.y) / atlas->texture_size(); + const float atlas_vmax = static_cast(entry.y + entry.h) / atlas->texture_size(); // The UVs of the trimmed patch in untrimmed UV space. // The command's UVs are in untrimmed UV space. @@ -542,27 +311,6 @@ void TwodeePass::prepass(Rhi& rhi) ); } - // Check for patches that are being freed after this frame. Those patches must be present in the atlases for this - // frame, but all atlases need to be cleared and rebuilt on next call to prepass. - // This is based on the assumption that patches are very rarely freed during runtime; occasionally repacking the - // atlases to free up space from patches that will never be referenced again is acceptable. - if (rebuild_atlases_) - { - for (auto& atlas : data_->patch_atlases) - { - rhi.destroy_texture(atlas.tex); - } - data_->patch_atlases.clear(); - data_->patch_lookup.clear(); - rebuild_atlases_ = false; - } - - if (data_->patch_atlases.size() > 2) - { - // Rebuild the atlases next frame because we have too many patches in the atlas cache. - rebuild_atlases_ = true; - } - // Stage 1 - command list patch detection std::unordered_set found_patches; std::unordered_set found_colormaps; @@ -587,19 +335,11 @@ void TwodeePass::prepass(Rhi& rhi) } } - std::unordered_set patch_cache_hits; - std::unordered_set patch_cache_misses; for (auto patch : found_patches) { - if (data_->patch_lookup.find(patch) != data_->patch_lookup.end()) - { - patch_cache_hits.insert(patch); - } - else - { - patch_cache_misses.insert(patch); - } + patch_atlas_cache_->queue_patch(patch); } + patch_atlas_cache_->pack(rhi); for (auto colormap : found_colormaps) { @@ -612,11 +352,6 @@ void TwodeePass::prepass(Rhi& rhi) data_->colormaps_to_upload.push_back(colormap); } - // Stage 2 - pack rects into atlases - std::vector patches_to_pack(patch_cache_misses.begin(), patch_cache_misses.end()); - pack_patches(rhi, *data_, patches_to_pack); - // We now know what patches need to be uploaded. - size_t list_index = 0; for (auto& list : *ctx_) { @@ -695,7 +430,6 @@ void TwodeePass::prepass(Rhi& rhi) // We need to split the merged commands based on the kind of texture // Patches are converted to atlas texture indexes, which we've just packed the patch rects for // Flats are uploaded as individual textures. - // TODO actually implement flat drawing auto tex_visitor = srb2::Overload { [&](const Draw2dPatchQuad& cmd) { @@ -705,8 +439,8 @@ void TwodeePass::prepass(Rhi& rhi) } else { - size_t atlas_index = data_->patch_lookup[cmd.patch]; - typeof(merged_cmd.texture) atlas_index_texture = atlas_index; + srb2::NotNull atlas = patch_atlas_cache_->find_patch(cmd.patch); + typeof(merged_cmd.texture) atlas_index_texture = atlas->texture(); new_cmd_needed = new_cmd_needed || (merged_cmd.texture != atlas_index_texture); } @@ -739,7 +473,8 @@ void TwodeePass::prepass(Rhi& rhi) { if (cmd.patch != nullptr) { - the_new_one.texture = data_->patch_lookup[cmd.patch]; + srb2::NotNull atlas = patch_atlas_cache_->find_patch(cmd.patch); + the_new_one.texture = atlas->texture(); } else { @@ -776,7 +511,7 @@ void TwodeePass::prepass(Rhi& rhi) // Perform coordinate transformations { auto vtx_transform_visitor = srb2::Overload { - [&](const Draw2dPatchQuad& cmd) { rewrite_patch_quad_vertices(list, cmd, data_.get()); }, + [&](const Draw2dPatchQuad& cmd) { rewrite_patch_quad_vertices(list, cmd); }, [&](const Draw2dVertices& cmd) {}}; std::visit(vtx_transform_visitor, cmd); } @@ -828,25 +563,6 @@ void TwodeePass::transfer(Rhi& rhi, Handle ctx) } data_->colormaps_to_upload.clear(); - // Convert patches to RG8 textures and upload to atlas pages - std::vector patch_data; - for (const patch_t* patch_to_upload : data_->patches_to_upload) - { - Atlas& atlas = data_->patch_atlases[data_->patch_lookup[patch_to_upload]]; - AtlasEntry& entry = atlas.entries[patch_to_upload]; - - convert_patch_to_trimmed_rg8_pixels(patch_to_upload, patch_data); - - rhi.update_texture( - ctx, - atlas.tex, - {static_cast(entry.x), static_cast(entry.y), entry.w, entry.h}, - PixelFormat::kRG8, - tcb::as_bytes(tcb::span(patch_data)) - ); - } - data_->patches_to_upload.clear(); - Handle palette_tex = palette_manager_->palette(); // Update the buffers for each list @@ -867,10 +583,9 @@ void TwodeePass::transfer(Rhi& rhi, Handle ctx) { TextureBinding tx[3]; auto tex_visitor = srb2::Overload { - [&](size_t atlas_index) + [&](Handle texture) { - Atlas& atlas = data_->patch_atlases[atlas_index]; - tx[0] = {SamplerName::kSampler0, atlas.tex}; + tx[0] = {SamplerName::kSampler0, texture}; tx[1] = {SamplerName::kSampler1, palette_tex}; }, [&](const MergedTwodeeCommandFlatTexture& tex) diff --git a/src/hwr2/pass_twodee.hpp b/src/hwr2/pass_twodee.hpp index 4edb495c2..15a0924a4 100644 --- a/src/hwr2/pass_twodee.hpp +++ b/src/hwr2/pass_twodee.hpp @@ -18,6 +18,7 @@ #include #include "../cxxutil.hpp" +#include "patch_atlas.hpp" #include "pass.hpp" #include "pass_resource_managers.hpp" #include "twodee.hpp" @@ -52,7 +53,7 @@ struct MergedTwodeeCommand { TwodeePipelineKey pipeline_key = {}; rhi::Handle binding_set = {}; - std::optional> texture; + std::optional, MergedTwodeeCommandFlatTexture>> texture; const uint8_t* colormap; uint32_t index_offset = 0; uint32_t elements = 0; @@ -78,17 +79,19 @@ struct TwodeePass final : public Pass std::shared_ptr data_; std::shared_ptr palette_manager_; std::shared_ptr flat_manager_; + std::shared_ptr patch_atlas_cache_; rhi::Handle us_1; rhi::Handle us_2; std::vector cmd_lists_; std::vector, std::size_t>> vbos_; std::vector, std::size_t>> ibos_; - bool rebuild_atlases_ = false; rhi::Handle render_pass_; rhi::Handle output_; uint32_t output_width_ = 0; uint32_t output_height_ = 0; + void rewrite_patch_quad_vertices(Draw2dList& list, const Draw2dPatchQuad& cmd) const; + TwodeePass(); virtual ~TwodeePass(); diff --git a/src/hwr2/patch_atlas.cpp b/src/hwr2/patch_atlas.cpp new file mode 100644 index 000000000..90db30483 --- /dev/null +++ b/src/hwr2/patch_atlas.cpp @@ -0,0 +1,379 @@ +// SONIC ROBO BLAST 2 +//----------------------------------------------------------------------------- +// Copyright (C) 2023 by Ronald "Eidolon" Kinard +// +// This program is free software distributed under the +// terms of the GNU General Public License, version 2. +// See the 'LICENSE' file for more details. +//----------------------------------------------------------------------------- + +#include "patch_atlas.hpp" + +#include + +using namespace srb2; +using namespace srb2::hwr2; +using namespace srb2::rhi; + +rhi::Rect srb2::hwr2::trimmed_patch_dimensions(const patch_t* patch) +{ + bool minx_found = false; + int32_t minx = 0; + int32_t maxx = 0; + int32_t miny = patch->height; + int32_t maxy = 0; + for (int32_t x = 0; x < patch->width; x++) + { + const int32_t columnofs = patch->columnofs[x]; + const column_t* column = reinterpret_cast(patch->columns + columnofs); + + // If the first pole is empty (topdelta = 255), there are no pixels in this column + if (!minx_found && column->topdelta == 0xFF) + { + // Thus, the minx is at least one higher than the current column. + minx = x + 1; + continue; + } + minx_found = true; + + if (minx_found && column->topdelta != 0xFF) + { + maxx = x; + } + + miny = std::min(static_cast(column->topdelta), miny); + + int32_t prevdelta = 0; + int32_t topdelta = 0; + while (column->topdelta != 0xFF) + { + topdelta = column->topdelta; + + // Tall patches hack + if (topdelta <= prevdelta) + { + topdelta += prevdelta; + } + prevdelta = topdelta; + + maxy = std::max(topdelta + column->length, maxy); + + column = reinterpret_cast(reinterpret_cast(column) + column->length + 4); + } + } + + maxx += 1; + maxx = std::max(minx, maxx); + maxy = std::max(miny, maxy); + + return {minx, miny, static_cast(maxx - minx), static_cast(maxy - miny)}; +} + +void srb2::hwr2::convert_patch_to_trimmed_rg8_pixels(const patch_t* patch, std::vector& out) +{ + Rect trimmed_rect = srb2::hwr2::trimmed_patch_dimensions(patch); + if (trimmed_rect.w % 2 > 0) + { + // In order to force 4-byte row alignment, an extra column is added to the image data. + // Look up GL_UNPACK_ALIGNMENT (which defaults to 4 bytes) + trimmed_rect.w += 1; + } + out.clear(); + // 2 bytes per pixel; 1 for the color index, 1 for the alpha. (RG8) + out.resize(trimmed_rect.w * trimmed_rect.h * 2, 0); + for (int32_t x = 0; x < static_cast(trimmed_rect.w) && x < (patch->width - trimmed_rect.x); x++) + { + const int32_t columnofs = patch->columnofs[x + trimmed_rect.x]; + const column_t* column = reinterpret_cast(patch->columns + columnofs); + + int32_t prevdelta = 0; + int32_t topdelta = 0; + while (column->topdelta != 0xFF) + { + topdelta = column->topdelta; + // prevdelta is used to implement tall patches hack + if (topdelta <= prevdelta) + { + topdelta += prevdelta; + } + + prevdelta = topdelta; + const uint8_t* source = reinterpret_cast(column) + 3; + int32_t count = column->length; // is this byte order safe...? + + for (int32_t i = 0; i < count; i++) + { + int32_t output_y = topdelta + i - trimmed_rect.y; + if (output_y < 0) + { + continue; + } + if (output_y >= static_cast(trimmed_rect.h)) + { + break; + } + size_t pixel_index = (output_y * trimmed_rect.w + x) * 2; + out[pixel_index + 0] = source[i]; // index in luminance/red channel + out[pixel_index + 1] = 0xFF; // alpha/green value of 1 + } + column = reinterpret_cast(reinterpret_cast(column) + column->length + 4); + } + } +} + +PatchAtlas::PatchAtlas(Handle texture, uint32_t size) : tex_(texture), size_(size) +{ + rp_ctx = std::make_unique(); + rp_nodes = std::make_unique(size * 2); + const size_t double_size = size * 2; + for (size_t i = 0; i < double_size; i++) + { + rp_nodes[i] = {}; + } + stbrp_init_target(rp_ctx.get(), size, size, rp_nodes.get(), double_size); +} + +PatchAtlas::PatchAtlas(PatchAtlas&&) = default; +PatchAtlas& PatchAtlas::operator=(PatchAtlas&&) = default; + +void PatchAtlas::pack_rects(tcb::span rects) +{ + stbrp_pack_rects(rp_ctx.get(), rects.data(), rects.size()); +} + +std::optional PatchAtlas::find_patch(srb2::NotNull patch) const +{ + auto itr = entries_.find(patch); + if (itr == entries_.end()) + { + return std::nullopt; + } + + return itr->second; +} + +PatchAtlasCache::PatchAtlasCache(uint32_t tex_size, size_t max_textures) + : tex_size_(tex_size) + , max_textures_(max_textures) +{ +} + +PatchAtlasCache::PatchAtlasCache(PatchAtlasCache&&) = default; +PatchAtlasCache& PatchAtlasCache::operator=(PatchAtlasCache&&) = default; +PatchAtlasCache::~PatchAtlasCache() = default; + +bool PatchAtlasCache::need_to_reset() const +{ + if (atlases_.size() > max_textures_) + { + return true; + } + return false; +} + +void PatchAtlasCache::reset(Rhi& rhi) +{ + for (auto& atlas : atlases_) + { + rhi.destroy_texture(atlas.texture()); + } + + atlases_.clear(); + patch_lookup_.clear(); +} + +bool PatchAtlasCache::ready_for_lookup() const +{ + if (!patches_to_pack_.empty()) + { + return false; + } + + return true; +} + +static PatchAtlas create_atlas(Rhi& rhi, uint32_t size) +{ + Handle texture = rhi.create_texture( + { + TextureFormat::kLuminanceAlpha, + size, + size, + TextureWrapMode::kClamp, + TextureWrapMode::kClamp + } + ); + + PatchAtlas new_atlas(texture, size); + + return new_atlas; +} + +void PatchAtlasCache::pack(Rhi& rhi) +{ + // Prepare stbrp rects for patches to be loaded. + std::vector rects; + + std::vector large_patches; + + std::vector patches; + for (auto patch : patches_to_pack_) + { + patches.push_back(patch); + } + + for (size_t i = 0; i < patches.size(); i++) + { + const patch_t* patch = patches[i]; + Rect trimmed_rect = trimmed_patch_dimensions(patch); + + if (rect_is_large(trimmed_rect.w, trimmed_rect.h)) + { + large_patches.push_back(patch); + continue; + } + + stbrp_rect rect {}; + + rect.id = i; + rect.w = trimmed_rect.w; + rect.h = trimmed_rect.h; + rects.push_back(std::move(rect)); + } + + while (rects.size() > 0) + { + if (atlases_.size() == 0) + { + atlases_.push_back(create_atlas(rhi, tex_size_)); + } + + for (size_t atlas_index = 0; atlas_index < atlases_.size(); atlas_index++) + { + auto& atlas = atlases_[atlas_index]; + atlas.pack_rects(rects); + for (auto itr = rects.begin(); itr != rects.end();) + { + auto& rect = *itr; + if (rect.was_packed) + { + PatchAtlas::Entry entry; + const patch_t* patch = patches[rect.id]; + Rect trimmed_rect = trimmed_patch_dimensions(patch); + entry.x = static_cast(rect.x); + entry.y = static_cast(rect.y); + entry.w = static_cast(rect.w); + entry.h = static_cast(rect.h); + entry.trim_x = static_cast(trimmed_rect.x); + entry.trim_y = static_cast(trimmed_rect.y); + entry.orig_w = static_cast(patch->width); + entry.orig_h = static_cast(patch->height); + atlas.entries_.insert_or_assign(patch, std::move(entry)); + patch_lookup_.insert_or_assign(patch, atlas_index); + patches_to_upload_.insert(patch); + rects.erase(itr); + continue; + } + ++itr; + } + + // If we still have rects to pack, and we're at the last atlas, create another atlas. + if (atlas_index == atlases_.size() - 1 && rects.size() > 0) + { + atlases_.push_back(create_atlas(rhi, tex_size_)); + } + } + } + + // TODO Create large patch "atlases" + + patches_to_pack_.clear(); +} + +PatchAtlas* PatchAtlasCache::find_patch(srb2::NotNull patch) +{ + SRB2_ASSERT(ready_for_lookup()); + + auto itr = patch_lookup_.find(patch); + if (itr == patch_lookup_.end()) + { + return nullptr; + } + + size_t atlas_index = itr->second; + + SRB2_ASSERT(atlas_index < atlases_.size()); + + return &atlases_[atlas_index]; +} + +const PatchAtlas* PatchAtlasCache::find_patch(srb2::NotNull patch) const +{ + SRB2_ASSERT(ready_for_lookup()); + + auto itr = patch_lookup_.find(patch); + if (itr == patch_lookup_.end()) + { + return nullptr; + } + + size_t atlas_index = itr->second; + + SRB2_ASSERT(atlas_index < atlases_.size()); + + return &atlases_[atlas_index]; +} + +void PatchAtlasCache::queue_patch(srb2::NotNull patch) +{ + if (patch_lookup_.find(patch) != patch_lookup_.end()) + { + return; + } + + patches_to_pack_.insert(patch); +} + +void PatchAtlasCache::prepass(Rhi& rhi) +{ + if (need_to_reset()) + { + reset(rhi); + } +} + +void PatchAtlasCache::transfer(Rhi& rhi, Handle ctx) +{ + SRB2_ASSERT(ready_for_lookup()); + + // Upload atlased patches + std::vector patch_data; + for (const patch_t* patch_to_upload : patches_to_upload_) + { + srb2::NotNull atlas = find_patch(patch_to_upload); + + std::optional entry = atlas->find_patch(patch_to_upload); + SRB2_ASSERT(entry.has_value()); + + convert_patch_to_trimmed_rg8_pixels(patch_to_upload, patch_data); + + rhi.update_texture( + ctx, + atlas->tex_, + {static_cast(entry->x), static_cast(entry->y), entry->w, entry->h}, + PixelFormat::kRG8, + tcb::as_bytes(tcb::span(patch_data)) + ); + + patch_data.clear(); + } + patches_to_upload_.clear(); +} + +void PatchAtlasCache::graphics(Rhi& rhi, Handle ctx) +{ +} + +void PatchAtlasCache::postpass(Rhi& rhi) +{ +} diff --git a/src/hwr2/patch_atlas.hpp b/src/hwr2/patch_atlas.hpp new file mode 100644 index 000000000..9707074b2 --- /dev/null +++ b/src/hwr2/patch_atlas.hpp @@ -0,0 +1,145 @@ +// SONIC ROBO BLAST 2 +//----------------------------------------------------------------------------- +// Copyright (C) 2023 by Ronald "Eidolon" Kinard +// +// This program is free software distributed under the +// terms of the GNU General Public License, version 2. +// See the 'LICENSE' file for more details. +//----------------------------------------------------------------------------- + +#ifndef __SRB2_HWR2_PATCH_ATLAS_HPP__ +#define __SRB2_HWR2_PATCH_ATLAS_HPP__ + +#include +#include +#include +#include +#include +#include + +#include + +#include "pass.hpp" +#include "../r_defs.h" + +extern "C" +{ +// Forward declare the stb_rect_pack types since they are only pointed to + +struct stbrp_context; +struct stbrp_node; +struct stbrp_rect; +}; + +namespace srb2::hwr2 +{ + +class PatchAtlas +{ +public: + struct Entry + { + uint32_t x; + uint32_t y; + uint32_t w; + uint32_t h; + + uint32_t trim_x; + uint32_t trim_y; + uint32_t orig_w; + uint32_t orig_h; + }; + +private: + rhi::Handle tex_; + uint32_t size_; + + std::unordered_map entries_; + + std::unique_ptr rp_ctx {nullptr}; + std::unique_ptr rp_nodes {nullptr}; + + friend class PatchAtlasCache; + +public: + PatchAtlas(rhi::Handle tex, uint32_t size); + PatchAtlas(const PatchAtlas&) = delete; + PatchAtlas& operator=(const PatchAtlas&) = delete; + PatchAtlas(PatchAtlas&&); + PatchAtlas& operator=(PatchAtlas&&); + + /// @brief Get the Luminance-Alpha RHI texture handle for this atlas texture + rhi::Handle texture() const noexcept { return tex_; } + + uint32_t texture_size() const noexcept { return size_; } + + std::optional find_patch(srb2::NotNull patch) const; + + void pack_rects(tcb::span rects); +}; + +/// @brief A resource-managing pass which creates and manages a set of Atlas Textures with +/// optimally packed Patches, allowing drawing passes to reuse the same texture binds for +/// drawing things like sprites and 2D elements. +class PatchAtlasCache : public Pass +{ + std::vector atlases_; + std::unordered_map patch_lookup_; + + std::unordered_set patches_to_pack_; + std::unordered_set patches_to_upload_; + + uint32_t tex_size_ = 2048; + size_t max_textures_ = 2; + + bool need_to_reset() const; + + /// @brief Clear the atlases and reset for lookup. + void reset(rhi::Rhi& rhi); + bool ready_for_lookup() const; + + /// @brief Decide if a rect's dimensions are Large, that is, the rect should not be packed and instead its patch + /// should be uploaded in isolation. + bool rect_is_large(uint32_t w, uint32_t h) const noexcept { return false; } + +public: + PatchAtlasCache(uint32_t tex_size, size_t max_textures); + + PatchAtlasCache(const PatchAtlasCache&) = delete; + PatchAtlasCache(PatchAtlasCache&&); + PatchAtlasCache& operator=(const PatchAtlasCache&) = delete; + PatchAtlasCache& operator=(PatchAtlasCache&&); + virtual ~PatchAtlasCache(); + + /// @brief Queue a patch to be packed. All patches will be packed after the prepass phase, + /// or the owner can explicitly request a pack. + void queue_patch(srb2::NotNull patch); + + /// @brief Pack queued patches, allowing them to be looked up with find_patch. + void pack(rhi::Rhi& rhi); + + /// @brief Find the atlas a patch belongs to, or nullopt if it is not cached. + /// This may not be called if there are still patches that need to be packed. + /// The return value of this function may change between invocations of prepass for any given input. + const PatchAtlas* find_patch(srb2::NotNull patch) const; + PatchAtlas* find_patch(srb2::NotNull patch); + + virtual void prepass(rhi::Rhi& rhi) override; + virtual void transfer(rhi::Rhi& rhi, rhi::Handle ctx) override; + virtual void graphics(rhi::Rhi& rhi, rhi::Handle ctx) override; + virtual void postpass(rhi::Rhi& rhi) override; +}; + +/// @brief Calculate the subregion of the patch which excludes empty space on the borders. +rhi::Rect trimmed_patch_dimensions(const patch_t* patch); + +/// @brief Convert a patch to RG8 pixel data. If the patch's trimmed width is not a multiple of 2, +/// an additional blank column will be emitted to the output; this pixel data is ignored by RHI +/// during upload, but required for the RHI device's Unpack Alignment of 4 bytes. +/// @param patch the patch to convert +/// @param out the output vector, cleared before writing. +void convert_patch_to_trimmed_rg8_pixels(const patch_t* patch, std::vector& out); + +} // namespace srb2::hwr2 + +#endif // __SRB2_HWR2_PATCH_ATLAS_HPP__ diff --git a/src/i_video_common.cpp b/src/i_video_common.cpp index 80379ad41..e41725a16 100644 --- a/src/i_video_common.cpp +++ b/src/i_video_common.cpp @@ -17,6 +17,7 @@ #include "cxxutil.hpp" #include "f_finale.h" +#include "hwr2/patch_atlas.hpp" #include "hwr2/pass_blit_postimg_screens.hpp" #include "hwr2/pass_blit_rect.hpp" #include "hwr2/pass_imgui.hpp" @@ -192,12 +193,14 @@ static InternalPassData build_pass_manager() auto palette_manager = std::make_shared(); auto common_resources_manager = std::make_shared(); auto flat_texture_manager = std::make_shared(); + auto patch_atlas_cache = std::make_shared(2048, 2); auto resource_manager = std::make_shared(); resource_manager->insert("framebuffer_manager", framebuffer_manager); resource_manager->insert("palette_manager", palette_manager); resource_manager->insert("common_resources_manager", common_resources_manager); resource_manager->insert("flat_texture_manager", flat_texture_manager); + resource_manager->insert("patch_atlas_cache", patch_atlas_cache); // Basic Rendering is responsible for drawing 3d, 2d, and postprocessing the image. // This is drawn to an alternating internal color buffer. @@ -209,6 +212,7 @@ static InternalPassData build_pass_manager() auto blit_postimg_screens = std::make_shared(palette_manager); auto twodee = std::make_shared(); twodee->flat_manager_ = flat_texture_manager; + twodee->patch_atlas_cache_ = patch_atlas_cache; twodee->data_ = make_twodee_pass_data(); twodee->ctx_ = &g_2d; auto pp_simple_blit_pass = std::make_shared(false);