mirror of
https://github.com/Nexoniarz/Fry3DX.git
synced 2026-11-03 04:24:26 +00:00
Fry3DX 0.4: an easy 3D modeller
Shapes with live settings, shape editing, knife and hole tools, drawing shapes onto other objects, combining objects (add / subtract / overlap), copies, thickness, painting with stickers and layers, a Layout tab, a Print tab with checks, packed .fry files with autosave, export to GLB, glTF, FBX, OBJ, 3MF, STL and PLY, folders in Layers, its own crash screen, and a website for GitHub Pages in docs/. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NMSrd5oe1GpUD7E6d825KP
This commit is contained in:
commit
f3e8eca87b
135 changed files with 23423 additions and 0 deletions
171
engine/platform/input.cpp
Normal file
171
engine/platform/input.cpp
Normal file
|
|
@ -0,0 +1,171 @@
|
|||
#include "platform/input.h"
|
||||
#include "platform/touch.h"
|
||||
#include "core/cvar.h"
|
||||
#include "core/log.h"
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <map>
|
||||
#include <set>
|
||||
|
||||
namespace keel::input {
|
||||
namespace {
|
||||
float g_dx = 0, g_dy = 0;
|
||||
bool g_captured = false;
|
||||
bool g_noCapture = false;
|
||||
std::map<std::string, std::string> g_binds; // key name → command
|
||||
std::set<std::string> g_held; // keys whose "+" command is active
|
||||
void (*g_capture)(const char*) = nullptr;
|
||||
|
||||
std::string lower(std::string s) {
|
||||
std::transform(s.begin(), s.end(), s.begin(), [](unsigned char c) { return char(std::tolower(c)); });
|
||||
return s;
|
||||
}
|
||||
|
||||
std::string keyName(SDL_Scancode sc) {
|
||||
switch (sc) {
|
||||
case SDL_SCANCODE_LCTRL: case SDL_SCANCODE_RCTRL: return "ctrl";
|
||||
case SDL_SCANCODE_LSHIFT: case SDL_SCANCODE_RSHIFT: return "shift";
|
||||
case SDL_SCANCODE_LALT: case SDL_SCANCODE_RALT: return "alt";
|
||||
default: return lower(SDL_GetScancodeName(sc));
|
||||
}
|
||||
}
|
||||
|
||||
void press(const std::string& key, bool down) {
|
||||
if (down && g_capture) {
|
||||
auto cb = g_capture;
|
||||
g_capture = nullptr;
|
||||
cb(key.c_str());
|
||||
return;
|
||||
}
|
||||
auto it = g_binds.find(key);
|
||||
if (it == g_binds.end()) return;
|
||||
const std::string& cmd = it->second;
|
||||
if (cmd.starts_with('+')) {
|
||||
if (down && g_held.insert(key).second) execute(cmd);
|
||||
else if (!down && g_held.erase(key)) execute("-" + cmd.substr(1));
|
||||
} else if (down) {
|
||||
execute(cmd);
|
||||
}
|
||||
}
|
||||
|
||||
Cmd cmd_bind("bind", [](const Args& a) {
|
||||
if (a.size() < 2) return log::info("usage: bind <key> [command]");
|
||||
if (a.size() == 2) {
|
||||
auto it = g_binds.find(lower(a[1]));
|
||||
return log::info("\"{}\" = \"{}\"", a[1], it == g_binds.end() ? "" : it->second);
|
||||
}
|
||||
std::string cmd;
|
||||
for (size_t i = 2; i < a.size(); ++i) cmd += (i > 2 ? " " : "") + a[i];
|
||||
bind(a[1].c_str(), cmd.c_str());
|
||||
}, "Bind a key to a command");
|
||||
Cmd cmd_unbind("unbind", [](const Args& a) {
|
||||
if (a.size() > 1) g_binds.erase(lower(a[1]));
|
||||
}, "Remove a key binding");
|
||||
Cmd cmd_unbindall("unbindall", [](const Args&) { g_binds.clear(); }, "Remove all key bindings");
|
||||
Cmd cmd_bindlist("key_listboundkeys", [](const Args&) {
|
||||
for (auto& [k, v] : g_binds) log::info(" \"{}\" = \"{}\"", k, v);
|
||||
log::info("{} keys bound", g_binds.size());
|
||||
}, "List every key binding");
|
||||
Cmd cmd_bindlist2("bindlist", [](const Args&) { execute("key_listboundkeys"); }, "List every key binding");
|
||||
// Key names for the console's suggestions.
|
||||
std::vector<std::string> keyNames() {
|
||||
std::vector<std::string> v;
|
||||
for (char c = 'a'; c <= 'z'; ++c) v.emplace_back(1, c);
|
||||
for (char c = '0'; c <= '9'; ++c) v.emplace_back(1, c);
|
||||
for (int f = 1; f <= 12; ++f) v.push_back("f" + std::to_string(f));
|
||||
for (const char* k : {"space", "tab", "escape", "return", "backspace", "ctrl", "shift", "alt", "left ctrl", "left shift", "left alt", "up", "down",
|
||||
"left", "right", "insert", "delete", "home", "end", "pageup", "pagedown", "mouse1", "mouse2", "mouse3", "mouse4", "mouse5",
|
||||
"mwheelup", "mwheeldown", "`", "-", "=", "[", "]", ";", "'", ",", ".", "/"})
|
||||
v.emplace_back(k);
|
||||
return v;
|
||||
}
|
||||
[[maybe_unused]] const bool g_keyCompleters = [] {
|
||||
cmd_bind.complete([](std::string_view p, size_t arg) { return matching(arg == 1 ? keyNames() : allNames(), p); });
|
||||
cmd_unbind.complete([](std::string_view p, size_t) {
|
||||
std::vector<std::string> v;
|
||||
for (auto& [k, _] : g_binds) v.push_back(k);
|
||||
return matching(v, p);
|
||||
});
|
||||
return true;
|
||||
}();
|
||||
} // namespace
|
||||
|
||||
void bind(const char* key, const char* command) {
|
||||
if (!*command) g_binds.erase(lower(key));
|
||||
else g_binds[lower(key)] = command;
|
||||
}
|
||||
|
||||
std::string bindingsConfig() {
|
||||
std::string out = "unbindall\n";
|
||||
for (auto& [k, v] : g_binds) out += "bind \"" + k + "\" \"" + v + "\"\n";
|
||||
return out;
|
||||
}
|
||||
|
||||
std::string keyFor(const char* command) {
|
||||
for (auto& [k, v] : g_binds)
|
||||
if (v == command) return k;
|
||||
return {};
|
||||
}
|
||||
|
||||
void captureNextKey(void (*callback)(const char*)) { g_capture = callback; }
|
||||
|
||||
void releaseAll() {
|
||||
for (const std::string& key : std::set<std::string>(g_held)) press(key, false);
|
||||
}
|
||||
|
||||
bool keyDown(int scancode) {
|
||||
if (!g_captured) return false;
|
||||
int n = 0;
|
||||
const bool* keys = SDL_GetKeyboardState(&n);
|
||||
return scancode >= 0 && scancode < n && keys[scancode];
|
||||
}
|
||||
|
||||
bool mouseDown(int button) { return g_captured && (SDL_GetMouseState(nullptr, nullptr) & SDL_BUTTON_MASK(button)); }
|
||||
|
||||
void takeMouseDelta(float& dx, float& dy) {
|
||||
float tx, ty; // looking with a finger counts as the mouse
|
||||
touch::takeLook(tx, ty);
|
||||
dx = g_dx + tx;
|
||||
dy = g_dy + ty;
|
||||
g_dx = g_dy = 0;
|
||||
}
|
||||
|
||||
bool captured() { return g_captured; }
|
||||
void disableCapture() { g_noCapture = true; }
|
||||
|
||||
void setCaptured(bool on) {
|
||||
if (on && g_noCapture) return;
|
||||
if (on == g_captured) return;
|
||||
g_captured = on;
|
||||
if (!on) releaseAll(), touch::releaseAll();
|
||||
if (SDL_Window* w = SDL_GetKeyboardFocus()) SDL_SetWindowRelativeMouseMode(w, on);
|
||||
g_dx = g_dy = 0;
|
||||
}
|
||||
|
||||
void handleEvent(const SDL_Event& ev) {
|
||||
// While playing, touches are the touch controls (platform/touch.cpp), not a mouse.
|
||||
if ((ev.type == SDL_EVENT_MOUSE_MOTION && ev.motion.which == SDL_TOUCH_MOUSEID) ||
|
||||
((ev.type == SDL_EVENT_MOUSE_BUTTON_DOWN || ev.type == SDL_EVENT_MOUSE_BUTTON_UP) && ev.button.which == SDL_TOUCH_MOUSEID)) {
|
||||
if (g_captured) return;
|
||||
}
|
||||
if (ev.type == SDL_EVENT_MOUSE_MOTION && g_captured) {
|
||||
g_dx += ev.motion.xrel;
|
||||
g_dy += ev.motion.yrel;
|
||||
}
|
||||
if (!g_captured && !g_capture) return;
|
||||
if ((ev.type == SDL_EVENT_KEY_DOWN || ev.type == SDL_EVENT_KEY_UP) && !ev.key.repeat)
|
||||
press(keyName(ev.key.scancode), ev.type == SDL_EVENT_KEY_DOWN);
|
||||
if (ev.type == SDL_EVENT_MOUSE_BUTTON_DOWN || ev.type == SDL_EVENT_MOUSE_BUTTON_UP)
|
||||
// Source's names: mouse1 left, mouse2 right, mouse3 middle (SDL has middle = 2, right = 3).
|
||||
press("mouse" + std::to_string(ev.button.button == SDL_BUTTON_RIGHT ? 2 : ev.button.button == SDL_BUTTON_MIDDLE ? 3 : ev.button.button),
|
||||
ev.type == SDL_EVENT_MOUSE_BUTTON_DOWN);
|
||||
if (ev.type == SDL_EVENT_MOUSE_WHEEL && ev.wheel.y != 0) {
|
||||
const std::string key = ev.wheel.y > 0 ? "mwheelup" : "mwheeldown";
|
||||
press(key, true);
|
||||
press(key, false);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace keel::input
|
||||
35
engine/platform/input.h
Normal file
35
engine/platform/input.h
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
#pragma once
|
||||
#include <string>
|
||||
|
||||
union SDL_Event;
|
||||
|
||||
namespace keel::input {
|
||||
|
||||
bool keyDown(int scancode); // SDL_SCANCODE_*
|
||||
bool mouseDown(int button); // SDL_BUTTON_LEFT etc.
|
||||
// Mouse movement since the last call, in pixels.
|
||||
void takeMouseDelta(float& dx, float& dy);
|
||||
// While captured the cursor is hidden and the game owns the mouse and keyboard.
|
||||
bool captured();
|
||||
|
||||
// Key bindings, Quake/Source style: "bind w +forward" runs "+forward" on press
|
||||
// and "-forward" on release; other commands run on press. Key names: letters,
|
||||
// digits, "space", "ctrl", "shift", "escape", "f1".., "mouse1".."mouse5",
|
||||
// "mwheelup", "mwheeldown".
|
||||
void bind(const char* key, const char* command);
|
||||
// Lines "unbindall" + "bind ..." for config.cfg.
|
||||
std::string bindingsConfig();
|
||||
// The key bound to `command` ("" if none), for menus.
|
||||
std::string keyFor(const char* command);
|
||||
// The next key pressed is reported here instead of running its binding (options menu).
|
||||
void captureNextKey(void (*callback)(const char* keyName));
|
||||
|
||||
// Engine side.
|
||||
void setCaptured(bool on);
|
||||
// Never grab the mouse (test mode).
|
||||
void disableCapture();
|
||||
void handleEvent(const SDL_Event& ev);
|
||||
// Releases every held "+" action (when a menu opens mid-press).
|
||||
void releaseAll();
|
||||
|
||||
} // namespace keel::input
|
||||
190
engine/platform/touch.cpp
Normal file
190
engine/platform/touch.cpp
Normal file
|
|
@ -0,0 +1,190 @@
|
|||
#include "platform/touch.h"
|
||||
#include "core/cvar.h"
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
#include <imgui.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
|
||||
namespace keel::touch {
|
||||
namespace {
|
||||
#if defined(__ANDROID__)
|
||||
Cvar touch_controls("touch_controls", "1", CVAR_ARCHIVE, "On-screen touch controls (0 off, 1 on)");
|
||||
#else
|
||||
Cvar touch_controls("touch_controls", "1", CVAR_ARCHIVE, "On-screen touch controls once the screen is touched (0 never)");
|
||||
#endif
|
||||
Cvar touch_sensitivity("touch_sensitivity", "1.6", CVAR_ARCHIVE, "How far looking turns for a finger's drag");
|
||||
Cvar touch_size("touch_size", "1", CVAR_ARCHIVE, "Size of the touch buttons and stick");
|
||||
|
||||
// A button: its command ("+x" held, else run once), label, and where (from the bottom-right
|
||||
// corner, in shorter-screen-sides), how big.
|
||||
struct Button {
|
||||
const char* cmd;
|
||||
const char* label;
|
||||
float x, y, r;
|
||||
};
|
||||
constexpr Button kButtons[] = {
|
||||
{"+attack", "FIRE", 0.15f, 0.34f, 0.085f}, {"+jump", "JUMP", 0.33f, 0.14f, 0.07f}, {"+duck", "DUCK", 0.13f, 0.13f, 0.06f},
|
||||
{"+use", "USE", 0.34f, 0.36f, 0.055f}, {"+attack2", "ALT", 0.13f, 0.57f, 0.055f}, {"+sprint", "RUN", 0.50f, 0.12f, 0.055f},
|
||||
{"flashlight", "LIGHT", 0.32f, 0.57f, 0.045f}, {"backpack", "BAG", 0.47f, 0.32f, 0.045f},
|
||||
};
|
||||
constexpr int kButtonCount = int(sizeof kButtons / sizeof kButtons[0]);
|
||||
|
||||
struct Finger {
|
||||
enum Role { Look, Stick, ButtonHeld, Pause } role;
|
||||
float x, y; // where it is now (pixels)
|
||||
float ox, oy; // the stick's centre
|
||||
int button = -1;
|
||||
};
|
||||
std::unordered_map<SDL_FingerID, Finger> g_fingers;
|
||||
bool g_used = false, g_escape = false;
|
||||
float g_lookX = 0, g_lookY = 0;
|
||||
bool g_held[kButtonCount] = {};
|
||||
|
||||
#if defined(__ANDROID__)
|
||||
constexpr bool kAlways = true;
|
||||
#else
|
||||
constexpr bool kAlways = false;
|
||||
#endif
|
||||
|
||||
ImVec2 screen() {
|
||||
const ImVec2 s = ImGui::GetIO().DisplaySize;
|
||||
return s.x > 0 ? s : ImVec2(1280, 720);
|
||||
}
|
||||
float unit() { // the shorter side, scaled
|
||||
const ImVec2 s = screen();
|
||||
return std::min(s.x, s.y) * std::clamp(touch_size.f(), 0.5f, 2.0f);
|
||||
}
|
||||
ImVec2 buttonAt(const Button& b) {
|
||||
const ImVec2 s = screen();
|
||||
const float m = unit();
|
||||
return ImVec2(s.x - b.x * m, s.y - b.y * m);
|
||||
}
|
||||
ImVec2 pauseAt() { return ImVec2(unit() * 0.07f, unit() * 0.07f); }
|
||||
void press(int i, bool down) {
|
||||
const Button& b = kButtons[i];
|
||||
if (b.cmd[0] == '+') {
|
||||
if (down == g_held[i]) return;
|
||||
g_held[i] = down;
|
||||
execute(down ? b.cmd : ("-" + std::string(b.cmd + 1)));
|
||||
} else if (down) {
|
||||
execute(b.cmd);
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
bool active() { return touch_controls.b() && (kAlways || g_used); }
|
||||
|
||||
void handleEvent(const SDL_Event& ev, bool inGame) {
|
||||
if (ev.type != SDL_EVENT_FINGER_DOWN && ev.type != SDL_EVENT_FINGER_MOTION && ev.type != SDL_EVENT_FINGER_UP) return;
|
||||
g_used = true;
|
||||
const ImVec2 s = screen();
|
||||
const float x = ev.tfinger.x * s.x, y = ev.tfinger.y * s.y;
|
||||
if (!inGame || !active()) { // menus: SDL's mouse emulation does the taps
|
||||
if (ev.type == SDL_EVENT_FINGER_UP) g_fingers.erase(ev.tfinger.fingerID);
|
||||
return;
|
||||
}
|
||||
const float m = unit();
|
||||
if (ev.type == SDL_EVENT_FINGER_DOWN) {
|
||||
Finger f{Finger::Look, x, y, x, y};
|
||||
const ImVec2 p = pauseAt();
|
||||
if (std::hypot(x - p.x, y - p.y) < m * 0.06f) {
|
||||
f.role = Finger::Pause;
|
||||
g_escape = true;
|
||||
} else {
|
||||
for (int i = 0; i < kButtonCount; ++i) {
|
||||
const ImVec2 c = buttonAt(kButtons[i]);
|
||||
if (std::hypot(x - c.x, y - c.y) < kButtons[i].r * m * 1.25f) {
|
||||
f.role = Finger::ButtonHeld, f.button = i;
|
||||
press(i, true);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (f.role == Finger::Look && x < s.x * 0.4f && y > s.y * 0.3f) f.role = Finger::Stick; // the left thumb's area
|
||||
}
|
||||
g_fingers[ev.tfinger.fingerID] = f;
|
||||
} else if (ev.type == SDL_EVENT_FINGER_MOTION) {
|
||||
auto it = g_fingers.find(ev.tfinger.fingerID);
|
||||
if (it == g_fingers.end()) return;
|
||||
Finger& f = it->second;
|
||||
if (f.role == Finger::Look) {
|
||||
g_lookX += (x - f.x) * touch_sensitivity.f();
|
||||
g_lookY += (y - f.y) * touch_sensitivity.f();
|
||||
}
|
||||
f.x = x, f.y = y;
|
||||
if (f.role == Finger::Stick) { // the stick's centre follows a thumb that goes past its edge
|
||||
const float r = m * 0.11f, dx = x - f.ox, dy = y - f.oy, d = std::hypot(dx, dy);
|
||||
if (d > r) f.ox = x - dx / d * r, f.oy = y - dy / d * r;
|
||||
}
|
||||
} else {
|
||||
auto it = g_fingers.find(ev.tfinger.fingerID);
|
||||
if (it == g_fingers.end()) return;
|
||||
if (it->second.role == Finger::ButtonHeld) press(it->second.button, false);
|
||||
g_fingers.erase(it);
|
||||
}
|
||||
}
|
||||
|
||||
void move(float& forward, float& side) {
|
||||
forward = side = 0.0f;
|
||||
const float r = unit() * 0.11f;
|
||||
for (const auto& [id, f] : g_fingers)
|
||||
if (f.role == Finger::Stick) {
|
||||
side = std::clamp((f.x - f.ox) / r, -1.0f, 1.0f);
|
||||
forward = std::clamp(-(f.y - f.oy) / r, -1.0f, 1.0f);
|
||||
}
|
||||
}
|
||||
|
||||
void takeLook(float& dx, float& dy) {
|
||||
dx = g_lookX, dy = g_lookY;
|
||||
g_lookX = g_lookY = 0;
|
||||
}
|
||||
|
||||
bool takeEscape() { return std::exchange(g_escape, false); }
|
||||
|
||||
void releaseAll() {
|
||||
for (int i = 0; i < kButtonCount; ++i)
|
||||
if (g_held[i]) press(i, false);
|
||||
g_fingers.clear();
|
||||
}
|
||||
|
||||
void draw(bool inGame) {
|
||||
if (!inGame || !active()) return;
|
||||
ImDrawList* dl = ImGui::GetForegroundDrawList();
|
||||
const float m = unit();
|
||||
const ImU32 ring = IM_COL32(255, 255, 255, 110), fill = IM_COL32(20, 20, 24, 70), hot = IM_COL32(220, 40, 40, 150), text = IM_COL32(255, 255, 255, 200);
|
||||
auto label = [&](ImVec2 c, const char* t) {
|
||||
const ImVec2 sz = ImGui::CalcTextSize(t);
|
||||
dl->AddText(ImVec2(c.x - sz.x * 0.5f, c.y - sz.y * 0.5f), text, t);
|
||||
};
|
||||
for (int i = 0; i < kButtonCount; ++i) {
|
||||
const ImVec2 c = buttonAt(kButtons[i]);
|
||||
const float r = kButtons[i].r * m;
|
||||
dl->AddCircleFilled(c, r, g_held[i] ? hot : fill, 40);
|
||||
dl->AddCircle(c, r, ring, 40, 2.0f);
|
||||
label(c, kButtons[i].label);
|
||||
}
|
||||
const ImVec2 p = pauseAt();
|
||||
dl->AddCircleFilled(p, m * 0.045f, fill, 32);
|
||||
dl->AddCircle(p, m * 0.045f, ring, 32, 2.0f);
|
||||
dl->AddRectFilled(ImVec2(p.x - m * 0.016f, p.y - m * 0.02f), ImVec2(p.x - m * 0.006f, p.y + m * 0.02f), text);
|
||||
dl->AddRectFilled(ImVec2(p.x + m * 0.006f, p.y - m * 0.02f), ImVec2(p.x + m * 0.016f, p.y + m * 0.02f), text);
|
||||
// The stick, where the thumb is (or a hint of where it goes).
|
||||
const ImVec2 s = screen();
|
||||
bool stick = false;
|
||||
for (const auto& [id, f] : g_fingers)
|
||||
if (f.role == Finger::Stick) {
|
||||
stick = true;
|
||||
dl->AddCircleFilled(ImVec2(f.ox, f.oy), m * 0.11f, fill, 48);
|
||||
dl->AddCircle(ImVec2(f.ox, f.oy), m * 0.11f, ring, 48, 2.0f);
|
||||
const float dx = f.x - f.ox, dy = f.y - f.oy, d = std::hypot(dx, dy), k = d > m * 0.11f ? m * 0.11f / d : 1.0f;
|
||||
dl->AddCircleFilled(ImVec2(f.ox + dx * k, f.oy + dy * k), m * 0.045f, hot, 32);
|
||||
}
|
||||
if (!stick) {
|
||||
const ImVec2 c(m * 0.22f, s.y - m * 0.22f);
|
||||
dl->AddCircle(c, m * 0.11f, IM_COL32(255, 255, 255, 50), 48, 2.0f);
|
||||
}
|
||||
}
|
||||
} // namespace keel::touch
|
||||
22
engine/platform/touch.h
Normal file
22
engine/platform/touch.h
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
#pragma once
|
||||
// Touch controls (phones, tablets, touch screens): while you play, a stick under your left thumb
|
||||
// moves, dragging on the right looks, and buttons on the right press the same "+jump",
|
||||
// "+attack"... a key would. In menus, touches are taps (SDL turns them into mouse clicks).
|
||||
// They show on Android, and anywhere once a finger touches the screen (touch_controls 0: never).
|
||||
union SDL_Event;
|
||||
|
||||
namespace keel::touch {
|
||||
bool active();
|
||||
// inGame: playing (the mouse is captured), when touches are controls, not taps.
|
||||
void handleEvent(const SDL_Event& ev, bool inGame);
|
||||
// The stick: forward and sideways, -1..1.
|
||||
void move(float& forward, float& side);
|
||||
// Looking since the last call, in mouse pixels (so mouse sensitivity applies).
|
||||
void takeLook(float& dx, float& dy);
|
||||
// The pause button was pressed (once).
|
||||
bool takeEscape();
|
||||
// Draws the controls over the game (playing only).
|
||||
void draw(bool inGame);
|
||||
// Lets go of everything held (a menu opened).
|
||||
void releaseAll();
|
||||
} // namespace keel::touch
|
||||
98
engine/platform/window.cpp
Normal file
98
engine/platform/window.cpp
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
#include "platform/window.h"
|
||||
#include "core/log.h"
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdlib>
|
||||
#include <filesystem>
|
||||
|
||||
#ifdef _WIN32
|
||||
// Hybrid laptops: ask the NVIDIA / AMD drivers for the dedicated GPU (read from the executable).
|
||||
extern "C" {
|
||||
__declspec(dllexport) unsigned long NvOptimusEnablement = 1;
|
||||
__declspec(dllexport) int AmdPowerXpressRequestHighPerformance = 1;
|
||||
}
|
||||
#endif
|
||||
|
||||
namespace keel {
|
||||
namespace {
|
||||
// Hybrid laptops on Linux: OpenGL goes to whatever GPU runs the desktop (often the integrated
|
||||
// one) unless asked. Vulkan picks the discrete GPU itself; this makes OpenGL do the same. Set
|
||||
// before SDL loads the GL library; anything the user already set wins. KEEL_IGPU=1 opts out.
|
||||
void preferDedicatedGpu() {
|
||||
#ifndef _WIN32
|
||||
if (std::getenv("KEEL_IGPU")) return;
|
||||
if (std::filesystem::exists("/proc/driver/nvidia")) { // NVIDIA's own driver: PRIME render offload
|
||||
setenv("__NV_PRIME_RENDER_OFFLOAD", "1", 0);
|
||||
setenv("__GLX_VENDOR_LIBRARY_NAME", "nvidia", 0);
|
||||
setenv("__VK_LAYER_NV_optimus", "NVIDIA_only", 0);
|
||||
} else {
|
||||
setenv("DRI_PRIME", "1", 0); // Mesa: the other GPU if there is one, else the same
|
||||
}
|
||||
#endif
|
||||
}
|
||||
} // namespace
|
||||
|
||||
bool Window::init(const char* title, int width, int height, bool opengl) {
|
||||
preferDedicatedGpu();
|
||||
if (!SDL_Init(SDL_INIT_VIDEO | SDL_INIT_EVENTS)) {
|
||||
log::error("SDL_Init failed: {}", SDL_GetError());
|
||||
return false;
|
||||
}
|
||||
window_ = SDL_CreateWindow(title, width, height, (opengl ? SDL_WINDOW_OPENGL : SDL_WINDOW_VULKAN) | SDL_WINDOW_RESIZABLE | SDL_WINDOW_HIGH_PIXEL_DENSITY);
|
||||
if (!window_) {
|
||||
log::error("SDL_CreateWindow failed: {}", SDL_GetError());
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void Window::shutdown() {
|
||||
if (window_) SDL_DestroyWindow(window_);
|
||||
window_ = nullptr;
|
||||
SDL_Quit();
|
||||
}
|
||||
|
||||
void Window::setIcon(const uint8_t* rgba, int width, int height) {
|
||||
if (SDL_Surface* s = SDL_CreateSurfaceFrom(width, height, SDL_PIXELFORMAT_ABGR8888, const_cast<uint8_t*>(rgba), width * 4)) {
|
||||
SDL_SetWindowIcon(window_, s);
|
||||
SDL_DestroySurface(s);
|
||||
}
|
||||
}
|
||||
|
||||
void Window::setMode(int mode, int width, int height) {
|
||||
if (!window_) return;
|
||||
if (mode == 0) {
|
||||
SDL_SetWindowFullscreen(window_, false);
|
||||
SDL_SetWindowSize(window_, width, height);
|
||||
SDL_SetWindowPosition(window_, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED);
|
||||
return;
|
||||
}
|
||||
SDL_DisplayMode closest{};
|
||||
const bool exclusive = mode == 2 && SDL_GetClosestFullscreenDisplayMode(SDL_GetDisplayForWindow(window_), width, height, 0.0f, true, &closest);
|
||||
SDL_SetWindowFullscreenMode(window_, exclusive ? &closest : nullptr); // nullptr = borderless desktop
|
||||
SDL_SetWindowFullscreen(window_, true);
|
||||
}
|
||||
|
||||
std::vector<std::pair<int, int>> Window::resolutions() const {
|
||||
std::vector<std::pair<int, int>> out;
|
||||
int count = 0;
|
||||
if (SDL_DisplayMode** modes = SDL_GetFullscreenDisplayModes(SDL_GetDisplayForWindow(window_), &count)) {
|
||||
for (int i = 0; i < count; ++i) {
|
||||
const std::pair<int, int> r{modes[i]->w, modes[i]->h};
|
||||
if (std::find(out.begin(), out.end(), r) == out.end()) out.push_back(r);
|
||||
}
|
||||
SDL_free(modes);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
void Window::pixelSize(uint32_t& w, uint32_t& h) const {
|
||||
int iw = 0, ih = 0;
|
||||
if (!(SDL_GetWindowFlags(window_) & SDL_WINDOW_MINIMIZED)) SDL_GetWindowSizeInPixels(window_, &iw, &ih);
|
||||
w = uint32_t(iw);
|
||||
h = uint32_t(ih);
|
||||
}
|
||||
|
||||
} // namespace keel
|
||||
31
engine/platform/window.h
Normal file
31
engine/platform/window.h
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
#pragma once
|
||||
#include <cstdint>
|
||||
|
||||
#include <cstdint>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
struct SDL_Window;
|
||||
|
||||
namespace keel {
|
||||
|
||||
class Window {
|
||||
public:
|
||||
// opengl: a window for an OpenGL context (the backend creates it); otherwise Vulkan.
|
||||
bool init(const char* title, int width, int height, bool opengl = false);
|
||||
void shutdown();
|
||||
SDL_Window* handle() const { return window_; }
|
||||
// Size in pixels (not points); 0×0 while minimized.
|
||||
void pixelSize(uint32_t& w, uint32_t& h) const;
|
||||
// mode: 0 = windowed (width × height), 1 = borderless fullscreen (desktop size),
|
||||
// 2 = exclusive fullscreen at the display mode closest to width × height.
|
||||
void setMode(int mode, int width, int height);
|
||||
void setIcon(const uint8_t* rgba, int width, int height);
|
||||
// Fullscreen resolutions the display offers, largest first, no duplicates.
|
||||
std::vector<std::pair<int, int>> resolutions() const;
|
||||
|
||||
private:
|
||||
SDL_Window* window_ = nullptr;
|
||||
};
|
||||
|
||||
} // namespace keel
|
||||
Loading…
Reference in a new issue