mirror of
https://github.com/tangerineturtle1/NX2navi.git
synced 2026-11-15 19:19:58 -04:00
A setting read at the start showed "after restart" under its value always. The values the game started with are now kept (cfg.c start_values), a row shows the note only while its value differs from them, and the Restart button counts the settings that wait, the upscaler, frame generation and HDR among them. A value the environment sets never waits. --probe-cfg checks the count. From pull request #377 by mooki000. Three conflicts in the menu resolved against the frame generation rows that came in since: the upscaling row keeps its description by what can load, and the frame generation restart test knows both ways NVIDIA's runs. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
437 lines
23 KiB
C++
437 lines
23 KiB
C++
// Draws the in-game settings menu and the performance overlay without the game, for looking at their design: the
|
|
// menu's own row code (runtime/ui/option_rows.cpp), its shapes (ui/menu_skin.cpp), the overlay panel
|
|
// (ui/perf_panel.cpp) and the text catalog (ui_text.c), with the few runtime calls they make answered here, laid out as
|
|
// menu.cpp layout_menu lays out the window. Dear ImGui's draw lists are rasterised on the CPU into an RGBA image.
|
|
// Without an art folder the window has its plain border in place of the game's ornamented frame and every box is plain
|
|
// shapes; with one (tools/menu_preview.py --romfs) the boxes use the game's textures as in the game. The given font
|
|
// stands in for the game's either way.
|
|
//
|
|
// tools/menu_preview.py builds and runs it:
|
|
// menu_preview <font.ttf> <scene: menu | overlay | boot> <width> <height> <out.rgba> [art folder]
|
|
#include <math.h>
|
|
#include <stdarg.h>
|
|
#include <stdint.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <time.h>
|
|
|
|
#include <vector>
|
|
|
|
#include "imgui.h"
|
|
#include "imgui_internal.h"
|
|
|
|
#include "ui/menu_art.h"
|
|
#include "ui/menu_skin.h"
|
|
#include "ui/option_rows.h"
|
|
#include "ui/perf_panel.h"
|
|
|
|
extern "C" {
|
|
#include "cfg.h"
|
|
#include "menu_sound.h"
|
|
#include "ui_text.h"
|
|
}
|
|
|
|
// ---- what the rows and the catalog ask of the runtime: a few settings with the menu's values and defaults
|
|
|
|
static const CfgMeta kMeta[] = {
|
|
{"FULLSCREEN", "1", "0|1", 1}, {"WINDOW_SIZE", "", NULL, 1}, {"VK_PRESENT", "mailbox", "mailbox|fifo|immediate", 1},
|
|
{"GPU_INDEX", "", NULL, 0}, {"PERF_OVERLAY", "off", "off|fps|full", 1},
|
|
{"GAME_ASPECT", "16:9", "16:9|window|21:9|32:9", 0}, {"RENDER_SCALE", "", NULL, 0},
|
|
{"UPSCALER", "bilinear", "bilinear|fsr", 0}, {"SHARPNESS", "50", "0..100", 1},
|
|
{"ASPECT", "fit", "fit|stretch", 1}, {"MENU_LANG", "", NULL, 1},
|
|
};
|
|
static const struct { const char *key, *value; } kSet[] = {{"VK_PRESENT", "mailbox"}, {"PERF_OVERLAY", "full"}};
|
|
|
|
static const char *strip(const char *name) { return strncmp(name, "NX2NAVI_", 8) ? name : name + 8; }
|
|
|
|
extern "C" {
|
|
const CfgMeta *cfg_meta(const char *key) {
|
|
for (const CfgMeta &m : kMeta)
|
|
if (!strcasecmp(m.key, strip(key))) return &m;
|
|
return NULL;
|
|
}
|
|
const CfgMeta *cfg_meta_at(int i) { return i >= 0 && i < (int)(sizeof kMeta / sizeof *kMeta) ? &kMeta[i] : NULL; }
|
|
const char *cfg_get(const char *name) {
|
|
for (auto &s : kSet)
|
|
if (!strcasecmp(s.key, strip(name))) return s.value;
|
|
return NULL;
|
|
}
|
|
const char *cfg_value(const char *name) {
|
|
const char *v = cfg_get(name);
|
|
const CfgMeta *m = cfg_meta(name);
|
|
return v ? v : m ? m->def : "";
|
|
}
|
|
const char *cfg_default(const char *key) { // no device profile here: the row's own default
|
|
const CfgMeta *m = cfg_meta(key);
|
|
return m ? m->def : NULL;
|
|
}
|
|
int cfg_int(const char *name) { return atoi(cfg_value(name)); }
|
|
double cfg_double(const char *name) { return atof(cfg_value(name)); }
|
|
int cfg_bool(const char *name) { return strcmp(cfg_value(name), "0") != 0; }
|
|
int cfg_from_env(const char *) { return 0; }
|
|
void cfg_set(const char *, const char *) {}
|
|
int cfg_save(void) { return 1; }
|
|
int cfg_reset(const char *const *, int) { return 0; }
|
|
int cfg_pending(const char *) { return 0; } // nothing changed since the start: no "after restart" notes
|
|
unsigned cfg_version(void) { return 1; }
|
|
void menu_sound_play(MenuSound) {}
|
|
const char *ov_language(void) { return "en-US"; }
|
|
const char *plat_ui_language(void) { return "en"; }
|
|
uint64_t plat_now_ns(void) {
|
|
struct timespec t;
|
|
clock_gettime(CLOCK_MONOTONIC, &t);
|
|
return (uint64_t)t.tv_sec * 1000000000ull + (uint64_t)t.tv_nsec;
|
|
}
|
|
void rt_log(const char *, ...) {}
|
|
}
|
|
|
|
// ---- the game's art, from files menu_preview.py decoded: NN.art, a width and a height (32-bit) and the RGBA pixels of
|
|
// the MenuArt NN, with the smaller levels the game uploads
|
|
|
|
struct Level {
|
|
int w, h;
|
|
std::vector<uint8_t> px;
|
|
};
|
|
static std::vector<Level> g_art[MENU_ART_COUNT];
|
|
|
|
static void art_load(const char *dir) {
|
|
for (int i = 0; i < MENU_ART_COUNT; i++) {
|
|
char path[1024];
|
|
snprintf(path, sizeof path, "%s/%02d.art", dir, i);
|
|
FILE *f = fopen(path, "rb");
|
|
if (!f) continue;
|
|
uint32_t size[2];
|
|
if (fread(size, 4, 2, f) == 2) {
|
|
Level top{(int)size[0], (int)size[1], std::vector<uint8_t>((size_t)size[0] * size[1] * 4)};
|
|
if (fread(top.px.data(), 1, top.px.size(), f) == top.px.size()) {
|
|
skin_art_prepare(i, top.px.data(), size[0], size[1]);
|
|
g_art[i].push_back(std::move(top));
|
|
while (g_art[i].size() < 6 && (g_art[i].back().w > 1 || g_art[i].back().h > 1)) {
|
|
const Level &last = g_art[i].back();
|
|
Level next{last.w > 1 ? last.w / 2 : 1, last.h > 1 ? last.h / 2 : 1, {}};
|
|
next.px.resize((size_t)next.w * next.h * 4);
|
|
skin_art_halve(last.px.data(), last.w, last.h, next.px.data(), next.w, next.h);
|
|
g_art[i].push_back(std::move(next));
|
|
}
|
|
}
|
|
}
|
|
fclose(f);
|
|
}
|
|
}
|
|
|
|
// Texture ids in the draw lists: 1 is the font atlas, 2 and up the art.
|
|
extern "C" int menu_art(MenuArt id, ImTextureID *tex, float *w, float *h) {
|
|
if ((unsigned)id >= MENU_ART_COUNT || g_art[id].empty()) return 0;
|
|
*tex = (ImTextureID)(intptr_t)(2 + id);
|
|
if (w) *w = (float)g_art[id][0].w;
|
|
if (h) *h = (float)g_art[id][0].h;
|
|
return 1;
|
|
}
|
|
|
|
// ---- the scenes
|
|
|
|
static void backdrop(float w, float h) {
|
|
// a dim field and sky, as the game shows behind the menu; then the menu's own dimming (menu.cpp layout_menu)
|
|
ImDrawList *bg = ImGui::GetBackgroundDrawList();
|
|
bg->AddRectFilledMultiColor(ImVec2(0, 0), ImVec2(w, h * 0.55f), IM_COL32(62, 70, 74, 255), IM_COL32(70, 76, 78, 255),
|
|
IM_COL32(52, 64, 52, 255), IM_COL32(48, 60, 50, 255));
|
|
bg->AddRectFilledMultiColor(ImVec2(0, h * 0.55f), ImVec2(w, h), IM_COL32(52, 64, 52, 255), IM_COL32(48, 60, 50, 255),
|
|
IM_COL32(30, 38, 30, 255), IM_COL32(28, 34, 28, 255));
|
|
}
|
|
|
|
static void triforce(ImDrawList *dl, float x, float y, float h, ImU32 col) {
|
|
float w = h * 1.1547f;
|
|
ImVec2 t(x, y), l(x - w / 2, y + h), r(x + w / 2, y + h), ml(x - w / 4, y + h / 2), mr(x + w / 4, y + h / 2), mb(x, y + h);
|
|
dl->AddTriangleFilled(t, mr, ml, col);
|
|
dl->AddTriangleFilled(ml, mb, l, col);
|
|
dl->AddTriangleFilled(mr, r, mb, col);
|
|
}
|
|
|
|
static void scene_menu(float w, float h, float scale, ImFont *title, bool first) {
|
|
backdrop(w, h);
|
|
ImGui::GetBackgroundDrawList()->AddRectFilled(ImVec2(0, 0), ImVec2(w, h), IM_COL32(10, 6, 2, 150));
|
|
float ww = fminf(w * 0.88f, 1400 * scale), wh = fminf(h * 0.86f, 820 * scale);
|
|
ImGui::SetNextWindowPos(ImVec2(w * 0.5f, h * 0.5f), ImGuiCond_Always, ImVec2(0.5f, 0.5f));
|
|
ImGui::SetNextWindowSize(ImVec2(ww, wh), ImGuiCond_Always);
|
|
ImTextureID frame_tex;
|
|
const bool framed = menu_art(MENU_ART_FRAME_DECO, &frame_tex, nullptr, nullptr);
|
|
if (framed) ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0);
|
|
ImGui::Begin("##menu", nullptr, ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoSavedSettings);
|
|
if (framed) ImGui::PopStyleVar();
|
|
const ImVec2 win_p = ImGui::GetWindowPos(), win_s = ImGui::GetWindowSize();
|
|
if (framed) { // as menu.cpp layout_menu draws it
|
|
ImDrawList *fdl = ImGui::GetWindowDrawList();
|
|
const float ox = 15 * scale, oy = 30 * scale;
|
|
fdl->PushClipRectFullScreen();
|
|
for (int turned = 0; turned < 2; turned++)
|
|
menu_art_draw9_turned(fdl, MENU_ART_FRAME_DECO, ImVec2(win_p.x - ox, win_p.y - oy),
|
|
ImVec2(win_p.x + win_s.x + ox, win_p.y + win_s.y + oy), 96 * scale, 0.2f,
|
|
IM_COL32(236, 196, 112, 235), 0, turned);
|
|
fdl->PopClipRect();
|
|
}
|
|
const ImVec4 gold(0.93f, 0.76f, 0.38f, 1);
|
|
ImGui::PushFont(title);
|
|
ImVec2 at = ImGui::GetCursorScreenPos();
|
|
float th = ImGui::GetTextLineHeight();
|
|
triforce(ImGui::GetWindowDrawList(), at.x + th * 0.6f, at.y + th * 0.02f, th * 0.96f, ImGui::GetColorU32(gold));
|
|
ImGui::SetCursorScreenPos(ImVec2(at.x + th * 1.5f, at.y));
|
|
ImGui::TextColored(gold, "Ocarina of Time");
|
|
ImGui::SameLine();
|
|
ImGui::TextDisabled("|");
|
|
ImGui::SameLine();
|
|
ImGui::TextUnformatted(menu_text(UI_MENU_TITLE));
|
|
const float title_end = ImGui::GetItemRectMax().x;
|
|
ImGui::PopFont();
|
|
const char *version = "NX2navi 0.5.0 - game 1.1.1";
|
|
float version_x = ImGui::GetWindowWidth() - ImGui::GetStyle().WindowPadding.x - ImGui::CalcTextSize(version).x;
|
|
if (ImGui::GetWindowPos().x + version_x >= title_end + ImGui::GetStyle().ItemSpacing.x * 2) {
|
|
ImGui::SameLine(version_x);
|
|
ImGui::TextDisabled("%s", version);
|
|
}
|
|
const ImGuiStyle &st = ImGui::GetStyle();
|
|
float bottom = ImGui::GetFrameHeight() + st.ItemSpacing.y * 2;
|
|
static const UiTextId tabs[] = {UI_MENU_TAB_SCREEN, UI_MENU_TAB_GAME, UI_MENU_TAB_SOUND, UI_MENU_TAB_CONTROLS};
|
|
if (ImGui::BeginTabBar("##tabs", ImGuiTabBarFlags_NoTabListScrollingButtons)) {
|
|
for (int k = 0; k < 4; k++) {
|
|
char label[96];
|
|
snprintf(label, sizeof label, " %s ###tab%d", menu_text(tabs[k]), k);
|
|
bool shown = skin_tab_item(label, menu_text(tabs[k]), 0, scale, title);
|
|
if (!shown) continue;
|
|
ImGui::Spacing();
|
|
opt_reserve_bottom(bottom);
|
|
if (first) opt_focus_first();
|
|
// the Screen tab's first rows, as menu.cpp tab_screen shows them
|
|
opt_begin("##screen", scale);
|
|
opt_toggle("NX2NAVI_FULLSCREEN", UI_MENU_FULLSCREEN, UI_MENU_FULLSCREEN_DESC);
|
|
static const char *const sizes[] = {"", "1920x1080"};
|
|
static const char *const size_names[] = {"1920 \xC3\x97 1080", "1920 \xC3\x97 1080"};
|
|
opt_choice_list("NX2NAVI_WINDOW_SIZE", UI_MENU_WINDOW_SIZE, sizes, size_names, 2, UI_MENU_WINDOW_SIZE_DESC, 0);
|
|
opt_toggle_value("NX2NAVI_VK_PRESENT", UI_MENU_PRESENT_VSYNC, UI_MENU_PRESENT_DESC, "mailbox", "immediate");
|
|
static const char *const gpus[] = {""};
|
|
static const char *const gpu_names[] = {"NVIDIA GeForce RTX 3050 Ti Laptop GPU"}; // a long name
|
|
opt_choice_list("NX2NAVI_GPU_INDEX", UI_MENU_GPU, gpus, gpu_names, 1, UI_MENU_GPU_DESC, 0);
|
|
static const UiTextId perf[] = {UI_MENU_PERF_OFF, UI_MENU_PERF_FPS, UI_MENU_PERF_FULL};
|
|
opt_choice("NX2NAVI_PERF_OVERLAY", UI_MENU_PERF_OVERLAY, perf, 3, UI_MENU_PERF_OVERLAY_DESC);
|
|
opt_group(UI_MENU_GRAPHICS);
|
|
static const UiTextId wide[] = {UI_MENU_GAME_ASPECT_16_9, UI_MENU_GAME_ASPECT_WINDOW, UI_MENU_GAME_ASPECT_21_9,
|
|
UI_MENU_GAME_ASPECT_32_9};
|
|
opt_choice("NX2NAVI_GAME_ASPECT", UI_MENU_GAME_ASPECT, wide, 4, UI_MENU_GAME_ASPECT_DESC);
|
|
opt_slider("NX2NAVI_SHARPNESS", UI_MENU_SHARPNESS, 0, 100, UI_MENU_SHARPNESS_DESC, 5, "%.0f %%");
|
|
opt_end();
|
|
ImGui::EndTabItem();
|
|
}
|
|
ImGui::EndTabBar();
|
|
}
|
|
ImGui::Spacing();
|
|
float y = ImGui::GetCursorPosY();
|
|
{
|
|
float sy = win_p.y + y, fh = ImGui::GetFrameHeight();
|
|
menu_art_draw(ImGui::GetWindowDrawList(), MENU_ART_GUIDE_BAR, ImVec2(win_p.x + st.WindowPadding.x, sy),
|
|
ImVec2(win_p.x + win_s.x - st.WindowPadding.x, sy + fh), IM_COL32(96, 70, 36, 170));
|
|
}
|
|
const char *cont = menu_text(UI_MENU_CONTINUE), *restart = menu_text(UI_MENU_RESTART_YES), *quit = menu_text(UI_MENU_QUIT);
|
|
float bw = fmaxf(fmaxf(ImGui::CalcTextSize(cont).x, ImGui::CalcTextSize(restart).x), ImGui::CalcTextSize(quit).x) + st.FramePadding.x * 4;
|
|
float gap = st.ItemSpacing.x + skin_cursor_reach(scale);
|
|
ImGui::SetCursorPos(ImVec2(fmaxf(st.WindowPadding.x, ImGui::GetWindowWidth() - st.WindowPadding.x - bw * 3 - gap * 2), y));
|
|
skin_widget_button(cont, ImVec2(bw, 0), scale);
|
|
ImGui::SameLine(0, gap);
|
|
skin_widget_button(restart, ImVec2(bw, 0), scale);
|
|
ImGui::SameLine(0, gap);
|
|
skin_widget_button(quit, ImVec2(bw, 0), scale);
|
|
ImGui::End();
|
|
}
|
|
|
|
static void scene_overlay(float w, float h, float scale) {
|
|
backdrop(w, h);
|
|
static PerfPanel p; // the numbers of the screenshot the overlay was redesigned from
|
|
PerfStats &s = p.s;
|
|
s.fps = 60, s.frame_ms_avg = 16.7f, s.frame_ms_max = 20.2f, s.low1_fps = 47, s.low01_fps = 8;
|
|
for (int i = 0; i < PERF_HISTORY; i++) s.frame_ms[i] = 16.7f + 1.4f * sinf(i * 0.7f) + (i % 17 == 0 ? 3.5f : 0);
|
|
snprintf(s.cpu_name, sizeof s.cpu_name, "AMD Ryzen 7 5800X3D 8-Core Processor");
|
|
s.cpu_total_pct = 50, s.cpu_game_pct = 44, s.cpu_temp_c = -1;
|
|
s.ram_used_mb = 23.0f * 1024, s.ram_total_mb = 31.9f * 1024, s.ram_game_mb = 8.6f * 1024;
|
|
snprintf(s.gpu_name, sizeof s.gpu_name, "AMD Radeon RX 7900 XT");
|
|
s.gpu_pct = 56, s.gpu_temp_c = 69, s.gpu_power_w = 107, s.gpu_clock_mhz = 1312, s.vram_used_mb = 5.3f * 1024,
|
|
s.vram_total_mb = 20.0f * 1024;
|
|
s.replay_ms = 0.60f, s.gpu_ms = 1.28f, s.pipe_wait_ms = 0, s.xlate_ms = 0, s.tex_uploads = 0;
|
|
p.version = "NX2navi 0.5.0, game 1.1.1";
|
|
snprintf(p.picture[0], 64, "1920x1080");
|
|
snprintf(p.picture[1], 64, "2560x1440");
|
|
snprintf(p.picture[3], 64, "AMD FSR 3.1 Performance");
|
|
snprintf(p.picture[4], 64, "60 FPS");
|
|
snprintf(p.picture[5], 64, "%s", menu_text(UI_MENU_ON));
|
|
snprintf(p.picture[6], 64, "21:9");
|
|
snprintf(p.picture[7], 64, "1440p");
|
|
snprintf(p.picture[8], 64, "%s", menu_text(UI_MENU_HDR_TONEMAPPED));
|
|
snprintf(p.picture[9], 64, "%s", menu_text(UI_MENU_FRAMEGEN_OFF));
|
|
p.scene_known = false, p.fg_fps = 60, p.under_menu = false;
|
|
perf_panel_draw(&p, w, scale);
|
|
}
|
|
|
|
static void scene_boot(float w, float h, float scale, ImFont *title) {
|
|
// the boot screen's box over the black the window shows then (menu.cpp menu_boot_frame), half way through the
|
|
// pipelines
|
|
ImGui::GetBackgroundDrawList()->AddRectFilled(ImVec2(0, 0), ImVec2(w, h), IM_COL32(0, 0, 0, 255));
|
|
char step[160];
|
|
snprintf(step, sizeof step, ui_text(UI_BOOT_PIPELINES), 1603LL, 3235LL);
|
|
skin_boot_panel(ImGui::GetForegroundDrawList(), title, title->FontSize, ImGui::GetFont(), ImGui::GetFontSize(),
|
|
ui_text(UI_BOOT_TITLE), step, 1603.0f / 3235.0f, 0, w, h, scale);
|
|
}
|
|
|
|
// ---- the rasteriser: every triangle of the draw lists, its colour times the font atlas, blended over the image
|
|
|
|
struct Image {
|
|
int w, h;
|
|
std::vector<float> px; // straight RGBA, 0..1
|
|
};
|
|
|
|
// Twice the signed area of (e0, e1, p), computed with the edge's ends in one fixed order, so the two triangles that share
|
|
// an edge get exactly opposite values for a pixel and none is shaded twice or left out along it.
|
|
static float edge(ImVec2 e0, ImVec2 e1, float px, float py) {
|
|
bool swap = e0.x > e1.x || (e0.x == e1.x && e0.y > e1.y);
|
|
if (swap) {
|
|
ImVec2 t = e0;
|
|
e0 = e1, e1 = t;
|
|
}
|
|
float v = (e0.x - px) * (e1.y - py) - (e0.y - py) * (e1.x - px);
|
|
return swap ? -v : v;
|
|
}
|
|
|
|
// The atlas at (u, v), bilinear, clamped to its edges; 0..1.
|
|
static void sample(const uint8_t *atlas, int aw, int ah, float u, float v, float out[4]) {
|
|
float x = u * aw - 0.5f, y = v * ah - 0.5f;
|
|
int x0 = (int)floorf(x), y0 = (int)floorf(y);
|
|
float fx = x - x0, fy = y - y0;
|
|
for (int k = 0; k < 4; k++) out[k] = 0;
|
|
for (int j = 0; j < 2; j++)
|
|
for (int i = 0; i < 2; i++) {
|
|
int xx = x0 + i < 0 ? 0 : x0 + i >= aw ? aw - 1 : x0 + i, yy = y0 + j < 0 ? 0 : y0 + j >= ah ? ah - 1 : y0 + j;
|
|
float wgt = (i ? fx : 1 - fx) * (j ? fy : 1 - fy);
|
|
const uint8_t *t = atlas + 4 * ((size_t)yy * aw + xx);
|
|
for (int k = 0; k < 4; k++) out[k] += wgt * t[k] / 255.0f;
|
|
}
|
|
}
|
|
|
|
// The art at (u, v) at level of detail lod, trilinear between its levels, as the game's sampler reads it.
|
|
static void sample_art(const std::vector<Level> &levels, float u, float v, float lod, float out[4]) {
|
|
const float top = (float)levels.size() - 1;
|
|
lod = lod < 0 ? 0 : lod > top ? top : lod;
|
|
int l0 = (int)floorf(lod), l1 = l0 + 1 < (int)levels.size() ? l0 + 1 : l0;
|
|
float a[4], b[4], f = lod - l0;
|
|
sample(levels[l0].px.data(), levels[l0].w, levels[l0].h, u, v, a);
|
|
sample(levels[l1].px.data(), levels[l1].w, levels[l1].h, u, v, b);
|
|
for (int k = 0; k < 4; k++) out[k] = a[k] + (b[k] - a[k]) * f;
|
|
}
|
|
|
|
static void raster(const ImDrawData *dd, Image &img, const uint8_t *atlas, int aw, int ah) {
|
|
for (int l = 0; l < dd->CmdListsCount; l++) {
|
|
const ImDrawList *dl = dd->CmdLists[l];
|
|
const ImDrawVert *vb = dl->VtxBuffer.Data;
|
|
const ImDrawIdx *ib = dl->IdxBuffer.Data;
|
|
for (const ImDrawCmd &cmd : dl->CmdBuffer) {
|
|
if (cmd.UserCallback) continue;
|
|
int cx0 = (int)fmaxf(0, cmd.ClipRect.x), cy0 = (int)fmaxf(0, cmd.ClipRect.y);
|
|
int cx1 = (int)fminf((float)img.w, cmd.ClipRect.z), cy1 = (int)fminf((float)img.h, cmd.ClipRect.w);
|
|
const intptr_t tex_id = (intptr_t)cmd.GetTexID();
|
|
const std::vector<Level> *art = tex_id >= 2 && tex_id < 2 + MENU_ART_COUNT ? &g_art[tex_id - 2] : nullptr;
|
|
for (unsigned i = 0; i + 2 < cmd.ElemCount; i += 3) {
|
|
const ImDrawVert &a = vb[cmd.VtxOffset + ib[cmd.IdxOffset + i]], &b = vb[cmd.VtxOffset + ib[cmd.IdxOffset + i + 1]],
|
|
&c = vb[cmd.VtxOffset + ib[cmd.IdxOffset + i + 2]];
|
|
float area = (b.pos.x - a.pos.x) * (c.pos.y - a.pos.y) - (b.pos.y - a.pos.y) * (c.pos.x - a.pos.x);
|
|
if (fabsf(area) < 1e-6f) continue;
|
|
// the top-left rule: a pixel centre on an edge two triangles share is shaded by one of them only
|
|
// (edge k is the one opposite vertex k, run in the triangle's own winding)
|
|
const ImVec2 *vtx[3] = {&a.pos, &b.pos, &c.pos};
|
|
bool top_left[3];
|
|
for (int k = 0; k < 3; k++) {
|
|
ImVec2 e0 = *vtx[(k + 1) % 3], e1 = *vtx[(k + 2) % 3];
|
|
float dx = (e1.x - e0.x) * (area > 0 ? 1 : -1), dy = (e1.y - e0.y) * (area > 0 ? 1 : -1);
|
|
top_left[k] = (dy == 0 && dx < 0) || dy > 0;
|
|
}
|
|
int x0 = (int)fmaxf((float)cx0, floorf(fminf(a.pos.x, fminf(b.pos.x, c.pos.x))));
|
|
int x1 = (int)fminf((float)cx1, ceilf(fmaxf(a.pos.x, fmaxf(b.pos.x, c.pos.x))));
|
|
int y0 = (int)fmaxf((float)cy0, floorf(fminf(a.pos.y, fminf(b.pos.y, c.pos.y))));
|
|
int y1 = (int)fminf((float)cy1, ceilf(fmaxf(a.pos.y, fmaxf(b.pos.y, c.pos.y))));
|
|
float lod = 0;
|
|
if (art && !art->empty()) { // texels per screen pixel along the steeper axis
|
|
const float tw = (float)(*art)[0].w, th = (float)(*art)[0].h;
|
|
const float e1x = b.pos.x - a.pos.x, e1y = b.pos.y - a.pos.y, e2x = c.pos.x - a.pos.x, e2y = c.pos.y - a.pos.y;
|
|
const float du1 = (b.uv.x - a.uv.x) * tw, dv1 = (b.uv.y - a.uv.y) * th, du2 = (c.uv.x - a.uv.x) * tw, dv2 = (c.uv.y - a.uv.y) * th;
|
|
const float dudx = (du1 * e2y - du2 * e1y) / area, dvdx = (dv1 * e2y - dv2 * e1y) / area;
|
|
const float dudy = (du2 * e1x - du1 * e2x) / area, dvdy = (dv2 * e1x - dv1 * e2x) / area;
|
|
const float rho = fmaxf(sqrtf(dudx * dudx + dvdx * dvdx), sqrtf(dudy * dudy + dvdy * dvdy));
|
|
lod = rho > 0 ? log2f(rho) : 0;
|
|
}
|
|
for (int y = y0; y < y1; y++)
|
|
for (int x = x0; x < x1; x++) {
|
|
float px = x + 0.5f, py = y + 0.5f;
|
|
float w0 = edge(b.pos, c.pos, px, py) / area, w1 = edge(c.pos, a.pos, px, py) / area,
|
|
w2 = edge(a.pos, b.pos, px, py) / area;
|
|
if (w0 < 0 || w1 < 0 || w2 < 0) continue;
|
|
if ((w0 == 0 && !top_left[0]) || (w1 == 0 && !top_left[1]) || (w2 == 0 && !top_left[2])) continue;
|
|
float u = w0 * a.uv.x + w1 * b.uv.x + w2 * c.uv.x, v = w0 * a.uv.y + w1 * b.uv.y + w2 * c.uv.y;
|
|
float col[4];
|
|
for (int k = 0; k < 4; k++) {
|
|
float ca = (a.col >> (8 * k)) & 255, cb = (b.col >> (8 * k)) & 255, cc = (c.col >> (8 * k)) & 255;
|
|
col[k] = (w0 * ca + w1 * cb + w2 * cc) / 255.0f;
|
|
}
|
|
float t[4];
|
|
if (art && !art->empty()) sample_art(*art, u, v, lod, t);
|
|
else sample(atlas, aw, ah, u, v, t); // filtered, as the GPU samples the atlas's baked lines
|
|
for (int k = 0; k < 4; k++) col[k] *= t[k];
|
|
float *d = &img.px[4 * ((size_t)y * img.w + x)], sa = col[3];
|
|
for (int k = 0; k < 3; k++) d[k] = col[k] * sa + d[k] * (1 - sa);
|
|
d[3] = sa + d[3] * (1 - sa);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
int main(int argc, char **argv) {
|
|
if (argc != 6 && argc != 7) {
|
|
fprintf(stderr, "usage: %s <font.ttf> <menu|overlay|boot> <width> <height> <out.rgba> [art folder]\n", argv[0]);
|
|
return 2;
|
|
}
|
|
const char *font = argv[1], *scene = argv[2], *out = argv[5];
|
|
if (argc == 7) art_load(argv[6]);
|
|
int w = atoi(argv[3]), h = atoi(argv[4]);
|
|
float scale = fmaxf(fminf((float)h, w * 0.68f), 720.0f) / 1080.0f; // as menu.cpp menu_scale at 100 % display scale
|
|
ImGui::CreateContext();
|
|
ImGuiIO &io = ImGui::GetIO();
|
|
io.IniFilename = nullptr;
|
|
io.DisplaySize = ImVec2((float)w, (float)h);
|
|
io.DeltaTime = 1.0f / 60;
|
|
io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard | ImGuiConfigFlags_NavEnableGamepad;
|
|
io.Fonts->AddFontFromFileTTF(font, 26.0f * scale);
|
|
ImFont *title = io.Fonts->AddFontFromFileTTF(font, 31.0f * scale);
|
|
skin_atlas_add(io.Fonts);
|
|
skin_atlas_fill(io.Fonts);
|
|
uint8_t *atlas;
|
|
int aw, ah;
|
|
io.Fonts->GetTexDataAsRGBA32(&atlas, &aw, &ah);
|
|
io.Fonts->SetTexID((ImTextureID)1);
|
|
ImGuiStyle &st = ImGui::GetStyle();
|
|
skin_style(st);
|
|
st.ScaleAllSizes(scale);
|
|
ImGui::SetCurrentFont(io.Fonts->Fonts[0]);
|
|
for (int frame = 0; frame < 4; frame++) { // a few frames: sizes settle and the first row takes the cursor
|
|
ImGui::NewFrame();
|
|
if (!strcmp(scene, "menu")) scene_menu((float)w, (float)h, scale, title, frame == 0);
|
|
else if (!strcmp(scene, "boot")) scene_boot((float)w, (float)h, scale, title);
|
|
else scene_overlay((float)w, (float)h, scale);
|
|
ImGui::Render();
|
|
}
|
|
Image img{w, h, std::vector<float>((size_t)w * h * 4, 0.0f)};
|
|
raster(ImGui::GetDrawData(), img, atlas, aw, ah);
|
|
FILE *f = fopen(out, "wb");
|
|
if (!f) return 1;
|
|
std::vector<uint8_t> bytes((size_t)w * h * 4);
|
|
for (size_t i = 0; i < bytes.size(); i++) bytes[i] = (uint8_t)(fminf(1, fmaxf(0, img.px[i])) * 255 + 0.5f);
|
|
fwrite(bytes.data(), 1, bytes.size(), f);
|
|
fclose(f);
|
|
ImGui::DestroyContext();
|
|
return 0;
|
|
}
|