Переглянути джерело

feat: add runtime bootflow texture overrides

zeex64 3 тижнів тому
батько
коміт
62b138a6df
37 змінених файлів з 557 додано та 0 видалено
  1. 2 0
      Sunrise/Sunrise.vcxproj
  2. BIN
      Sunrise/resources/bootflow/texture_80A145C1.dds
  3. BIN
      Sunrise/resources/bootflow/texture_80A145DF.dds
  4. BIN
      Sunrise/resources/bootflow/texture_80A145E4.dds
  5. BIN
      Sunrise/resources/bootflow/texture_80A145E7.dds
  6. BIN
      Sunrise/resources/bootflow/texture_80A145EF.dds
  7. BIN
      Sunrise/resources/bootflow/texture_80A145FB.dds
  8. BIN
      Sunrise/resources/bootflow/texture_80A145FF.dds
  9. BIN
      Sunrise/resources/bootflow/texture_80A14601.dds
  10. BIN
      Sunrise/resources/bootflow/texture_80A14604.dds
  11. BIN
      Sunrise/resources/bootflow/texture_80A14607.dds
  12. BIN
      Sunrise/resources/bootflow/texture_80A1460B.dds
  13. BIN
      Sunrise/resources/bootflow/texture_80A1460E.dds
  14. BIN
      Sunrise/resources/bootflow/texture_80A14613.dds
  15. BIN
      Sunrise/resources/bootflow/texture_80A1461D.dds
  16. BIN
      Sunrise/resources/bootflow/texture_80A1461F.dds
  17. BIN
      Sunrise/resources/bootflow/texture_80A14621.dds
  18. BIN
      Sunrise/resources/bootflow/texture_80A14624.dds
  19. BIN
      Sunrise/resources/bootflow/texture_80A14625.dds
  20. BIN
      Sunrise/resources/bootflow/texture_80A14628.dds
  21. BIN
      Sunrise/resources/bootflow/texture_80A14629.dds
  22. BIN
      Sunrise/resources/bootflow/texture_80A1462B.dds
  23. BIN
      Sunrise/resources/bootflow/texture_80A1462E.dds
  24. BIN
      Sunrise/resources/bootflow/texture_80A1462F.dds
  25. BIN
      Sunrise/resources/bootflow/texture_80A14631.dds
  26. BIN
      Sunrise/resources/bootflow/texture_80A14633.dds
  27. BIN
      Sunrise/resources/bootflow/texture_80A14636.dds
  28. BIN
      Sunrise/resources/bootflow/texture_80A14639.dds
  29. BIN
      Sunrise/resources/bootflow/texture_80A146CC.dds
  30. BIN
      Sunrise/resources/bootflow/texture_80A146CF.dds
  31. BIN
      Sunrise/resources/bootflow/texture_80A146D3.dds
  32. 33 0
      Sunrise/resources/resource.h
  33. 30 0
      Sunrise/resources/sunrise.rc
  34. 456 0
      Sunrise/src/client/hooks/bootflow/bootflow_texture_override.cpp
  35. 19 0
      Sunrise/src/client/hooks/bootflow/bootflow_texture_override.h
  36. 8 0
      Sunrise/src/client/runtime/client_runtime_lifecycle.cpp
  37. 9 0
      Sunrise/src/steam/runtime/steam_lifecycle.cpp

+ 2 - 0
Sunrise/Sunrise.vcxproj

@@ -268,6 +268,7 @@
     <ClCompile Include="src\client\hooks\external_server\external_server_route.cpp" />
     <ClCompile Include="src\client\hooks\external_server\external_server_setopt_guard.cpp" />
     <ClCompile Include="src\client\hooks\bootflow\bootflow_hook_lifecycle.cpp" />
+    <ClCompile Include="src\client\hooks\bootflow\bootflow_texture_override.cpp" />
     <ClCompile Include="src\client\hooks\bootflow\character_select_hold.cpp" />
     <ClCompile Include="src\client\hooks\bootflow\composition_check.cpp" />
     <ClCompile Include="src\client\hooks\bootflow\orbit_handoff.cpp" />
@@ -1755,6 +1756,7 @@
     <ClInclude Include="src\client\hooks\net_tick_probe\net_tick_probe.h" />
     <ClInclude Include="src\client\hooks\bootflow\internal.h" />
     <ClInclude Include="src\client\hooks\bootflow\bootflow_hook_lifecycle.h" />
+    <ClInclude Include="src\client\hooks\bootflow\bootflow_texture_override.h" />
     <ClInclude Include="src\client\hooks\bootflow\spawn\spawn_gate_record_dump.h" />
     <ClInclude Include="src\client\hooks\bootflow\spawn\probe.h" />
     <ClInclude Include="src\client\hooks\config_getter\config_getter_answers.h" />

BIN
Sunrise/resources/bootflow/texture_80A145C1.dds


BIN
Sunrise/resources/bootflow/texture_80A145DF.dds


BIN
Sunrise/resources/bootflow/texture_80A145E4.dds


BIN
Sunrise/resources/bootflow/texture_80A145E7.dds


BIN
Sunrise/resources/bootflow/texture_80A145EF.dds


BIN
Sunrise/resources/bootflow/texture_80A145FB.dds


BIN
Sunrise/resources/bootflow/texture_80A145FF.dds


BIN
Sunrise/resources/bootflow/texture_80A14601.dds


BIN
Sunrise/resources/bootflow/texture_80A14604.dds


BIN
Sunrise/resources/bootflow/texture_80A14607.dds


BIN
Sunrise/resources/bootflow/texture_80A1460B.dds


BIN
Sunrise/resources/bootflow/texture_80A1460E.dds


BIN
Sunrise/resources/bootflow/texture_80A14613.dds


BIN
Sunrise/resources/bootflow/texture_80A1461D.dds


BIN
Sunrise/resources/bootflow/texture_80A1461F.dds


BIN
Sunrise/resources/bootflow/texture_80A14621.dds


BIN
Sunrise/resources/bootflow/texture_80A14624.dds


BIN
Sunrise/resources/bootflow/texture_80A14625.dds


BIN
Sunrise/resources/bootflow/texture_80A14628.dds


BIN
Sunrise/resources/bootflow/texture_80A14629.dds


BIN
Sunrise/resources/bootflow/texture_80A1462B.dds


BIN
Sunrise/resources/bootflow/texture_80A1462E.dds


BIN
Sunrise/resources/bootflow/texture_80A1462F.dds


BIN
Sunrise/resources/bootflow/texture_80A14631.dds


BIN
Sunrise/resources/bootflow/texture_80A14633.dds


BIN
Sunrise/resources/bootflow/texture_80A14636.dds


BIN
Sunrise/resources/bootflow/texture_80A14639.dds


BIN
Sunrise/resources/bootflow/texture_80A146CC.dds


BIN
Sunrise/resources/bootflow/texture_80A146CF.dds


BIN
Sunrise/resources/bootflow/texture_80A146D3.dds


+ 33 - 0
Sunrise/resources/resource.h

@@ -10,6 +10,39 @@
 #define IDR_LOGO_SHEET 104
 /** The next module-local RCDATA identifier embeds the required PUC-Lua MIT notice. */
 #define IDR_LUA_LICENSE 105
+
+/** User-authored bootflow DDS files consumed by the runtime TagHash override. */
+#define IDR_BOOTFLOW_TEXTURE_80A145C1 2001
+#define IDR_BOOTFLOW_TEXTURE_80A145DF 2002
+#define IDR_BOOTFLOW_TEXTURE_80A145E4 2003
+#define IDR_BOOTFLOW_TEXTURE_80A145E7 2004
+#define IDR_BOOTFLOW_TEXTURE_80A145EF 2005
+#define IDR_BOOTFLOW_TEXTURE_80A145FB 2006
+#define IDR_BOOTFLOW_TEXTURE_80A145FF 2007
+#define IDR_BOOTFLOW_TEXTURE_80A14601 2008
+#define IDR_BOOTFLOW_TEXTURE_80A14604 2009
+#define IDR_BOOTFLOW_TEXTURE_80A14607 2010
+#define IDR_BOOTFLOW_TEXTURE_80A1460B 2011
+#define IDR_BOOTFLOW_TEXTURE_80A1460E 2012
+#define IDR_BOOTFLOW_TEXTURE_80A14613 2013
+#define IDR_BOOTFLOW_TEXTURE_80A1461D 2014
+#define IDR_BOOTFLOW_TEXTURE_80A1461F 2015
+#define IDR_BOOTFLOW_TEXTURE_80A14621 2016
+#define IDR_BOOTFLOW_TEXTURE_80A14624 2017
+#define IDR_BOOTFLOW_TEXTURE_80A14625 2018
+#define IDR_BOOTFLOW_TEXTURE_80A14628 2019
+#define IDR_BOOTFLOW_TEXTURE_80A14629 2020
+#define IDR_BOOTFLOW_TEXTURE_80A1462B 2021
+#define IDR_BOOTFLOW_TEXTURE_80A1462E 2022
+#define IDR_BOOTFLOW_TEXTURE_80A1462F 2023
+#define IDR_BOOTFLOW_TEXTURE_80A14631 2024
+#define IDR_BOOTFLOW_TEXTURE_80A14633 2025
+#define IDR_BOOTFLOW_TEXTURE_80A14636 2026
+#define IDR_BOOTFLOW_TEXTURE_80A14639 2027
+#define IDR_BOOTFLOW_TEXTURE_80A146CC 2028
+#define IDR_BOOTFLOW_TEXTURE_80A146CF 2029
+#define IDR_BOOTFLOW_TEXTURE_80A146D3 2030
+
 /** The four numeric fields of the version resource, in FILEVERSION order. */
 #define SUNRISE_VER_MAJOR 0
 #define SUNRISE_VER_MINOR 4

+ 30 - 0
Sunrise/resources/sunrise.rc

@@ -6,6 +6,36 @@ IDR_DEFAULT_SETTINGS RCDATA "default_settings.json"
 IDR_IMGUI_LICENSE RCDATA "../vendor/imgui/LICENSE.txt"
 IDR_DETOURS_LICENSE RCDATA "../vendor/detours/LICENSE.md"
 IDR_LUA_LICENSE RCDATA "../vendor/lua/LICENSE"
+IDR_BOOTFLOW_TEXTURE_80A145C1 RCDATA "bootflow/texture_80A145C1.dds"
+IDR_BOOTFLOW_TEXTURE_80A145DF RCDATA "bootflow/texture_80A145DF.dds"
+IDR_BOOTFLOW_TEXTURE_80A145E4 RCDATA "bootflow/texture_80A145E4.dds"
+IDR_BOOTFLOW_TEXTURE_80A145E7 RCDATA "bootflow/texture_80A145E7.dds"
+IDR_BOOTFLOW_TEXTURE_80A145EF RCDATA "bootflow/texture_80A145EF.dds"
+IDR_BOOTFLOW_TEXTURE_80A145FB RCDATA "bootflow/texture_80A145FB.dds"
+IDR_BOOTFLOW_TEXTURE_80A145FF RCDATA "bootflow/texture_80A145FF.dds"
+IDR_BOOTFLOW_TEXTURE_80A14601 RCDATA "bootflow/texture_80A14601.dds"
+IDR_BOOTFLOW_TEXTURE_80A14604 RCDATA "bootflow/texture_80A14604.dds"
+IDR_BOOTFLOW_TEXTURE_80A14607 RCDATA "bootflow/texture_80A14607.dds"
+IDR_BOOTFLOW_TEXTURE_80A1460B RCDATA "bootflow/texture_80A1460B.dds"
+IDR_BOOTFLOW_TEXTURE_80A1460E RCDATA "bootflow/texture_80A1460E.dds"
+IDR_BOOTFLOW_TEXTURE_80A14613 RCDATA "bootflow/texture_80A14613.dds"
+IDR_BOOTFLOW_TEXTURE_80A1461D RCDATA "bootflow/texture_80A1461D.dds"
+IDR_BOOTFLOW_TEXTURE_80A1461F RCDATA "bootflow/texture_80A1461F.dds"
+IDR_BOOTFLOW_TEXTURE_80A14621 RCDATA "bootflow/texture_80A14621.dds"
+IDR_BOOTFLOW_TEXTURE_80A14624 RCDATA "bootflow/texture_80A14624.dds"
+IDR_BOOTFLOW_TEXTURE_80A14625 RCDATA "bootflow/texture_80A14625.dds"
+IDR_BOOTFLOW_TEXTURE_80A14628 RCDATA "bootflow/texture_80A14628.dds"
+IDR_BOOTFLOW_TEXTURE_80A14629 RCDATA "bootflow/texture_80A14629.dds"
+IDR_BOOTFLOW_TEXTURE_80A1462B RCDATA "bootflow/texture_80A1462B.dds"
+IDR_BOOTFLOW_TEXTURE_80A1462E RCDATA "bootflow/texture_80A1462E.dds"
+IDR_BOOTFLOW_TEXTURE_80A1462F RCDATA "bootflow/texture_80A1462F.dds"
+IDR_BOOTFLOW_TEXTURE_80A14631 RCDATA "bootflow/texture_80A14631.dds"
+IDR_BOOTFLOW_TEXTURE_80A14633 RCDATA "bootflow/texture_80A14633.dds"
+IDR_BOOTFLOW_TEXTURE_80A14636 RCDATA "bootflow/texture_80A14636.dds"
+IDR_BOOTFLOW_TEXTURE_80A14639 RCDATA "bootflow/texture_80A14639.dds"
+IDR_BOOTFLOW_TEXTURE_80A146CC RCDATA "bootflow/texture_80A146CC.dds"
+IDR_BOOTFLOW_TEXTURE_80A146CF RCDATA "bootflow/texture_80A146CF.dds"
+IDR_BOOTFLOW_TEXTURE_80A146D3 RCDATA "bootflow/texture_80A146D3.dds"
 IDR_LOGO_SHEET RCDATA "logo_sheet.png"
 
 VS_VERSION_INFO VERSIONINFO

+ 456 - 0
Sunrise/src/client/hooks/bootflow/bootflow_texture_override.cpp

@@ -0,0 +1,456 @@
+#include "bootflow_texture_override.h"
+
+#include <Windows.h>
+
+#include <algorithm>
+#include <array>
+#include <cstddef>
+#include <cstdint>
+#include <cstdio>
+#include <cstring>
+#include <span>
+#include <string_view>
+
+#include "../../../../resources/resource.h"
+#include "../../../core/logging/log.h"
+#include "../../hooking/detour.h"
+#include "../../patterns/image_scan.h"
+
+namespace sunrise::client::hooks::bootflow::texture_override {
+namespace {
+
+using patterns::scan_main_image_unique;
+using patterns::signature;
+using patterns::signature_length;
+
+/**
+ * Resourcerer's GPU-entry dispatcher. Its second argument is the TagHash and its third and fourth
+ * arguments are the decoded entry pointer and byte count. The switch immediately after this
+ * prologue distinguishes the GPU resource classes.
+ */
+constexpr std::string_view kGpuEntryDispatcherText =
+    "48 89 5C 24 08 48 89 6C 24 10 48 89 74 24 18 57 48 83 EC 20 "
+    "49 8B F9 49 8B F0 8B DA 8B E9 E8 ? ? ? ? 84 C0 0F 85 ? ? ? ? "
+    "8D 45 FF 83 F8 12";
+constexpr auto kGpuEntryDispatcher =
+    signature<signature_length(kGpuEntryDispatcherText)>(kGpuEntryDispatcherText);
+
+constexpr std::size_t kTigerTextureHeaderSize = 0x28;
+constexpr std::size_t kDdsLegacyHeaderSize = 4 + 124;
+constexpr std::size_t kDdsDx10HeaderSize = kDdsLegacyHeaderSize + 20;
+constexpr std::uint32_t kDdsMagic = 0x20534444U;
+constexpr std::uint32_t kDx10FourCc = 0x30315844U;
+constexpr std::uint32_t kDdsPixelAlphaPixels = 0x1U;
+constexpr std::uint32_t kDdsPixelAlpha = 0x2U;
+constexpr std::uint32_t kDdsPixelFourCc = 0x4U;
+constexpr std::uint32_t kDdsPixelRgb = 0x40U;
+constexpr std::uint32_t kDdsPixelLuminance = 0x20000U;
+constexpr std::uint32_t kDdsResourceTexture2d = 3U;
+constexpr std::uint32_t kDdsResourceMiscCube = 0x4U;
+constexpr std::uint16_t kTigerTextureMarker = 0xCAFEU;
+constexpr std::uint32_t kGpuTextureClass = 1U;
+
+constexpr std::size_t kTigerDataSizeOffset = 0x00;
+constexpr std::size_t kTigerFormatOffset = 0x04;
+constexpr std::size_t kTigerMarkerOffset = 0x0C;
+constexpr std::size_t kTigerWidthOffset = 0x0E;
+constexpr std::size_t kTigerHeightOffset = 0x10;
+constexpr std::size_t kTigerDepthOffset = 0x12;
+constexpr std::size_t kTigerArraySizeOffset = 0x14;
+
+/** Exact stock texture-header/data pairs selected from package 0x010A. */
+struct AssetSpec final {
+    std::uint32_t headerTag{};
+    std::uint32_t dataTag{};
+    int resourceId{};
+};
+
+constexpr std::array kAssetSpecs{
+    AssetSpec{0x80A145C0U, 0x80A145C1U, IDR_BOOTFLOW_TEXTURE_80A145C1},
+    AssetSpec{0x80A145DEU, 0x80A145DFU, IDR_BOOTFLOW_TEXTURE_80A145DF},
+    AssetSpec{0x80A145E3U, 0x80A145E4U, IDR_BOOTFLOW_TEXTURE_80A145E4},
+    AssetSpec{0x80A145E6U, 0x80A145E7U, IDR_BOOTFLOW_TEXTURE_80A145E7},
+    AssetSpec{0x80A145EEU, 0x80A145EFU, IDR_BOOTFLOW_TEXTURE_80A145EF},
+    AssetSpec{0x80A145FAU, 0x80A145FBU, IDR_BOOTFLOW_TEXTURE_80A145FB},
+    AssetSpec{0x80A145FEU, 0x80A145FFU, IDR_BOOTFLOW_TEXTURE_80A145FF},
+    AssetSpec{0x80A14602U, 0x80A14601U, IDR_BOOTFLOW_TEXTURE_80A14601},
+    AssetSpec{0x80A14605U, 0x80A14604U, IDR_BOOTFLOW_TEXTURE_80A14604},
+    AssetSpec{0x80A14608U, 0x80A14607U, IDR_BOOTFLOW_TEXTURE_80A14607},
+    AssetSpec{0x80A1460AU, 0x80A1460BU, IDR_BOOTFLOW_TEXTURE_80A1460B},
+    AssetSpec{0x80A1460DU, 0x80A1460EU, IDR_BOOTFLOW_TEXTURE_80A1460E},
+    AssetSpec{0x80A14614U, 0x80A14613U, IDR_BOOTFLOW_TEXTURE_80A14613},
+    AssetSpec{0x80A1461CU, 0x80A1461DU, IDR_BOOTFLOW_TEXTURE_80A1461D},
+    AssetSpec{0x80A14620U, 0x80A1461FU, IDR_BOOTFLOW_TEXTURE_80A1461F},
+    AssetSpec{0x80A14622U, 0x80A14621U, IDR_BOOTFLOW_TEXTURE_80A14621},
+    AssetSpec{0x80A14623U, 0x80A14624U, IDR_BOOTFLOW_TEXTURE_80A14624},
+    AssetSpec{0x80A14626U, 0x80A14625U, IDR_BOOTFLOW_TEXTURE_80A14625},
+    AssetSpec{0x80A14627U, 0x80A14628U, IDR_BOOTFLOW_TEXTURE_80A14628},
+    AssetSpec{0x80A1462AU, 0x80A14629U, IDR_BOOTFLOW_TEXTURE_80A14629},
+    AssetSpec{0x80A1462CU, 0x80A1462BU, IDR_BOOTFLOW_TEXTURE_80A1462B},
+    AssetSpec{0x80A1462DU, 0x80A1462EU, IDR_BOOTFLOW_TEXTURE_80A1462E},
+    AssetSpec{0x80A14630U, 0x80A1462FU, IDR_BOOTFLOW_TEXTURE_80A1462F},
+    AssetSpec{0x80A14632U, 0x80A14631U, IDR_BOOTFLOW_TEXTURE_80A14631},
+    AssetSpec{0x80A14634U, 0x80A14633U, IDR_BOOTFLOW_TEXTURE_80A14633},
+    AssetSpec{0x80A14635U, 0x80A14636U, IDR_BOOTFLOW_TEXTURE_80A14636},
+    AssetSpec{0x80A14638U, 0x80A14639U, IDR_BOOTFLOW_TEXTURE_80A14639},
+    AssetSpec{0x80A146CDU, 0x80A146CCU, IDR_BOOTFLOW_TEXTURE_80A146CC},
+    AssetSpec{0x80A146D0U, 0x80A146CFU, IDR_BOOTFLOW_TEXTURE_80A146CF},
+    AssetSpec{0x80A146D2U, 0x80A146D3U, IDR_BOOTFLOW_TEXTURE_80A146D3},
+};
+
+struct DdsView final {
+    const std::byte* pixels{};
+    std::uint32_t pixelSize{};
+    std::uint32_t format{};
+    std::uint16_t width{};
+    std::uint16_t height{};
+    std::uint16_t depth{};
+    std::uint16_t arraySize{};
+};
+
+struct Asset final {
+    AssetSpec spec{};
+    DdsView dds{};
+    std::array<std::byte, kTigerTextureHeaderSize> header{};
+    bool headerReady{};
+    bool reported{};
+};
+
+using GpuEntryDispatcher = std::uint64_t(__fastcall*)(std::uint32_t resourceClass,
+                                                      std::uint32_t tag,
+                                                      const void* decoded,
+                                                      std::uint64_t decodedSize) noexcept;
+
+hooking::detour::Handle g_handle{};
+SRWLOCK g_assetLock{SRWLOCK_INIT};
+std::array<Asset, kAssetSpecs.size()> g_assets{};
+
+template <typename Value>
+[[nodiscard]] bool
+load_value(const std::byte* bytes, std::size_t size, std::size_t offset, Value& output) noexcept {
+    if (bytes == nullptr || offset > size || sizeof(Value) > size - offset) {
+        return false;
+    }
+    std::memcpy(&output, bytes + offset, sizeof output);
+    return true;
+}
+
+template <typename Value>
+void store_value(std::span<std::byte> bytes, std::size_t offset, Value value) noexcept {
+    if (offset <= bytes.size() && sizeof(Value) <= bytes.size() - offset) {
+        std::memcpy(bytes.data() + offset, &value, sizeof value);
+    }
+}
+
+[[nodiscard]] constexpr std::uint32_t
+four_cc(char first, char second, char third, char fourth) noexcept {
+    return static_cast<std::uint32_t>(static_cast<unsigned char>(first))
+           | (static_cast<std::uint32_t>(static_cast<unsigned char>(second)) << 8U)
+           | (static_cast<std::uint32_t>(static_cast<unsigned char>(third)) << 16U)
+           | (static_cast<std::uint32_t>(static_cast<unsigned char>(fourth)) << 24U);
+}
+
+/** Converts the legacy DDS formats used by the supplied bootflow assets to DXGI values. */
+[[nodiscard]] bool
+legacy_format(const std::byte* bytes, std::size_t size, std::uint32_t& output) noexcept {
+    std::uint32_t flags = 0;
+    std::uint32_t formatFourCc = 0;
+    std::uint32_t bits = 0;
+    std::uint32_t red = 0;
+    std::uint32_t green = 0;
+    std::uint32_t blue = 0;
+    std::uint32_t alpha = 0;
+    if (!load_value(bytes, size, 0x50, flags) || !load_value(bytes, size, 0x54, formatFourCc)
+        || !load_value(bytes, size, 0x58, bits) || !load_value(bytes, size, 0x5C, red)
+        || !load_value(bytes, size, 0x60, green) || !load_value(bytes, size, 0x64, blue)
+        || !load_value(bytes, size, 0x68, alpha)) {
+        return false;
+    }
+    if ((flags & kDdsPixelFourCc) != 0) {
+        switch (formatFourCc) {
+        case four_cc('D', 'X', 'T', '1'):
+            output = 71U;
+            return true;
+        case four_cc('D', 'X', 'T', '3'):
+            output = 74U;
+            return true;
+        case four_cc('D', 'X', 'T', '5'):
+            output = 77U;
+            return true;
+        case four_cc('A', 'T', 'I', '1'):
+        case four_cc('B', 'C', '4', 'U'):
+            output = 80U;
+            return true;
+        case four_cc('A', 'T', 'I', '2'):
+        case four_cc('B', 'C', '5', 'U'):
+            output = 83U;
+            return true;
+        default:
+            return false;
+        }
+    }
+    if ((flags & kDdsPixelRgb) != 0 && bits == 32U) {
+        if (red == 0x000000FFU && green == 0x0000FF00U && blue == 0x00FF0000U
+            && alpha == 0xFF000000U) {
+            output = 28U;
+            return true;
+        }
+        if (red == 0x00FF0000U && green == 0x0000FF00U && blue == 0x000000FFU) {
+            output = alpha == 0xFF000000U ? 87U : 88U;
+            return alpha == 0xFF000000U || alpha == 0U;
+        }
+    }
+    if ((flags & kDdsPixelRgb) != 0 && bits == 16U) {
+        if (red == 0xF800U && green == 0x07E0U && blue == 0x001FU && alpha == 0U) {
+            output = 85U;
+            return true;
+        }
+        if (red == 0x7C00U && green == 0x03E0U && blue == 0x001FU && alpha == 0x8000U) {
+            output = 86U;
+            return true;
+        }
+        if (red == 0x0F00U && green == 0x00F0U && blue == 0x000FU && alpha == 0xF000U) {
+            output = 115U;
+            return true;
+        }
+    }
+    if ((flags & kDdsPixelLuminance) != 0 && bits == 8U && red == 0xFFU) {
+        output = 61U;
+        return true;
+    }
+    if ((flags & kDdsPixelLuminance) != 0 && (flags & kDdsPixelAlphaPixels) != 0 && bits == 16U
+        && red == 0x00FFU && alpha == 0xFF00U) {
+        output = 49U;
+        return true;
+    }
+    if ((flags & kDdsPixelAlpha) != 0 && bits == 8U && alpha == 0xFFU) {
+        output = 65U;
+        return true;
+    }
+    return false;
+}
+
+/** Parses one embedded 2D DDS without allocating or copying its pixel payload. */
+[[nodiscard]] bool parse_dds(const std::byte* bytes, std::size_t size, DdsView& output) noexcept {
+    output = {};
+    std::uint32_t magic = 0;
+    std::uint32_t headerSize = 0;
+    std::uint32_t pixelHeaderSize = 0;
+    std::uint32_t pixelFlags = 0;
+    std::uint32_t formatFourCc = 0;
+    if (size < kDdsLegacyHeaderSize || !load_value(bytes, size, 0x00, magic)
+        || !load_value(bytes, size, 0x04, headerSize)
+        || !load_value(bytes, size, 0x4C, pixelHeaderSize)
+        || !load_value(bytes, size, 0x50, pixelFlags)
+        || !load_value(bytes, size, 0x54, formatFourCc) || magic != kDdsMagic || headerSize != 124U
+        || pixelHeaderSize != 32U) {
+        return false;
+    }
+    const bool dx10 = (pixelFlags & kDdsPixelFourCc) != 0 && formatFourCc == kDx10FourCc;
+    const std::size_t pixelOffset = dx10 ? kDdsDx10HeaderSize : kDdsLegacyHeaderSize;
+    std::uint32_t width = 0;
+    std::uint32_t height = 0;
+    std::uint32_t depth = 0;
+    std::uint32_t arraySize = 1;
+    std::uint32_t resourceDimension = kDdsResourceTexture2d;
+    std::uint32_t miscFlag = 0;
+    std::uint32_t format = 0;
+    if (size <= pixelOffset || !load_value(bytes, size, 0x10, width)
+        || !load_value(bytes, size, 0x0C, height) || !load_value(bytes, size, 0x18, depth)) {
+        return false;
+    }
+    if (dx10) {
+        if (!load_value(bytes, size, 0x80, format)
+            || !load_value(bytes, size, 0x84, resourceDimension)
+            || !load_value(bytes, size, 0x88, miscFlag)
+            || !load_value(bytes, size, 0x8C, arraySize)) {
+            return false;
+        }
+    } else if (!legacy_format(bytes, size, format)) {
+        return false;
+    }
+    const std::size_t pixelSize = size - pixelOffset;
+    if (width == 0 || height == 0 || width > 0xFFFFU || height > 0xFFFFU || pixelSize == 0
+        || pixelSize > 0xFFFFFFFFULL || format == 0 || resourceDimension != kDdsResourceTexture2d
+        || (miscFlag & kDdsResourceMiscCube) != 0 || arraySize == 0 || arraySize > 0xFFFFU) {
+        return false;
+    }
+    output = DdsView{bytes + pixelOffset,
+                     static_cast<std::uint32_t>(pixelSize),
+                     format,
+                     static_cast<std::uint16_t>(width),
+                     static_cast<std::uint16_t>(height),
+                     1,
+                     static_cast<std::uint16_t>(arraySize)};
+    return true;
+}
+
+/** Loads and validates every embedded DDS before the detour can expose any of them. */
+[[nodiscard]] bool load_assets(HMODULE module) noexcept {
+    if (module == nullptr) {
+        return false;
+    }
+    for (std::size_t index = 0; index < kAssetSpecs.size(); ++index) {
+        const AssetSpec& spec = kAssetSpecs[index];
+        const HRSRC resource = FindResourceW(module, MAKEINTRESOURCEW(spec.resourceId), RT_RCDATA);
+        if (resource == nullptr) {
+            return false;
+        }
+        const HGLOBAL loaded = LoadResource(module, resource);
+        const DWORD size = SizeofResource(module, resource);
+        const auto* bytes = static_cast<const std::byte*>(LockResource(loaded));
+        Asset asset{};
+        asset.spec = spec;
+        if (loaded == nullptr || bytes == nullptr || size == 0
+            || !parse_dds(bytes, static_cast<std::size_t>(size), asset.dds)) {
+            return false;
+        }
+        g_assets[index] = asset;
+    }
+    return true;
+}
+
+/** @return The asset owning this exact header or data TagHash. */
+[[nodiscard]] Asset* find_asset(std::uint32_t tag, bool& header) noexcept {
+    for (Asset& asset : g_assets) {
+        if (asset.spec.headerTag == tag) {
+            header = true;
+            return &asset;
+        }
+        if (asset.spec.dataTag == tag) {
+            header = false;
+            return &asset;
+        }
+    }
+    return nullptr;
+}
+
+/** Builds one replacement Tiger descriptor from its stock descriptor and embedded DDS. */
+[[nodiscard]] const void*
+prepare_header(Asset& asset, const void* stock, std::uint64_t stockSize, bool& report) noexcept {
+    report = false;
+    if (stock == nullptr || stockSize < kTigerTextureHeaderSize) {
+        return stock;
+    }
+    AcquireSRWLockExclusive(&g_assetLock);
+    if (!asset.headerReady) {
+        std::memcpy(asset.header.data(), stock, asset.header.size());
+        std::uint16_t marker = 0;
+        std::memcpy(&marker, asset.header.data() + kTigerMarkerOffset, sizeof marker);
+        if (marker == kTigerTextureMarker) {
+            const std::span header(asset.header);
+            store_value(header, kTigerDataSizeOffset, asset.dds.pixelSize);
+            store_value(header, kTigerFormatOffset, asset.dds.format);
+            store_value(header, kTigerWidthOffset, asset.dds.width);
+            store_value(header, kTigerHeightOffset, asset.dds.height);
+            store_value(header, kTigerDepthOffset, asset.dds.depth);
+            store_value(header, kTigerArraySizeOffset, asset.dds.arraySize);
+            asset.headerReady = true;
+        }
+    }
+    if (asset.headerReady && !asset.reported) {
+        asset.reported = true;
+        report = true;
+    }
+    const void* result = asset.headerReady ? asset.header.data() : stock;
+    ReleaseSRWLockExclusive(&g_assetLock);
+    return result;
+}
+
+void report_override(const Asset& asset) noexcept {
+    std::array<char, 160> line{};
+    const int written = std::snprintf(line.data(),
+                                      line.size(),
+                                      "ev=bootflow_texture stage=entry tag=0x%08X size=%u "
+                                      "width=%u height=%u result=override",
+                                      static_cast<unsigned>(asset.spec.dataTag),
+                                      static_cast<unsigned>(asset.dds.pixelSize),
+                                      static_cast<unsigned>(asset.dds.width),
+                                      static_cast<unsigned>(asset.dds.height));
+    if (written > 0) {
+        core::log::write(
+            core::log::Channel::client,
+            core::log::Level::info,
+            {line.data(), (std::min)(static_cast<std::size_t>(written), line.size() - 1)});
+    }
+}
+
+/** Replaces only selected decoded GPU texture entries, then preserves the native dispatcher. */
+std::uint64_t __fastcall dispatch(std::uint32_t resourceClass,
+                                  std::uint32_t tag,
+                                  const void* decoded,
+                                  std::uint64_t decodedSize) noexcept {
+    const auto original = reinterpret_cast<GpuEntryDispatcher>(g_handle.original);
+    if (original == nullptr) {
+        return 7;
+    }
+    bool header = false;
+    Asset* const asset = resourceClass == kGpuTextureClass ? find_asset(tag, header) : nullptr;
+    if (asset == nullptr) {
+        return original(resourceClass, tag, decoded, decodedSize);
+    }
+    if (!header) {
+        return original(resourceClass, tag, asset->dds.pixels, asset->dds.pixelSize);
+    }
+    bool report = false;
+    const void* const replacement = prepare_header(*asset, decoded, decodedSize, report);
+    if (report) {
+        report_override(*asset);
+    }
+    return original(resourceClass,
+                    tag,
+                    replacement,
+                    replacement == decoded ? decodedSize : kTigerTextureHeaderSize);
+}
+
+void clear_assets() noexcept {
+    AcquireSRWLockExclusive(&g_assetLock);
+    g_assets = {};
+    ReleaseSRWLockExclusive(&g_assetLock);
+}
+
+} // namespace
+
+/** Loads embedded DDS files and attaches the decoded GPU-entry dispatcher. */
+bool install(void* module) noexcept {
+    if (g_handle.attached) {
+        return true;
+    }
+    if (!load_assets(static_cast<HMODULE>(module))) {
+        clear_assets();
+        core::log::write(core::log::Channel::client,
+                         core::log::Level::error,
+                         "ev=bootflow_texture stage=resources result=fail");
+        return false;
+    }
+    std::byte* const target =
+        scan_main_image_unique(kGpuEntryDispatcher, "bootflow_gpu_entry_dispatcher");
+    const hooking::detour::Spec spec{target, reinterpret_cast<void*>(&dispatch)};
+    if (target == nullptr || !hooking::detour::install(spec, g_handle)) {
+        clear_assets();
+        core::log::write(core::log::Channel::client,
+                         core::log::Level::error,
+                         "ev=bootflow_texture stage=attach result=fail");
+        return false;
+    }
+    core::log::write(core::log::Channel::client,
+                     core::log::Level::info,
+                     "ev=bootflow_texture stage=attach count=30 result=ok");
+    return true;
+}
+
+/** Detaches before releasing the resource views and generated Tiger descriptors. */
+bool uninstall() noexcept {
+    if (g_handle.attached && !hooking::detour::uninstall(g_handle)) {
+        return false;
+    }
+    clear_assets();
+    return true;
+}
+
+/** @return True while the decoded GPU-entry dispatcher is attached. */
+bool is_installed() noexcept {
+    return g_handle.attached;
+}
+
+} // namespace sunrise::client::hooks::bootflow::texture_override

+ 19 - 0
Sunrise/src/client/hooks/bootflow/bootflow_texture_override.h

@@ -0,0 +1,19 @@
+#pragma once
+
+namespace sunrise::client::hooks::bootflow::texture_override {
+
+/**
+ * Replaces selected decoded bootflow texture entries by TagHash with DDS data embedded in Sunrise.
+ * The stock package remains registered and unchanged.
+ * @param module Sunrise DLL module that owns the RCDATA resources.
+ * @return True while the native GPU-resource callback detour is attached.
+ */
+[[nodiscard]] bool install(void* module) noexcept;
+
+/** @return True when the callback detour is detached. */
+[[nodiscard]] bool uninstall() noexcept;
+
+/** @return True while decoded bootflow texture entries are being overridden. */
+[[nodiscard]] bool is_installed() noexcept;
+
+} // namespace sunrise::client::hooks::bootflow::texture_override

+ 8 - 0
Sunrise/src/client/runtime/client_runtime_lifecycle.cpp

@@ -7,6 +7,7 @@
 #include "../hooks/async_io/async_io_lifetime_guard.h"
 #include "../hooks/bitmap/bitmap_hook_lifecycle.h"
 #include "../hooks/bootflow/bootflow_hook_lifecycle.h"
+#include "../hooks/bootflow/bootflow_texture_override.h"
 #include "../hooks/config_getter/config_getter_lifecycle.h"
 #include "../hooks/cursor/runtime.h"
 #include "../hooks/graphics/graphics_hook_lifecycle.h"
@@ -76,6 +77,13 @@ bool shutdown() noexcept {
         ReleaseSRWLockExclusive(&runtime::g_lock);
         return false;
     }
+    if (!hooks::bootflow::texture_override::uninstall()) {
+        core::log::write(core::log::Channel::client,
+                         core::log::Level::error,
+                         "ev=shutdown stage=bootflow_texture result=fail");
+        ReleaseSRWLockExclusive(&runtime::g_lock);
+        return false;
+    }
     if (!hooks::package_trust::uninstall()) {
         core::log::write(core::log::Channel::client,
                          core::log::Level::error,

+ 9 - 0
Sunrise/src/steam/runtime/steam_lifecycle.cpp

@@ -3,6 +3,7 @@
 #include <atomic>
 
 #include "../../client/graphics/wine_compat.h"
+#include "../../client/hooks/bootflow/bootflow_texture_override.h"
 #include "../../client/hooks/egress/runtime.h"
 #include "../../client/hooks/package_trust/package_trust_bypass.h"
 #include "../../client/runtime/runtime.h"
@@ -76,6 +77,14 @@ bool initialize(void* module) noexcept {
             (void)core::shutdown();
             return false;
         }
+        // Bootflow GPU entries can load before the first Steam callback pump. The decoded-entry
+        // override must therefore attach here while the stock `_unp1` package remains registered
+        // through its native path.
+        if (!client::hooks::bootflow::texture_override::install(module)) {
+            core::log::write(core::log::Channel::client,
+                             core::log::Level::warn,
+                             "ev=steam_init stage=bootflow_texture result=fail");
+        }
         advance_context_generation();
         g_initialized.store(true, std::memory_order_release);
         core::log::write(