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/3] 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/3] 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/3] 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;