mirror of
https://github.com/KartKrewDev/RingRacers.git
synced 2026-01-03 13:42:31 +00:00
rhi: Extract patch atlas cache to own class
This commit is contained in:
parent
5f78620bd9
commit
3ad3dd5cd9
6 changed files with 555 additions and 307 deletions
|
|
@ -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
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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<Texture> tex;
|
||||
uint32_t tex_width;
|
||||
uint32_t tex_height;
|
||||
std::unordered_map<const patch_t*, AtlasEntry> entries;
|
||||
|
||||
std::unique_ptr<stbrp_context> rp_ctx {nullptr};
|
||||
std::unique_ptr<stbrp_node[]> rp_nodes {nullptr};
|
||||
|
||||
Atlas& operator=(Atlas&&) = default;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
struct srb2::hwr2::TwodeePassData
|
||||
{
|
||||
Handle<Texture> default_tex;
|
||||
Handle<Texture> default_colormap_tex;
|
||||
std::vector<Atlas> patch_atlases;
|
||||
std::unordered_map<const patch_t*, size_t> patch_lookup;
|
||||
std::vector<const patch_t*> patches_to_upload;
|
||||
std::unordered_map<const uint8_t*, Handle<Texture>> colormaps;
|
||||
std::vector<const uint8_t*> colormaps_to_upload;
|
||||
std::unordered_map<TwodeePipelineKey, Handle<Pipeline>> 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<stbrp_context>();
|
||||
new_atlas.rp_nodes = std::make_unique<stbrp_node[]>(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<const patch_t*> patches)
|
||||
{
|
||||
// Prepare stbrp rects for patches to be loaded.
|
||||
std::vector<stbrp_rect> 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<uint32_t>(rect.x);
|
||||
entry.y = static_cast<uint32_t>(rect.y);
|
||||
entry.w = static_cast<uint32_t>(rect.w);
|
||||
entry.h = static_cast<uint32_t>(rect.h);
|
||||
entry.trim_x = static_cast<uint32_t>(trimmed_rect.x);
|
||||
entry.trim_y = static_cast<uint32_t>(trimmed_rect.y);
|
||||
entry.orig_w = static_cast<uint32_t>(patch->width);
|
||||
entry.orig_h = static_cast<uint32_t>(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<const column_t*>(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<int32_t>(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<const column_t*>(reinterpret_cast<const uint8_t*>(column) + column->length + 4);
|
||||
}
|
||||
}
|
||||
|
||||
maxx += 1;
|
||||
maxx = std::max(minx, maxx);
|
||||
maxy = std::max(miny, maxy);
|
||||
|
||||
return {minx, miny, static_cast<uint32_t>(maxx - minx), static_cast<uint32_t>(maxy - miny)};
|
||||
}
|
||||
|
||||
static void convert_patch_to_trimmed_rg8_pixels(const patch_t* patch, std::vector<uint8_t>& 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<int32_t>(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<const column_t*>(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<const uint8_t*>(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<int32_t>(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<const column_t*>(reinterpret_cast<const uint8_t*>(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<const PatchAtlas*> atlas = patch_atlas_cache_->find_patch(patch);
|
||||
std::optional<PatchAtlas::Entry> 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<float>(entry.x) / atlas.tex_width;
|
||||
const float atlas_umax = static_cast<float>(entry.x + entry.w) / atlas.tex_width;
|
||||
const float atlas_vmin = static_cast<float>(entry.y) / atlas.tex_height;
|
||||
const float atlas_vmax = static_cast<float>(entry.y + entry.h) / atlas.tex_height;
|
||||
const float atlas_umin = static_cast<float>(entry.x) / atlas->texture_size();
|
||||
const float atlas_umax = static_cast<float>(entry.x + entry.w) / atlas->texture_size();
|
||||
const float atlas_vmin = static_cast<float>(entry.y) / atlas->texture_size();
|
||||
const float atlas_vmax = static_cast<float>(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<const patch_t*> found_patches;
|
||||
std::unordered_set<const uint8_t*> found_colormaps;
|
||||
|
|
@ -587,19 +335,11 @@ void TwodeePass::prepass(Rhi& rhi)
|
|||
}
|
||||
}
|
||||
|
||||
std::unordered_set<const patch_t*> patch_cache_hits;
|
||||
std::unordered_set<const patch_t*> 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<const patch_t*> 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<const PatchAtlas*> 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<const PatchAtlas*> 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<TransferContext> ctx)
|
|||
}
|
||||
data_->colormaps_to_upload.clear();
|
||||
|
||||
// Convert patches to RG8 textures and upload to atlas pages
|
||||
std::vector<uint8_t> 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<int32_t>(entry.x), static_cast<int32_t>(entry.y), entry.w, entry.h},
|
||||
PixelFormat::kRG8,
|
||||
tcb::as_bytes(tcb::span(patch_data))
|
||||
);
|
||||
}
|
||||
data_->patches_to_upload.clear();
|
||||
|
||||
Handle<Texture> palette_tex = palette_manager_->palette();
|
||||
|
||||
// Update the buffers for each list
|
||||
|
|
@ -867,10 +583,9 @@ void TwodeePass::transfer(Rhi& rhi, Handle<TransferContext> ctx)
|
|||
{
|
||||
TextureBinding tx[3];
|
||||
auto tex_visitor = srb2::Overload {
|
||||
[&](size_t atlas_index)
|
||||
[&](Handle<Texture> 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)
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@
|
|||
#include <vector>
|
||||
|
||||
#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<rhi::BindingSet> binding_set = {};
|
||||
std::optional<std::variant<size_t, MergedTwodeeCommandFlatTexture>> texture;
|
||||
std::optional<std::variant<rhi::Handle<rhi::Texture>, 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<TwodeePassData> data_;
|
||||
std::shared_ptr<MainPaletteManager> palette_manager_;
|
||||
std::shared_ptr<FlatTextureManager> flat_manager_;
|
||||
std::shared_ptr<PatchAtlasCache> patch_atlas_cache_;
|
||||
rhi::Handle<rhi::UniformSet> us_1;
|
||||
rhi::Handle<rhi::UniformSet> us_2;
|
||||
std::vector<MergedTwodeeCommandList> cmd_lists_;
|
||||
std::vector<std::tuple<rhi::Handle<rhi::Buffer>, std::size_t>> vbos_;
|
||||
std::vector<std::tuple<rhi::Handle<rhi::Buffer>, std::size_t>> ibos_;
|
||||
bool rebuild_atlases_ = false;
|
||||
rhi::Handle<rhi::RenderPass> render_pass_;
|
||||
rhi::Handle<rhi::Texture> 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();
|
||||
|
||||
|
|
|
|||
379
src/hwr2/patch_atlas.cpp
Normal file
379
src/hwr2/patch_atlas.cpp
Normal file
|
|
@ -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 <stb_rect_pack.h>
|
||||
|
||||
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<const column_t*>(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<int32_t>(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<const column_t*>(reinterpret_cast<const uint8_t*>(column) + column->length + 4);
|
||||
}
|
||||
}
|
||||
|
||||
maxx += 1;
|
||||
maxx = std::max(minx, maxx);
|
||||
maxy = std::max(miny, maxy);
|
||||
|
||||
return {minx, miny, static_cast<uint32_t>(maxx - minx), static_cast<uint32_t>(maxy - miny)};
|
||||
}
|
||||
|
||||
void srb2::hwr2::convert_patch_to_trimmed_rg8_pixels(const patch_t* patch, std::vector<uint8_t>& 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<int32_t>(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<const column_t*>(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<const uint8_t*>(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<int32_t>(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<const column_t*>(reinterpret_cast<const uint8_t*>(column) + column->length + 4);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
PatchAtlas::PatchAtlas(Handle<Texture> texture, uint32_t size) : tex_(texture), size_(size)
|
||||
{
|
||||
rp_ctx = std::make_unique<stbrp_context>();
|
||||
rp_nodes = std::make_unique<stbrp_node[]>(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<stbrp_rect> rects)
|
||||
{
|
||||
stbrp_pack_rects(rp_ctx.get(), rects.data(), rects.size());
|
||||
}
|
||||
|
||||
std::optional<PatchAtlas::Entry> PatchAtlas::find_patch(srb2::NotNull<const patch_t*> 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> 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<stbrp_rect> rects;
|
||||
|
||||
std::vector<const patch_t*> large_patches;
|
||||
|
||||
std::vector<const patch_t*> 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<uint32_t>(rect.x);
|
||||
entry.y = static_cast<uint32_t>(rect.y);
|
||||
entry.w = static_cast<uint32_t>(rect.w);
|
||||
entry.h = static_cast<uint32_t>(rect.h);
|
||||
entry.trim_x = static_cast<uint32_t>(trimmed_rect.x);
|
||||
entry.trim_y = static_cast<uint32_t>(trimmed_rect.y);
|
||||
entry.orig_w = static_cast<uint32_t>(patch->width);
|
||||
entry.orig_h = static_cast<uint32_t>(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<const patch_t*> 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<const patch_t*> 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<const patch_t*> 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<TransferContext> ctx)
|
||||
{
|
||||
SRB2_ASSERT(ready_for_lookup());
|
||||
|
||||
// Upload atlased patches
|
||||
std::vector<uint8_t> patch_data;
|
||||
for (const patch_t* patch_to_upload : patches_to_upload_)
|
||||
{
|
||||
srb2::NotNull<PatchAtlas*> atlas = find_patch(patch_to_upload);
|
||||
|
||||
std::optional<PatchAtlas::Entry> 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<int32_t>(entry->x), static_cast<int32_t>(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<GraphicsContext> ctx)
|
||||
{
|
||||
}
|
||||
|
||||
void PatchAtlasCache::postpass(Rhi& rhi)
|
||||
{
|
||||
}
|
||||
145
src/hwr2/patch_atlas.hpp
Normal file
145
src/hwr2/patch_atlas.hpp
Normal file
|
|
@ -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 <cstdint>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
#include <tcb/span.hpp>
|
||||
|
||||
#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<rhi::Texture> tex_;
|
||||
uint32_t size_;
|
||||
|
||||
std::unordered_map<const patch_t*, Entry> entries_;
|
||||
|
||||
std::unique_ptr<stbrp_context> rp_ctx {nullptr};
|
||||
std::unique_ptr<stbrp_node[]> rp_nodes {nullptr};
|
||||
|
||||
friend class PatchAtlasCache;
|
||||
|
||||
public:
|
||||
PatchAtlas(rhi::Handle<rhi::Texture> 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<rhi::Texture> texture() const noexcept { return tex_; }
|
||||
|
||||
uint32_t texture_size() const noexcept { return size_; }
|
||||
|
||||
std::optional<Entry> find_patch(srb2::NotNull<const patch_t*> patch) const;
|
||||
|
||||
void pack_rects(tcb::span<stbrp_rect> 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<PatchAtlas> atlases_;
|
||||
std::unordered_map<const patch_t*, size_t> patch_lookup_;
|
||||
|
||||
std::unordered_set<const patch_t*> patches_to_pack_;
|
||||
std::unordered_set<const patch_t*> 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<const patch_t*> 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<const patch_t*> patch) const;
|
||||
PatchAtlas* find_patch(srb2::NotNull<const patch_t*> patch);
|
||||
|
||||
virtual void prepass(rhi::Rhi& rhi) override;
|
||||
virtual void transfer(rhi::Rhi& rhi, rhi::Handle<rhi::TransferContext> ctx) override;
|
||||
virtual void graphics(rhi::Rhi& rhi, rhi::Handle<rhi::GraphicsContext> 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<uint8_t>& out);
|
||||
|
||||
} // namespace srb2::hwr2
|
||||
|
||||
#endif // __SRB2_HWR2_PATCH_ATLAS_HPP__
|
||||
|
|
@ -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<MainPaletteManager>();
|
||||
auto common_resources_manager = std::make_shared<CommonResourcesManager>();
|
||||
auto flat_texture_manager = std::make_shared<FlatTextureManager>();
|
||||
auto patch_atlas_cache = std::make_shared<PatchAtlasCache>(2048, 2);
|
||||
auto resource_manager = std::make_shared<PassManager>();
|
||||
|
||||
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<BlitPostimgScreens>(palette_manager);
|
||||
auto twodee = std::make_shared<TwodeePass>();
|
||||
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<BlitRectPass>(false);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue