Files
Goober5000andClaude Opus 4.8 f3817c80fa Scale font line and tab advance by the font scale factor
Multi-line and tabbed strings drawn with a scaleMultiplier != 1 overlapped
or misaligned because the font code advanced newlines and tabs by the
unscaled font height/tab width -- most visible in FRED's viewport labels
now that they honor an adjustable font scale.

Scale the newline and tab advance in both the rendering and measurement
paths of each backend: the NVG path (gr_string and NVGFont::getStringSize)
and the VFNT path (gr_string_old and VFNTFont::getStringSize), which also
gains tab handling.  The tab width is hoisted onto the shared FSFont base
so both backends share it.

While here, pass the FSFont wrapper to gr_string_old instead of a bag of
scalars, make its font pointer const, and consolidate the
get_char_width_old externs into a single declaration in font_internal.h.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-09-03 01:55:03 -04:00

1494 lines
37 KiB
C++

#include "graphics/render.h"
#include "graphics/material.h"
#include "graphics/matrix.h"
#include "graphics/paths/PathRenderer.h"
#include "graphics/software/FSFont.h"
#include "graphics/software/NVGFont.h"
#include "graphics/software/VFNTFont.h"
#include "graphics/software/font_internal.h"
#include "localization/localize.h"
#include "mod_table/mod_table.h"
#include "render/3d.h"
static void gr_flash_internal(int r, int g, int b, int a, bool alpha_flash)
{
CLAMP(r, 0, 255);
CLAMP(g, 0, 255);
CLAMP(b, 0, 255);
CLAMP(a, 0, 255);
int x1 = (gr_screen.clip_left + gr_screen.offset_x);
int y1 = (gr_screen.clip_top + gr_screen.offset_y);
int x2 = (gr_screen.clip_right + gr_screen.offset_x) + 1;
int y2 = (gr_screen.clip_bottom + gr_screen.offset_y) + 1;
material render_material;
// By default materials use the immediate mode shader which is fine for us
render_material.set_depth_mode(ZBUFFER_TYPE_NONE);
if (alpha_flash) {
render_material.set_color(r, g, b, a);
render_material.set_blend_mode(ALPHA_BLEND_ALPHA_BLEND_ALPHA);
} else {
render_material.set_color(r, g, b, 255);
render_material.set_blend_mode(ALPHA_BLEND_ALPHA_ADDITIVE);
}
float glVertices[8] = { (float)x1, (float)y1, (float)x1, (float)y2,
(float)x2, (float)y1, (float)x2, (float)y2 };
vertex_layout vert_def;
vert_def.add_vertex_component(vertex_format_data::POSITION2, sizeof(float) * 2, 0);
gr_render_primitives_2d_immediate(&render_material, PRIM_TYPE_TRISTRIP, &vert_def, 4, glVertices, sizeof(float) * 8);
}
void gr_flash(int r, int g, int b) {
if (gr_screen.mode == GraphicsAPI::Stub) {
return;
}
if (!(r || g || b)) {
return;
}
gr_flash_internal(r, g, b, 255, false);
}
void gr_flash_alpha(int r, int g, int b, int a) {
if (gr_screen.mode == GraphicsAPI::Stub) {
return;
}
if (!(r || g || b || a)) {
return;
}
gr_flash_internal(r, g, b, a, true);
}
static void
draw_textured_quad(material* mat, float x1, float y1, float u1, float v1, float x2, float y2, float u2, float v2) {
GR_DEBUG_SCOPE("Draw textured quad");
float glVertices[4][4] = {{ x1, y1, u1, v1 },
{ x1, y2, u1, v2 },
{ x2, y1, u2, v1 },
{ x2, y2, u2, v2 }};
vertex_layout vert_def;
vert_def.add_vertex_component(vertex_format_data::POSITION2, sizeof(float) * 4, 0);
vert_def.add_vertex_component(vertex_format_data::TEX_COORD2, sizeof(float) * 4, sizeof(float) * 2);
gr_render_primitives_immediate(mat, PRIM_TYPE_TRISTRIP, &vert_def, 4, glVertices, sizeof(float) * 4 * 4);
}
static void bitmap_ex_internal(int x,
int y,
int w,
int h,
int sx,
int sy,
int resize_mode,
bool aabitmap,
bool mirror,
color* clr,
float scale_factor = 1.0f) {
if ((w < 1) || (h < 1)) {
return;
}
if (aabitmap && !clr->is_alphacolor) {
return;
}
float u0, u1, v0, v1;
float x1, x2, y1, y2;
int bw, bh;
bool do_resize;
if (resize_mode != GR_RESIZE_NONE && (gr_screen.custom_size || (gr_screen.rendering_to_texture != -1))) {
do_resize = true;
} else {
do_resize = false;
}
bm_get_info(gr_screen.current_bitmap, &bw, &bh);
if (scale_factor != 1.0f) {
bw = static_cast<int>(bw * scale_factor);
bh = static_cast<int>(bh * scale_factor);
}
u0 = (i2fl(sx) / i2fl(bw));
v0 = (i2fl(sy) / i2fl(bh));
u1 = (i2fl(sx + w) / i2fl(bw));
v1 = (i2fl(sy + h) / i2fl(bh));
x1 = i2fl(x + ((do_resize) ? gr_screen.offset_x_unscaled : gr_screen.offset_x));
y1 = i2fl(y + ((do_resize) ? gr_screen.offset_y_unscaled : gr_screen.offset_y));
x2 = x1 + i2fl(w);
y2 = y1 + i2fl(h);
if (do_resize) {
gr_resize_screen_posf(&x1, &y1, NULL, NULL, resize_mode);
gr_resize_screen_posf(&x2, &y2, NULL, NULL, resize_mode);
}
if (mirror) {
float temp = u0;
u0 = u1;
u1 = temp;
}
material render_mat;
render_mat.set_blend_mode(ALPHA_BLEND_ALPHA_BLEND_ALPHA);
render_mat.set_depth_mode(ZBUFFER_TYPE_NONE);
render_mat.set_texture_map(TM_BASE_TYPE, gr_screen.current_bitmap);
render_mat.set_color(clr->red, clr->green, clr->blue, clr->alpha);
render_mat.set_cull_mode(false);
if (aabitmap) {
render_mat.set_texture_type(material::TEX_TYPE_AABITMAP);
} else {
if (bm_has_alpha_channel(gr_screen.current_bitmap)) {
render_mat.set_texture_type(material::TEX_TYPE_XPARENT);
} else {
render_mat.set_texture_type(material::TEX_TYPE_NORMAL);
}
}
draw_textured_quad(&render_mat, x1, y1, u0, v0, x2, y2, u1, v1);
}
void gr_aabitmap(int x, int y, int resize_mode, bool mirror, float scale_factor) {
if (gr_screen.mode == GraphicsAPI::Stub) {
return;
}
GR_DEBUG_SCOPE("Draw AA-bitmap");
int w, h, do_resize;
bm_get_info(gr_screen.current_bitmap, &w, &h);
if (scale_factor != 1.0f) {
w = static_cast<int>(w * scale_factor);
h = static_cast<int>(h * scale_factor);
}
if (resize_mode != GR_RESIZE_NONE && (gr_screen.custom_size || (gr_screen.rendering_to_texture != -1))) {
do_resize = 1;
} else {
do_resize = 0;
}
int dx1 = x;
int dx2 = x + w - 1;
int dy1 = y;
int dy2 = y + h - 1;
int sx = 0, sy = 0;
int clip_left = ((do_resize) ? gr_screen.clip_left_unscaled : gr_screen.clip_left);
int clip_right = ((do_resize) ? gr_screen.clip_right_unscaled : gr_screen.clip_right);
int clip_top = ((do_resize) ? gr_screen.clip_top_unscaled : gr_screen.clip_top);
int clip_bottom = ((do_resize) ? gr_screen.clip_bottom_unscaled : gr_screen.clip_bottom);
if ((dx1 > clip_right) || (dx2 < clip_left)) {
return;
}
if ((dy1 > clip_bottom) || (dy2 < clip_top)) {
return;
}
int delta_left = 0, delta_right = 0;
if (dx1 < clip_left) {
delta_left = clip_left - dx1;
dx1 = clip_left;
}
if (dy1 < clip_top) {
sy = clip_top - dy1;
dy1 = clip_top;
}
if (dx2 > clip_right) {
delta_right = dx2 - clip_right;
dx2 = clip_right;
}
if (dy2 > clip_bottom) {
dy2 = clip_bottom;
}
// When mirrored, the destination's left edge maps to the source's right edge,
// so a left-side dest clip drops pixels from the right of the source (and vice versa).
sx = mirror ? delta_right : delta_left;
if ((sx < 0) || (sy < 0)) {
return;
}
if ((sx >= w) || (sy >= h)) {
return;
}
// Draw bitmap bm[sx,sy] into (dx1,dy1)-(dx2,dy2)
bitmap_ex_internal(dx1,
dy1,
(dx2 - dx1 + 1),
(dy2 - dy1 + 1),
sx,
sy,
resize_mode,
true,
mirror,
&GR_CURRENT_COLOR,
scale_factor);
}
void gr_aabitmap_ex(int x, int y, int w, int h, int sx, int sy, int resize_mode, bool mirror, float scale_factor) {
if (gr_screen.mode == GraphicsAPI::Stub) {
return;
}
int reclip;
#ifndef NDEBUG
int count = 0;
#endif
int bw, bh, do_resize;
bm_get_info(gr_screen.current_bitmap, &bw, &bh);
if (scale_factor != 1.0f) {
bw = fl2i(bw * scale_factor);
bh = fl2i(bh * scale_factor);
// If we're scaling then we need to scale these to make sure the right and bottom clip planes are scaled as well
w = fl2i(w * scale_factor);
h = fl2i(h * scale_factor);
sx = fl2i(sx * scale_factor);
sy = fl2i(sy * scale_factor);
}
int dx1 = x;
int dx2 = x + w - 1;
int dy1 = y;
int dy2 = y + h - 1;
if (resize_mode != GR_RESIZE_NONE && (gr_screen.custom_size || (gr_screen.rendering_to_texture != -1))) {
do_resize = 1;
} else {
do_resize = 0;
}
int clip_left = ((do_resize) ? gr_screen.clip_left_unscaled : gr_screen.clip_left);
int clip_right = ((do_resize) ? gr_screen.clip_right_unscaled : gr_screen.clip_right);
int clip_top = ((do_resize) ? gr_screen.clip_top_unscaled : gr_screen.clip_top);
int clip_bottom = ((do_resize) ? gr_screen.clip_bottom_unscaled : gr_screen.clip_bottom);
do {
reclip = 0;
#ifndef NDEBUG
if (count > 1) {
Int3();
}
count++;
#endif
if ((dx1 > clip_right) || (dx2 < clip_left)) {
return;
}
if ((dy1 > clip_bottom) || (dy2 < clip_top)) {
return;
}
if (dx1 < clip_left) {
int delta = clip_left - dx1;
// When mirrored, dest-left maps to source-right, so drop from the source's right
// edge (handled via reduced w below) instead of advancing sx.
if (!mirror) {
sx += delta;
}
dx1 = clip_left;
}
if (dy1 < clip_top) {
sy += clip_top - dy1;
dy1 = clip_top;
}
if (dx2 > clip_right) {
int delta = dx2 - clip_right;
// When mirrored, dest-right maps to source-left, so advance sx by the right delta.
if (mirror) {
sx += delta;
}
dx2 = clip_right;
}
if (dy2 > clip_bottom) {
dy2 = clip_bottom;
}
if (sx < 0) {
dx1 -= sx;
sx = 0;
reclip = 1;
}
if (sy < 0) {
dy1 -= sy;
sy = 0;
reclip = 1;
}
w = dx2 - dx1 + 1;
h = dy2 - dy1 + 1;
if (sx + w > bw) {
w = bw - sx;
dx2 = dx1 + w - 1;
}
if (sy + h > bh) {
h = bh - sy;
dy2 = dy1 + h - 1;
}
if ((w < 1) || (h < 1)) {
// clipped away!
return;
}
} while (reclip);
// Make sure clipping algorithm works
#ifndef NDEBUG
Assert(w > 0);
Assert(h > 0);
Assert(w == (dx2 - dx1 + 1));
Assert(h == (dy2 - dy1 + 1));
Assert(sx >= 0);
Assert(sy >= 0);
Assert(sx + w <= bw);
Assert(sy + h <= bh);
Assert(dx2 >= dx1);
Assert(dy2 >= dy1);
Assert((dx1 >= clip_left) && (dx1 <= clip_right));
Assert((dx2 >= clip_left) && (dx2 <= clip_right));
Assert((dy1 >= clip_top) && (dy1 <= clip_bottom));
Assert((dy2 >= clip_top) && (dy2 <= clip_bottom));
#endif
// We now have dx1,dy1 and dx2,dy2 and sx, sy all set validly within clip regions.
bitmap_ex_internal(dx1,
dy1,
(dx2 - dx1 + 1),
(dy2 - dy1 + 1),
sx,
sy,
resize_mode,
true,
mirror,
&GR_CURRENT_COLOR,
scale_factor);
}
//these are penguins bitmap functions
void gr_bitmap_ex(int x, int y, int w, int h, int sx, int sy, int resize_mode, bool mirror, float scale_factor) {
if (gr_screen.mode == GraphicsAPI::Stub) {
return;
}
int reclip;
#ifndef NDEBUG
int count = 0;
#endif
int bw, bh, do_resize;
bm_get_info(gr_screen.current_bitmap, &bw, &bh);
if (scale_factor != 1.0f) {
bw = fl2i(bw * scale_factor);
bh = fl2i(bh * scale_factor);
// If we're scaling then we need to scale these to make sure the right and bottom clip planes are scaled as well
w = fl2i(w * scale_factor);
h = fl2i(h * scale_factor);
sx = fl2i(sx * scale_factor);
sy = fl2i(sy * scale_factor);
}
int dx1 = x;
int dx2 = x + w - 1;
int dy1 = y;
int dy2 = y + h - 1;
if (resize_mode != GR_RESIZE_NONE && (gr_screen.custom_size || (gr_screen.rendering_to_texture != -1))) {
do_resize = 1;
} else {
do_resize = 0;
}
int clip_left = ((do_resize) ? gr_screen.clip_left_unscaled : gr_screen.clip_left);
int clip_right = ((do_resize) ? gr_screen.clip_right_unscaled : gr_screen.clip_right);
int clip_top = ((do_resize) ? gr_screen.clip_top_unscaled : gr_screen.clip_top);
int clip_bottom = ((do_resize) ? gr_screen.clip_bottom_unscaled : gr_screen.clip_bottom);
do {
reclip = 0;
#ifndef NDEBUG
if (count > 1) {
Int3();
}
count++;
#endif
if ((dx1 > clip_right) || (dx2 < clip_left)) {
return;
}
if ((dy1 > clip_bottom) || (dy2 < clip_top)) {
return;
}
if (dx1 < clip_left) {
int delta = clip_left - dx1;
// When mirrored, dest-left maps to source-right, so drop from the source's right
// edge (handled via reduced w below) instead of advancing sx.
if (!mirror) {
sx += delta;
}
dx1 = clip_left;
}
if (dy1 < clip_top) {
sy += clip_top - dy1;
dy1 = clip_top;
}
if (dx2 > clip_right) {
int delta = dx2 - clip_right;
// When mirrored, dest-right maps to source-left, so advance sx by the right delta.
if (mirror) {
sx += delta;
}
dx2 = clip_right;
}
if (dy2 > clip_bottom) {
dy2 = clip_bottom;
}
if (sx < 0) {
dx1 -= sx;
sx = 0;
reclip = 1;
}
if (sy < 0) {
dy1 -= sy;
sy = 0;
reclip = 1;
}
w = dx2 - dx1 + 1;
h = dy2 - dy1 + 1;
if ((sx + w) > bw) {
w = bw - sx;
dx2 = dx1 + w - 1;
}
if ((sy + h) > bh) {
h = bh - sy;
dy2 = dy1 + h - 1;
}
if ((w < 1) || (h < 1)) {
// clipped away!
return;
}
} while (reclip);
// Make sure clipping algorithm works
#ifndef NDEBUG
Assert(w > 0);
Assert(h > 0);
Assert(w == (dx2 - dx1 + 1));
Assert(h == (dy2 - dy1 + 1));
Assert(sx >= 0);
Assert(sy >= 0);
Assert((sx + w) <= bw);
Assert((sy + h) <= bh);
Assert(dx2 >= dx1);
Assert(dy2 >= dy1);
Assert((dx1 >= clip_left) && (dx1 <= clip_right));
Assert((dx2 >= clip_left) && (dx2 <= clip_right));
Assert((dy1 >= clip_top) && (dy1 <= clip_bottom));
Assert((dy2 >= clip_top) && (dy2 <= clip_bottom));
#endif
color clr;
gr_init_alphacolor(&clr, 255, 255, 255, fl2i(gr_screen.current_alpha * 255.0f));
// We now have dx1,dy1 and dx2,dy2 and sx, sy all set validly within clip regions.
bitmap_ex_internal(dx1, dy1, (dx2 - dx1 + 1), (dy2 - dy1 + 1), sx, sy, resize_mode, false, mirror, &clr, scale_factor);
}
#define MAX_VERTS_PER_DRAW 300
struct v4 {
float x, y, u, v;
};
static v4 String_render_buff[MAX_VERTS_PER_DRAW];
static void gr_string_old(float sx,
float sy,
const char* s,
const char* end,
const font::font* fontData,
const font::FSFont* fontMetrics,
int resize_mode,
float scaleMultiplier)
{
GR_DEBUG_SCOPE("Render VFNT string");
const float height = fontMetrics->getHeight();
const bool canAutoScale = fontMetrics->getAutoScaleBehavior();
const bool canScale = fontMetrics->getScaleBehavior();
const float tabWidth = fontMetrics->getTabWidth();
float x = sx;
float y = sy;
material render_mat;
render_mat.set_blend_mode(ALPHA_BLEND_ALPHA_BLEND_ALPHA);
render_mat.set_depth_mode(ZBUFFER_TYPE_NONE);
render_mat.set_texture_map(TM_BASE_TYPE, fontData->bitmap_id);
render_mat.set_color(GR_CURRENT_COLOR.red,
GR_CURRENT_COLOR.green,
GR_CURRENT_COLOR.blue,
GR_CURRENT_COLOR.alpha);
render_mat.set_cull_mode(false);
render_mat.set_texture_type(material::TEX_TYPE_AABITMAP);
int buffer_offset = 0;
int ibw, ibh;
bm_get_info(fontData->bitmap_id, &ibw, &ibh);
float bw = i2fl(ibw);
float bh = i2fl(ibh);
bool do_resize;
if (resize_mode != GR_RESIZE_NONE && (gr_screen.custom_size || (gr_screen.rendering_to_texture != -1))) {
do_resize = true;
} else {
do_resize = false;
}
int clip_left = ((do_resize) ? gr_screen.clip_left_unscaled : gr_screen.clip_left);
int clip_right = ((do_resize) ? gr_screen.clip_right_unscaled : gr_screen.clip_right);
int clip_top = ((do_resize) ? gr_screen.clip_top_unscaled : gr_screen.clip_top);
int clip_bottom = ((do_resize) ? gr_screen.clip_bottom_unscaled : gr_screen.clip_bottom);
vertex_layout vert_def;
vert_def.add_vertex_component(vertex_format_data::POSITION2, sizeof(v4), (int)offsetof(v4, x));
vert_def.add_vertex_component(vertex_format_data::TEX_COORD2, sizeof(v4), (int)offsetof(v4, u));
gr_set_2d_matrix();
float scale_factor = (canScale && !Fred_running) ? get_font_scale_factor() : 1.0f;
if (canAutoScale && !Fred_running) {
float autoSizedFont = calculate_auto_font_size(height);
// Calculate the auto scale factor
float auto_scale_factor = autoSizedFont / height;
scale_factor *= auto_scale_factor;
}
scale_factor *= scaleMultiplier;
int letter;
while (s < end) {
// Handle line breaks
while (*s == '\n') {
s++;
y += height * scale_factor;
x = sx;
}
if (*s == 0) {
break;
}
// Handle tabs
if (*s == '\t') {
s++;
x += tabWidth * scale_factor;
continue;
}
// Get character width and spacing
int raw_width = 0, raw_spacing = 0;
letter = font::get_char_width_old(fontData,
(ubyte)s[0],
(ubyte)s[1],
&raw_width,
&raw_spacing);
s++;
// Not in font, draw as space
if (letter < 0) {
x += raw_spacing * scale_factor;
continue;
}
// UV coordinates
int u = fontData->bm_u[letter];
int v = fontData->bm_v[letter];
float char_width = i2fl(raw_width);
float char_height = i2fl(height);
// Scale output dimensions and positions
float xc = x; // Keep the X position unscaled
float yc = y; // Keep the Y position unscaled
float wc = char_width * scale_factor; // Scale width
float hc = char_height * scale_factor; // Scale height
// Check if the character is completely out of bounds. This uses scaled width and height
if ((xc > clip_right) || ((xc + wc) < clip_left) || (yc > clip_bottom) || ((yc + hc) < clip_top)) {
x += raw_spacing * scale_factor;
continue;
}
// Apply offsets
float x1 = xc + ((do_resize) ? gr_screen.offset_x_unscaled : gr_screen.offset_x);
float y1 = yc + ((do_resize) ? gr_screen.offset_y_unscaled : gr_screen.offset_y);
float x2 = x1 + wc;
float y2 = y1 + hc;
// Resize screen positions
if (do_resize) {
gr_resize_screen_posf(&x1, &y1, NULL, NULL, resize_mode);
gr_resize_screen_posf(&x2, &y2, NULL, NULL, resize_mode);
}
// Get the character from the UV
float u0 = u / bw;
float v0 = v / bh;
float u1 = (u + char_width) / bw;
float v1 = (v + char_height) / bh;
// Add vertices for the character
String_render_buff[buffer_offset++] = {x1, y1, u0, v0};
String_render_buff[buffer_offset++] = {x1, y2, u0, v1};
String_render_buff[buffer_offset++] = {x2, y1, u1, v0};
String_render_buff[buffer_offset++] = {x1, y2, u0, v1};
String_render_buff[buffer_offset++] = {x2, y1, u1, v0};
String_render_buff[buffer_offset++] = {x2, y2, u1, v1};
// If the buffer is full, render it now
if (buffer_offset == MAX_VERTS_PER_DRAW) {
gr_render_primitives_immediate(&render_mat,
PRIM_TYPE_TRIS,
&vert_def,
buffer_offset,
String_render_buff,
sizeof(v4) * buffer_offset);
buffer_offset = 0;
}
// Advance x for the next character
x += raw_spacing * scale_factor;
}
// Render remaining vertices in the buffer
if (buffer_offset) {
gr_render_primitives_immediate(&render_mat,
PRIM_TYPE_TRIS,
&vert_def,
buffer_offset,
String_render_buff,
sizeof(v4) * buffer_offset);
}
gr_end_2d_matrix();
}
namespace {
bool buffering_nanovg = false; //!< flag for when NanoVG buffering is enabled
void setupDrawingState(graphics::paths::PathRenderer* path) {
path->resetState();
}
void setupTransforms(graphics::paths::PathRenderer* path, int resize_mode) {
float x = 0.0f;
float y = 0.0f;
float w = 1.0f;
float h = 1.0f;
bool do_resize = gr_resize_screen_posf(&x, &y, &w, &h, resize_mode);
if (gr_screen.rendering_to_texture != -1) {
// Flip the Y-axis when rendering to texture
path->translate(0.f, i2fl(gr_screen.max_h));
path->scale(1.f, -1.f);
}
path->translate(x, y);
path->scale(w, h);
int clip_width = ((do_resize) ? gr_screen.clip_width_unscaled : gr_screen.clip_width);
int clip_height = ((do_resize) ? gr_screen.clip_height_unscaled : gr_screen.clip_height);
int offset_x = ((do_resize) ? gr_screen.offset_x_unscaled : gr_screen.offset_x);
int offset_y = ((do_resize) ? gr_screen.offset_y_unscaled : gr_screen.offset_y);
path->translate(i2fl(offset_x), i2fl(offset_y));
path->scissor(0.0f, 0.0f, i2fl(clip_width), i2fl(clip_height));
}
graphics::paths::PathRenderer* beginDrawing(int resize_mode) {
auto path = graphics::paths::PathRenderer::instance();
path->saveState();
setupDrawingState(path);
if (!buffering_nanovg) {
// If buffering is enabled then this has already been called
path->beginFrame();
}
setupTransforms(path, resize_mode);
path->beginPath();
path->setStrokeWidth(GR_CURRENT_LINE_WIDTH);
return path;
}
void endDrawing(graphics::paths::PathRenderer* path) {
if (!buffering_nanovg) {
// If buffering is enabled then this will be called later
path->endFrame();
}
path->restoreState();
}
}
void gr_string(float sx, float sy, const char* s, int resize_mode, float scaleMultiplier, size_t in_length)
{
if (gr_screen.mode == GraphicsAPI::Stub) {
return;
}
GR_DEBUG_SCOPE("Render string");
using namespace font;
using namespace graphics::paths;
namespace fo = font;
Assertion(s != NULL, "NULL pointer passed to gr_string!");
if (!FontManager::isReady() || (*s == '\0')) {
return;
}
size_t length;
if (in_length == std::string::npos) {
length = strlen(s);
} else {
length = in_length;
}
FSFont* currentFont = FontManager::getCurrentFont();
if (currentFont->getType() == VFNT_FONT) {
VFNTFont* fnt = static_cast<VFNTFont*>(currentFont);
fo::font* fontData = fnt->getFontData();
gr_string_old(sx, sy, s, s + length, fontData, currentFont, resize_mode, scaleMultiplier);
} else if (currentFont->getType() == NVG_FONT) {
GR_DEBUG_SCOPE("Render TTF string");
auto path = beginDrawing(resize_mode);
auto nvgFont = static_cast<NVGFont*>(currentFont);
float scale_factor = (nvgFont->getScaleBehavior() && !Fred_running) ? get_font_scale_factor() : 1.0f;
scale_factor *= scaleMultiplier;
float originalSize = nvgFont->getSize();
float scaledSize = originalSize * scale_factor;
// Calculate the offset to center the text
float offsetX = 0.0f;
// This is a compromise to try and size the text around center to minimize text offsets during scaling behavior.
// TODO Update this if multiline text is found to be negatively affected or a proper method of setting text anchors is added
float offsetY = (scaledSize - originalSize) * 0.5f;
path->translate(sx - offsetX, sy - offsetY);
path->fontFaceId(nvgFont->getHandle());
path->fontSize(scaledSize);
path->textLetterSpacing(nvgFont->getLetterSpacing());
path->textAlign(static_cast<TextAlign>(ALIGN_TOP | ALIGN_LEFT));
float scaleX = 1.0f;
float scaleY = 1.0f;
gr_resize_screen_posf(nullptr, nullptr, &scaleX, &scaleY, resize_mode);
float invscaleX = 1.f / scaleX;
bool twoPassRequired = false;
path->setFillColor(&GR_CURRENT_COLOR);
// Do a two pass algorithm, first render text using NanoVG, then render old characters
for (int pass = 0; pass < 2; ++pass) {
const char* text = s;
size_t textLen = length;
float x = 0.0f;
float y = 0.0f;
size_t tokenLength;
while ((tokenLength = NVGFont::getTokenLength(text, textLen)) > 0) {
textLen -= tokenLength;
bool doRender = true;
bool specialChar = false;
if (tokenLength == 1) {
// We may have encountered a special character
switch (*text) {
case '\n':
doRender = false;
y += nvgFont->getHeight() * scale_factor;
x = 0;
break;
case '\t':
doRender = false;
x += nvgFont->getTabWidth() * scale_factor;
break;
case '\r':
// Ignore Carriage return chars
doRender = false;
break;
default:
// Only do special character handling if we are not in unicode mode. Otherwise this is just a normal character for us
if (!Unicode_text_mode) {
if (*text >= Lcl_special_chars || *text < 0) {
specialChar = true;
twoPassRequired = true;
}
else {
doRender = true;
}
}
break;
}
}
if (specialChar) {
if (pass == 1) {
// We compute the top offset of the special character by aligning it to the base line of the string
// This is done by moving to the base line of the string by adding the ascender value and then
// accounting for the height of the text with the height of the special font
auto yOffset = nvgFont->getTopOffset() +
(nvgFont->getAscender() - nvgFont->getSpecialCharacterFont()->h);
gr_string_old(sx + x * scaleX,
sy + (y + yOffset) * scaleY,
text,
text + 1,
nvgFont->getSpecialCharacterFont(),
nvgFont,
resize_mode,
scaleMultiplier);
}
int width;
int spacing;
get_char_width_old(nvgFont->getSpecialCharacterFont(), (ubyte)*text, (ubyte)'\0', &width, &spacing);
x += i2fl(spacing) * invscaleX;
} else if (doRender) {
if (doRender && tokenLength > 0) {
float advance;
float currentX = x * scaleX;
float currentY = y + nvgFont->getTopOffset();
if (pass == 0) {
path->text(currentX, currentY, text, text + tokenLength);
}
advance = path->textBounds(0.f, 0.f, text, text + tokenLength, nullptr);
x += advance * invscaleX;
}
}
text = text + tokenLength;
}
if (pass == 0) {
endDrawing(path);
}
if (!twoPassRequired) {
break;
}
}
} else {
Error(LOCATION, "Invalid type enumeration for font \"%s\". Get a coder!", currentFont->getName().c_str());
}
}
void gr_string_outlined(int x, int y, const char* text, const color* foreground, const color* outline, int offset, int resize_mode, float scaleMultiplier, size_t length)
{
// draw outline by rendering text at all surrounding offsets
gr_set_color_fast(outline);
for (int dx = -offset; dx <= offset; dx++)
for (int dy = -offset; dy <= offset; dy++)
if (dx || dy)
gr_string(x + dx, y + dy, text, resize_mode, scaleMultiplier, length);
// draw foreground text on top
gr_set_color_fast(foreground);
gr_string(x, y, text, resize_mode, scaleMultiplier, length);
}
static void gr_line(float x1, float y1, float x2, float y2, int resize_mode) {
auto path = beginDrawing(resize_mode);
if ((x1 == x2) && (y1 == y2)) {
path->circle(x1, y1, 1.5);
path->setFillColor(&GR_CURRENT_COLOR);
path->fill();
} else {
path->moveTo(x1, y1);
path->lineTo(x2, y2);
path->setStrokeColor(&GR_CURRENT_COLOR);
path->stroke();
}
endDrawing(path);
}
void gr_line(int x1, int y1, int x2, int y2, int resize_mode) {
if (gr_screen.mode == GraphicsAPI::Stub) {
return;
}
gr_line(i2fl(x1), i2fl(y1), i2fl(x2), i2fl(y2), resize_mode);
}
void gr_aaline(vertex* v1, vertex* v2) {
if (gr_screen.mode == GraphicsAPI::Stub) {
return;
}
float x1 = v1->screen.xyw.x;
float y1 = v1->screen.xyw.y;
float x2 = v2->screen.xyw.x;
float y2 = v2->screen.xyw.y;
// AA is now standard
gr_line(x1, y1, x2, y2, GR_RESIZE_NONE);
}
void gr_gradient(int x1, int y1, int x2, int y2, int resize_mode) {
if (gr_screen.mode == GraphicsAPI::Stub) {
return;
}
if (!GR_CURRENT_COLOR.is_alphacolor) {
gr_line(x1, y1, x2, y2, resize_mode);
return;
}
auto path = beginDrawing(resize_mode);
color endColor = GR_CURRENT_COLOR;
endColor.alpha = 0;
auto gradientPaint =
path->createLinearGradient(i2fl(x1), i2fl(y1), i2fl(x2), i2fl(y2), &GR_CURRENT_COLOR, &endColor);
path->moveTo(i2fl(x1), i2fl(y1));
path->lineTo(i2fl(x2), i2fl(y2));
path->setStrokePaint(gradientPaint);
path->stroke();
endDrawing(path);
}
void gr_pixel(int x, int y, int resize_mode) {
if (gr_screen.mode == GraphicsAPI::Stub) {
return;
}
gr_line(x, y, x, y, resize_mode);
}
void gr_circle(int xc, int yc, int d, int resize_mode) {
if (gr_screen.mode == GraphicsAPI::Stub) {
return;
}
auto path = beginDrawing(resize_mode);
path->circle(i2fl(xc), i2fl(yc), d / 2.0f);
path->setFillColor(&GR_CURRENT_COLOR);
path->fill();
endDrawing(path);
}
void gr_unfilled_circle(int xc, int yc, int d, int resize_mode) {
if (gr_screen.mode == GraphicsAPI::Stub) {
return;
}
auto path = beginDrawing(resize_mode);
path->circle(i2fl(xc), i2fl(yc), d / 2.0f);
path->setStrokeColor(&GR_CURRENT_COLOR);
path->stroke();
endDrawing(path);
}
void gr_arc(int xc, int yc, float r, float angle_start, float angle_end, bool fill, int resize_mode) {
if (gr_screen.mode == GraphicsAPI::Stub) {
return;
}
// Ensure that angle_start < angle_end
if (angle_end < angle_start) {
float temp = angle_start;
angle_start = angle_end;
angle_end = temp;
}
using namespace graphics::paths;
auto path = beginDrawing(resize_mode);
if (fill) {
path->arc(i2fl(xc), i2fl(yc), r, fl_radians(angle_start), fl_radians(angle_end), DIR_CW);
path->lineTo(i2fl(xc), i2fl(yc));
path->setFillColor(&GR_CURRENT_COLOR);
path->fill();
} else {
path->arc(i2fl(xc), i2fl(yc), r, fl_radians(angle_start), fl_radians(angle_end), DIR_CW);
path->setStrokeColor(&GR_CURRENT_COLOR);
path->stroke();
}
endDrawing(path);
}
void gr_curve(int xc, int yc, int r, int direction, int resize_mode) {
if (gr_screen.mode == GraphicsAPI::Stub) {
return;
}
using namespace graphics::paths;
auto path = beginDrawing(resize_mode);
float centerX, centerY;
float beginAngle, endAngle;
switch (direction) {
case 0: {
centerX = i2fl(xc + r);
centerY = i2fl(yc + r);
beginAngle = fl_radians(180.f);
endAngle = fl_radians(270.f);
break;
}
case 1: {
centerX = i2fl(xc);
centerY = i2fl(yc + r);
beginAngle = fl_radians(270.f);
endAngle = fl_radians(360.f);
break;
}
case 2: {
centerX = i2fl(xc + r);
centerY = i2fl(yc);
beginAngle = fl_radians(90.f);
endAngle = fl_radians(180.f);
break;
}
case 3: {
centerX = i2fl(xc);
centerY = i2fl(yc);
beginAngle = fl_radians(0.f);
endAngle = fl_radians(90.f);
break;
}
default:
return;
}
path->arc(centerX, centerY, i2fl(r), beginAngle, endAngle, DIR_CW);
path->setStrokeColor(&GR_CURRENT_COLOR);
path->stroke();
endDrawing(path);
}
void gr_rect(int x, int y, int w, int h, int resize_mode, float angle) {
if (gr_screen.mode == GraphicsAPI::Stub) {
return;
}
auto path = beginDrawing(resize_mode);
if (angle != 0) {
// If we don't do this translation before and after rotating, the rotation will use 0,0 as the pivot, flinging the rectangle far away.
float offsetX = x + w / 2.0f;
float offsetY = y + h / 2.0f;
path->translate(offsetX, offsetY);
path->rotate(angle);
path->translate(-offsetX, -offsetY);
}
path->rectangle(i2fl(x), i2fl(y), i2fl(w), i2fl(h));
path->setFillColor(&GR_CURRENT_COLOR);
path->fill();
endDrawing(path);
}
void gr_shade(int x, int y, int w, int h, int resize_mode) {
if (gr_screen.mode == GraphicsAPI::Stub) {
return;
}
auto r = (int) gr_screen.current_shader.r;
auto g = (int) gr_screen.current_shader.g;
auto b = (int) gr_screen.current_shader.b;
auto a = (int) gr_screen.current_shader.c;
color clr;
gr_init_alphacolor(&clr, r, g, b, a);
auto path = beginDrawing(resize_mode);
path->rectangle(i2fl(x), i2fl(y), i2fl(w), i2fl(h));
path->setFillColor(&clr);
path->fill();
endDrawing(path);
}
void gr_2d_start_buffer() {
if (gr_screen.mode == GraphicsAPI::Stub) {
return;
}
Assertion(!buffering_nanovg, "Tried to enable 2D buffering but it was already enabled!");
buffering_nanovg = true;
auto path = graphics::paths::PathRenderer::instance();
path->beginFrame();
}
void gr_2d_stop_buffer() {
if (gr_screen.mode == GraphicsAPI::Stub) {
return;
}
Assertion(buffering_nanovg, "Tried to stop 2D buffering but it was not enabled!");
buffering_nanovg = false;
auto path = graphics::paths::PathRenderer::instance();
path->endFrame();
}
gr_buffer_handle gr_immediate_buffer_handle;
static size_t immediate_buffer_offset = 0;
static size_t immediate_buffer_size = 0;
static const size_t IMMEDIATE_BUFFER_RESIZE_BLOCK_SIZE = 2048;
size_t gr_add_to_immediate_buffer(size_t size, void* data) {
if (gr_screen.mode == GraphicsAPI::Stub) {
return 0;
}
GR_DEBUG_SCOPE("Add data to immediate buffer");
if (!gr_immediate_buffer_handle.isValid()) {
gr_immediate_buffer_handle = gr_create_buffer(BufferType::Vertex, BufferUsageHint::Dynamic);
}
Assert(size > 0 && data != NULL);
if ( immediate_buffer_offset + size > immediate_buffer_size ) {
// incoming data won't fit the immediate buffer. time to reallocate.
immediate_buffer_offset = 0;
immediate_buffer_size += MAX(IMMEDIATE_BUFFER_RESIZE_BLOCK_SIZE, size);
gr_update_buffer_data(gr_immediate_buffer_handle, immediate_buffer_size, NULL);
}
// only update a section of the immediate vertex buffer
gr_update_buffer_data_offset(gr_immediate_buffer_handle, immediate_buffer_offset, size, data);
auto old_offset = immediate_buffer_offset;
immediate_buffer_offset += size;
return old_offset;
}
void gr_reset_immediate_buffer() {
if (gr_screen.mode == GraphicsAPI::Stub) {
return;
}
if (!gr_immediate_buffer_handle.isValid()) {
// we haven't used the immediate buffer yet
return;
}
// orphan the immediate buffer so we can start fresh in a new frame
gr_update_buffer_data(gr_immediate_buffer_handle, immediate_buffer_size, NULL);
// bring our offset to the beginning of the immediate buffer
immediate_buffer_offset = 0;
}
void gr_render_primitives_immediate(material* material_info,
primitive_type prim_type,
vertex_layout* layout,
int n_verts,
void* data,
size_t size) {
if (gr_screen.mode == GraphicsAPI::Stub) {
return;
}
auto offset = gr_add_to_immediate_buffer(size, data);
gr_render_primitives(material_info, prim_type, layout, 0, n_verts, gr_immediate_buffer_handle, offset);
}
void gr_render_primitives_2d_immediate(material* material_info,
primitive_type prim_type,
vertex_layout* layout,
int n_verts,
void* data,
size_t size)
{
if (gr_screen.mode == GraphicsAPI::Stub) {
return;
}
gr_set_2d_matrix();
gr_render_primitives_immediate(material_info, prim_type, layout, n_verts, data, size);
gr_end_2d_matrix();
}
// _->NEW<-_ NEW new bitmap functions -Bobboau
// takes a list of rectangles that have assosiated rectangles in a texture
static void draw_bitmap_list(bitmap_rect_list* list, int n_bm, int resize_mode, material* render_mat, float angle = 0.f)
{
GR_DEBUG_SCOPE("2D Bitmap list");
// adapted from g3_draw_2d_poly_bitmap_list
for (int i = 0; i < n_bm; i++) {
bitmap_2d_list* l = &list[i].screen_rect;
// if no valid hight or width values were given get some from the bitmap
if ((l->w <= 0) || (l->h <= 0)) {
bm_get_info(gr_screen.current_bitmap, &l->w, &l->h, nullptr, nullptr, nullptr);
}
if (resize_mode != GR_RESIZE_NONE && (gr_screen.custom_size || (gr_screen.rendering_to_texture != -1))) {
gr_resize_screen_pos(&l->x, &l->y, &l->w, &l->h, resize_mode);
}
}
auto vert_list = new vertex[6 * n_bm];
float sw = 0.1f;
for (int i = 0; i < n_bm; i++) {
// stuff coords
bitmap_2d_list* b = &list[i].screen_rect;
texture_rect_list* t = &list[i].texture_rect;
float centerX = 0;
float centerY = 0;
if (angle != 0.f) {
centerX = (b->x + (b->x + b->w)) / 2.0f;
centerY = (b->y + (b->y + b->h)) / 2.0f;
}
// tri one
vertex* V = &vert_list[i * 6];
if (angle != 0.f) {
V->screen.xyw.x = cosf(angle) * (b->x - centerX) - sinf(angle) * (b->y - centerY) + centerX;
V->screen.xyw.y = sinf(angle) * (b->x - centerX) + cosf(angle) * (b->y - centerY) + centerY;
}
else {
V->screen.xyw.x = (float)b->x;
V->screen.xyw.y = (float)b->y;
}
V->screen.xyw.w = sw;
V->texture_position.u = (float)t->u0;
V->texture_position.v = (float)t->v0;
V->flags = PF_PROJECTED;
V->codes = 0;
V++;
if (angle != 0.f) {
V->screen.xyw.x = cosf(angle) * (b->x + b->w - centerX) - sinf(angle) * (b->y - centerY) + centerX;
V->screen.xyw.y = sinf(angle) * (b->x + b->w - centerX) + cosf(angle) * (b->y - centerY) + centerY;
}
else {
V->screen.xyw.x = (float)(b->x + b->w);
V->screen.xyw.y = (float)b->y;
}
V->screen.xyw.w = sw;
V->texture_position.u = (float)t->u1;
V->texture_position.v = (float)t->v0;
V->flags = PF_PROJECTED;
V->codes = 0;
V++;
if (angle != 0.f) {
V->screen.xyw.x = cosf(angle) * (b->x + b->w - centerX) - sinf(angle) * (b->y + b->h - centerY) + centerX;
V->screen.xyw.y = sinf(angle) * (b->x + b->w - centerX) + cosf(angle) * (b->y + b->h - centerY) + centerY;
}
else {
V->screen.xyw.x = (float)(b->x + b->w);
V->screen.xyw.y = (float)(b->y + b->h);
}
V->screen.xyw.w = sw;
V->texture_position.u = (float)t->u1;
V->texture_position.v = (float)t->v1;
V->flags = PF_PROJECTED;
V->codes = 0;
// tri two
V++;
if (angle != 0.f) {
V->screen.xyw.x = cosf(angle) * (b->x - centerX) - sinf(angle) * (b->y - centerY) + centerX;
V->screen.xyw.y = sinf(angle) * (b->x - centerX) + cosf(angle) * (b->y - centerY) + centerY;
}
else {
V->screen.xyw.x = (float)b->x;
V->screen.xyw.y = (float)b->y;
}
V->screen.xyw.w = sw;
V->texture_position.u = (float)t->u0;
V->texture_position.v = (float)t->v0;
V->flags = PF_PROJECTED;
V->codes = 0;
V++;
if (angle != 0.f) {
V->screen.xyw.x = cosf(angle) * (b->x + b->w - centerX) - sinf(angle) * (b->y + b->h - centerY) + centerX;
V->screen.xyw.y = sinf(angle) * (b->x + b->w - centerX) + cosf(angle) * (b->y + b->h - centerY) + centerY;
}
else {
V->screen.xyw.x = (float)(b->x + b->w);
V->screen.xyw.y = (float)(b->y + b->h);
}
V->screen.xyw.w = sw;
V->texture_position.u = (float)t->u1;
V->texture_position.v = (float)t->v1;
V->flags = PF_PROJECTED;
V->codes = 0;
V++;
if (angle != 0.f) {
V->screen.xyw.x = cosf(angle) * (b->x - centerX) - sinf(angle) * (b->y + b->h - centerY) + centerX;
V->screen.xyw.y = sinf(angle) * (b->x - centerX) + cosf(angle) * (b->y + b->h - centerY) + centerY;
}
else {
V->screen.xyw.x = (float)b->x;
V->screen.xyw.y = (float)(b->y + b->h);
}
V->screen.xyw.w = sw;
V->texture_position.u = (float)t->u0;
V->texture_position.v = (float)t->v1;
V->flags = PF_PROJECTED;
V->codes = 0;
}
g3_render_primitives_textured(render_mat, vert_list, 6 * n_bm, PRIM_TYPE_TRIS, true);
delete[] vert_list;
}
void gr_bitmap_list(bitmap_rect_list* list, int n_bm, int resize_mode, float angle)
{
if (gr_screen.mode == GraphicsAPI::Stub) {
return;
}
material mat_params;
material_set_interface(&mat_params,
gr_screen.current_bitmap,
gr_screen.current_alphablend_mode == GR_ALPHABLEND_FILTER,
gr_screen.current_alpha);
draw_bitmap_list(list, n_bm, resize_mode, &mat_params, angle);
}
void gr_aabitmap_list(bitmap_rect_list* list, int n_bm, int resize_mode, float angle)
{
if (gr_screen.mode == GraphicsAPI::Stub) {
return;
}
material render_mat;
render_mat.set_blend_mode(ALPHA_BLEND_ALPHA_BLEND_ALPHA);
render_mat.set_depth_mode(ZBUFFER_TYPE_NONE);
render_mat.set_texture_map(TM_BASE_TYPE, gr_screen.current_bitmap);
render_mat.set_color(GR_CURRENT_COLOR);
render_mat.set_cull_mode(false);
render_mat.set_texture_type(material::TEX_TYPE_AABITMAP);
draw_bitmap_list(list, n_bm, resize_mode, &render_mat, angle);
}