bootflow_texture_override.cpp 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462
  1. #include "bootflow_texture_override.h"
  2. #include <Windows.h>
  3. #include <algorithm>
  4. #include <array>
  5. #include <cstddef>
  6. #include <cstdint>
  7. #include <cstdio>
  8. #include <cstring>
  9. #include <span>
  10. #include <string_view>
  11. #include "../../../../resources/resource.h"
  12. #include "../../../core/logging/log.h"
  13. #include "../../../core/settings/settings.h"
  14. #include "../../hooking/detour.h"
  15. #include "../../patterns/image_scan.h"
  16. namespace sunrise::client::hooks::bootflow::texture_override {
  17. namespace {
  18. using patterns::scan_main_image_unique;
  19. using patterns::signature;
  20. using patterns::signature_length;
  21. /**
  22. * Resourcerer's GPU-entry dispatcher, whose switch follows this prologue.
  23. * Arguments are the resource class, the TagHash, and the decoded entry pointer and byte count.
  24. */
  25. constexpr std::string_view kGpuEntryDispatcherText =
  26. "48 89 5C 24 08 48 89 6C 24 10 48 89 74 24 18 57 48 83 EC 20 "
  27. "49 8B F9 49 8B F0 8B DA 8B E9 E8 ? ? ? ? 84 C0 0F 85 ? ? ? ? "
  28. "8D 45 FF 83 F8 12";
  29. /** Compiled form of that prologue, scanned for as a unique site in the main image. */
  30. constexpr auto kGpuEntryDispatcher =
  31. signature<signature_length(kGpuEntryDispatcherText)>(kGpuEntryDispatcherText);
  32. /** Bytes of one Tiger texture descriptor; the dispatcher reads exactly this much. */
  33. constexpr std::size_t kTigerTextureHeaderSize = 0x28;
  34. /** DDS magic plus its 124-byte header, which is where a legacy file's pixels start. */
  35. constexpr std::size_t kDdsLegacyHeaderSize = 4 + 124;
  36. /** A DX10 file adds a 20-byte extension header before its pixels. */
  37. constexpr std::size_t kDdsDx10HeaderSize = kDdsLegacyHeaderSize + 20;
  38. /** "DDS " in file order. */
  39. constexpr std::uint32_t kDdsMagic = 0x20534444U;
  40. /** "DX10" in the FourCC field, which marks the extension header. */
  41. constexpr std::uint32_t kDx10FourCc = 0x30315844U;
  42. /** DDS header size the format fixes at 124 bytes. */
  43. constexpr std::uint32_t kDdsHeaderSizeValue = 124U;
  44. /** DDS pixel-format block size the format fixes at 32 bytes. */
  45. constexpr std::uint32_t kDdsPixelFormatSize = 32U;
  46. /** DDPF_FOURCC: the pixel format is named by the FourCC field. */
  47. constexpr std::uint32_t kDdsPixelFourCc = 0x4U;
  48. /** DDPF_RGB: the pixel format is named by the channel masks. */
  49. constexpr std::uint32_t kDdsPixelRgb = 0x40U;
  50. /** D3D10_RESOURCE_DIMENSION_TEXTURE2D, the only shape this override accepts. */
  51. constexpr std::uint32_t kDdsResourceTexture2d = 3U;
  52. /** D3D10_RESOURCE_MISC_TEXTURECUBE, which this override refuses. */
  53. constexpr std::uint32_t kDdsResourceMiscCube = 0x4U;
  54. /** DXGI_FORMAT_R8G8B8A8_UNORM, the one legacy layout the embedded assets use. */
  55. constexpr std::uint32_t kDxgiR8G8B8A8Unorm = 28U;
  56. /** Bits per pixel of that layout. */
  57. constexpr std::uint32_t kRgba8BitCount = 32U;
  58. /** Channel masks of that layout, in DDS field order. */
  59. constexpr std::uint32_t kRgba8RedMask = 0x000000FFU;
  60. constexpr std::uint32_t kRgba8GreenMask = 0x0000FF00U;
  61. constexpr std::uint32_t kRgba8BlueMask = 0x00FF0000U;
  62. constexpr std::uint32_t kRgba8AlphaMask = 0xFF000000U;
  63. /** Largest width, height or array size a Tiger descriptor field can hold. */
  64. constexpr std::uint32_t kTigerDimensionLimit = 0xFFFFU;
  65. /** A Tiger texture descriptor carries this marker; anything else is not one. */
  66. constexpr std::uint16_t kTigerTextureMarker = 0xCAFEU;
  67. /** Resource class the dispatcher uses for GPU textures. */
  68. constexpr std::uint32_t kGpuTextureClass = 1U;
  69. /** The dispatcher's own bad-argument result, returned when the trampoline is gone. */
  70. constexpr std::uint64_t kDispatchUnavailable = 7U;
  71. /** Fields of a DDS file header, as byte offsets from the magic. */
  72. struct DdsLayout {
  73. // The DDS file format fixes every offset below.
  74. static constexpr std::size_t magic = 0x00;
  75. static constexpr std::size_t headerSize = 0x04;
  76. static constexpr std::size_t height = 0x0C;
  77. static constexpr std::size_t width = 0x10;
  78. static constexpr std::size_t depth = 0x18;
  79. static constexpr std::size_t pixelFormatSize = 0x4C;
  80. static constexpr std::size_t pixelFlags = 0x50;
  81. static constexpr std::size_t fourCc = 0x54;
  82. static constexpr std::size_t bitCount = 0x58;
  83. static constexpr std::size_t redMask = 0x5C;
  84. static constexpr std::size_t greenMask = 0x60;
  85. static constexpr std::size_t blueMask = 0x64;
  86. static constexpr std::size_t alphaMask = 0x68;
  87. static constexpr std::size_t dxgiFormat = 0x80;
  88. static constexpr std::size_t resourceDimension = 0x84;
  89. static constexpr std::size_t miscFlag = 0x88;
  90. static constexpr std::size_t arraySize = 0x8C;
  91. };
  92. /** Fields of a Tiger texture descriptor, as byte offsets from its base. */
  93. struct TigerLayout {
  94. // The client's descriptor layout fixes every offset below.
  95. static constexpr std::size_t dataSize = 0x00;
  96. static constexpr std::size_t format = 0x04;
  97. static constexpr std::size_t marker = 0x0C;
  98. static constexpr std::size_t width = 0x0E;
  99. static constexpr std::size_t height = 0x10;
  100. static constexpr std::size_t depth = 0x12;
  101. static constexpr std::size_t arraySize = 0x14;
  102. };
  103. /** Exact stock texture-header/data pairs selected from package 0x010A. */
  104. struct AssetSpec final {
  105. std::uint32_t headerTag{};
  106. std::uint32_t dataTag{};
  107. int resourceId{};
  108. };
  109. /** One row per replaced texture; the dispatcher is intercepted on both of its tags. */
  110. constexpr std::array kAssetSpecs{
  111. AssetSpec{0x80A145FEU, 0x80A145FFU, IDR_BOOTFLOW_TEXTURE_80A145FF},
  112. AssetSpec{0x80A14602U, 0x80A14601U, IDR_BOOTFLOW_TEXTURE_80A14601},
  113. AssetSpec{0x80A14608U, 0x80A14607U, IDR_BOOTFLOW_TEXTURE_80A14607},
  114. AssetSpec{0x80A1460DU, 0x80A1460EU, IDR_BOOTFLOW_TEXTURE_80A1460E},
  115. AssetSpec{0x80A1461CU, 0x80A1461DU, IDR_BOOTFLOW_TEXTURE_80A1461D},
  116. AssetSpec{0x80A14620U, 0x80A1461FU, IDR_BOOTFLOW_TEXTURE_80A1461F},
  117. AssetSpec{0x80A14622U, 0x80A14621U, IDR_BOOTFLOW_TEXTURE_80A14621},
  118. AssetSpec{0x80A14623U, 0x80A14624U, IDR_BOOTFLOW_TEXTURE_80A14624},
  119. AssetSpec{0x80A14626U, 0x80A14625U, IDR_BOOTFLOW_TEXTURE_80A14625},
  120. AssetSpec{0x80A14627U, 0x80A14628U, IDR_BOOTFLOW_TEXTURE_80A14628},
  121. AssetSpec{0x80A1462AU, 0x80A14629U, IDR_BOOTFLOW_TEXTURE_80A14629},
  122. AssetSpec{0x80A1462CU, 0x80A1462BU, IDR_BOOTFLOW_TEXTURE_80A1462B},
  123. AssetSpec{0x80A1462DU, 0x80A1462EU, IDR_BOOTFLOW_TEXTURE_80A1462E},
  124. AssetSpec{0x80A14630U, 0x80A1462FU, IDR_BOOTFLOW_TEXTURE_80A1462F},
  125. AssetSpec{0x80A14632U, 0x80A14631U, IDR_BOOTFLOW_TEXTURE_80A14631},
  126. AssetSpec{0x80A14634U, 0x80A14633U, IDR_BOOTFLOW_TEXTURE_80A14633},
  127. AssetSpec{0x80A14635U, 0x80A14636U, IDR_BOOTFLOW_TEXTURE_80A14636},
  128. AssetSpec{0x80A146D4U, 0x80A146D5U, IDR_BOOTFLOW_TEXTURE_80A146D5},
  129. };
  130. struct DdsView final {
  131. const std::byte* pixels{};
  132. std::uint32_t pixelSize{};
  133. std::uint32_t format{};
  134. std::uint16_t width{};
  135. std::uint16_t height{};
  136. std::uint16_t depth{};
  137. std::uint16_t arraySize{};
  138. };
  139. struct Asset final {
  140. AssetSpec spec{};
  141. DdsView dds{};
  142. std::array<std::byte, kTigerTextureHeaderSize> header{};
  143. bool headerReady{};
  144. bool reported{};
  145. };
  146. using GpuEntryDispatcher = std::uint64_t(__fastcall*)(std::uint32_t resourceClass,
  147. std::uint32_t tag,
  148. const void* decoded,
  149. std::uint64_t decodedSize) noexcept;
  150. hooking::detour::Handle g_handle{};
  151. SRWLOCK g_assetLock{SRWLOCK_INIT};
  152. std::array<Asset, kAssetSpecs.size()> g_assets{};
  153. template <typename Value>
  154. [[nodiscard]] bool
  155. load_value(const std::byte* bytes, std::size_t size, std::size_t offset, Value& output) noexcept {
  156. if (bytes == nullptr || offset > size || sizeof(Value) > size - offset) {
  157. return false;
  158. }
  159. std::memcpy(&output, bytes + offset, sizeof output);
  160. return true;
  161. }
  162. template <typename Value>
  163. void store_value(std::span<std::byte> bytes, std::size_t offset, Value value) noexcept {
  164. if (offset <= bytes.size() && sizeof(Value) <= bytes.size() - offset) {
  165. std::memcpy(bytes.data() + offset, &value, sizeof value);
  166. }
  167. }
  168. /**
  169. * Names the DXGI format of a legacy DDS pixel block.
  170. * Only R8G8B8A8_UNORM is accepted; any other authored layout is refused rather than converted.
  171. * @param bytes DDS file bytes.
  172. * @param size Bytes available.
  173. * @param output Receives the DXGI format value.
  174. * @return False when the file is not the accepted layout.
  175. */
  176. [[nodiscard]] bool
  177. legacy_format(const std::byte* bytes, std::size_t size, std::uint32_t& output) noexcept {
  178. std::uint32_t flags = 0;
  179. std::uint32_t bits = 0;
  180. std::uint32_t red = 0;
  181. std::uint32_t green = 0;
  182. std::uint32_t blue = 0;
  183. std::uint32_t alpha = 0;
  184. if (!load_value(bytes, size, DdsLayout::pixelFlags, flags)
  185. || !load_value(bytes, size, DdsLayout::bitCount, bits)
  186. || !load_value(bytes, size, DdsLayout::redMask, red)
  187. || !load_value(bytes, size, DdsLayout::greenMask, green)
  188. || !load_value(bytes, size, DdsLayout::blueMask, blue)
  189. || !load_value(bytes, size, DdsLayout::alphaMask, alpha)) {
  190. return false;
  191. }
  192. if ((flags & kDdsPixelRgb) == 0 || bits != kRgba8BitCount || red != kRgba8RedMask
  193. || green != kRgba8GreenMask || blue != kRgba8BlueMask || alpha != kRgba8AlphaMask) {
  194. return false;
  195. }
  196. output = kDxgiR8G8B8A8Unorm;
  197. return true;
  198. }
  199. /**
  200. * Parses one embedded 2D DDS without allocating or copying its pixel payload.
  201. * @param bytes DDS file bytes, which outlive the view.
  202. * @param size Bytes available.
  203. * @param output Receives a view over the file's pixels; cleared on failure.
  204. * @return False when the file is not an accepted 2D texture.
  205. */
  206. [[nodiscard]] bool parse_dds(const std::byte* bytes, std::size_t size, DdsView& output) noexcept {
  207. output = {};
  208. std::uint32_t magic = 0;
  209. std::uint32_t headerSize = 0;
  210. std::uint32_t pixelHeaderSize = 0;
  211. std::uint32_t pixelFlags = 0;
  212. std::uint32_t formatFourCc = 0;
  213. if (size < kDdsLegacyHeaderSize || !load_value(bytes, size, DdsLayout::magic, magic)
  214. || !load_value(bytes, size, DdsLayout::headerSize, headerSize)
  215. || !load_value(bytes, size, DdsLayout::pixelFormatSize, pixelHeaderSize)
  216. || !load_value(bytes, size, DdsLayout::pixelFlags, pixelFlags)
  217. || !load_value(bytes, size, DdsLayout::fourCc, formatFourCc) || magic != kDdsMagic
  218. || headerSize != kDdsHeaderSizeValue || pixelHeaderSize != kDdsPixelFormatSize) {
  219. return false;
  220. }
  221. const bool dx10 = (pixelFlags & kDdsPixelFourCc) != 0 && formatFourCc == kDx10FourCc;
  222. const std::size_t pixelOffset = dx10 ? kDdsDx10HeaderSize : kDdsLegacyHeaderSize;
  223. std::uint32_t width = 0;
  224. std::uint32_t height = 0;
  225. std::uint32_t arraySize = 1;
  226. std::uint32_t resourceDimension = kDdsResourceTexture2d;
  227. std::uint32_t miscFlag = 0;
  228. std::uint32_t format = 0;
  229. if (size <= pixelOffset || !load_value(bytes, size, DdsLayout::width, width)
  230. || !load_value(bytes, size, DdsLayout::height, height)) {
  231. return false;
  232. }
  233. if (dx10) {
  234. if (!load_value(bytes, size, DdsLayout::dxgiFormat, format)
  235. || !load_value(bytes, size, DdsLayout::resourceDimension, resourceDimension)
  236. || !load_value(bytes, size, DdsLayout::miscFlag, miscFlag)
  237. || !load_value(bytes, size, DdsLayout::arraySize, arraySize)) {
  238. return false;
  239. }
  240. } else if (!legacy_format(bytes, size, format)) {
  241. return false;
  242. }
  243. const std::size_t pixelSize = size - pixelOffset;
  244. if (width == 0 || height == 0 || width > kTigerDimensionLimit || height > kTigerDimensionLimit
  245. || pixelSize == 0 || format == 0 || resourceDimension != kDdsResourceTexture2d
  246. || (miscFlag & kDdsResourceMiscCube) != 0 || arraySize == 0
  247. || arraySize > kTigerDimensionLimit) {
  248. return false;
  249. }
  250. output = DdsView{bytes + pixelOffset,
  251. static_cast<std::uint32_t>(pixelSize),
  252. format,
  253. static_cast<std::uint16_t>(width),
  254. static_cast<std::uint16_t>(height),
  255. 1,
  256. static_cast<std::uint16_t>(arraySize)};
  257. return true;
  258. }
  259. /** Loads and validates every embedded DDS before the detour can expose any of them. */
  260. [[nodiscard]] bool load_assets(HMODULE module) noexcept {
  261. if (module == nullptr) {
  262. return false;
  263. }
  264. for (std::size_t index = 0; index < kAssetSpecs.size(); ++index) {
  265. const AssetSpec& spec = kAssetSpecs[index];
  266. const HRSRC resource = FindResourceW(module, MAKEINTRESOURCEW(spec.resourceId), RT_RCDATA);
  267. if (resource == nullptr) {
  268. return false;
  269. }
  270. const HGLOBAL loaded = LoadResource(module, resource);
  271. const DWORD size = SizeofResource(module, resource);
  272. const auto* bytes = static_cast<const std::byte*>(LockResource(loaded));
  273. Asset asset{};
  274. asset.spec = spec;
  275. if (loaded == nullptr || bytes == nullptr || size == 0
  276. || !parse_dds(bytes, static_cast<std::size_t>(size), asset.dds)) {
  277. return false;
  278. }
  279. g_assets[index] = asset;
  280. }
  281. return true;
  282. }
  283. /** @return The asset owning this exact header or data TagHash. */
  284. [[nodiscard]] Asset* find_asset(std::uint32_t tag, bool& header) noexcept {
  285. for (Asset& asset : g_assets) {
  286. if (asset.spec.headerTag == tag) {
  287. header = true;
  288. return &asset;
  289. }
  290. if (asset.spec.dataTag == tag) {
  291. header = false;
  292. return &asset;
  293. }
  294. }
  295. return nullptr;
  296. }
  297. /** Builds one replacement Tiger descriptor from its stock descriptor and embedded DDS. */
  298. [[nodiscard]] const void*
  299. prepare_header(Asset& asset, const void* stock, std::uint64_t stockSize, bool& report) noexcept {
  300. report = false;
  301. if (stock == nullptr || stockSize < kTigerTextureHeaderSize) {
  302. return stock;
  303. }
  304. AcquireSRWLockExclusive(&g_assetLock);
  305. if (!asset.headerReady) {
  306. std::memcpy(asset.header.data(), stock, asset.header.size());
  307. std::uint16_t marker = 0;
  308. std::memcpy(&marker, asset.header.data() + TigerLayout::marker, sizeof marker);
  309. if (marker == kTigerTextureMarker) {
  310. const std::span header(asset.header);
  311. store_value(header, TigerLayout::dataSize, asset.dds.pixelSize);
  312. store_value(header, TigerLayout::format, asset.dds.format);
  313. store_value(header, TigerLayout::width, asset.dds.width);
  314. store_value(header, TigerLayout::height, asset.dds.height);
  315. store_value(header, TigerLayout::depth, asset.dds.depth);
  316. store_value(header, TigerLayout::arraySize, asset.dds.arraySize);
  317. asset.headerReady = true;
  318. }
  319. }
  320. if (asset.headerReady && !asset.reported) {
  321. asset.reported = true;
  322. report = true;
  323. }
  324. const void* result = asset.headerReady ? asset.header.data() : stock;
  325. ReleaseSRWLockExclusive(&g_assetLock);
  326. return result;
  327. }
  328. /** Reports one asset the first time its descriptor is handed to the dispatcher. */
  329. void report_override(const Asset& asset) noexcept {
  330. std::array<char, core::log::kLineCapacity> line{};
  331. const int written = std::snprintf(line.data(),
  332. line.size(),
  333. "ev=bootflow_texture stage=entry tag=0x%08X size=%u "
  334. "width=%u height=%u result=override",
  335. static_cast<unsigned>(asset.spec.dataTag),
  336. static_cast<unsigned>(asset.dds.pixelSize),
  337. static_cast<unsigned>(asset.dds.width),
  338. static_cast<unsigned>(asset.dds.height));
  339. if (written > 0) {
  340. core::log::write(
  341. core::log::Channel::client,
  342. core::log::Level::info,
  343. {line.data(), (std::min)(static_cast<std::size_t>(written), line.size() - 1)});
  344. }
  345. }
  346. /** Replaces only selected decoded GPU texture entries, then preserves the native dispatcher. */
  347. std::uint64_t __fastcall dispatch(std::uint32_t resourceClass,
  348. std::uint32_t tag,
  349. const void* decoded,
  350. std::uint64_t decodedSize) noexcept {
  351. const auto original = reinterpret_cast<GpuEntryDispatcher>(g_handle.original);
  352. if (original == nullptr) {
  353. return kDispatchUnavailable;
  354. }
  355. bool header = false;
  356. Asset* const asset = resourceClass == kGpuTextureClass ? find_asset(tag, header) : nullptr;
  357. if (asset == nullptr) {
  358. return original(resourceClass, tag, decoded, decodedSize);
  359. }
  360. if (!header) {
  361. return original(resourceClass, tag, asset->dds.pixels, asset->dds.pixelSize);
  362. }
  363. bool report = false;
  364. const void* const replacement = prepare_header(*asset, decoded, decodedSize, report);
  365. if (report) {
  366. report_override(*asset);
  367. }
  368. return original(resourceClass,
  369. tag,
  370. replacement,
  371. replacement == decoded ? decodedSize : kTigerTextureHeaderSize);
  372. }
  373. void clear_assets() noexcept {
  374. AcquireSRWLockExclusive(&g_assetLock);
  375. g_assets = {};
  376. ReleaseSRWLockExclusive(&g_assetLock);
  377. }
  378. } // namespace
  379. /** Loads embedded DDS files and attaches the decoded GPU-entry dispatcher. */
  380. bool install(void* module) noexcept {
  381. if (g_handle.attached) {
  382. return true;
  383. }
  384. if (!core::settings::get().client.customBootflowTextures) {
  385. core::log::write(core::log::Channel::client,
  386. core::log::Level::info,
  387. "ev=bootflow_texture stage=setting enabled=0 result=skip");
  388. return true;
  389. }
  390. if (!load_assets(static_cast<HMODULE>(module))) {
  391. clear_assets();
  392. core::log::write(core::log::Channel::client,
  393. core::log::Level::error,
  394. "ev=bootflow_texture stage=resources result=fail");
  395. return false;
  396. }
  397. std::byte* const target =
  398. scan_main_image_unique(kGpuEntryDispatcher, "bootflow_gpu_entry_dispatcher");
  399. const hooking::detour::Spec spec{target, reinterpret_cast<void*>(&dispatch)};
  400. if (target == nullptr || !hooking::detour::install(spec, g_handle)) {
  401. clear_assets();
  402. core::log::write(core::log::Channel::client,
  403. core::log::Level::error,
  404. "ev=bootflow_texture stage=attach result=fail");
  405. return false;
  406. }
  407. std::array<char, core::log::kLineCapacity> line{};
  408. const int written = std::snprintf(line.data(),
  409. line.size(),
  410. "ev=bootflow_texture stage=attach count=%zu result=ok",
  411. kAssetSpecs.size());
  412. if (written > 0) {
  413. core::log::write(
  414. core::log::Channel::client,
  415. core::log::Level::info,
  416. {line.data(), (std::min)(static_cast<std::size_t>(written), line.size() - 1)});
  417. }
  418. return true;
  419. }
  420. /** Detaches before releasing the resource views and generated Tiger descriptors. */
  421. bool uninstall() noexcept {
  422. if (g_handle.attached && !hooking::detour::uninstall(g_handle)) {
  423. return false;
  424. }
  425. clear_assets();
  426. return true;
  427. }
  428. /** @return True while the decoded GPU-entry dispatcher is attached. */
  429. bool is_installed() noexcept {
  430. return g_handle.attached;
  431. }
  432. } // namespace sunrise::client::hooks::bootflow::texture_override