mirror of
https://github.com/Nexoniarz/Fry3DX.git
synced 2026-11-03 04:24:26 +00:00
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
394 lines
16 KiB
C++
394 lines
16 KiB
C++
#include "crash.h"
|
||
|
||
#include <core/log.h>
|
||
|
||
#include <SDL3/SDL.h>
|
||
|
||
#include <algorithm>
|
||
#include <atomic>
|
||
#include <cmath>
|
||
#include <chrono>
|
||
#include <cstdio>
|
||
#include <cstring>
|
||
#include <exception>
|
||
#include <filesystem>
|
||
#include <fstream>
|
||
#include <sstream>
|
||
#include <thread>
|
||
#include <vector>
|
||
|
||
#ifdef _WIN32
|
||
#include <process.h>
|
||
#ifndef NOMINMAX
|
||
#define NOMINMAX
|
||
#endif
|
||
#include <windows.h>
|
||
#else
|
||
#include <csignal>
|
||
#if !defined(__ANDROID__) // (bionic: backtrace() from Android 13 only)
|
||
#include <execinfo.h>
|
||
#endif
|
||
#include <fcntl.h>
|
||
#include <pthread.h>
|
||
#include <unistd.h>
|
||
#endif
|
||
|
||
namespace keel::crash {
|
||
namespace {
|
||
std::string g_report, g_exe;
|
||
std::vector<std::string> g_args; // reporter's command line (exe -crashreport file -- args)
|
||
std::vector<char*> g_argv; // ...as pointers, made before anything goes wrong
|
||
std::atomic<double> g_beat{0.0};
|
||
std::atomic<int> g_busy{0};
|
||
std::atomic<bool> g_fired{false};
|
||
#ifndef _WIN32
|
||
pthread_t g_main;
|
||
#endif
|
||
|
||
double seconds() { return std::chrono::duration<double>(std::chrono::steady_clock::now().time_since_epoch()).count(); }
|
||
|
||
// The report: a headline, then the backtrace and the end of the log.
|
||
void writeReport(const char* what) {
|
||
#ifdef _WIN32
|
||
FILE* f = std::fopen(g_report.c_str(), "w");
|
||
if (!f) return;
|
||
std::fprintf(f, "%s\n\n", what);
|
||
const auto& h = log::history();
|
||
std::fprintf(f, "--- last log lines ---\n");
|
||
for (size_t i = h.size() > 80 ? h.size() - 80 : 0; i < h.size(); ++i) std::fprintf(f, "%s\n", h[i].text.c_str());
|
||
std::fclose(f);
|
||
#else
|
||
const int fd = open(g_report.c_str(), O_WRONLY | O_CREAT | O_TRUNC, 0644);
|
||
if (fd < 0) return;
|
||
auto put = [&](const char* s) { (void)!::write(fd, s, std::strlen(s)); };
|
||
put(what);
|
||
put("\n\n--- backtrace ---\n");
|
||
#if defined(__ANDROID__)
|
||
put("(none on Android: see logcat)\n");
|
||
#else
|
||
void* frames[64];
|
||
backtrace_symbols_fd(frames, backtrace(frames, 64), fd);
|
||
#endif
|
||
put("\n--- last log lines ---\n");
|
||
const auto& h = log::history(); // best effort: we're crashing anyway
|
||
for (size_t i = h.size() > 80 ? h.size() - 80 : 0; i < h.size(); ++i) put(h[i].text.c_str()), put("\n");
|
||
close(fd);
|
||
#endif
|
||
}
|
||
|
||
void launchReporter() {
|
||
#ifdef _WIN32
|
||
_spawnv(_P_NOWAIT, g_exe.c_str(), g_argv.data());
|
||
#elif defined(__ANDROID__)
|
||
// (an app is a library in the Java runtime: no second process to report with; the report
|
||
// file and logcat have it)
|
||
#else
|
||
if (fork() == 0) {
|
||
setsid(); // on its own: whatever waited on the app (a terminal, a pipe) isn't kept waiting
|
||
if (const int null = open("/dev/null", O_RDWR); null >= 0) dup2(null, 0), dup2(null, 1), dup2(null, 2);
|
||
execv(g_exe.c_str(), g_argv.data());
|
||
_exit(127);
|
||
}
|
||
#endif
|
||
}
|
||
|
||
[[noreturn]] void fail(const char* what) {
|
||
if (!g_fired.exchange(true)) {
|
||
writeReport(what);
|
||
launchReporter();
|
||
}
|
||
#ifdef _WIN32
|
||
TerminateProcess(GetCurrentProcess(), 3);
|
||
#endif
|
||
_exit(3);
|
||
}
|
||
|
||
#ifdef _WIN32
|
||
LONG WINAPI onException(EXCEPTION_POINTERS* e) {
|
||
char what[128];
|
||
std::snprintf(what, sizeof what, "Fry3DX crashed (exception 0x%08lX).", e->ExceptionRecord->ExceptionCode);
|
||
fail(what);
|
||
}
|
||
#else
|
||
void onSignal(int sig) {
|
||
const char* what = sig == SIGSEGV ? "Fry3DX crashed: it touched memory it shouldn't have (SIGSEGV)."
|
||
: sig == SIGBUS ? "Fry3DX crashed: a bad memory access (SIGBUS)."
|
||
: sig == SIGFPE ? "Fry3DX crashed: a maths error, like dividing by zero (SIGFPE)."
|
||
: sig == SIGILL ? "Fry3DX crashed: an illegal instruction (SIGILL)."
|
||
: sig == SIGABRT ? "Fry3DX crashed: it gave up on an error it couldn't handle (abort)."
|
||
: sig == SIGUSR2 ? "Fry3DX froze: no frame for a long time (the watchdog stopped it). Backtrace of the stuck thread:"
|
||
: "Fry3DX crashed.";
|
||
fail(what);
|
||
}
|
||
#endif
|
||
|
||
bool debuggerAttached() {
|
||
#ifdef _WIN32
|
||
return IsDebuggerPresent();
|
||
#else
|
||
std::ifstream f("/proc/self/status");
|
||
for (std::string line; std::getline(f, line);)
|
||
if (line.rfind("TracerPid:", 0) == 0) return std::atoi(line.c_str() + 10) != 0;
|
||
return false;
|
||
#endif
|
||
}
|
||
|
||
void watchdog() {
|
||
constexpr double kTimeout = 45.0;
|
||
for (;;) {
|
||
std::this_thread::sleep_for(std::chrono::seconds(1));
|
||
if (g_busy > 0 || g_beat == 0.0) {
|
||
g_beat = seconds();
|
||
continue;
|
||
}
|
||
if (seconds() - g_beat < kTimeout || debuggerAttached()) continue;
|
||
#ifndef _WIN32
|
||
pthread_kill(g_main, SIGUSR2); // its handler reports from the stuck thread (its backtrace)
|
||
std::this_thread::sleep_for(std::chrono::seconds(3));
|
||
#endif
|
||
fail("Fry3DX froze: no frame for a long time (the watchdog stopped it).");
|
||
}
|
||
}
|
||
} // namespace
|
||
|
||
void install(const std::string& prefPath, const std::vector<std::string>& args, bool dog) {
|
||
g_report = prefPath + "crash.txt";
|
||
#ifdef _WIN32
|
||
char exe[1024] = {};
|
||
GetModuleFileNameA(nullptr, exe, sizeof exe);
|
||
g_exe = exe;
|
||
#else
|
||
g_exe = std::filesystem::read_symlink("/proc/self/exe").string();
|
||
g_main = pthread_self();
|
||
#endif
|
||
g_args = {g_exe, "-crashreport", g_report, "--"};
|
||
for (size_t i = 1; i < args.size(); ++i) g_args.push_back(args[i]);
|
||
#ifdef _WIN32
|
||
for (std::string& x : g_args)
|
||
if (x.find(' ') != std::string::npos) x = "\"" + x + "\"";
|
||
#endif
|
||
for (std::string& a : g_args) g_argv.push_back(a.data());
|
||
g_argv.push_back(nullptr);
|
||
#ifdef _WIN32
|
||
SetUnhandledExceptionFilter(onException);
|
||
#else
|
||
// On a stack of its own, so a stack overflow can still be reported.
|
||
static std::vector<char> altStack(1 << 16);
|
||
stack_t ss{};
|
||
ss.ss_sp = altStack.data(), ss.ss_size = altStack.size();
|
||
sigaltstack(&ss, nullptr);
|
||
struct sigaction sa{};
|
||
sa.sa_handler = onSignal;
|
||
sa.sa_flags = SA_ONSTACK;
|
||
for (int sig : {SIGSEGV, SIGBUS, SIGFPE, SIGILL, SIGABRT, SIGUSR2}) sigaction(sig, &sa, nullptr);
|
||
#endif
|
||
std::set_terminate([] {
|
||
std::string what = "Fry3DX crashed: an error nobody caught";
|
||
try {
|
||
if (auto e = std::current_exception()) std::rethrow_exception(e);
|
||
} catch (const std::exception& e) {
|
||
what += std::string(" (") + e.what() + ")";
|
||
} catch (...) {
|
||
}
|
||
what += ".";
|
||
fail(what.c_str());
|
||
});
|
||
if (dog) std::thread(watchdog).detach();
|
||
}
|
||
|
||
void heartbeat() { g_beat = seconds(); }
|
||
Busy::Busy(const char*) { ++g_busy; }
|
||
Busy::~Busy() {
|
||
heartbeat();
|
||
--g_busy;
|
||
}
|
||
|
||
// ── The crash screen: SDL's 2D renderer and its built-in 8×8 font, scaled up ──
|
||
int runReporter(int argc, char** argv) {
|
||
if (argc < 3 || std::strcmp(argv[1], "-crashreport") != 0) return -1;
|
||
std::string report;
|
||
{
|
||
std::ifstream f(argv[2]);
|
||
std::stringstream ss;
|
||
ss << f.rdbuf();
|
||
report = ss.str();
|
||
}
|
||
std::vector<std::string> restart;
|
||
for (int i = 3; i < argc; ++i)
|
||
if (std::strcmp(argv[i], "--") != 0) restart.emplace_back(argv[i]);
|
||
std::vector<std::string> lines;
|
||
{
|
||
std::stringstream ss(report);
|
||
for (std::string l; std::getline(ss, l);) lines.push_back(l);
|
||
}
|
||
const std::string headline = lines.empty() ? "Fry3DX crashed." : lines[0];
|
||
|
||
if (!SDL_Init(SDL_INIT_VIDEO)) return 1;
|
||
SDL_Window* win = nullptr;
|
||
SDL_Renderer* ren = nullptr;
|
||
if (!SDL_CreateWindowAndRenderer("Fry3DX - something got burnt", 1000, 620, SDL_WINDOW_RESIZABLE | SDL_WINDOW_HIGH_PIXEL_DENSITY, &win, &ren)) return 1;
|
||
SDL_SetRenderVSync(ren, 1);
|
||
SDL_SetRenderDrawBlendMode(ren, SDL_BLENDMODE_BLEND);
|
||
auto text = [&](float x, float y, float scale, const std::string& s, SDL_Color c) {
|
||
SDL_SetRenderScale(ren, scale, scale);
|
||
SDL_SetRenderDrawColor(ren, c.r, c.g, c.b, c.a);
|
||
SDL_RenderDebugText(ren, x / scale, y / scale, s.c_str());
|
||
SDL_SetRenderScale(ren, 1, 1);
|
||
};
|
||
// Shapes for the picture: an ellipse (filled), a box.
|
||
auto ellipse = [&](float cx, float cy, float rx, float ry, SDL_FColor c) {
|
||
std::vector<SDL_Vertex> v;
|
||
const int n = 48;
|
||
for (int i = 0; i < n; ++i) {
|
||
const float a0 = 6.2831853f * i / n, a1 = 6.2831853f * (i + 1) / n;
|
||
v.push_back({{cx, cy}, c, {}});
|
||
v.push_back({{cx + std::cos(a0) * rx, cy + std::sin(a0) * ry}, c, {}});
|
||
v.push_back({{cx + std::cos(a1) * rx, cy + std::sin(a1) * ry}, c, {}});
|
||
}
|
||
SDL_RenderGeometry(ren, nullptr, v.data(), int(v.size()), nullptr, 0);
|
||
};
|
||
auto quad = [&](SDL_FPoint a, SDL_FPoint b, SDL_FPoint c, SDL_FPoint d, SDL_FColor col) {
|
||
const SDL_Vertex v[6] = {{a, col, {}}, {b, col, {}}, {c, col, {}}, {a, col, {}}, {c, col, {}}, {d, col, {}}};
|
||
SDL_RenderGeometry(ren, nullptr, v, 6, nullptr, 0);
|
||
};
|
||
auto rgb = [](int r, int g, int b, float a = 1.0f) { return SDL_FColor{r / 255.0f, g / 255.0f, b / 255.0f, a}; };
|
||
// Word wrap for the 8-pixel font.
|
||
auto wrap = [](const std::string& s, size_t cols) {
|
||
std::vector<std::string> out;
|
||
std::string line;
|
||
std::stringstream ss(s);
|
||
for (std::string w; ss >> w;) {
|
||
if (!line.empty() && line.size() + 1 + w.size() > cols) out.push_back(line), line.clear();
|
||
line += (line.empty() ? "" : " ") + w;
|
||
}
|
||
if (!line.empty()) out.push_back(line);
|
||
return out;
|
||
};
|
||
const SDL_Color ink{236, 232, 226, 255}, dim{150, 146, 140, 255}, orange{255, 140, 60, 255};
|
||
int scroll = 0, result = 0;
|
||
std::string toast;
|
||
double toastUntil = 0;
|
||
for (bool running = true; running;) {
|
||
int w = 0, h = 0;
|
||
SDL_GetRenderOutputSize(ren, &w, &h);
|
||
const float s = std::min(h / 620.0f, w / 1000.0f);
|
||
const double t = SDL_GetTicks() / 1000.0;
|
||
struct Button { const char* label; float x; } buttons[] = {{"OPEN IT AGAIN", 60 * s}, {"COPY REPORT", 290 * s}, {"REPORT ON GITHUB", 520 * s}, {"CLOSE", 750 * s}};
|
||
const float by = h - 90 * s, bw = 210 * s, bh = 50 * s;
|
||
float mx = 0, my = 0;
|
||
SDL_GetMouseState(&mx, &my);
|
||
float px = 1, py = 1; // window → pixels
|
||
{
|
||
int ww, wh;
|
||
SDL_GetWindowSize(win, &ww, &wh);
|
||
px = float(w) / ww, py = float(h) / wh;
|
||
}
|
||
mx *= px, my *= py;
|
||
int clicked = -1;
|
||
for (SDL_Event e; SDL_PollEvent(&e);) {
|
||
if (e.type == SDL_EVENT_QUIT) running = false;
|
||
if (e.type == SDL_EVENT_KEY_DOWN) {
|
||
if (e.key.key == SDLK_ESCAPE || e.key.key == SDLK_Q) running = false;
|
||
if (e.key.key == SDLK_R) clicked = 0;
|
||
if (e.key.key == SDLK_C) clicked = 1;
|
||
}
|
||
if (e.type == SDL_EVENT_MOUSE_WHEEL) scroll = std::max(0, scroll - int(e.wheel.y * 3));
|
||
if (e.type == SDL_EVENT_MOUSE_BUTTON_DOWN && e.button.button == SDL_BUTTON_LEFT)
|
||
for (int i = 0; i < 4; ++i)
|
||
if (mx >= buttons[i].x && mx < buttons[i].x + bw && my >= by && my < by + bh) clicked = i;
|
||
}
|
||
if (clicked == 0) { // opened again as it was started (it offers the autosave back)
|
||
std::vector<char*> a;
|
||
std::string exe = argv[0];
|
||
a.push_back(exe.data());
|
||
for (std::string& r : restart) a.push_back(r.data());
|
||
a.push_back(nullptr);
|
||
SDL_DestroyRenderer(ren);
|
||
SDL_DestroyWindow(win);
|
||
SDL_Quit();
|
||
#ifdef _WIN32
|
||
_spawnv(_P_NOWAIT, exe.c_str(), a.data());
|
||
return 0;
|
||
#else
|
||
execv(std::filesystem::read_symlink("/proc/self/exe").c_str(), a.data());
|
||
return 1;
|
||
#endif
|
||
}
|
||
if (clicked == 1 || clicked == 2) {
|
||
SDL_SetClipboardText(report.c_str());
|
||
toast = clicked == 1 ? "Copied: paste it into a bug report." : "Copied: paste it into the new issue.";
|
||
toastUntil = t + 3.0;
|
||
if (clicked == 2) SDL_OpenURL("https://github.com/Nexoniarz/Fry3DX/issues/new");
|
||
}
|
||
if (clicked == 3) running = false;
|
||
|
||
SDL_SetRenderDrawColor(ren, 22, 22, 25, 255);
|
||
SDL_RenderClear(ren);
|
||
// The picture, top right: a pan, a cube on it burnt black, smoke going up.
|
||
{
|
||
const float cx = w - 230 * s, cy = 170 * s;
|
||
quad({cx + 80 * s, cy - 6 * s}, {cx + 200 * s, cy - 30 * s}, {cx + 204 * s, cy - 16 * s}, {cx + 84 * s, cy + 8 * s}, rgb(58, 58, 64)); // handle
|
||
ellipse(cx, cy + 10 * s, 105 * s, 34 * s, rgb(34, 34, 38));
|
||
ellipse(cx, cy, 105 * s, 34 * s, rgb(70, 70, 78));
|
||
ellipse(cx, cy + 2 * s, 88 * s, 26 * s, rgb(28, 28, 32));
|
||
for (int i = 0; i < 3; ++i) { // flames under it
|
||
const float f = float(std::sin(t * 7 + i * 2.1) * 0.5 + 0.5);
|
||
const float fx = cx + (i - 1) * 50 * s;
|
||
ellipse(fx, cy + 46 * s, (14 + 4 * f) * s, (12 + 6 * f) * s, rgb(255, 74, 28, 0.85f));
|
||
ellipse(fx, cy + 48 * s, 7 * s, (6 + 4 * f) * s, rgb(255, 194, 74, 0.9f));
|
||
}
|
||
const float k = 26 * s; // the cube (seen from a corner), charred
|
||
quad({cx, cy - 2 * k}, {cx + k, cy - 1.5f * k}, {cx, cy - k}, {cx - k, cy - 1.5f * k}, rgb(64, 52, 46));
|
||
quad({cx - k, cy - 1.5f * k}, {cx, cy - k}, {cx, cy}, {cx - k, cy - 0.5f * k}, rgb(40, 32, 28));
|
||
quad({cx, cy - k}, {cx + k, cy - 1.5f * k}, {cx + k, cy - 0.5f * k}, {cx, cy}, rgb(26, 21, 19));
|
||
for (int i = 0; i < 6; ++i) { // smoke, rising and fading
|
||
const double life = std::fmod(t * 0.45 + i / 6.0, 1.0);
|
||
const float sx = cx + float(std::sin(life * 6 + i) * 18 * s), sy = cy - 2.2f * k - float(life * 80 * s);
|
||
ellipse(sx, sy, float(10 + life * 22) * s, float(8 + life * 16) * s, rgb(150, 150, 158, float(0.35 * (1 - life))));
|
||
}
|
||
}
|
||
text(60 * s, 60 * s, 4 * s, "Fry3DX", orange);
|
||
text(60 * s, 110 * s, 2.5f * s, "Something got burnt.", ink);
|
||
float y = 160 * s;
|
||
for (const std::string& l : wrap(headline, size_t((w - 400 * s) / (16 * s))))
|
||
text(60 * s, y, 2 * s, l, ink), y += 24 * s;
|
||
y += 6 * s;
|
||
for (const std::string& l : wrap("Your work isn't lost: Open it again and Fry3DX offers back the last autosave.", size_t((w - 400 * s) / (12 * s))))
|
||
text(60 * s, y, 1.5f * s, l, {130, 210, 120, 255}), y += 18 * s;
|
||
text(60 * s, y + 4 * s, 1.25f * s, "Report saved to " + std::string(argv[2]), dim);
|
||
y += 30 * s;
|
||
// The report's tail, in a panel, scrollable.
|
||
const SDL_FRect panel{50 * s, y - 8 * s, w - 100 * s, by - 20 * s - (y - 8 * s)};
|
||
SDL_SetRenderDrawColor(ren, 15, 15, 17, 255);
|
||
SDL_RenderFillRect(ren, &panel);
|
||
SDL_SetRenderDrawColor(ren, 255, 120, 40, 255);
|
||
const SDL_FRect edge{panel.x, panel.y, 3 * s, panel.h};
|
||
SDL_RenderFillRect(ren, &edge);
|
||
const int visible = std::max(1, int((panel.h - 10 * s) / (13 * s)));
|
||
const int start = std::clamp(int(lines.size()) - visible - scroll, 1, std::max(1, int(lines.size()) - 1));
|
||
for (int i = start; i < int(lines.size()) && y < by - 30 * s; ++i, y += 13 * s)
|
||
text(64 * s, y, 1.25f * s, lines[size_t(i)].substr(0, size_t((w - 140 * s) / (10 * s))), {165, 160, 152, 255});
|
||
for (int i = 0; i < 4; ++i) {
|
||
const bool hover = mx >= buttons[i].x && mx < buttons[i].x + bw && my >= by && my < by + bh;
|
||
SDL_FRect r{buttons[i].x, by, bw, bh};
|
||
if (i == 0 || hover) SDL_SetRenderDrawColor(ren, hover ? 255 : 220, hover ? 140 : 100, hover ? 60 : 30, 255);
|
||
else SDL_SetRenderDrawColor(ren, 46, 46, 52, 255);
|
||
SDL_RenderFillRect(ren, &r);
|
||
const float n = float(std::strlen(buttons[i].label)), ts = std::min(1.75f * s, (bw - 24 * s) / (n * 8)); // (fits its button)
|
||
text(buttons[i].x + (bw - n * 8 * ts) / 2, by + bh / 2 - 4 * ts, ts, buttons[i].label, ink);
|
||
}
|
||
text(60 * s, by + bh + 12 * s, 1.25f * s, "R open again C copy Esc close", dim);
|
||
if (t < toastUntil) text(60 * s, by - 16 * s, 1.5f * s, toast, orange);
|
||
if (const char* shot = std::getenv("KEEL_CRASHSHOT")) { // for checking the screen: one frame to a file, then quit
|
||
if (SDL_Surface* px = SDL_RenderReadPixels(ren, nullptr)) SDL_SaveBMP(px, shot), SDL_DestroySurface(px);
|
||
running = false;
|
||
}
|
||
SDL_RenderPresent(ren);
|
||
}
|
||
SDL_DestroyRenderer(ren);
|
||
SDL_DestroyWindow(win);
|
||
SDL_Quit();
|
||
return result;
|
||
}
|
||
|
||
} // namespace keel::crash
|