diff --git a/common/CMakeLists.txt b/common/CMakeLists.txt index 50c56453..9cc0e6cc 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 00000000..54ef650d --- /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(f)) { + } + ~ScopeGuard() noexcept { _f(); } + ScopeGuard(const ScopeGuard&) = delete; + ScopeGuard& operator=(const ScopeGuard&) = delete; +}; diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 0f9c8e4a..82b135fd 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 (D3D11 only) + ### 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 db71f3bf..80cf5994 100644 --- a/game_patch/bmpman/bmpman.cpp +++ b/game_patch/bmpman/bmpman.cpp @@ -5,12 +5,14 @@ #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 "bmpman.h" int bm_calculate_pitch(int w, rf::bm::Format format) { @@ -244,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; @@ -325,6 +328,19 @@ 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); + 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) { + 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); @@ -336,6 +352,126 @@ 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 src_handle, + const int src_x, + const int src_y, + const int w, + const int h +) { + if (src_handle == dst_handle) { + return false; + } + + rf::gr::LockInfo src_lock{}; + if (!rf::gr::lock(src_handle, 0, &src_lock, rf::gr::LOCK_READ_ONLY)) { + return false; + } + + 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; + } + + 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 + ); +} + void bm_apply_patch() { bm_read_header_hook.install(); diff --git a/game_patch/bmpman/bmpman.h b/game_patch/bmpman/bmpman.h index 7aa5a21a..35a5c09f 100644 --- a/game_patch/bmpman/bmpman.h +++ b/game_patch/bmpman/bmpman.h @@ -2,7 +2,11 @@ #include #include "../rf/bmpman.h" -#include "../rf/gr/gr.h" + +namespace rf::gr { + struct Color; + struct LockInfo; +} // 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 @@ -12,16 +16,38 @@ 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 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); 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_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 src_handle, + int src_x, + int src_y, + int w, + int h +); 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 d9ec3aed..7e7c657d 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/d3d11/gr_d3d11_texture.cpp b/game_patch/graphics/d3d11/gr_d3d11_texture.cpp index 0d3e0011..9c2804fd 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 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); + } 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,7 +209,11 @@ 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); + + // Only generate mips initially, if we uploaded data. + if (bits) { + device_context_->GenerateMips(srv); + } Texture texture{bm_handle, dxgi_format, {}}; texture.gpu_texture = std::move(d3d_texture); @@ -286,6 +300,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_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; } @@ -572,12 +589,27 @@ 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) { 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 each mip level. + if (bm_is_user_mipmap(lock->bm_handle)) { + 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 c5cde238..bcae3188 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 @@ -54,6 +55,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 +374,56 @@ 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 { + 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_; + 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_pixels( + dst_lock, + pen_x + glyph_info.x, + pen_y + glyph_info.y, + src_lock, + glyph_info.bm_x, + glyph_info.bm_y, + glyph_info.bm_w, + glyph_info.bm_h + ); + } + pen_x += glyph_info.advance_x; + if (pen_x >= dst_lock.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 +597,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 +734,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/hud/hud_world.cpp b/game_patch/hud/hud_world.cpp index ed236461..bf7b3d2a 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, g_alpine_game_config.big_hud); + // keep resident rf::bm::texture_add_ref(slot.bm); @@ -524,8 +529,9 @@ 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; + 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/rf/bmpman.h b/game_patch/rf/bmpman.h index 1c13a9ed..82f479ce 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;