Use texture replacement API for HQ minimap textures (#2303)

This commit is contained in:
doop
2026-08-14 01:44:36 -04:00
committed by GitHub
parent 008a18c142
commit 46da33be37
7 changed files with 243 additions and 128 deletions
+1
View File
@@ -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
+3 -128
View File
@@ -16,23 +16,14 @@
#ifdef TARGET_PC
#include "dusk/settings.h"
#include "dusk/hq_minimap.hpp"
#include "m_Do/m_Do_graphic.h"
#include <dolphin/gx/GXAurora.h>
#include <aurora/math.hpp>
#include <algorithm>
#include <array>
#include <cmath>
#include <functional>
#include <limits>
#include <numbers>
#include <span>
constexpr u16 kMapIconResolutionMultiplier = 4;
constexpr u16 kMapImageSide = 16 * kMapIconResolutionMultiplier;
constexpr u32 kMapImageTotalPixels = kMapImageSide * kMapImageSide;
typedef std::function<u8(size_t, size_t)> PaintI8Fn;
u16 scaled_map_axis(u16 value, f32 scale) {
const auto scaledValue =
@@ -59,27 +50,6 @@ aurora::Vec2<u16> map_render_size_for(u16 width, u16 height) {
scaled_map_axis(height, irScaleY * hudScale),
};
}
void paint_i8(std::span<u8> 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<u8*>(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<size_t>(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<float>) -
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<float> * 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<std::pair<size_t, const u8*> >({
{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() {
+210
View File
@@ -0,0 +1,210 @@
#include "aurora/texture.hpp"
#include "dusk/texture_replacements.hpp"
#include "fmt/format.h"
#include <array>
#include <cassert>
#include <span>
#include <numbers>
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<u8(size_t, size_t)> 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<u8[]> newData;
std::string label;
std::optional<aurora::texture::ReplacementRegistration> reg;
};
bool s_initialized = false;
bool s_active = false;
std::unordered_map<int, Icon> s_icons;
void paint_i8(std::span<u8> 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<Replacement>({
{
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<size_t>(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<float>) -
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<float> * 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<u8[]>(kMapImageTotalPixels);
paint_i8(std::span{pixels.get(), kMapImageTotalPixels}, kMapImageSide, r.painter);
auto& icon = s_icons[static_cast<int>(r.index)];
icon.newData = std::move(pixels);
icon.label = fmt::format("hq minimap icon {}", static_cast<int>(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
+21
View File
@@ -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();
}
+2
View File
@@ -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);
+1
View File
@@ -208,6 +208,7 @@ struct UserSettings {
ConfigVar<Resampler> resampler;
ConfigVar<bool> enableMapBackground;
ConfigVar<bool> disableCutscenePillarboxing;
ConfigVar<bool> enableHighQualityMinimapTextures;
// Audio
ConfigVar<bool> noLowHpSound;
+5
View File
@@ -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<dusk::ui::Overlay>(), true, true);