From 46da33be37198a5ea65c721aef04fe4d4cfb9d3c Mon Sep 17 00:00:00 2001 From: doop <56421834+dooplecks@users.noreply.github.com> Date: Fri, 14 Aug 2026 01:44:36 -0400 Subject: [PATCH] Use texture replacement API for HQ minimap textures (#2303) --- files.cmake | 1 + src/d/d_map_path.cpp | 131 +------------------------ src/dusk/hq_minimap.cpp | 210 ++++++++++++++++++++++++++++++++++++++++ src/dusk/hq_minimap.hpp | 21 ++++ src/dusk/settings.cpp | 2 + src/dusk/settings.h | 1 + src/m_Do/m_Do_main.cpp | 5 + 7 files changed, 243 insertions(+), 128 deletions(-) create mode 100644 src/dusk/hq_minimap.cpp create mode 100644 src/dusk/hq_minimap.hpp diff --git a/files.cmake b/files.cmake index 6fc4762be3..675ba70fdc 100644 --- a/files.cmake +++ b/files.cmake @@ -1436,6 +1436,7 @@ set(DUSK_FILES src/dusk/gamepad_color.cpp src/dusk/globals.cpp src/dusk/gyro.cpp + src/dusk/hq_minimap.cpp src/dusk/imgui/ImGuiActorSpawner.cpp src/dusk/imgui/ImGuiBloomWindow.cpp src/dusk/imgui/ImGuiBloomWindow.hpp diff --git a/src/d/d_map_path.cpp b/src/d/d_map_path.cpp index 08591007ec..41d5108e32 100644 --- a/src/d/d_map_path.cpp +++ b/src/d/d_map_path.cpp @@ -16,23 +16,14 @@ #ifdef TARGET_PC #include "dusk/settings.h" +#include "dusk/hq_minimap.hpp" #include "m_Do/m_Do_graphic.h" #include #include #include -#include #include -#include #include -#include -#include - -constexpr u16 kMapIconResolutionMultiplier = 4; -constexpr u16 kMapImageSide = 16 * kMapIconResolutionMultiplier; -constexpr u32 kMapImageTotalPixels = kMapImageSide * kMapImageSide; - -typedef std::function PaintI8Fn; u16 scaled_map_axis(u16 value, f32 scale) { const auto scaledValue = @@ -59,27 +50,6 @@ aurora::Vec2 map_render_size_for(u16 width, u16 height) { scaled_map_axis(height, irScaleY * hudScale), }; } - -void paint_i8(std::span dst, size_t width, PaintI8Fn paint) { - const auto blocksAcross = width >> 3; - - for (size_t i = 0; i < dst.size(); i++) { - // 8x4 block swizzling for I8 - const auto blockIdx = i >> 5; - const auto localIdx = i & 31; - - const auto blockY = blockIdx / blocksAcross; - const auto blockX = blockIdx % blocksAcross; - - const auto localY = localIdx >> 3; - const auto localX = localIdx & 7; - - const auto x = (blockX << 3) + localX; - const auto y = (blockY << 2) + localY; - - dst[i] = paint(x, y); - } -} #endif void dMpath_n::dTexObjAggregate_c::create() { @@ -100,106 +70,11 @@ void dMpath_n::dTexObjAggregate_c::create() { JUT_ASSERT(72, image != NULL); JUT_ASSERT(73, image->minFilter == GX_NEAR); JUT_ASSERT(74, image->magFilter == GX_NEAR); + IF_DUSK(dusk::hq_minimap::register_pointer(data[lp1], reinterpret_cast(image) + image->imageOffset)); mDoLib_setResTimgObj(image, mp_texObj[lp1], 0, NULL); } -#if TARGET_PC - static bool hqTexsDrawn = false; - - static u8 hqCircleData[kMapImageTotalPixels]; - static u8 hqCircleAltData[kMapImageTotalPixels]; - static u8 hqNijumaruData[kMapImageTotalPixels]; - static u8 hqEnterData[kMapImageTotalPixels]; - static u8 hqTryForceData[kMapImageTotalPixels]; - - if (!hqTexsDrawn) { - constexpr auto center = kMapImageSide / 2.0f; - constexpr auto radiusSq = center * center; - - // 6: map_icon_circle16x16_4i.bti - simple circle - paint_i8(std::span{hqCircleData}, kMapImageSide, [=](auto x, auto y) { - const auto dx = (x + 0.5f) - center; - const auto dy = (y + 0.5f) - center; - return (dx * dx + dy * dy < radiusSq) ? 0x11 : 0; - }); - - // 4: im_map_icon_circle_4i.bti - outlined circle - paint_i8(std::span{hqCircleAltData}, kMapImageSide, [=](auto x, auto y) { - constexpr auto innerRadius = kMapImageSide * 3.0f / 8.0f; - constexpr auto innerRadiusSq = innerRadius * innerRadius; - - const auto dx = (x + 0.5f) - center; - const auto dy = (y + 0.5f) - center; - const auto dSq = dx * dx + dy * dy; - - return dSq < radiusSq ? (dSq < innerRadiusSq ? 0x22 : 0x11) : 0; - }); - - // 3: im_map_icon_nijumaru_4i.bti - concentric rings - paint_i8(std::span{hqNijumaruData}, kMapImageSide, [=](auto x, auto y) { - constexpr u8 nijumaruRings[] = {0x11, 0x22, 0x11, 0x11, 0x22, 0x22}; - - const auto dx = (x + 0.5f) - center; - const auto dy = (y + 0.5f) - center; - const auto dSq = dx * dx + dy * dy; - - if (dSq < radiusSq) { - const auto ringIndex = - static_cast(std::trunc(std::sqrt(dSq) / kMapImageSide * 12)); - return nijumaruRings[ringIndex]; - } - return u8{0}; - }); - - // 2: im_map_icon_enter_4i.bti - outlined octagram - paint_i8(std::span{hqEnterData}, kMapImageSide, [=](auto x, auto y) { - constexpr auto outlineWidth = kMapImageSide / 6.0f; - - const auto adx = std::abs((x + 0.5f) - center); - const auto ady = std::abs((y + 0.5f) - center); - const auto dist = - std::min(adx + ady, std::max(adx, ady) * std::numbers::sqrt2_v) - - kMapImageSide / 2.0f; - - return dist > 0.0f ? 0 : (dist > -outlineWidth ? 0x22 : 0x33); - }); - - // 5: im_map_icon_try_force_4i.bti - outlined circle with triangle - paint_i8(std::span{hqTryForceData}, kMapImageSide, [=](auto x, auto y) { - constexpr auto innerRadiusNorm = 5.0f / 12.0f; - constexpr auto innerRadius = kMapImageSide * innerRadiusNorm; - constexpr auto innerRadiusSq = innerRadius * innerRadius; - constexpr auto triRadius = kMapImageSide * innerRadiusNorm / 2.0f; - - const auto dx = (x + 0.5f) - center; - const auto dy = (y + 0.5f) - center; - const auto dSq = dx * dx + dy * dy; - const auto triSideDist = (std::numbers::sqrt3_v * std::abs(dx) - dy) * 0.5f; - const auto insideTri = std::max(dy, triSideDist) < triRadius; - - return insideTri ? 0x22 : (dSq < radiusSq ? (dSq < innerRadiusSq ? 0x33 : 0x22) : 0); - }); - - hqTexsDrawn = true; - } - - constexpr auto replacements = std::to_array >({ - {2, hqEnterData}, - {3, hqNijumaruData}, - {4, hqCircleAltData}, - {5, hqTryForceData}, - {6, hqCircleData}, - }); - - for (const auto& [idx, data] : replacements) { - JKR_DELETE(mp_texObj[idx]); - const auto texobj = JKR_NEW TGXTexObj(); - GXInitTexObj( - texobj, data, kMapImageSide, kMapImageSide, GX_TF_I8, GX_CLAMP, GX_CLAMP, GX_FALSE); - GXInitTexObjLOD(texobj, GX_NEAR, GX_NEAR, 0.0f, 0.0f, 0.0f, GX_FALSE, GX_FALSE, GX_ANISO_1); - mp_texObj[idx] = texobj; - } -#endif + IF_DUSK(dusk::hq_minimap::initialize_if_needed()); } void dMpath_n::dTexObjAggregate_c::remove() { diff --git a/src/dusk/hq_minimap.cpp b/src/dusk/hq_minimap.cpp new file mode 100644 index 0000000000..a4d73cbcf1 --- /dev/null +++ b/src/dusk/hq_minimap.cpp @@ -0,0 +1,210 @@ +#include "aurora/texture.hpp" +#include "dusk/texture_replacements.hpp" +#include "fmt/format.h" + +#include +#include +#include +#include + +namespace { + +constexpr u16 kMapIconResolutionMultiplier = 4; +constexpr u16 kMapImageSide = 16 * kMapIconResolutionMultiplier; +constexpr u32 kMapImageTotalPixels = kMapImageSide * kMapImageSide; + +// give higher priority to user and mod replacements +constexpr auto kInternalTextureReplacementPriority = dusk::texture_replacements::kUserTextureReplacementPriority - 1; + +typedef std::function PaintI8Fn; + +enum class ArcIndex : int { + Circle16 = 82, // map_icon_circle16x16_4i.bti - simple circle + Circle = 76, // im_map_icon_circle_4i.bti - outlined circle + Nijumaru = 78, // im_map_icon_nijumaru_4i.bti - concentric rings + Enter = 77, // im_map_icon_enter_4i.bti - outlined octagram + TryForce = 81, // im_map_icon_try_force_4i.bti - outlined circle with triangle +}; + +struct Replacement { + ArcIndex index; + PaintI8Fn painter; +}; + +struct Icon { + u8* origData = nullptr; + std::unique_ptr newData; + std::string label; + std::optional reg; +}; + +bool s_initialized = false; +bool s_active = false; +std::unordered_map s_icons; + +void paint_i8(std::span dst, size_t width, PaintI8Fn paint) { + assert(width % 8 == 0 && dst.size() % 32 == 0); + + const auto blocksAcross = width >> 3; + + for (size_t i = 0; i < dst.size(); i++) { + // 8x4 block swizzling for I8 + const auto blockIdx = i >> 5; + const auto localIdx = i & 31; + + const auto blockY = blockIdx / blocksAcross; + const auto blockX = blockIdx % blocksAcross; + + const auto localY = localIdx >> 3; + const auto localX = localIdx & 7; + + const auto x = (blockX << 3) + localX; + const auto y = (blockY << 2) + localY; + + dst[i] = paint(x, y); + } +} + +void draw_all_replacements() { + constexpr auto center = kMapImageSide / 2.0f; + constexpr auto radiusSq = center * center; + + // clang-format off + const auto replacements = std::to_array({ + { + ArcIndex::Circle16, + [=](auto x, auto y) { + const auto dx = (x + 0.5f) - center; + const auto dy = (y + 0.5f) - center; + return (dx * dx + dy * dy < radiusSq) ? 0x11 : 0; + } + }, + { + ArcIndex::Circle, + [=](auto x, auto y) { + constexpr auto innerRadius = kMapImageSide * 3.0f / 8.0f; + constexpr auto innerRadiusSq = innerRadius * innerRadius; + + const auto dx = (x + 0.5f) - center; + const auto dy = (y + 0.5f) - center; + const auto dSq = dx * dx + dy * dy; + + return dSq < radiusSq ? (dSq < innerRadiusSq ? 0x22 : 0x11) : 0; + } + }, + { + ArcIndex::Nijumaru, + [=](auto x, auto y) { + constexpr u8 nijumaruRings[] = {0x11, 0x22, 0x11, 0x11, 0x22, 0x22}; + + const auto dx = (x + 0.5f) - center; + const auto dy = (y + 0.5f) - center; + const auto dSq = dx * dx + dy * dy; + + if (dSq < radiusSq) { + auto ringIndex = static_cast(std::trunc(std::sqrt(dSq) / kMapImageSide * 12)); + ringIndex = std::min(ringIndex, sizeof(nijumaruRings) - 1); + return nijumaruRings[ringIndex]; + } + return u8{0}; + } + }, + { + ArcIndex::Enter, + [=](auto x, auto y) { + constexpr auto outlineWidth = kMapImageSide / 6.0f; + + const auto adx = std::abs((x + 0.5f) - center); + const auto ady = std::abs((y + 0.5f) - center); + const auto dist = + std::min(adx + ady, std::max(adx, ady) * std::numbers::sqrt2_v) - + kMapImageSide / 2.0f; + + return dist > 0.0f ? 0 : (dist > -outlineWidth ? 0x22 : 0x33); + } + }, + { + ArcIndex::TryForce, + [=](auto x, auto y) { + constexpr auto innerRadiusNorm = 5.0f / 12.0f; + constexpr auto innerRadius = kMapImageSide * innerRadiusNorm; + constexpr auto innerRadiusSq = innerRadius * innerRadius; + constexpr auto triRadius = kMapImageSide * innerRadiusNorm / 2.0f; + + const auto dx = (x + 0.5f) - center; + const auto dy = (y + 0.5f) - center; + const auto dSq = dx * dx + dy * dy; + const auto triSideDist = (std::numbers::sqrt3_v * std::abs(dx) - dy) * 0.5f; + const auto insideTri = std::max(dy, triSideDist) < triRadius; + + return insideTri ? 0x22 : (dSq < radiusSq ? (dSq < innerRadiusSq ? 0x33 : 0x22) : 0); + } + } + }); + // clang-format on + + for (const auto r : replacements) { + auto pixels = std::make_unique_for_overwrite(kMapImageTotalPixels); + paint_i8(std::span{pixels.get(), kMapImageTotalPixels}, kMapImageSide, r.painter); + + auto& icon = s_icons[static_cast(r.index)]; + icon.newData = std::move(pixels); + icon.label = fmt::format("hq minimap icon {}", static_cast(r.index)); + } +} + +} // namespace + +namespace dusk::hq_minimap { + +void register_pointer(int idx, u8* ptr) { + if (s_initialized) { + return; + } + + s_icons[idx].origData = ptr; +} + +void set_active(bool active) { + s_active = active; +} + +void update() { + if (!s_initialized) { + return; + } + + for (auto& [idx, icon] : s_icons) { + const bool shouldBeRegistered = s_active && icon.origData && icon.newData; + + if (shouldBeRegistered && !icon.reg) { + aurora::texture::ReplacementKey key{aurora::texture::TexturePointerKey{icon.origData}}; + aurora::texture::RawTextureReplacement repl{ + .bytes = std::span{icon.newData.get(), kMapImageTotalPixels}, + .width = kMapImageSide, + .height = kMapImageSide, + .mipCount = 1, + .gxFormat = GX_TF_I8, + .label = icon.label, + }; + icon.reg = aurora::texture::register_replacement( + key, repl, {.priority = kInternalTextureReplacementPriority}); + } else if (!shouldBeRegistered && icon.reg) { + aurora::texture::unregister_replacement(*icon.reg); + icon.reg.reset(); + } + } +} + +void initialize_if_needed() { + if (s_initialized) { + return; + } + + draw_all_replacements(); + s_initialized = true; + + update(); +} + +} // namespace dusk::hq_minimap diff --git a/src/dusk/hq_minimap.hpp b/src/dusk/hq_minimap.hpp new file mode 100644 index 0000000000..5ca89ac7d1 --- /dev/null +++ b/src/dusk/hq_minimap.hpp @@ -0,0 +1,21 @@ +#pragma once + +namespace dusk::hq_minimap { + +/// Adds a mapping of an image resource index (into `Always.arc`) to the pointer to its image data. +/// If `initialize_if_needed` has been called, this is a no-op. Pointers are expected to be stable +/// and valid for the program's entire lifetime. +void register_pointer(int idx, u8* ptr); + +/// Sets whether HQ minimap texture replacements should be active or not. Does not manage +/// replacement registrations itself; see `update`. +void set_active(bool active); + +/// Registers or unregisters texture replacements depending on active state. +void update(); + +/// Called once after registering image pointers, in which their HQ replacements are procedurally +/// drawn and `update` is called. Further calls are no-ops. +void initialize_if_needed(); + +} diff --git a/src/dusk/settings.cpp b/src/dusk/settings.cpp index 00191c3015..a6c3149983 100644 --- a/src/dusk/settings.cpp +++ b/src/dusk/settings.cpp @@ -76,6 +76,7 @@ UserSettings g_userSettings = { .resampler {"game.resampler", Resampler::Bilinear}, .enableMapBackground {"game.enableMapBackground", true}, .disableCutscenePillarboxing {"game.disableCutscenePillarboxing", false}, + .enableHighQualityMinimapTextures {"game.enableHighQualityMinimapTextures", true}, // Audio .noLowHpSound {"game.noLowHpSound", false}, @@ -283,6 +284,7 @@ void registerSettings() { Register(g_userSettings.game.shadowResolutionMultiplier); Register(g_userSettings.game.enableMapBackground); Register(g_userSettings.game.disableCutscenePillarboxing); + Register(g_userSettings.game.enableHighQualityMinimapTextures); Register(g_userSettings.game.enableFastIronBoots); Register(g_userSettings.game.canTransformAnywhere); Register(g_userSettings.game.fastRoll); diff --git a/src/dusk/settings.h b/src/dusk/settings.h index 19aad6ee09..e31dc5eb42 100644 --- a/src/dusk/settings.h +++ b/src/dusk/settings.h @@ -208,6 +208,7 @@ struct UserSettings { ConfigVar resampler; ConfigVar enableMapBackground; ConfigVar disableCutscenePillarboxing; + ConfigVar enableHighQualityMinimapTextures; // Audio ConfigVar noLowHpSound; diff --git a/src/m_Do/m_Do_main.cpp b/src/m_Do/m_Do_main.cpp index ccb6c35c1c..6a6d38c890 100644 --- a/src/m_Do/m_Do_main.cpp +++ b/src/m_Do/m_Do_main.cpp @@ -65,6 +65,7 @@ #include "dusk/iso_validate.hpp" #include "dusk/logging.h" #include "dusk/main.h" +#include "dusk/hq_minimap.hpp" #include "dusk/mod_loader.hpp" #include "dusk/mods/svc/window.hpp" #include "dusk/mouse.h" @@ -711,6 +712,10 @@ int game_main(int argc, char* argv[]) { return 0; } + if (dusk::getSettings().game.enableHighQualityMinimapTextures.getValue()) { + dusk::hq_minimap::set_active(true); + } + dusk::texture_replacements::reload(); dusk::ui::initialize(); dusk::ui::push_document(std::make_unique(), true, true);