444 lines
15 KiB
C++
444 lines
15 KiB
C++
/*
|
|
* Copyright (C) 2026 Behdad Esfahbod
|
|
*
|
|
* This is part of HarfBuzz, a text shaping library.
|
|
*
|
|
* Permission is hereby granted, without written agreement and without
|
|
* license or royalty fees, to use, copy, modify, and distribute this
|
|
* software and its documentation for any purpose, provided that the
|
|
* above copyright notice and the following two paragraphs appear in
|
|
* all copies of this software.
|
|
*
|
|
* IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE TO ANY PARTY FOR
|
|
* DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES
|
|
* ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN
|
|
* IF THE COPYRIGHT HOLDER HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
|
|
* DAMAGE.
|
|
*
|
|
* THE COPYRIGHT HOLDER SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING,
|
|
* BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
|
|
* FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS
|
|
* ON AN "AS IS" BASIS, AND THE COPYRIGHT HOLDER HAS NO OBLIGATION TO
|
|
* PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
|
|
*
|
|
* Author(s): Behdad Esfahbod
|
|
*/
|
|
|
|
#ifndef GPU_OUTPUT_HH
|
|
#define GPU_OUTPUT_HH
|
|
|
|
#include "gpu/demo-common.h"
|
|
#include "gpu/demo-buffer.h"
|
|
#include "gpu/demo-font.h"
|
|
#include "gpu/demo-renderer.h"
|
|
#include "gpu/demo-view.h"
|
|
|
|
#include "options.hh"
|
|
#include "font-options.hh"
|
|
#include "view-options.hh"
|
|
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
#define WINDOW_W 700
|
|
#define WINDOW_H 700
|
|
|
|
struct gpu_output_t
|
|
{
|
|
static constexpr bool repeat_shape = false;
|
|
|
|
~gpu_output_t ()
|
|
{
|
|
g_free (output_file);
|
|
}
|
|
|
|
hb_bool_t use_metal = false;
|
|
hb_bool_t use_d3d11 = false;
|
|
hb_bool_t demo = false;
|
|
hb_bool_t bench = false;
|
|
hb_bool_t show_extents = false; /* --show-extents */
|
|
/* Initial state for interactive toggles; each corresponds to
|
|
* a keyboard shortcut in demo_view_t. */
|
|
hb_bool_t dark = false; /* --dark -> b */
|
|
hb_bool_t no_vsync = false; /* --no-vsync -> v */
|
|
hb_bool_t fullscreen = false; /* --fullscreen -> f */
|
|
hb_bool_t no_stem_darken = false; /* --no-stem-darken -> s */
|
|
hb_bool_t debug = false; /* --debug -> d */
|
|
char *type_text = nullptr;
|
|
char *output_file = nullptr; /* PPM only; "-" for stdout */
|
|
view_options_t view;
|
|
|
|
static gboolean
|
|
parse_bench (const char *name G_GNUC_UNUSED,
|
|
const char *arg G_GNUC_UNUSED,
|
|
gpointer data,
|
|
GError **error G_GNUC_UNUSED)
|
|
{
|
|
auto *self = (gpu_output_t *) data;
|
|
self->demo = true;
|
|
self->bench = true;
|
|
if (!self->type_text)
|
|
self->type_text = g_strdup ("v30=f ");
|
|
return true;
|
|
}
|
|
|
|
void add_options (option_parser_t *parser)
|
|
{
|
|
GOptionEntry entries[] =
|
|
{
|
|
{"demo", 0, 0, G_OPTION_ARG_NONE, &this->demo, "Use built-in demo font and text", nullptr},
|
|
{"bench", 0, G_OPTION_FLAG_NO_ARG,
|
|
G_OPTION_ARG_CALLBACK, (gpointer) &parse_bench,"Demo in fullscreen benchmark mode", nullptr},
|
|
{"type", 'T', 0, G_OPTION_ARG_STRING, &this->type_text, "Type these keystrokes on start", "keys"},
|
|
{"draw", 0, 0, G_OPTION_ARG_NONE, &this->view.force_draw, "Force monochrome draw path", nullptr},
|
|
{"paint", 0, 0, G_OPTION_ARG_NONE, &this->view.force_paint, "Force color paint path", nullptr},
|
|
{"output-file", 'o', 0, G_OPTION_ARG_STRING, &this->output_file, "Render one frame to PPM file (\"-\" for stdout) and exit","filename"},
|
|
{"show-extents", 0, 0, G_OPTION_ARG_NONE, &this->show_extents, "Draw a frame around each glyph's ink extents", nullptr},
|
|
/* Interactive-toggle defaults. Each mirrors a keybinding
|
|
* in the running demo. */
|
|
{"dark", 0, 0, G_OPTION_ARG_NONE, &this->dark, "Start in dark mode (toggle: b)", nullptr},
|
|
{"no-vsync", 0, 0, G_OPTION_ARG_NONE, &this->no_vsync, "Start with vsync off (toggle: v)", nullptr},
|
|
{"fullscreen", 0, 0, G_OPTION_ARG_NONE, &this->fullscreen, "Start fullscreen (toggle: f)", nullptr},
|
|
{"no-stem-darken",0, 0, G_OPTION_ARG_NONE, &this->no_stem_darken, "Start with stem darkening off (toggle: s)",nullptr},
|
|
{"debug", 0, 0, G_OPTION_ARG_NONE, &this->debug, "Start with debug heatmap on (toggle: d)",nullptr},
|
|
/* Reuse the storage in `view` for the four options hb-gpu
|
|
* actually honors. view_options_t::post_parse() does the
|
|
* parsing for us; gpu_output_t::post_parse() below forwards
|
|
* to it. */
|
|
{"background", 0, 0, G_OPTION_ARG_STRING, &this->view.back, "Set background color (default: " DEFAULT_BACK ")","rrggbb/rrggbbaa"},
|
|
{"foreground", 0, 0, G_OPTION_ARG_STRING, &this->view.fore, "Set foreground color (default: " DEFAULT_FORE ")","rrggbb/rrggbbaa"},
|
|
{"font-palette", 0, 0, G_OPTION_ARG_INT, &this->view.palette, "Set font palette (default: 0)", "index"},
|
|
{"custom-palette",0, 0, G_OPTION_ARG_STRING, &this->view.custom_palette, "Custom palette", "comma-separated colors"},
|
|
#ifdef __APPLE__
|
|
{"metal", 0, 0, G_OPTION_ARG_NONE, &this->use_metal, "Use Metal renderer", nullptr},
|
|
#endif
|
|
#ifdef _WIN32
|
|
{"direct3d", 0, 0, G_OPTION_ARG_NONE, &this->use_d3d11, "Use Direct3D 11 renderer", nullptr},
|
|
#endif
|
|
{nullptr}
|
|
};
|
|
parser->add_group (entries,
|
|
"gpu",
|
|
"GPU options:",
|
|
"Options for GPU rendering",
|
|
this);
|
|
}
|
|
|
|
void post_parse (GError **error)
|
|
{
|
|
/* Delegate to view_options_t: parses foreground / background
|
|
* / custom-palette strings into rgba_color_t / GArray. */
|
|
view.post_parse (error);
|
|
}
|
|
|
|
template <typename app_t>
|
|
void init (hb_buffer_t *buffer_ HB_UNUSED, const app_t *app)
|
|
{
|
|
font = app->font;
|
|
face = hb_font_get_face (font);
|
|
|
|
int x_scale, y_scale;
|
|
hb_font_get_scale (font, &x_scale, &y_scale);
|
|
font_size = (double) y_scale;
|
|
|
|
glfwSetErrorCallback ([] (int error, const char *desc) {
|
|
fprintf (stderr, "GLFW error %d: %s\n", error, desc);
|
|
});
|
|
if (!glfwInit ())
|
|
fail (false, "Failed to initialize GLFW");
|
|
|
|
/* Pick draw vs paint mode. Explicit --draw / --paint wins;
|
|
* otherwise auto: fonts without color paint get the draw path
|
|
* (smaller shader, better perf). A mismatched pair of flags
|
|
* is a user error; prefer paint. */
|
|
draw_only = view.force_paint ? false
|
|
: view.force_draw ? true
|
|
: !(hb_ot_color_has_paint (face) ||
|
|
hb_ot_color_has_layers (face));
|
|
|
|
if (use_metal)
|
|
init_metal ();
|
|
#ifdef _WIN32
|
|
else if (use_d3d11)
|
|
init_d3d11 ();
|
|
#endif
|
|
else
|
|
init_gl ();
|
|
|
|
vu = demo_view_create (renderer, window);
|
|
|
|
glfwSetWindowUserPointer (window, this);
|
|
glfwSetFramebufferSizeCallback (window, [] (GLFWwindow *w, int width, int height) {
|
|
auto *self = (gpu_output_t *) glfwGetWindowUserPointer (w);
|
|
demo_view_reshape_func (self->vu, width, height);
|
|
});
|
|
glfwSetKeyCallback (window, [] (GLFWwindow *w, int key, int scancode, int action, int mods) {
|
|
auto *self = (gpu_output_t *) glfwGetWindowUserPointer (w);
|
|
demo_view_key_func (self->vu, key, scancode, action, mods);
|
|
});
|
|
glfwSetCharCallback (window, [] (GLFWwindow *w, unsigned int cp) {
|
|
auto *self = (gpu_output_t *) glfwGetWindowUserPointer (w);
|
|
demo_view_char_func (self->vu, cp);
|
|
});
|
|
glfwSetMouseButtonCallback (window, [] (GLFWwindow *w, int button, int action, int mods) {
|
|
auto *self = (gpu_output_t *) glfwGetWindowUserPointer (w);
|
|
demo_view_mouse_func (self->vu, button, action, mods);
|
|
});
|
|
glfwSetScrollCallback (window, [] (GLFWwindow *w, double xoff, double yoff) {
|
|
auto *self = (gpu_output_t *) glfwGetWindowUserPointer (w);
|
|
demo_view_scroll_func (self->vu, xoff, yoff);
|
|
});
|
|
glfwSetCursorPosCallback (window, [] (GLFWwindow *w, double x, double y) {
|
|
auto *self = (gpu_output_t *) glfwGetWindowUserPointer (w);
|
|
demo_view_motion_func (self->vu, x, y);
|
|
});
|
|
|
|
demo_font_ = demo_font_create (font, renderer->get_atlas (), draw_only);
|
|
demo_font_set_palette (demo_font_, view.palette);
|
|
demo_font_set_show_extents (demo_font_, show_extents);
|
|
if (view.custom_palette_entries)
|
|
{
|
|
for (unsigned i = 0; i < view.custom_palette_entries->len; i++)
|
|
{
|
|
auto &e = g_array_index (view.custom_palette_entries,
|
|
view_options_t::custom_palette_entry_t, i);
|
|
hb_color_t c = HB_COLOR (e.color.b, e.color.g, e.color.r, e.color.a);
|
|
demo_font_set_custom_palette_color (demo_font_, e.index, c);
|
|
}
|
|
}
|
|
buf = demo_buffer_create ();
|
|
|
|
demo_point_t top_left = {0, 0};
|
|
demo_buffer_move_to (buf, &top_left);
|
|
demo_buffer_current_line (buf, font_size);
|
|
}
|
|
|
|
void init_gl ()
|
|
{
|
|
glfwWindowHint (GLFW_CONTEXT_VERSION_MAJOR, 3);
|
|
glfwWindowHint (GLFW_CONTEXT_VERSION_MINOR, 3);
|
|
glfwWindowHint (GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
|
|
#ifdef __APPLE__
|
|
glfwWindowHint (GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
|
|
#endif
|
|
if (output_file)
|
|
glfwWindowHint (GLFW_VISIBLE, GLFW_FALSE);
|
|
|
|
window = glfwCreateWindow (WINDOW_W, WINDOW_H, "HarfBuzz GPU", NULL, NULL);
|
|
if (!window) {
|
|
glfwTerminate ();
|
|
fail (false, "Failed to create GLFW window");
|
|
}
|
|
glfwMakeContextCurrent (window);
|
|
|
|
glewExperimental = GL_TRUE;
|
|
glewInit ();
|
|
while (glGetError () != GL_NO_ERROR)
|
|
;
|
|
if (!glewIsSupported ("GL_VERSION_3_3"))
|
|
fail (false, "OpenGL 3.3 not supported");
|
|
|
|
{
|
|
int fb_width, fb_height;
|
|
glfwGetFramebufferSize (window, &fb_width, &fb_height);
|
|
glViewport (0, 0, fb_width, fb_height);
|
|
}
|
|
|
|
renderer = demo_renderer_create_gl (window, draw_only);
|
|
}
|
|
|
|
void init_metal ()
|
|
{
|
|
#ifdef __APPLE__
|
|
glfwWindowHint (GLFW_CLIENT_API, GLFW_NO_API);
|
|
|
|
window = glfwCreateWindow (WINDOW_W, WINDOW_H, "HarfBuzz GPU (Metal)", NULL, NULL);
|
|
if (!window) {
|
|
glfwTerminate ();
|
|
fail (false, "Failed to create GLFW window");
|
|
}
|
|
|
|
renderer = demo_renderer_create_metal (window, draw_only);
|
|
if (!renderer) {
|
|
glfwTerminate ();
|
|
fail (false, "Failed to initialize Metal");
|
|
}
|
|
#else
|
|
fail (false, "Metal is only available on macOS");
|
|
#endif
|
|
}
|
|
|
|
void init_d3d11 ()
|
|
{
|
|
#ifdef _WIN32
|
|
glfwWindowHint (GLFW_CLIENT_API, GLFW_NO_API);
|
|
|
|
window = glfwCreateWindow (WINDOW_W, WINDOW_H, "HarfBuzz GPU (Direct3D)", NULL, NULL);
|
|
if (!window) {
|
|
glfwTerminate ();
|
|
fail (false, "Failed to create GLFW window");
|
|
}
|
|
|
|
renderer = demo_renderer_create_d3d11 (window, draw_only);
|
|
if (!renderer) {
|
|
glfwTerminate ();
|
|
fail (false, "Failed to initialize Direct3D 11");
|
|
}
|
|
#else
|
|
fail (false, "Direct3D is only available on Windows");
|
|
#endif
|
|
}
|
|
|
|
void write_output_file ()
|
|
{
|
|
int fb_w = 0, fb_h = 0;
|
|
glfwGetFramebufferSize (window, &fb_w, &fb_h);
|
|
if (fb_w <= 0 || fb_h <= 0)
|
|
fail (false, "Framebuffer has zero size");
|
|
|
|
FILE *fp;
|
|
bool use_stdout = (0 == strcmp (output_file, "-"));
|
|
if (use_stdout)
|
|
{
|
|
#if defined(_WIN32) || defined(__CYGWIN__)
|
|
setmode (fileno (stdout), O_BINARY);
|
|
#endif
|
|
fp = stdout;
|
|
}
|
|
else
|
|
{
|
|
fp = fopen (output_file, "wb");
|
|
if (!fp)
|
|
fail (false, "Cannot open output file `%s': %s",
|
|
output_file, strerror (errno));
|
|
}
|
|
|
|
/* glReadPixels delivers rows bottom-up; PPM writes top-down,
|
|
* so we flip while assembling. */
|
|
std::vector<uint8_t> pixels ((size_t) fb_w * (size_t) fb_h * 3);
|
|
glPixelStorei (GL_PACK_ALIGNMENT, 1);
|
|
glReadPixels (0, 0, fb_w, fb_h, GL_RGB, GL_UNSIGNED_BYTE, pixels.data ());
|
|
|
|
fprintf (fp, "P6\n%d %d\n255\n", fb_w, fb_h);
|
|
const unsigned row_bytes = (unsigned) fb_w * 3u;
|
|
for (int y = fb_h - 1; y >= 0; y--)
|
|
fwrite (pixels.data () + (size_t) y * row_bytes, 1, row_bytes, fp);
|
|
|
|
if (!use_stdout)
|
|
fclose (fp);
|
|
}
|
|
|
|
void new_line ()
|
|
{
|
|
demo_buffer_current_line (buf, font_size);
|
|
}
|
|
|
|
void consume_text (hb_buffer_t *buffer HB_UNUSED,
|
|
const char *text HB_UNUSED,
|
|
unsigned text_len HB_UNUSED,
|
|
hb_bool_t utf8_clusters HB_UNUSED) {}
|
|
|
|
void consume_glyphs (hb_buffer_t *buffer,
|
|
const char *text HB_UNUSED,
|
|
unsigned text_len HB_UNUSED,
|
|
hb_bool_t utf8_clusters HB_UNUSED)
|
|
{
|
|
unsigned count = hb_buffer_get_length (buffer);
|
|
hb_glyph_info_t *infos = hb_buffer_get_glyph_infos (buffer, nullptr);
|
|
hb_glyph_position_t *pos = hb_buffer_get_glyph_positions (buffer, nullptr);
|
|
|
|
for (unsigned i = 0; i < count; i++)
|
|
demo_buffer_add_glyph (buf, demo_font_,
|
|
font_size,
|
|
infos[i].codepoint,
|
|
pos[i].x_offset,
|
|
pos[i].y_offset,
|
|
pos[i].x_advance);
|
|
}
|
|
|
|
void error (const char *message)
|
|
{
|
|
fprintf (stderr, "error: %s\n", message);
|
|
}
|
|
|
|
template <typename app_t>
|
|
void finish (hb_buffer_t *buffer_ HB_UNUSED, const app_t *app HB_UNUSED)
|
|
{
|
|
hb_gpu_demo_quiet = output_file ? 1 : 0;
|
|
demo_font_print_stats (demo_font_);
|
|
demo_view_print_help (vu);
|
|
demo_view_setup (vu);
|
|
|
|
/* Apply view-options foreground/background. These default to
|
|
* #000000 / #FFFFFF which match the demo's default LIGHT mode,
|
|
* so always setting them is a no-op unless the user passed a
|
|
* flag. The dark-mode toggle (b key) mutates renderer state
|
|
* afterwards and stays in effect. */
|
|
renderer->set_foreground (view.foreground_color.r / 255.f,
|
|
view.foreground_color.g / 255.f,
|
|
view.foreground_color.b / 255.f,
|
|
view.foreground_color.a / 255.f);
|
|
renderer->set_background (view.background_color.r / 255.f,
|
|
view.background_color.g / 255.f,
|
|
view.background_color.b / 255.f,
|
|
view.background_color.a / 255.f);
|
|
|
|
/* Apply cmdline-driven toggle defaults via the same keystroke
|
|
* path the user would use interactively. */
|
|
std::string toggles;
|
|
if (dark) toggles += 'b';
|
|
if (no_vsync) toggles += 'v';
|
|
if (fullscreen) toggles += 'f';
|
|
if (no_stem_darken) toggles += 's';
|
|
if (debug) toggles += 'd';
|
|
if (!toggles.empty ())
|
|
demo_view_type (vu, toggles.c_str ());
|
|
|
|
if (type_text)
|
|
demo_view_type (vu, type_text);
|
|
if (bench)
|
|
demo_view_set_fps_quit (vu, 3);
|
|
|
|
demo_view_display (vu, buf);
|
|
glfwPollEvents ();
|
|
demo_view_display (vu, buf);
|
|
|
|
if (output_file)
|
|
write_output_file ();
|
|
else
|
|
{
|
|
while (!glfwWindowShouldClose (window))
|
|
{
|
|
glfwPollEvents ();
|
|
if (demo_view_should_redraw (vu))
|
|
demo_view_display (vu, buf);
|
|
else
|
|
glfwWaitEvents ();
|
|
}
|
|
}
|
|
|
|
demo_buffer_destroy (buf);
|
|
demo_font_destroy (demo_font_);
|
|
demo_view_destroy (vu);
|
|
delete renderer;
|
|
|
|
glfwDestroyWindow (window);
|
|
glfwTerminate ();
|
|
}
|
|
|
|
private:
|
|
|
|
bool draw_only = false;
|
|
double font_size = 1;
|
|
hb_font_t *font = nullptr;
|
|
hb_face_t *face = nullptr;
|
|
|
|
GLFWwindow *window = nullptr;
|
|
demo_renderer_t *renderer = nullptr;
|
|
demo_view_t *vu = nullptr;
|
|
demo_font_t *demo_font_ = nullptr;
|
|
demo_buffer_t *buf = nullptr;
|
|
};
|
|
|
|
#endif /* GPU_OUTPUT_HH */
|