Texture loading fixes (#170)

* Implement PNG & arb texture replacements

Fixes #119

For arb textures it's just making sure the file parsing can handle them. Idk if we need to do something special for it.

* Allow non-mipmapped textures to be replaced by mipmapped textures

Some of Henriko's textures (like the cuttable grass) rely on this.

The mipmap bool is no longer part of the runtime key.

* Make texture name the texture replacement key

Might as well? Helped me debug some stuff

* Oops that broke it bad

* libpng instead of spng

* I did it again

* Update libpng fetch

* Add zlib fetch

* Forgot to set the hash

* Add zlib redirect

* Add INCLUDE_DIR(S) to redirect

* Prefer shared for libpng

* Set CMAKE_FIND_PACKAGE_TARGETS_GLOBAL

---------

Co-authored-by: Luke Street <luke@street.dev>
This commit is contained in:
Pieter-Jan Briers
2026-05-13 04:32:43 +02:00
committed by GitHub
parent 974d11dfe7
commit f7f3d52cfc
8 changed files with 243 additions and 42 deletions
+3 -1
View File
@@ -34,10 +34,12 @@ add_library(aurora_gx STATIC
lib/dolphin/gx/GXTransform.cpp
lib/dolphin/gx/GXVert.cpp
lib/dolphin/gx/GXAurora.cpp
lib/gfx/png_io.cpp
lib/gfx/png_io.hpp
)
add_library(aurora::gx ALIAS aurora_gx)
set_target_properties(aurora_gx PROPERTIES FOLDER "aurora")
target_link_libraries(aurora_gx PUBLIC aurora::core xxhash)
target_link_libraries(aurora_gx PRIVATE absl::btree absl::flat_hash_map dawn::webgpu_dawn sqlite3 TracyClient)
target_link_libraries(aurora_gx PRIVATE absl::btree absl::flat_hash_map dawn::webgpu_dawn sqlite3 TracyClient PNG::PNG)
target_compile_definitions(aurora_gx PRIVATE WEBGPU_DAWN)
+71 -17
View File
@@ -3,6 +3,13 @@ include(FetchContent)
# Don't build tests for third party dependencies
set(BUILD_TESTING OFF CACHE INTERNAL "Build tests")
function(aurora_find_package_global)
set(_PREV_FIND_PACKAGE_TARGETS_GLOBAL ${CMAKE_FIND_PACKAGE_TARGETS_GLOBAL})
set(CMAKE_FIND_PACKAGE_TARGETS_GLOBAL ON)
find_package(${ARGV})
set(CMAKE_FIND_PACKAGE_TARGETS_GLOBAL ${_PREV_FIND_PACKAGE_TARGETS_GLOBAL})
endfunction()
if (AURORA_ENABLE_GX)
include(${CMAKE_CURRENT_SOURCE_DIR}/../cmake/AuroraDawnProvider.cmake)
endif ()
@@ -10,7 +17,7 @@ endif ()
# Abseil is needed for core libraries. It normally comes via Dawn's vendor build.
# Otherwise prefer a system package and only fetch it as a last resort.
if (NOT TARGET absl::flat_hash_map OR NOT TARGET absl::btree)
find_package(absl CONFIG QUIET GLOBAL)
aurora_find_package_global(absl CONFIG QUIET GLOBAL)
endif ()
if (TARGET absl::flat_hash_map AND TARGET absl::btree)
@@ -77,7 +84,61 @@ else ()
endif ()
if (AURORA_ENABLE_GX)
find_package(Freetype QUIET)
if (NOT TARGET ZLIB::ZLIB)
aurora_find_package_global(ZLIB QUIET)
endif ()
if (NOT TARGET ZLIB::ZLIB)
message(STATUS "aurora: Fetching zlib")
FetchContent_Declare(
ZLIB
URL https://github.com/madler/zlib/releases/download/v1.3.2/zlib-1.3.2.tar.gz
URL_HASH SHA256=bb329a0a2cd0274d05519d61c667c062e06990d72e125ee2dfa8de64f0119d16
DOWNLOAD_EXTRACT_TIMESTAMP TRUE
EXCLUDE_FROM_ALL
)
FetchContent_MakeAvailable(ZLIB)
if (NOT TARGET ZLIB::ZLIB)
message(FATAL_ERROR "Failed to make zlib available")
endif ()
# libpng calls find_package(ZLIB) even when zlib was already provided by FetchContent.
file(WRITE "${CMAKE_FIND_PACKAGE_REDIRECTS_DIR}/ZLIBConfig.cmake"
"set(ZLIB_FOUND TRUE)\n"
"set(ZLIB_INCLUDE_DIR \"${zlib_SOURCE_DIR}\")\n"
"set(ZLIB_INCLUDE_DIRS \"${zlib_BINARY_DIR};${zlib_SOURCE_DIR}\")\n"
)
endif ()
if (NOT TARGET PNG::PNG)
if (TARGET png_shared)
add_library(PNG::PNG ALIAS png_shared)
elseif (TARGET png_static)
add_library(PNG::PNG ALIAS png_static)
else ()
aurora_find_package_global(PNG QUIET)
endif ()
endif ()
if (NOT TARGET PNG::PNG)
message(STATUS "aurora: Fetching libpng")
FetchContent_Declare(
PNG
URL https://github.com/pnggroup/libpng/archive/refs/tags/v1.6.58.tar.gz
URL_HASH SHA256=A9D4DF463D36A6E5F9C29BD6F4967312D17E996C1854F3511F833924EB1993CF
DOWNLOAD_EXTRACT_TIMESTAMP TRUE
EXCLUDE_FROM_ALL
)
FetchContent_MakeAvailable(PNG)
if (TARGET png_shared)
add_library(PNG::PNG ALIAS png_shared)
elseif (TARGET png_static)
add_library(PNG::PNG ALIAS png_static)
else ()
message(FATAL_ERROR "Failed to make libpng available")
endif ()
else ()
message(STATUS "aurora: Using existing libpng")
endif ()
aurora_find_package_global(Freetype QUIET)
if (NOT TARGET Freetype::Freetype)
message(STATUS "aurora: Fetching Freetype")
set(FT_DISABLE_BROTLI ON CACHE BOOL "" FORCE)
@@ -175,30 +236,22 @@ if (AURORA_ENABLE_GX)
endif ()
endif ()
if(AURORA_ENABLE_RMLUI AND NOT TARGET RmlUi)
if (AURORA_ENABLE_RMLUI AND NOT TARGET RmlUi)
message(STATUS "aurora: Fetching RmlUi")
FetchContent_Declare(rmlui
URL https://github.com/mikke89/RmlUi/archive/f9b8c9e2935d5df2c7dff2c190d3968e99b0c3dc.tar.gz
URL_HASH SHA256=42b830abc2509a8c07cf02ba3313505de3a4642725654f489402022d8fefdd79
DOWNLOAD_EXTRACT_TIMESTAMP TRUE
EXCLUDE_FROM_ALL
URL https://github.com/mikke89/RmlUi/archive/f9b8c9e2935d5df2c7dff2c190d3968e99b0c3dc.tar.gz
URL_HASH SHA256=42b830abc2509a8c07cf02ba3313505de3a4642725654f489402022d8fefdd79
DOWNLOAD_EXTRACT_TIMESTAMP TRUE
EXCLUDE_FROM_ALL
)
FetchContent_MakeAvailable(rmlui)
add_library(rmlui_backends STATIC
${rmlui_SOURCE_DIR}/Backends/RmlUi_Platform_SDL.cpp
)
add_library(rmlui_backends STATIC ${rmlui_SOURCE_DIR}/Backends/RmlUi_Platform_SDL.cpp)
# TODO: maybe dont hardcode this to SDL3?
target_compile_definitions(rmlui_backends PUBLIC RMLUI_SDL_VERSION_MAJOR=3)
target_include_directories(rmlui_backends PUBLIC
${rmlui_SOURCE_DIR}/Backends
)
target_include_directories(rmlui_backends PUBLIC ${rmlui_SOURCE_DIR}/Backends)
target_link_libraries(rmlui_backends PRIVATE rmlui ${AURORA_SDL3_TARGET})
endif ()
endif ()
if (NOT TARGET TracyClient)
@@ -212,6 +265,7 @@ if (NOT TARGET TracyClient)
URL https://github.com/wolfpld/tracy/archive/a64b9a20294d59421a2f57aeca3c6383d8c48169.tar.gz
URL_HASH SHA256=24d342b5127d7f659dc3cf94f24347b348cc736f25b17a691fd7f69541937658
DOWNLOAD_EXTRACT_TIMESTAMP TRUE
EXCLUDE_FROM_ALL
)
FetchContent_MakeAvailable(tracy)
+3
View File
@@ -11,4 +11,7 @@ std::optional<ConvertedTexture> parse_dds_bytes(ArrayRef<uint8_t> bytes) noexcep
std::optional<ConvertedTexture> load_dds_file(const std::filesystem::path& path) noexcept;
ByteBuffer encode_rgba8_dds(uint32_t width, uint32_t height, ArrayRef<uint8_t> pixels);
bool write_rgba8_dds(const std::filesystem::path& path, uint32_t width, uint32_t height, ArrayRef<u8> pixels) noexcept;
std::optional<ByteBuffer> read_binary_file(const std::filesystem::path& path) noexcept;
} // namespace aurora::gfx::dds
+102
View File
@@ -0,0 +1,102 @@
#include "png_io.hpp"
#include <fstream>
#include "dds_io.hpp"
#include "png.h"
#include "../fs_helper.hpp"
static aurora::Module Log("aurora::gfx::png");
namespace aurora::gfx::png {
struct PngStructs {
png_structp pStruct;
png_infop pInfo;
std::ifstream file;
~PngStructs() {
png_destroy_read_struct(&pStruct, &pInfo, nullptr);
}
};
static void readPngData(png_structp png, png_bytep data, const size_t length) {
auto structs = static_cast<PngStructs*>(png_get_io_ptr(png));
structs->file.read(reinterpret_cast<char*>(data), static_cast<std::streamsize>(length));
if (structs->file.eof() || structs->file.fail()) {
png_error(png, "file read failed!");
}
}
std::optional<ConvertedTexture>
load_png_file(const std::filesystem::path& path) noexcept {
PngStructs structs{};
structs.file = std::move(std::ifstream(path, std::ifstream::in | std::ifstream::binary));
if (!structs.file) {
Log.error("failed to open file: {}", fs_path_to_string(path));
return std::nullopt;
}
structs.pStruct = png_create_read_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr);
if (!structs.pStruct) {
Log.error("png_create_read_struct failed");
return std::nullopt;
}
structs.pInfo = png_create_info_struct(structs.pStruct);
if (!structs.pInfo) {
Log.error("png_create_info_struct failed");
return std::nullopt;
}
// I'm scared of putting any locals after that setjmp.
std::vector<png_bytep> rowPointers;
ByteBuffer imageData{};
png_uint_32 width, height;
int bit_depth, color_type, interlace_type, compression_type, filter_type;
size_t rowBytes;
int i;
if (setjmp(png_jmpbuf(structs.pStruct))) {
Log.error("libpng encountered an error");
return std::nullopt;
}
png_set_read_fn(structs.pStruct, &structs, readPngData);
png_read_info(structs.pStruct, structs.pInfo);
if (!png_get_IHDR(structs.pStruct, structs.pInfo, &width, &height, &bit_depth, &color_type, &interlace_type, &compression_type, &filter_type)) {
Log.error("libpng unable to read IHDR");
return std::nullopt;
}
// Always read as RGBA8.
png_set_gray_to_rgb(structs.pStruct);
png_set_filler(structs.pStruct, 0xFF, PNG_FILLER_AFTER);
png_set_expand(structs.pStruct);
png_set_strip_16(structs.pStruct);
png_read_update_info(structs.pStruct, structs.pInfo);
rowBytes = png_get_rowbytes(structs.pStruct, structs.pInfo);
rowPointers.resize(height);
imageData.append_zeroes(rowBytes * height);
for (i = 0; i < height; i++) {
rowPointers[i] = imageData.data() + i * rowBytes;
}
png_read_image(structs.pStruct, rowPointers.data());
png_read_end(structs.pStruct, nullptr);
return ConvertedTexture{
.format = wgpu::TextureFormat::RGBA8Unorm,
.width = width,
.height = height,
.mips = 1,
.data = std::move(imageData)
};
}
}
+10
View File
@@ -0,0 +1,10 @@
#pragma once
#include <filesystem>
#include <optional>
#include "texture_convert.hpp"
namespace aurora::gfx::png {
std::optional<ConvertedTexture> load_png_file(const std::filesystem::path& path) noexcept;
}
+46 -23
View File
@@ -21,6 +21,7 @@
#include <optional>
#include <string_view>
#include "png_io.hpp"
#include "../fs_helper.hpp"
using namespace aurora::gx;
@@ -34,7 +35,6 @@ struct RuntimeTextureKey {
uint64_t tlutHash = 0;
uint32_t width = 0;
uint32_t height = 0;
bool hasMips = false;
bool hasTlut = false;
uint32_t format = 0;
@@ -42,8 +42,7 @@ struct RuntimeTextureKey {
template <typename H>
friend H AbslHashValue(H h, const RuntimeTextureKey& key) {
return H::combine(std::move(h), key.textureHash, key.tlutHash, key.width, key.height, key.hasMips, key.hasTlut,
key.format);
return H::combine(std::move(h), key.textureHash, key.tlutHash, key.width, key.height, key.hasTlut, key.format);
}
};
@@ -61,7 +60,12 @@ struct CachedReplacement {
std::list<RuntimeTextureKey>::iterator lruIt;
};
absl::flat_hash_map<RuntimeTextureKey, std::filesystem::path> s_replacementIndex;
struct ReplacementIndexEntry {
std::filesystem::path path;
bool hasMips;
};
absl::flat_hash_map<RuntimeTextureKey, ReplacementIndexEntry> s_replacementIndex;
absl::flat_hash_map<RuntimeTextureKey, CachedReplacement> s_replacementCache;
absl::flat_hash_set<RuntimeTextureKey> s_failedKeys;
absl::flat_hash_set<RuntimeTextureKey> s_reportedMisses;
@@ -258,7 +262,6 @@ RuntimeTextureKey build_runtime_key(const GXTexObj_& obj) noexcept {
RuntimeTextureKey key{
.width = obj.width(),
.height = obj.height(),
.hasMips = obj.has_mips(),
.hasTlut = is_palette_format(obj.format()),
.format = obj.format(),
};
@@ -275,16 +278,19 @@ RuntimeTextureKey build_runtime_key(const GXTexObj_& obj) noexcept {
std::string format_replacement_filename(const RuntimeTextureKey& key) {
if (key.hasTlut) {
return fmt::format("tex1_{}x{}{}_{:016x}_{:016x}_{}.dds", key.width, key.height, key.hasMips ? "_m" : "",
return fmt::format("tex1_{}x{}_{:016x}_{:016x}_{}.dds", key.width, key.height,
key.textureHash, key.tlutHash, key.format);
}
return fmt::format("tex1_{}x{}{}_{:016x}_{}.dds", key.width, key.height, key.hasMips ? "_m" : "", key.textureHash,
key.format);
return fmt::format("tex1_{}x{}_{:016x}_{}.dds", key.width, key.height, key.textureHash, key.format);
}
std::optional<RuntimeTextureKey> parse_replacement_filename(std::string_view filename) noexcept {
const size_t dot = filename.rfind('.');
if (dot == std::string_view::npos || !iequals_ascii(filename.substr(dot), ".dds")) {
if (dot == std::string_view::npos) {
return std::nullopt;
}
if (!iequals_ascii(filename.substr(dot), ".dds") && !iequals_ascii(filename.substr(dot), ".png")) {
return std::nullopt;
}
@@ -323,7 +329,7 @@ std::optional<RuntimeTextureKey> parse_replacement_filename(std::string_view fil
++index;
}
const size_t remaining = partCount - index;
size_t remaining = partCount - index;
if (remaining != 2 && remaining != 3) {
return std::nullopt;
}
@@ -339,7 +345,12 @@ std::optional<RuntimeTextureKey> parse_replacement_filename(std::string_view fil
textureHash = *parsedTex;
}
const auto format = parse_u32(parts[partCount - 1]);
auto formatPart = parts[partCount - 1];
if (formatPart == "arb") {
formatPart = parts[partCount - 2];
remaining -= 1;
}
const auto format = parse_u32(formatPart);
if (!format.has_value()) {
return std::nullopt;
}
@@ -364,32 +375,38 @@ std::optional<RuntimeTextureKey> parse_replacement_filename(std::string_view fil
.tlutHash = tlutHash,
.width = dimensions->first,
.height = dimensions->second,
.hasMips = hasMips,
.hasTlut = hasTlut,
.format = *format,
};
}
std::optional<ConvertedTexture> load_replacement(const std::filesystem::path& path, bool hasMips) noexcept {
auto base = dds::load_dds_file(path);
static std::optional<ConvertedTexture> load_texture_file(const std::filesystem::path& path) {
if (path.extension() == ".png") {
return png::load_png_file(path);
} else {
return dds::load_dds_file(path);
}
}
std::optional<ConvertedTexture> load_replacement(const ReplacementIndexEntry& entry) noexcept {
auto base = load_texture_file(entry.path);
if (!base.has_value()) {
Log.warn("texture_replacement: failed to load texture {}", fs_path_to_string(path));
Log.warn("texture_replacement: failed to load texture {}", fs_path_to_string(entry.path));
return std::nullopt;
}
if (!hasMips) {
if (!entry.hasMips) {
return base;
}
std::vector<ConvertedTexture> more;
std::error_code ec;
for (uint32_t mipLevel = 1;; ++mipLevel) {
const auto mipPath = path.parent_path() / fmt::format("{}_mip{}{}", fs_path_to_string(path.stem()), mipLevel,
fs_path_to_string(path.extension()));
const auto mipPath = entry.path.parent_path() / fmt::format("{}_mip{}{}", fs_path_to_string(entry.path.stem()), mipLevel, fs_path_to_string(entry.path.extension()));
if (!std::filesystem::is_regular_file(mipPath, ec)) {
break;
}
auto lvl = dds::load_dds_file(mipPath);
auto lvl = load_texture_file(mipPath);
const uint32_t ew = std::max(base->width >> mipLevel, 1u);
const uint32_t eh = std::max(base->height >> mipLevel, 1u);
const bool ok = lvl.has_value() && lvl->format == base->format && lvl->width == ew && lvl->height == eh;
@@ -509,7 +526,7 @@ void build_index() noexcept {
continue;
}
if (!iequals_ascii(fs_path_to_string(path.extension()), ".dds")) {
if (!iequals_ascii(fs_path_to_string(path.extension()), ".dds") && !iequals_ascii(fs_path_to_string(path.extension()), ".png")) {
continue;
}
@@ -528,7 +545,7 @@ void build_index() noexcept {
Log.info("Indexed {} texture replacements", s_replacementIndex.size());
}
const std::filesystem::path* find_replacement_path(const RuntimeTextureKey& key) noexcept {
const ReplacementIndexEntry* find_replacement_path(const RuntimeTextureKey& key) noexcept {
if (const auto it = s_replacementIndex.find(key); it != s_replacementIndex.end()) {
return &it->second;
}
@@ -560,8 +577,8 @@ const gfx::TextureHandle* find_cached_replacement(const RuntimeTextureKey& key)
return &cached->second.handle;
}
gfx::TextureHandle load_replacement_texture(const RuntimeTextureKey& key, const std::filesystem::path& path) noexcept {
const auto replacement = load_replacement(path, key.hasMips);
gfx::TextureHandle load_replacement_texture(const RuntimeTextureKey& key, const ReplacementIndexEntry& entry) noexcept {
const auto replacement = load_replacement(entry);
if (!replacement.has_value()) {
s_failedKeys.insert(key);
return {};
@@ -753,4 +770,10 @@ std::optional<TextureHandle> find_replacement(const GXTexObj_& obj) noexcept {
cache_replacement(key, handle);
return handle;
}
std::string build_texture_replacement_name(const GXTexObj_& obj) noexcept {
const RuntimeTextureKey key = build_runtime_key(obj);
return format_replacement_filename(key);
}
} // namespace aurora::gfx::texture_replacement
+1
View File
@@ -9,5 +9,6 @@ void shutdown() noexcept;
void register_tlut(const GXTlutObj* obj, const void* data, GXTlutFmt format, uint16_t entries) noexcept;
void load_tlut(const GXTlutObj* obj, uint32_t idx) noexcept;
std::optional<TextureHandle> find_replacement(const GXTexObj_& obj) noexcept;
std::string build_texture_replacement_name(const GXTexObj_& obj) noexcept;
bool try_bind_replacement(GXTexObj_& obj, GXTexMapID id) noexcept;
} // namespace aurora::gfx::texture_replacement
+7 -1
View File
@@ -161,9 +161,15 @@ gfx::TextureHandle resolve_static_texture(const GXTexObj_& obj) {
if (const auto replacement = gfx::texture_replacement::find_replacement(obj); replacement.has_value()) {
handle = *replacement;
} else {
#if DEBUG
const auto name = gfx::texture_replacement::build_texture_replacement_name(obj);
const auto nameStr = name.c_str();
#else
const auto nameStr = "GX Static Texture";
#endif
handle =
gfx::new_static_texture_2d(obj.width(), obj.height(), obj.mip_count(), obj.format(),
{static_cast<const uint8_t*>(obj.data), UINT32_MAX}, false, "GX Static Texture");
{static_cast<const uint8_t*>(obj.data), UINT32_MAX}, false, nameStr);
}
if (!obj.no_cache()) {
store_cached_texture(obj, handle);