Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
76 changes: 76 additions & 0 deletions game_patch/bmpman/bmpman.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down Expand Up @@ -244,6 +246,7 @@ FunHook<void(int)> 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;
Expand Down Expand Up @@ -325,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);
Expand All @@ -336,6 +349,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();
Expand Down
19 changes: 17 additions & 2 deletions game_patch/bmpman/bmpman.h
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@

#include <xlog/xlog.h>
#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
Expand All @@ -12,16 +15,28 @@ 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);
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)
{
Expand Down
2 changes: 1 addition & 1 deletion game_patch/bmpman/fmt_conv.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
62 changes: 62 additions & 0 deletions game_patch/graphics/d3d11/gr_d3d11_texture.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<UINT>(w),
static_cast<UINT>(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<ID3D11Texture2D> 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<ID3D11ShaderResourceView> 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<ID3D11Texture2D> gpu_ss_texture{};
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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);
}
}
}
}
}
Expand Down
1 change: 1 addition & 0 deletions game_patch/graphics/d3d11/gr_d3d11_texture.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<DXGI_FORMAT, rf::bm::Format> determine_supported_texture_format(rf::bm::Format fmt);
Expand Down
74 changes: 74 additions & 0 deletions game_patch/graphics/gr_font.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<uint8_t>(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) {
Expand Down Expand Up @@ -545,6 +581,43 @@ FunHook<int(int)> gr_get_font_height_hook{
},
};

FunHook<void(int, int, int, const char*, int)> 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<unsigned>(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<void(int, int, const char*, int, rf::gr::Mode)> gr_string_hook{
0x0051FEB0,
[](int x, int y, const char *text, int font_num, rf::gr::Mode mode) {
Expand Down Expand Up @@ -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();
Expand Down
9 changes: 7 additions & 2 deletions game_patch/hud/hud_world.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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;
Expand Down
Loading
Loading