From 7f8e94ee34eae21badf88504c9881a06125f2387 Mon Sep 17 00:00:00 2001 From: is-this-c <87069698+is-this-c@users.noreply.github.com> Date: Fri, 28 Aug 2026 20:43:10 +1200 Subject: [PATCH 1/7] Add `draw_into_bitmap` and `bm_copy` --- game_patch/bmpman/bmpman.cpp | 65 +++++++++++++++++++++++++++++ game_patch/bmpman/bmpman.h | 17 +++++++- game_patch/bmpman/fmt_conv.cpp | 2 +- game_patch/graphics/gr_font.cpp | 74 +++++++++++++++++++++++++++++++++ game_patch/misc/misc.h | 10 +++++ 5 files changed, 165 insertions(+), 3 deletions(-) diff --git a/game_patch/bmpman/bmpman.cpp b/game_patch/bmpman/bmpman.cpp index db71f3bf4..873cbf187 100644 --- a/game_patch/bmpman/bmpman.cpp +++ b/game_patch/bmpman/bmpman.cpp @@ -11,6 +11,8 @@ #include "atx.h" #include "dds.h" #include "stb_image_loader.h" +#include "../misc/misc.h" +#include "bmpman.h" int bm_calculate_pitch(int w, rf::bm::Format format) { @@ -336,6 +338,69 @@ void bm_change_format(int bm_handle, rf::bm::Format format) } } +bool bm_copy( + const int dst_x, + const int dst_y, + const int dst_bm_handle, + const int src_x, + const int src_y, + const int w, + const int h, + const int src_bm_handle) +{ + rf::gr::LockInfo src_lock{}; + if (!rf::gr::lock(src_bm_handle, 0, &src_lock, rf::gr::LOCK_READ_ONLY)) { + return false; + } + + rf::gr::LockInfo dst_lock{}; + if (!rf::gr::lock(dst_bm_handle, 0, &dst_lock, rf::gr::LOCK_WRITE_ONLY)) { + rf::gr::unlock(&src_lock); + return false; + } + + ScopeGuard guard{[&] { + rf::gr::unlock(&dst_lock); + rf::gr::unlock(&src_lock); + }}; + + if (w <= 0 + || h <= 0 + || src_x < 0 + || src_y < 0 + || dst_x < 0 + || dst_y < 0 + || src_lock.w - src_x < w + || src_lock.h - src_y < h + || dst_lock.w - dst_x < w + || dst_lock.h - dst_y < h) { + return false; + } + + uint8_t* const dst_ptr = dst_lock.data + + dst_x + * bm_bytes_per_pixel(dst_lock.format) + + dst_y + * dst_lock.stride_in_bytes; + + const uint8_t* const src_ptr = src_lock.data + + src_x + * bm_bytes_per_pixel(src_lock.format) + + src_y + * src_lock.stride_in_bytes; + + return bm_convert_format( + dst_ptr, + dst_lock.format, + src_ptr, + src_lock.format, + w, + h, + dst_lock.stride_in_bytes, + src_lock.stride_in_bytes + ); +} + void bm_apply_patch() { bm_read_header_hook.install(); diff --git a/game_patch/bmpman/bmpman.h b/game_patch/bmpman/bmpman.h index 7aa5a21a1..cc2abb91c 100644 --- a/game_patch/bmpman/bmpman.h +++ b/game_patch/bmpman/bmpman.h @@ -2,7 +2,10 @@ #include #include "../rf/bmpman.h" -#include "../rf/gr/gr.h" + +namespace rf::gr { + struct Color; +} // rf::bm::load never fails: a missing file gets a generated 32x32 checkerboard // placeholder with a real handle, so "handle != -1" is not a presence test. Probe @@ -18,10 +21,20 @@ bool bm_is_compressed_format(rf::bm::Format format); bool bm_convert_format(void* dst_bits_ptr, rf::bm::Format dst_fmt, const void* src_bits_ptr, rf::bm::Format src_fmt, int width, int height, int dst_pitch, int src_pitch, const uint8_t* palette = nullptr); -rf::Color bm_get_pixel(uint8_t* data, rf::bm::Format format, int stride_in_bytes, int x, int y); +rf::gr::Color bm_get_pixel(uint8_t* data, rf::bm::Format format, int stride_in_bytes, int x, int y); size_t bm_calculate_total_bytes(int w, int h, rf::bm::Format format); int bm_calculate_pitch(int w, rf::bm::Format format); int bm_calculate_rows(int h, rf::bm::Format format); +bool bm_copy( + int dst_x, + int dst_y, + int dst_bm_handle, + int src_x, + int src_y, + int w, + int h, + int src_bm_handle +); inline int bm_bytes_per_pixel(rf::bm::Format format) { diff --git a/game_patch/bmpman/fmt_conv.cpp b/game_patch/bmpman/fmt_conv.cpp index d9ec3aed0..7e7c657d5 100644 --- a/game_patch/bmpman/fmt_conv.cpp +++ b/game_patch/bmpman/fmt_conv.cpp @@ -39,7 +39,7 @@ bool bm_convert_format(void* dst_bits_ptr, rf::bm::Format dst_fmt, const void* s } } -rf::Color bm_get_pixel(uint8_t* data, rf::bm::Format format, int stride_in_bytes, int x, int y) +rf::gr::Color bm_get_pixel(uint8_t* data, rf::bm::Format format, int stride_in_bytes, int x, int y) { if (bm_is_compressed_format(format)) { constexpr int block_w = 4; diff --git a/game_patch/graphics/gr_font.cpp b/game_patch/graphics/gr_font.cpp index c5cde2382..ff8e9c3d1 100644 --- a/game_patch/graphics/gr_font.cpp +++ b/game_patch/graphics/gr_font.cpp @@ -54,6 +54,7 @@ class GrNewFont { public: GrNewFont(std::string_view name); + void draw_into_bitmap(int x, int y, int bm_handle, std::string_view text) const; void draw(int x, int y, std::string_view text, rf::gr::Mode state) const; void draw_aligned(rf::gr::TextAlignment align, int x, int y, std::string_view text, rf::gr::Mode state) const; void get_size(int* w, int* h, std::string_view text) const; @@ -372,6 +373,41 @@ GrNewFont::GrNewFont(std::string_view name) : rf::gr::tcache_add_ref(bitmap_); } +void GrNewFont::draw_into_bitmap( + const int x, + const int y, + const int bm_handle, + const std::string_view text +) const { + int bm_w = 0, bm_h = 0; + rf::bm::get_dimensions(bm_handle, &bm_w, &bm_h); + + int pen_x = x; + const int pen_y = y + baseline_y_; + for (const char ch : text) { + const int glyph_idx = char_map_[static_cast(ch)]; + if (glyph_idx != -1) { + const GlyphInfo& glyph_info = glyphs_[glyph_idx]; + if (glyph_info.bm_w) { + bm_copy( + pen_x + glyph_info.x, + pen_y + glyph_info.y, + bm_handle, + glyph_info.bm_x, + glyph_info.bm_y, + glyph_info.bm_w, + glyph_info.bm_h, + bitmap_ + ); + } + pen_x += glyph_info.advance_x; + if (pen_x + glyph_info.x >= bm_w) { + break; + } + } + } +} + void GrNewFont::draw(int x, int y, std::string_view text, rf::gr::Mode state) const { if (x == rf::gr::center_x) { @@ -545,6 +581,43 @@ FunHook gr_get_font_height_hook{ }, }; +FunHook gr_string_render_into_bitmap_hook{ + 0x005203A0, + [] ( + const int x, + const int y, + const int bm_handle, + const char* const text, + int font_num + ) { + if (font_num == -1) { + font_num = g_default_font_id; + } + if (font_num & ttf_font_flag) { + const unsigned idx = static_cast(font_num & ~ttf_font_flag); + if (idx >= g_fonts.size()) { + if (report_bad_font_id_once(font_num)) { + xlog::error( + "gr_string_render_into_bitmap_hook: bad TTF font id {:#x} (have {})", + font_num, + g_fonts.size() + ); + } + } else { + g_fonts[idx].draw_into_bitmap(x, y, bm_handle, text); + } + } else { + gr_string_render_into_bitmap_hook.call_target( + x, + y, + bm_handle, + text, + font_num + ); + } + }, +}; + FunHook gr_string_hook{ 0x0051FEB0, [](int x, int y, const char *text, int font_num, rf::gr::Mode mode) { @@ -645,6 +718,7 @@ void gr_font_apply_patch() gr_init_font_hook.install(); gr_set_default_font_hook.install(); gr_get_font_height_hook.install(); + gr_string_render_into_bitmap_hook.install(); gr_string_hook.install(); gr_get_string_size_hook.install(); init_freetype_lib(); diff --git a/game_patch/misc/misc.h b/game_patch/misc/misc.h index dd7d4a35d..149c697a9 100644 --- a/game_patch/misc/misc.h +++ b/game_patch/misc/misc.h @@ -27,3 +27,13 @@ void clear_explicit_upcoming_game_type_request(); bool file_loaded_from_alpinefaction_vpp(const char* filename); bool weapon_reticle_is_customized(int weapon_id, bool bighud); bool rocket_locked_reticle_is_customized(bool bighud); + +template +struct ScopeGuard { + F _f; + explicit ScopeGuard(F f) noexcept(std::is_nothrow_move_constructible::value) + : _f(std::move(f)) { } + ~ScopeGuard() noexcept { _f(); } + ScopeGuard(const ScopeGuard&) = delete; + ScopeGuard& operator=(const ScopeGuard&) = delete; +}; From f06349e41c55ad0af7d948e8ba34fc1af18f1ab9 Mon Sep 17 00:00:00 2001 From: is-this-c <87069698+is-this-c@users.noreply.github.com> Date: Sat, 29 Aug 2026 00:33:23 +1200 Subject: [PATCH 2/7] Add `user_mipmap` Rename --- docs/CHANGELOG.md | 3 + game_patch/bmpman/bmpman.cpp | 11 ++++ game_patch/bmpman/bmpman.h | 2 + .../graphics/d3d11/gr_d3d11_texture.cpp | 62 +++++++++++++++++++ game_patch/graphics/d3d11/gr_d3d11_texture.h | 1 + game_patch/rf/bmpman.h | 1 + 6 files changed, 80 insertions(+) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 0f9c8e4af..e182e92be 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -11,6 +11,9 @@ Version 1.5.0 (TBD): Not yet released - Restore cut first person weapon aim sway, toggleable with `cl_weaponsway` - Add terms of use and notices document to installer +[@is-this-c](https://github.com/is-this-c) +- Allow TrueType fonts to be rendered into bitmaps with mipmaps + ### Bug fixes [@GooberRF](https://github.com/GooberRF) - Fix phantom visual flag mesh being visible after Salvage flag is picked up on rare occasions diff --git a/game_patch/bmpman/bmpman.cpp b/game_patch/bmpman/bmpman.cpp index 873cbf187..4ac837824 100644 --- a/game_patch/bmpman/bmpman.cpp +++ b/game_patch/bmpman/bmpman.cpp @@ -246,6 +246,7 @@ FunHook bm_free_entry_hook{ atx_free(bm_entry); } bm_entry.dynamic = false; + bm_entry.user_mipmap = false; if (!bm_entry.prev || !bm_entry.next) { return; @@ -327,6 +328,16 @@ bool bm_is_dynamic(int bm_handle) return rf::bm::bitmaps[bm_index].dynamic; } +void bm_set_user_mipmap(const int bm_handle, const bool mipmap) { + const int bm_index = rf::bm::get_cache_slot(bm_handle); + rf::bm::bitmaps[bm_index].user_mipmap = mipmap; +} + +bool bm_is_user_mipmap(const int bm_handle) { + const int bm_index = rf::bm::get_cache_slot(bm_handle); + return rf::bm::bitmaps[bm_index].user_mipmap; +} + void bm_change_format(int bm_handle, rf::bm::Format format) { int bm_idx = rf::bm::get_cache_slot(bm_handle); diff --git a/game_patch/bmpman/bmpman.h b/game_patch/bmpman/bmpman.h index cc2abb91c..470dce841 100644 --- a/game_patch/bmpman/bmpman.h +++ b/game_patch/bmpman/bmpman.h @@ -15,6 +15,8 @@ int bm_load_if_exists(const char* name, int unk, bool generate_mipmaps); void bm_set_dynamic(int bm_handle, bool dynamic); bool bm_is_dynamic(int bm_handle); +void bm_set_user_mipmap(int bm_handle, bool force_mipmap); +bool bm_is_user_mipmap(int bm_handle); void bm_change_format(int bm_handle, rf::bm::Format format); void bm_apply_patch(); bool bm_is_compressed_format(rf::bm::Format format); diff --git a/game_patch/graphics/d3d11/gr_d3d11_texture.cpp b/game_patch/graphics/d3d11/gr_d3d11_texture.cpp index 0d3e00113..62a7f2d04 100644 --- a/game_patch/graphics/d3d11/gr_d3d11_texture.cpp +++ b/game_patch/graphics/d3d11/gr_d3d11_texture.cpp @@ -207,6 +207,59 @@ namespace gr::d3d11 return texture; } + TextureManager::Texture TextureManager::create_user_texture_auto_mips( + const int bm_handle, + const rf::bm::Format fmt, + const int w, + const int h + ) { + // Same format negotiation as create_texture_auto_mips: GenerateMips requires + // RENDER_TARGET + MIP_AUTOGEN, so fall back to 8888 when the narrower format + // lacks that combination on this driver. + auto dxgi_format = get_supported_texture_format(fmt).first; + UINT format_support = 0; + device_->CheckFormatSupport(dxgi_format, &format_support); + constexpr UINT required_support = + D3D11_FORMAT_SUPPORT_RENDER_TARGET | D3D11_FORMAT_SUPPORT_MIP_AUTOGEN; + if ((format_support & required_support) != required_support) { + bool has_alpha = dxgi_format == DXGI_FORMAT_B5G5R5A1_UNORM + || dxgi_format == DXGI_FORMAT_B4G4R4A4_UNORM + || dxgi_format == DXGI_FORMAT_B8G8R8A8_UNORM; + dxgi_format = has_alpha ? DXGI_FORMAT_B8G8R8A8_UNORM : DXGI_FORMAT_B8G8R8X8_UNORM; + } + + CD3D11_TEXTURE2D_DESC desc{ + dxgi_format, + static_cast(w), + static_cast(h), + 1, // arraySize + 0, // mipLevels = full chain + D3D11_BIND_SHADER_RESOURCE | D3D11_BIND_RENDER_TARGET, + D3D11_USAGE_DEFAULT, + 0, + }; + desc.MiscFlags = D3D11_RESOURCE_MISC_GENERATE_MIPS; + + ComPtr d3d_texture; + HRESULT hr = device_->CreateTexture2D(&desc, nullptr, &d3d_texture); + if (FAILED(hr)) { + xlog::warn("User mip-mapped texture creation failed ({}x{}), falling back to single mip", w, h); + return create_texture(bm_handle, fmt, w, h, nullptr, nullptr, 1, false); + } + + ComPtr srv; + hr = device_->CreateShaderResourceView(d3d_texture, nullptr, &srv); + if (FAILED(hr)) { + xlog::warn("User mip-mapped SRV creation failed ({}x{}), falling back to single mip", w, h); + return create_texture(bm_handle, fmt, w, h, nullptr, nullptr, 1, false); + } + + Texture texture{bm_handle, dxgi_format, {}}; + texture.gpu_texture = std::move(d3d_texture); + texture.shader_resource_view = std::move(srv); + return texture; + } + TextureManager::Texture TextureManager::create_render_target(int bm_handle, int w, int h) { ComPtr gpu_ss_texture{}; @@ -286,6 +339,9 @@ namespace gr::d3d11 if (rf::bm::get_type(bm_handle) == rf::bm::TYPE_USER) { xlog::trace("Creating user bitmap texture: handle {}", bm_handle); + if (bm_is_user_mipmap(bm_handle) && !staging) { + return create_user_texture_auto_mips(bm_handle, fmt, w, h); + } auto texture = create_texture(bm_handle, fmt, w, h, nullptr, nullptr, 1, staging); return texture; } @@ -578,6 +634,12 @@ namespace gr::d3d11 device_context_->Unmap(texture.cpu_texture, 0); if (lock->mode != rf::gr::LOCK_READ_ONLY && texture.gpu_texture) { device_context_->CopySubresourceRegion(texture.gpu_texture, 0, 0, 0, 0, texture.cpu_texture, 0, nullptr); + // Rebuild lower mips after the write (only mip 0 is written via gr::lock). + if (bm_is_user_mipmap(lock->bm_handle)) { + if (ID3D11ShaderResourceView* srv = texture.get_or_create_texture_view(device_, device_context_)) { + device_context_->GenerateMips(srv); + } + } } } } diff --git a/game_patch/graphics/d3d11/gr_d3d11_texture.h b/game_patch/graphics/d3d11/gr_d3d11_texture.h index 8878a9316..2a8921ab1 100644 --- a/game_patch/graphics/d3d11/gr_d3d11_texture.h +++ b/game_patch/graphics/d3d11/gr_d3d11_texture.h @@ -162,6 +162,7 @@ namespace gr::d3d11 Texture create_texture(int bm_handle, rf::bm::Format fmt, int w, int h, rf::ubyte* bits, rf::ubyte* pal, int mip_levels, bool staging, int src_w = 0, int src_h = 0); Texture create_texture_auto_mips(int bm_handle, rf::bm::Format fmt, int w, int h, rf::ubyte* bits, rf::ubyte* pal); + Texture create_user_texture_auto_mips(int bm_handle, rf::bm::Format fmt, int w, int h); Texture create_render_target(int bm_handle, int w, int h); Texture load_texture(int bm_handle, bool staging); std::pair determine_supported_texture_format(rf::bm::Format fmt); diff --git a/game_patch/rf/bmpman.h b/game_patch/rf/bmpman.h index 1c13a9ed5..82f479ce0 100644 --- a/game_patch/rf/bmpman.h +++ b/game_patch/rf/bmpman.h @@ -73,6 +73,7 @@ namespace rf::bm ubyte cached_material_idx; #ifdef ALPINE_FACTION bool dynamic; + bool user_mipmap; #endif int total_bytes_for_all_levels; int file_open_unk_arg; From 26cdfb1e873891435d58d31f26c03264b5a6e4ae Mon Sep 17 00:00:00 2001 From: is-this-c <87069698+is-this-c@users.noreply.github.com> Date: Sat, 29 Aug 2026 00:33:26 +1200 Subject: [PATCH 3/7] Update hud_world.cpp --- game_patch/hud/hud_world.cpp | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/game_patch/hud/hud_world.cpp b/game_patch/hud/hud_world.cpp index ed2364615..f78dc201d 100644 --- a/game_patch/hud/hud_world.cpp +++ b/game_patch/hud/hud_world.cpp @@ -22,6 +22,7 @@ #include "../rf/player/camera.h" #include "../rf/entity.h" #include "../rf/bmpman.h" +#include "../bmpman/bmpman.h" #include "../rf/multi.h" #include "../rf/gameseq.h" #include "../rf/level.h" @@ -373,6 +374,10 @@ static NameLabelTex& ensure_hill_name_tex(const HillInfo& h, int font) slot.bm = rf::bm::create(rf::bm::FORMAT_4444_ARGB, bw, bh); + // The label is drawn at a range of on-screen sizes, so generate a mip chain + // like disk-loaded textures do to avoid shimmering when minified. + bm_set_user_mipmap(slot.bm, true); + // keep resident rf::bm::texture_add_ref(slot.bm); @@ -524,8 +529,8 @@ static void render_koth_icon_for_hill(const HillInfo& h, WorldHUDRenderMode rm) } // hill name label - //const int font = get_world_hud_font(g_alpine_game_config.world_hud_text_scale); - const int font = 0; + static const int big_font = rf::gr::load_font("boldfont.ttf:56"); + const int font = g_alpine_game_config.big_hud ? big_font : 0; NameLabelTex& lbl = ensure_hill_name_tex(h, font); const float text_h_world = ring_scale * 0.55f; From 0750297a91a2e84934f6d51135fbd910e2c3fa7c Mon Sep 17 00:00:00 2001 From: is-this-c <87069698+is-this-c@users.noreply.github.com> Date: Mon, 31 Aug 2026 01:14:02 +1200 Subject: [PATCH 4/7] Fix --- common/CMakeLists.txt | 1 + common/include/common/scope_guard.h | 15 ++ docs/CHANGELOG.md | 2 +- game_patch/bmpman/bmpman.cpp | 152 ++++++++++++------ game_patch/bmpman/bmpman.h | 19 ++- .../graphics/d3d11/gr_d3d11_texture.cpp | 102 +++++------- game_patch/graphics/gr_font.cpp | 30 +++- game_patch/hud/hud_world.cpp | 7 +- game_patch/misc/misc.h | 10 -- 9 files changed, 201 insertions(+), 137 deletions(-) create mode 100644 common/include/common/scope_guard.h diff --git a/common/CMakeLists.txt b/common/CMakeLists.txt index 50c564531..9cc0e6cc1 100644 --- a/common/CMakeLists.txt +++ b/common/CMakeLists.txt @@ -4,6 +4,7 @@ set(SRCS include/common/ComPtr.h include/common/afd_format.h include/common/tlv.h + include/common/scope_guard.h include/common/config/BuildConfig.h include/common/config/CfgVar.h include/common/config/GameConfig.h diff --git a/common/include/common/scope_guard.h b/common/include/common/scope_guard.h new file mode 100644 index 000000000..d7029a766 --- /dev/null +++ b/common/include/common/scope_guard.h @@ -0,0 +1,15 @@ +#pragma once + +#include + +template +struct ScopeGuard { + F _f; + constexpr explicit ScopeGuard(F&& f) + noexcept(std::is_nothrow_move_constructible::value) + : _f(std::forward(func)) { + } + ~ScopeGuard() noexcept { _f(); } + ScopeGuard(const ScopeGuard&) = delete; + ScopeGuard& operator=(const ScopeGuard&) = delete; +}; diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index e182e92be..82b135fd9 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -12,7 +12,7 @@ Version 1.5.0 (TBD): Not yet released - Add terms of use and notices document to installer [@is-this-c](https://github.com/is-this-c) -- Allow TrueType fonts to be rendered into bitmaps with mipmaps +- Allow TrueType fonts to be rendered into bitmaps with mipmaps (D3D11 only) ### Bug fixes [@GooberRF](https://github.com/GooberRF) diff --git a/game_patch/bmpman/bmpman.cpp b/game_patch/bmpman/bmpman.cpp index 4ac837824..80cf59941 100644 --- a/game_patch/bmpman/bmpman.cpp +++ b/game_patch/bmpman/bmpman.cpp @@ -5,13 +5,13 @@ #include #include #include +#include #include "../graphics/gr.h" #include "../rf/file/file.h" #include "../misc/vpackfile.h" #include "atx.h" #include "dds.h" #include "stb_image_loader.h" -#include "../misc/misc.h" #include "bmpman.h" int bm_calculate_pitch(int w, rf::bm::Format format) @@ -330,7 +330,10 @@ bool bm_is_dynamic(int bm_handle) void bm_set_user_mipmap(const int bm_handle, const bool mipmap) { const int bm_index = rf::bm::get_cache_slot(bm_handle); - rf::bm::bitmaps[bm_index].user_mipmap = mipmap; + if (rf::bm::bitmaps[bm_index].user_mipmap != mipmap) { + rf::bm::bitmaps[bm_index].user_mipmap = mipmap; + rf::gr::mark_texture_dirty(bm_handle); + } } bool bm_is_user_mipmap(const int bm_handle) { @@ -349,66 +352,123 @@ void bm_change_format(int bm_handle, rf::bm::Format format) } } +bool bm_copy_pixels( + const rf::gr::LockInfo& dst_lock, + int dst_x, + int dst_y, + const rf::gr::LockInfo& src_lock, + int src_x, + int src_y, + int w, + int h +) { + if (!dst_lock.data + || !src_lock.data + || dst_lock.bm_handle == src_lock.bm_handle + || w <= 0 + || h <= 0) + { + return false; + } + + // Clip against left and top edges cleanly. + if (src_x < 0) { + const int shift = -src_x; + w -= shift; + dst_x += shift; + src_x = 0; + } + if (dst_x < 0) { + const int shift = -dst_x; + w -= shift; + src_x += shift; + dst_x = 0; + } + + // Clip against right and bottom edges. + if (src_x + w > src_lock.w) { + w = src_lock.w - src_x; + } + if (src_y + h > src_lock.h) { + h = src_lock.h - src_y; + } + if (dst_x + w > dst_lock.w) { + w = dst_lock.w - dst_x; + } + if (dst_y + h > dst_lock.h) { + h = dst_lock.h - dst_y; + } + + if (w <= 0 || h <= 0) { + return false; + } + + // Compute pointer offsets using bm_bytes_per_pixel AFTER clipping. + const uint8_t* src_ptr = src_lock.data + + (src_x * bm_bytes_per_pixel(src_lock.format)) + + (src_y * src_lock.stride_in_bytes); + + uint8_t* dst_ptr = dst_lock.data + + (dst_x * bm_bytes_per_pixel(dst_lock.format)) + + (dst_y * dst_lock.stride_in_bytes); + + // Row-by-row transfer. + for (int row = 0; row < h; ++row) { + bm_convert_format( + dst_ptr, + dst_lock.format, + src_ptr, + src_lock.format, + w, + 1, + dst_lock.stride_in_bytes, + src_lock.stride_in_bytes, + nullptr + ); + src_ptr += src_lock.stride_in_bytes; + dst_ptr += dst_lock.stride_in_bytes; + } + + return true; +} + bool bm_copy( + const int dst_handle, const int dst_x, const int dst_y, - const int dst_bm_handle, + const int src_handle, const int src_x, const int src_y, const int w, - const int h, - const int src_bm_handle) -{ - rf::gr::LockInfo src_lock{}; - if (!rf::gr::lock(src_bm_handle, 0, &src_lock, rf::gr::LOCK_READ_ONLY)) { + const int h +) { + if (src_handle == dst_handle) { return false; } - rf::gr::LockInfo dst_lock{}; - if (!rf::gr::lock(dst_bm_handle, 0, &dst_lock, rf::gr::LOCK_WRITE_ONLY)) { - rf::gr::unlock(&src_lock); + rf::gr::LockInfo src_lock{}; + if (!rf::gr::lock(src_handle, 0, &src_lock, rf::gr::LOCK_READ_ONLY)) { return false; } - ScopeGuard guard{[&] { - rf::gr::unlock(&dst_lock); - rf::gr::unlock(&src_lock); - }}; - - if (w <= 0 - || h <= 0 - || src_x < 0 - || src_y < 0 - || dst_x < 0 - || dst_y < 0 - || src_lock.w - src_x < w - || src_lock.h - src_y < h - || dst_lock.w - dst_x < w - || dst_lock.h - dst_y < h) { + ScopeGuard src_guard{[&] { rf::gr::unlock(&src_lock); }}; + + rf::gr::LockInfo dst_lock{}; + if (!rf::gr::lock(dst_handle, 0, &dst_lock, rf::gr::LOCK_READ_ONLY_WRITE)) { return false; } - uint8_t* const dst_ptr = dst_lock.data - + dst_x - * bm_bytes_per_pixel(dst_lock.format) - + dst_y - * dst_lock.stride_in_bytes; - - const uint8_t* const src_ptr = src_lock.data - + src_x - * bm_bytes_per_pixel(src_lock.format) - + src_y - * src_lock.stride_in_bytes; - - return bm_convert_format( - dst_ptr, - dst_lock.format, - src_ptr, - src_lock.format, + ScopeGuard dst_guard{[&] { rf::gr::unlock(&dst_lock); }}; + + return bm_copy_pixels( + dst_lock, + dst_x, + dst_y, + src_lock, + src_x, + src_y, w, - h, - dst_lock.stride_in_bytes, - src_lock.stride_in_bytes + h ); } diff --git a/game_patch/bmpman/bmpman.h b/game_patch/bmpman/bmpman.h index 470dce841..35a5c09f2 100644 --- a/game_patch/bmpman/bmpman.h +++ b/game_patch/bmpman/bmpman.h @@ -5,6 +5,7 @@ namespace rf::gr { struct Color; + struct LockInfo; } // rf::bm::load never fails: a missing file gets a generated 32x32 checkerboard @@ -15,7 +16,7 @@ int bm_load_if_exists(const char* name, int unk, bool generate_mipmaps); void bm_set_dynamic(int bm_handle, bool dynamic); bool bm_is_dynamic(int bm_handle); -void bm_set_user_mipmap(int bm_handle, bool force_mipmap); +void bm_set_user_mipmap(int bm_handle, bool mipmap); bool bm_is_user_mipmap(int bm_handle); void bm_change_format(int bm_handle, rf::bm::Format format); void bm_apply_patch(); @@ -27,15 +28,25 @@ rf::gr::Color bm_get_pixel(uint8_t* data, rf::bm::Format format, int stride_in_b size_t bm_calculate_total_bytes(int w, int h, rf::bm::Format format); int bm_calculate_pitch(int w, rf::bm::Format format); int bm_calculate_rows(int h, rf::bm::Format format); +bool bm_copy_pixels( + const rf::gr::LockInfo& dst_lock, + int dst_x, + int dst_y, + const rf::gr::LockInfo& src_lock, + int src_x, + int src_y, + int w, + int h +); bool bm_copy( + int dst_handle, int dst_x, int dst_y, - int dst_bm_handle, + int src_handle, int src_x, int src_y, int w, - int h, - int src_bm_handle + int h ); inline int bm_bytes_per_pixel(rf::bm::Format format) diff --git a/game_patch/graphics/d3d11/gr_d3d11_texture.cpp b/game_patch/graphics/d3d11/gr_d3d11_texture.cpp index 62a7f2d04..3a383ff81 100644 --- a/game_patch/graphics/d3d11/gr_d3d11_texture.cpp +++ b/game_patch/graphics/d3d11/gr_d3d11_texture.cpp @@ -154,6 +154,13 @@ namespace gr::d3d11 constexpr UINT required_support = D3D11_FORMAT_SUPPORT_RENDER_TARGET | D3D11_FORMAT_SUPPORT_MIP_AUTOGEN; if ((format_support & required_support) != required_support) { + if (rf::bm::get_type(bm_handle) == rf::bm::TYPE_USER) { + xlog::warn( + "Auto-mip not supported for user format {}, falling back to single mip", + static_cast(fmt) + ); + return create_texture(bm_handle, fmt, w, h, bits, pal, 1, false, w, h); + } bool has_alpha = dxgi_format == DXGI_FORMAT_B5G5R5A1_UNORM || dxgi_format == DXGI_FORMAT_B4G4R4A4_UNORM || dxgi_format == DXGI_FORMAT_B8G8R8A8_UNORM; @@ -180,18 +187,21 @@ namespace gr::d3d11 return create_texture(bm_handle, fmt, w, h, bits, pal, 1, false, w, h); } - std::unique_ptr converted_bits; - rf::ubyte* upload_bits = bits; - int upload_pitch = bm_calculate_pitch(w, fmt); - if (fmt != supported_fmt) { - int dst_pitch = bm_calculate_pitch(w, supported_fmt); - converted_bits = std::make_unique(dst_pitch * h); - ::bm_convert_format(converted_bits.get(), supported_fmt, bits, fmt, w, h, dst_pitch, upload_pitch, pal); - upload_bits = converted_bits.get(); - upload_pitch = dst_pitch; - } + // Only convert and upload, if we have pixel data. + if (bits) { + std::unique_ptr converted_bits; + rf::ubyte* upload_bits = bits; + int upload_pitch = bm_calculate_pitch(w, fmt); + if (fmt != supported_fmt) { + int dst_pitch = bm_calculate_pitch(w, supported_fmt); + converted_bits = std::make_unique(dst_pitch * h); + ::bm_convert_format(converted_bits.get(), supported_fmt, bits, fmt, w, h, dst_pitch, upload_pitch, pal); + upload_bits = converted_bits.get(); + upload_pitch = dst_pitch; + } - device_context_->UpdateSubresource(d3d_texture, 0, nullptr, upload_bits, upload_pitch, 0); + device_context_->UpdateSubresource(d3d_texture, 0, nullptr, upload_bits, upload_pitch, 0); + } ComPtr srv; hr = device_->CreateShaderResourceView(d3d_texture, nullptr, &srv); @@ -199,59 +209,10 @@ namespace gr::d3d11 xlog::warn("Auto-mip SRV creation failed ({}x{}), falling back to single mip", w, h); return create_texture(bm_handle, fmt, w, h, bits, pal, 1, false, w, h); } - device_context_->GenerateMips(srv); - - Texture texture{bm_handle, dxgi_format, {}}; - texture.gpu_texture = std::move(d3d_texture); - texture.shader_resource_view = std::move(srv); - return texture; - } - - TextureManager::Texture TextureManager::create_user_texture_auto_mips( - const int bm_handle, - const rf::bm::Format fmt, - const int w, - const int h - ) { - // Same format negotiation as create_texture_auto_mips: GenerateMips requires - // RENDER_TARGET + MIP_AUTOGEN, so fall back to 8888 when the narrower format - // lacks that combination on this driver. - auto dxgi_format = get_supported_texture_format(fmt).first; - UINT format_support = 0; - device_->CheckFormatSupport(dxgi_format, &format_support); - constexpr UINT required_support = - D3D11_FORMAT_SUPPORT_RENDER_TARGET | D3D11_FORMAT_SUPPORT_MIP_AUTOGEN; - if ((format_support & required_support) != required_support) { - bool has_alpha = dxgi_format == DXGI_FORMAT_B5G5R5A1_UNORM - || dxgi_format == DXGI_FORMAT_B4G4R4A4_UNORM - || dxgi_format == DXGI_FORMAT_B8G8R8A8_UNORM; - dxgi_format = has_alpha ? DXGI_FORMAT_B8G8R8A8_UNORM : DXGI_FORMAT_B8G8R8X8_UNORM; - } - CD3D11_TEXTURE2D_DESC desc{ - dxgi_format, - static_cast(w), - static_cast(h), - 1, // arraySize - 0, // mipLevels = full chain - D3D11_BIND_SHADER_RESOURCE | D3D11_BIND_RENDER_TARGET, - D3D11_USAGE_DEFAULT, - 0, - }; - desc.MiscFlags = D3D11_RESOURCE_MISC_GENERATE_MIPS; - - ComPtr d3d_texture; - HRESULT hr = device_->CreateTexture2D(&desc, nullptr, &d3d_texture); - if (FAILED(hr)) { - xlog::warn("User mip-mapped texture creation failed ({}x{}), falling back to single mip", w, h); - return create_texture(bm_handle, fmt, w, h, nullptr, nullptr, 1, false); - } - - ComPtr srv; - hr = device_->CreateShaderResourceView(d3d_texture, nullptr, &srv); - if (FAILED(hr)) { - xlog::warn("User mip-mapped SRV creation failed ({}x{}), falling back to single mip", w, h); - return create_texture(bm_handle, fmt, w, h, nullptr, nullptr, 1, false); + // Only generate mips initially, if we uploaded data. + if (bits) { + device_context_->GenerateMips(srv); } Texture texture{bm_handle, dxgi_format, {}}; @@ -340,7 +301,7 @@ namespace gr::d3d11 if (rf::bm::get_type(bm_handle) == rf::bm::TYPE_USER) { xlog::trace("Creating user bitmap texture: handle {}", bm_handle); if (bm_is_user_mipmap(bm_handle) && !staging) { - return create_user_texture_auto_mips(bm_handle, fmt, w, h); + return create_texture_auto_mips(bm_handle, fmt, w, h, nullptr, nullptr); } auto texture = create_texture(bm_handle, fmt, w, h, nullptr, nullptr, 1, staging); return texture; @@ -628,6 +589,9 @@ namespace gr::d3d11 void TextureManager::unlock(rf::gr::LockInfo *lock) { + if (!lock) { + return; + } xlog::trace("unlocking texture: handle {} format {} size {}x{} data {}", lock->bm_handle, lock->format, lock->w, lock->h, lock->data); Texture& texture = get_or_load_texture(lock->bm_handle, true); if (texture.cpu_texture) { @@ -636,8 +600,14 @@ namespace gr::d3d11 device_context_->CopySubresourceRegion(texture.gpu_texture, 0, 0, 0, 0, texture.cpu_texture, 0, nullptr); // Rebuild lower mips after the write (only mip 0 is written via gr::lock). if (bm_is_user_mipmap(lock->bm_handle)) { - if (ID3D11ShaderResourceView* srv = texture.get_or_create_texture_view(device_, device_context_)) { - device_context_->GenerateMips(srv); + D3D11_TEXTURE2D_DESC desc{}; + texture.gpu_texture->GetDesc(&desc); + if (desc.MiscFlags & D3D11_RESOURCE_MISC_GENERATE_MIPS) { + ID3D11ShaderResourceView* srv = + texture.get_or_create_texture_view(device_, device_context_); + if (srv) { + device_context_->GenerateMips(srv); + } } } } diff --git a/game_patch/graphics/gr_font.cpp b/game_patch/graphics/gr_font.cpp index ff8e9c3d1..bcae31883 100644 --- a/game_patch/graphics/gr_font.cpp +++ b/game_patch/graphics/gr_font.cpp @@ -17,6 +17,7 @@ #include "../bmpman/bmpman.h" #include +#include #include FT_FREETYPE_H struct ParsedFontName @@ -379,8 +380,23 @@ void GrNewFont::draw_into_bitmap( const int bm_handle, const std::string_view text ) const { - int bm_w = 0, bm_h = 0; - rf::bm::get_dimensions(bm_handle, &bm_w, &bm_h); + if (text.empty() || bm_handle < 0 || bitmap_ == bm_handle) { + return; + } + + rf::gr::LockInfo src_lock{}; + if (!rf::gr::lock(bitmap_, 0, &src_lock, rf::gr::LOCK_READ_ONLY)) { + return; + } + + ScopeGuard src_guard{[&] { rf::gr::unlock(&src_lock); }}; + + rf::gr::LockInfo dst_lock{}; + if (!rf::gr::lock(bm_handle, 0, &dst_lock, rf::gr::LOCK_READ_ONLY_WRITE)) { + return; + } + + ScopeGuard dst_guard{[&] { rf::gr::unlock(&dst_lock); }}; int pen_x = x; const int pen_y = y + baseline_y_; @@ -389,19 +405,19 @@ void GrNewFont::draw_into_bitmap( if (glyph_idx != -1) { const GlyphInfo& glyph_info = glyphs_[glyph_idx]; if (glyph_info.bm_w) { - bm_copy( + bm_copy_pixels( + dst_lock, pen_x + glyph_info.x, pen_y + glyph_info.y, - bm_handle, + src_lock, glyph_info.bm_x, glyph_info.bm_y, glyph_info.bm_w, - glyph_info.bm_h, - bitmap_ + glyph_info.bm_h ); } pen_x += glyph_info.advance_x; - if (pen_x + glyph_info.x >= bm_w) { + if (pen_x >= dst_lock.w) { break; } } diff --git a/game_patch/hud/hud_world.cpp b/game_patch/hud/hud_world.cpp index f78dc201d..bf7b3d2aa 100644 --- a/game_patch/hud/hud_world.cpp +++ b/game_patch/hud/hud_world.cpp @@ -376,7 +376,7 @@ static NameLabelTex& ensure_hill_name_tex(const HillInfo& h, int font) // The label is drawn at a range of on-screen sizes, so generate a mip chain // like disk-loaded textures do to avoid shimmering when minified. - bm_set_user_mipmap(slot.bm, true); + bm_set_user_mipmap(slot.bm, g_alpine_game_config.big_hud); // keep resident rf::bm::texture_add_ref(slot.bm); @@ -529,8 +529,9 @@ static void render_koth_icon_for_hill(const HillInfo& h, WorldHUDRenderMode rm) } // hill name label - static const int big_font = rf::gr::load_font("boldfont.ttf:56"); - const int font = g_alpine_game_config.big_hud ? big_font : 0; + const int font = g_alpine_game_config.big_hud + ? rf::gr::load_font("boldfont.ttf:56") + : 0; NameLabelTex& lbl = ensure_hill_name_tex(h, font); const float text_h_world = ring_scale * 0.55f; diff --git a/game_patch/misc/misc.h b/game_patch/misc/misc.h index 149c697a9..dd7d4a35d 100644 --- a/game_patch/misc/misc.h +++ b/game_patch/misc/misc.h @@ -27,13 +27,3 @@ void clear_explicit_upcoming_game_type_request(); bool file_loaded_from_alpinefaction_vpp(const char* filename); bool weapon_reticle_is_customized(int weapon_id, bool bighud); bool rocket_locked_reticle_is_customized(bool bighud); - -template -struct ScopeGuard { - F _f; - explicit ScopeGuard(F f) noexcept(std::is_nothrow_move_constructible::value) - : _f(std::move(f)) { } - ~ScopeGuard() noexcept { _f(); } - ScopeGuard(const ScopeGuard&) = delete; - ScopeGuard& operator=(const ScopeGuard&) = delete; -}; From 792c59d6d95ff49cedddc740c2ec64c22b5171cc Mon Sep 17 00:00:00 2001 From: is-this-c <87069698+is-this-c@users.noreply.github.com> Date: Mon, 31 Aug 2026 03:01:11 +1200 Subject: [PATCH 5/7] Update scope_guard.h --- common/include/common/scope_guard.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/include/common/scope_guard.h b/common/include/common/scope_guard.h index d7029a766..54ef650dc 100644 --- a/common/include/common/scope_guard.h +++ b/common/include/common/scope_guard.h @@ -7,7 +7,7 @@ struct ScopeGuard { F _f; constexpr explicit ScopeGuard(F&& f) noexcept(std::is_nothrow_move_constructible::value) - : _f(std::forward(func)) { + : _f(std::forward(f)) { } ~ScopeGuard() noexcept { _f(); } ScopeGuard(const ScopeGuard&) = delete; From 10ea5dbb378fadc35065c4350217a54feb2dbad1 Mon Sep 17 00:00:00 2001 From: is-this-c <87069698+is-this-c@users.noreply.github.com> Date: Mon, 31 Aug 2026 03:14:06 +1200 Subject: [PATCH 6/7] Fix --- game_patch/graphics/d3d11/gr_d3d11_texture.cpp | 4 ++-- game_patch/graphics/d3d11/gr_d3d11_texture.h | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/game_patch/graphics/d3d11/gr_d3d11_texture.cpp b/game_patch/graphics/d3d11/gr_d3d11_texture.cpp index 3a383ff81..4197bff69 100644 --- a/game_patch/graphics/d3d11/gr_d3d11_texture.cpp +++ b/game_patch/graphics/d3d11/gr_d3d11_texture.cpp @@ -156,7 +156,7 @@ namespace gr::d3d11 if ((format_support & required_support) != required_support) { if (rf::bm::get_type(bm_handle) == rf::bm::TYPE_USER) { xlog::warn( - "Auto-mip not supported for user format {}, falling back to single mip", + "Auto-mip is not supported for user bitmaps that are format {}", static_cast(fmt) ); return create_texture(bm_handle, fmt, w, h, bits, pal, 1, false, w, h); @@ -598,7 +598,7 @@ namespace gr::d3d11 device_context_->Unmap(texture.cpu_texture, 0); if (lock->mode != rf::gr::LOCK_READ_ONLY && texture.gpu_texture) { device_context_->CopySubresourceRegion(texture.gpu_texture, 0, 0, 0, 0, texture.cpu_texture, 0, nullptr); - // Rebuild lower mips after the write (only mip 0 is written via gr::lock). + // We need to rebuild mips. if (bm_is_user_mipmap(lock->bm_handle)) { D3D11_TEXTURE2D_DESC desc{}; texture.gpu_texture->GetDesc(&desc); diff --git a/game_patch/graphics/d3d11/gr_d3d11_texture.h b/game_patch/graphics/d3d11/gr_d3d11_texture.h index 2a8921ab1..8878a9316 100644 --- a/game_patch/graphics/d3d11/gr_d3d11_texture.h +++ b/game_patch/graphics/d3d11/gr_d3d11_texture.h @@ -162,7 +162,6 @@ namespace gr::d3d11 Texture create_texture(int bm_handle, rf::bm::Format fmt, int w, int h, rf::ubyte* bits, rf::ubyte* pal, int mip_levels, bool staging, int src_w = 0, int src_h = 0); Texture create_texture_auto_mips(int bm_handle, rf::bm::Format fmt, int w, int h, rf::ubyte* bits, rf::ubyte* pal); - Texture create_user_texture_auto_mips(int bm_handle, rf::bm::Format fmt, int w, int h); Texture create_render_target(int bm_handle, int w, int h); Texture load_texture(int bm_handle, bool staging); std::pair determine_supported_texture_format(rf::bm::Format fmt); From f6815269719271d9d9f460a525f342f1759e6279 Mon Sep 17 00:00:00 2001 From: is-this-c <87069698+is-this-c@users.noreply.github.com> Date: Mon, 31 Aug 2026 03:16:04 +1200 Subject: [PATCH 7/7] Update gr_d3d11_texture.cpp --- game_patch/graphics/d3d11/gr_d3d11_texture.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/game_patch/graphics/d3d11/gr_d3d11_texture.cpp b/game_patch/graphics/d3d11/gr_d3d11_texture.cpp index 4197bff69..9c2804fde 100644 --- a/game_patch/graphics/d3d11/gr_d3d11_texture.cpp +++ b/game_patch/graphics/d3d11/gr_d3d11_texture.cpp @@ -598,7 +598,7 @@ namespace gr::d3d11 device_context_->Unmap(texture.cpu_texture, 0); if (lock->mode != rf::gr::LOCK_READ_ONLY && texture.gpu_texture) { device_context_->CopySubresourceRegion(texture.gpu_texture, 0, 0, 0, 0, texture.cpu_texture, 0, nullptr); - // We need to rebuild mips. + // We need to rebuild each mip level. if (bm_is_user_mipmap(lock->bm_handle)) { D3D11_TEXTURE2D_DESC desc{}; texture.gpu_texture->GetDesc(&desc);