From 739a72e82643de85d164930f1cfd12e31e651a94 Mon Sep 17 00:00:00 2001 From: tettou771 Date: Fri, 3 Jul 2026 23:34:27 +0900 Subject: [PATCH 01/87] =?UTF-8?q?test:=20add=20core/tests/filePath=20?= =?UTF-8?q?=E2=80=94=20path=20edge=20cases=20across=20OSes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Headless regression test (auto-discovered by build_all.py, runs in CI on macOS/Linux/Windows): data-path root composition with an absolute root, absolute-path passthrough, Japanese filenames and directory names, spaces/parentheses, and round-trips through every C-library boundary (stb PNG, miniaudio WAV, nlohmann JSON, pugixml XML, FileWriter/Reader). On Windows this is the gate for UTF-8 -> wide conversion regressions. --- addons/tcxDepthRecord/src/tcxDepthPlayback.h | 4 +- addons/tcxDepthRecord/src/tcxDepthRecorder.h | 7 +- addons/tcxGltf/src/tcxGltf.cpp | 2 +- addons/tcxHap/src/tcxHapPlayer.h | 2 +- addons/tcxHap/src/tcxMovParser.h | 2 +- addons/tcxLua/exampleFileReload/src/tcApp.cpp | 4 +- .../tcxLua/src/generated/trussc_generated.cpp | 4 +- addons/tcxObj/src/tcxObjLoader.cpp | 2 +- core/include/TrussC.h | 4 +- core/include/impl/stb_impl.cpp | 6 + core/include/tc/3d/tcEnvironment.h | 2 +- core/include/tc/3d/tcIesProfile.h | 4 +- core/include/tc/app/tcHotReloadHost.h | 2 +- core/include/tc/graphics/tcFont.h | 21 +- core/include/tc/graphics/tcImage.h | 2 +- core/include/tc/graphics/tcPixels.cpp | 5 +- core/include/tc/graphics/tcPixels.h | 5 +- core/include/tc/sound/tcAudio_impl.cpp | 36 ++-- core/include/tc/sound/tcSound.h | 26 +-- core/include/tc/sound/tcSound_impl.cpp | 53 +++-- core/include/tc/utils/tcFile.h | 66 +++--- core/include/tc/utils/tcJson.h | 8 +- core/include/tc/utils/tcLog.h | 11 +- core/include/tc/utils/tcStandardTools.h | 6 +- core/include/tc/utils/tcUtils.h | 41 ++-- core/include/tc/utils/tcXml.h | 13 +- core/include/tc/video/tcVideoPlayer.h | 20 +- core/include/tc/video/tcVideoPlayerBase.h | 3 +- core/include/tc/video/tcVideoRecorder.h | 24 +-- core/platform/android/tcPlatform_android.cpp | 4 +- core/platform/android/tcSound_android.cpp | 2 +- .../android/tcVideoPlayer_android.cpp | 6 +- core/platform/ios/tcPlatform_ios.mm | 2 +- core/platform/ios/tcSound_ios.mm | 7 +- core/platform/ios/tcVideoPlayer_ios.mm | 4 +- core/platform/linux/tcPlatform_linux.cpp | 4 +- core/platform/linux/tcSound_linux.cpp | 4 +- core/platform/linux/tcVideoPlayer_linux.cpp | 16 +- core/platform/mac/tcPixels_mac.mm | 2 +- core/platform/mac/tcPlatform_mac.mm | 2 +- core/platform/mac/tcSound_mac.mm | 7 +- core/platform/mac/tcVideoPlayer_mac.mm | 15 +- core/platform/web/tcSound_web.cpp | 6 +- core/platform/web/tcVideoPlayer_web.cpp | 7 +- core/platform/win/tcPlatform_win.cpp | 4 +- core/platform/win/tcSound_win.cpp | 8 +- core/platform/win/tcVideoPlayer_win.cpp | 16 +- core/tests/filePath/.gitignore | 40 ++++ core/tests/filePath/addons.make | 1 + core/tests/filePath/src/main.cpp | 193 ++++++++++++++++++ docs/FOR_AI_ASSISTANT.md | 4 + docs/reference/api-reference.toml | 18 +- .../sound/soundPlayerExample/src/tcApp.cpp | 2 +- examples/sound/soundPlayerExample/src/tcApp.h | 2 +- .../sound/soundPlayerFFTExample/src/tcApp.cpp | 2 +- .../sound/soundStreamExample/src/tcApp.cpp | 2 +- examples/sound/soundStreamExample/src/tcApp.h | 2 +- 57 files changed, 528 insertions(+), 239 deletions(-) create mode 100644 core/tests/filePath/.gitignore create mode 100644 core/tests/filePath/addons.make create mode 100644 core/tests/filePath/src/main.cpp diff --git a/addons/tcxDepthRecord/src/tcxDepthPlayback.h b/addons/tcxDepthRecord/src/tcxDepthPlayback.h index c19e3a95..6fc0d1d3 100644 --- a/addons/tcxDepthRecord/src/tcxDepthPlayback.h +++ b/addons/tcxDepthRecord/src/tcxDepthPlayback.h @@ -63,8 +63,8 @@ class PlaybackDepthCamera : public DepthCamera { protected: bool openDevice() override { - std::filesystem::path p(path_); - std::string resolved = p.is_relative() ? getDataPath(path_) : path_; + // getDataPath passes absolute paths through unchanged + std::filesystem::path resolved = getDataPath(path_); file_.open(resolved, std::ios::binary); if (!file_) { logError("tcxDepthRecord") << "PlaybackDepthCamera: cannot open " << resolved; diff --git a/addons/tcxDepthRecord/src/tcxDepthRecorder.h b/addons/tcxDepthRecord/src/tcxDepthRecorder.h index f742cba7..41e8075c 100644 --- a/addons/tcxDepthRecord/src/tcxDepthRecorder.h +++ b/addons/tcxDepthRecord/src/tcxDepthRecorder.h @@ -44,16 +44,15 @@ class DepthRecorder { std::uint32_t streams = REC_ALL, DepthCodecId depthCodec = DepthCodecId::HiloLZ4, ColorCodecId colorCodec = ColorCodecId::LZ4) { - std::filesystem::path p(path); - std::string resolved = p.is_relative() ? getDataPath(path) : path; - std::filesystem::path rp(resolved); + // getDataPath passes absolute paths through unchanged + std::filesystem::path rp = getDataPath(path); if (rp.has_parent_path()) { std::error_code ec; std::filesystem::create_directories(rp.parent_path(), ec); } file_.open(rp, std::ios::binary | std::ios::trunc); if (!file_) { - logError("tcxDepthRecord") << "DepthRecorder: cannot open " << resolved; + logError("tcxDepthRecord") << "DepthRecorder: cannot open " << rp; return false; } streams_ = streams; diff --git a/addons/tcxGltf/src/tcxGltf.cpp b/addons/tcxGltf/src/tcxGltf.cpp index 5ce78a43..f28c87c4 100644 --- a/addons/tcxGltf/src/tcxGltf.cpp +++ b/addons/tcxGltf/src/tcxGltf.cpp @@ -226,7 +226,7 @@ bool GltfModel::load(const string& path) { textures_.clear(); loaded_ = false; - string resolved = getDataPath(path); + string resolved = getDataPath(path).string(); // Parse with cgltf cgltf_options options = {}; diff --git a/addons/tcxHap/src/tcxHapPlayer.h b/addons/tcxHap/src/tcxHapPlayer.h index 1024cd94..04236b28 100644 --- a/addons/tcxHap/src/tcxHapPlayer.h +++ b/addons/tcxHap/src/tcxHapPlayer.h @@ -74,7 +74,7 @@ class HapPlayer : public tc::VideoPlayerBase { // Load / Close // ========================================================================= - bool load(const std::string& path) override { + bool load(const tc::fs::path& path) override { if (initialized_) { close(); } diff --git a/addons/tcxHap/src/tcxMovParser.h b/addons/tcxHap/src/tcxMovParser.h index 184c51b7..62cb241a 100644 --- a/addons/tcxHap/src/tcxMovParser.h +++ b/addons/tcxHap/src/tcxMovParser.h @@ -185,7 +185,7 @@ class MovParser { return *this; } - bool open(const std::string& path) { + bool open(const tc::fs::path& path) { close(); // Create a completely fresh fstream object (not reusing old one) diff --git a/addons/tcxLua/exampleFileReload/src/tcApp.cpp b/addons/tcxLua/exampleFileReload/src/tcApp.cpp index 331f5082..c1478178 100644 --- a/addons/tcxLua/exampleFileReload/src/tcApp.cpp +++ b/addons/tcxLua/exampleFileReload/src/tcApp.cpp @@ -25,10 +25,10 @@ std::string sep = "/"; void tcApp::reloadLuaFile(){ #ifdef FILE_RELOAD_SUPPORTED std::string luaScriptBaseName = "sketch.lua"; - std::string luaScriptPath = getDataPath(luaScriptBaseName); + fs::path luaScriptPath = getDataPath(luaScriptBaseName); if(fileExists(luaScriptPath)){ - sol::optional result = lua->safe_script_file(luaScriptPath); + sol::optional result = lua->safe_script_file(luaScriptPath.string()); if (result.has_value()) { std::cerr << "Lua execution failed: " << result.value().what() << std::endl; diff --git a/addons/tcxLua/src/generated/trussc_generated.cpp b/addons/tcxLua/src/generated/trussc_generated.cpp index e6b49d87..fe2b970b 100644 --- a/addons/tcxLua/src/generated/trussc_generated.cpp +++ b/addons/tcxLua/src/generated/trussc_generated.cpp @@ -210,8 +210,8 @@ void tcxLua::setTrussCGeneratedBindings(const std::shared_ptr& lua) lua->set_function("shutdownAudio", []() { return trussc::shutdownAudio(); }); lua->set_function("getMicInput", []() -> decltype(auto) { return trussc::getMicInput(); }); lua->set_function("setDataPathRoot", [](const std::string & path) { return trussc::setDataPathRoot(path); }); - lua->set_function("getDataPathRoot", []() { return trussc::getDataPathRoot(); }); - lua->set_function("getDataPath", [](const std::string & filename) { return trussc::getDataPath(filename); }); + lua->set_function("getDataPathRoot", []() { return trussc::getDataPathRoot().string(); }); + lua->set_function("getDataPath", [](const std::string & filename) { return trussc::getDataPath(filename).string(); }); lua->set_function("setDataPathToResources", []() { return trussc::setDataPathToResources(); }); lua->set_function("toInt", [](const std::string & str) { return trussc::toInt(str); }); lua->set_function("toInt64", [](const std::string & str) { return trussc::toInt64(str); }); diff --git a/addons/tcxObj/src/tcxObjLoader.cpp b/addons/tcxObj/src/tcxObjLoader.cpp index 1a6055dc..be794216 100644 --- a/addons/tcxObj/src/tcxObjLoader.cpp +++ b/addons/tcxObj/src/tcxObjLoader.cpp @@ -16,7 +16,7 @@ namespace tcx::obj { bool ObjLoader::load(const fs::path& path) { clear(); - fs::path objPath = path.is_absolute() ? path : fs::path(getDataPath(path.string())); + fs::path objPath = getDataPath(path); // absolute paths pass through if (!fs::exists(objPath)) { logError() << "ObjLoader::load: file not found: " << objPath; diff --git a/core/include/TrussC.h b/core/include/TrussC.h index 49e0c3f4..737d5c40 100644 --- a/core/include/TrussC.h +++ b/core/include/TrussC.h @@ -84,6 +84,7 @@ #include "tc/events/tcCoreEvents.h" // TrussC utilities +#include "tc/utils/tcFileIO.h" // fs::path boundary helpers (before all path consumers) #include "tc/utils/tcUtils.h" #include "tc/utils/tcMainThread.h" // runOnMainThread / drainMainThreadQueue #include "tc/utils/tcTime.h" @@ -1843,8 +1844,7 @@ namespace internal { // Relative paths resolve against the data path. Supported formats: png/jpg/bmp. inline bool saveScreenshot(const std::filesystem::path& path) { // Resolve relative paths up front so the deferred worker gets an absolute one. - std::filesystem::path resolved = - path.is_relative() ? std::filesystem::path(getDataPath(path.string())) : path; + std::filesystem::path resolved = getDataPath(path); // absolute passes through // Auto-create the parent directory (mirrors VideoRecorder). This is the // failure users want to catch synchronously (missing/unwritable folder). diff --git a/core/include/impl/stb_impl.cpp b/core/include/impl/stb_impl.cpp index 8cca4008..193a1da7 100644 --- a/core/include/impl/stb_impl.cpp +++ b/core/include/impl/stb_impl.cpp @@ -10,6 +10,12 @@ # pragma GCC diagnostic ignored "-Wunused-function" #endif +// Treat filenames as UTF-8 on Windows (stb converts to the wide API +// internally). No effect on other platforms. Callers pass UTF-8 via +// internal::pathToUtf8() — see tc/utils/tcFileIO.h. +#define STBI_WINDOWS_UTF8 +#define STBIW_WINDOWS_UTF8 + #define STB_IMAGE_IMPLEMENTATION #include "stb/stb_image.h" diff --git a/core/include/tc/3d/tcEnvironment.h b/core/include/tc/3d/tcEnvironment.h index cf8f2582..4714d156 100644 --- a/core/include/tc/3d/tcEnvironment.h +++ b/core/include/tc/3d/tcEnvironment.h @@ -55,7 +55,7 @@ class Environment { // Load an equirectangular HDR image from disk and bake all IBL maps. // Returns false if the file cannot be loaded. - bool loadFromHDR(const std::string& path) { + bool loadFromHDR(const fs::path& path) { Pixels src; if (!src.loadHDR(path)) { logError("Environment") << "loadHDR failed: " << path; diff --git a/core/include/tc/3d/tcIesProfile.h b/core/include/tc/3d/tcIesProfile.h index b52fbc59..e91e76c8 100644 --- a/core/include/tc/3d/tcIesProfile.h +++ b/core/include/tc/3d/tcIesProfile.h @@ -59,8 +59,8 @@ class IesProfile { // Load from file path (.ies). Path is resolved via getDataPath() // (relative to the project data/ directory). - bool load(const std::string& path) { - std::string resolved = getDataPath(path); + bool load(const fs::path& path) { + fs::path resolved = getDataPath(path); std::ifstream ifs(resolved); if (!ifs.is_open()) { logWarning() << "[IesProfile] cannot open: " << resolved; diff --git a/core/include/tc/app/tcHotReloadHost.h b/core/include/tc/app/tcHotReloadHost.h index dd9eb7d5..d3dd7429 100644 --- a/core/include/tc/app/tcHotReloadHost.h +++ b/core/include/tc/app/tcHotReloadHost.h @@ -207,7 +207,7 @@ inline string findCMake(const string& buildDir = "") { // On Windows, PATH may resolve to an older VS cmake that doesn't // know the generator used by the current build (e.g. "Visual Studio 18 2026"). if (!buildDir.empty()) { - string cachePath = buildDir + "/CMakeCache.txt"; + fs::path cachePath = fs::path(buildDir) / "CMakeCache.txt"; if (fs::exists(cachePath)) { ifstream cache(cachePath); string line; diff --git a/core/include/tc/graphics/tcFont.h b/core/include/tc/graphics/tcFont.h index 45af5312..da3f16f3 100644 --- a/core/include/tc/graphics/tcFont.h +++ b/core/include/tc/graphics/tcFont.h @@ -191,8 +191,9 @@ class FontAtlasManager { bool setup(const std::string& fontPath, int fontSize) { cleanup(); - // Load font file - std::ifstream file(fontPath, std::ios::binary | std::ios::ate); + // Load font file (fontPath is UTF-8 — convert so non-ASCII paths + // survive on Windows) + std::ifstream file(internal::utf8ToPath(fontPath), std::ios::binary | std::ios::ate); if (!file) { logError() << "FontAtlasManager: failed to open " << fontPath; return false; @@ -884,7 +885,7 @@ class Font { // - A system font name (PostScript or family name) — resolved via // tc::systemFontPath when the path doesn't exist on disk. Lets users // write `font.load("HiraginoSans-W3", 24)` cross-platform. - bool load(const std::string& nameOrPath, int size) { + bool load(const fs::path& nameOrPath, int size) { // Render glyphs at physical pixel size for sharp text on HiDPI displays. // All metrics/drawing are scaled back to logical coordinates. dpiScale_ = sapp_dpi_scale(); @@ -896,16 +897,20 @@ class Font { initResources(); } - // Resolve input to a concrete path (file / URL). - std::string actualPath = nameOrPath; - if (!isUrl(nameOrPath)) { + // Resolve input to a concrete path (file / URL). A font NAME + // ("HiraginoSans-W3") is a valid relative fs::path, so both spellings + // arrive here; the UTF-8 string form is what cache keys and the + // system-font lookup use. + std::string nameStr = internal::pathToUtf8(nameOrPath); + std::string actualPath = nameStr; + if (!isUrl(nameStr)) { std::ifstream test(nameOrPath, std::ios::binary); if (!test.good()) { // Not a usable file path — try as a system font name. - std::string resolved = systemFontPath(nameOrPath); + std::string resolved = systemFontPath(nameStr); if (!resolved.empty()) { actualPath = resolved; - logNotice("Font") << "Resolved \"" << nameOrPath + logNotice("Font") << "Resolved \"" << nameStr << "\" → " << resolved; } // If resolution failed, fall through with the original input so diff --git a/core/include/tc/graphics/tcImage.h b/core/include/tc/graphics/tcImage.h index 3f182522..7762bed8 100644 --- a/core/include/tc/graphics/tcImage.h +++ b/core/include/tc/graphics/tcImage.h @@ -70,7 +70,7 @@ class Image : public HasTexture { bool load(const fs::path& path, bool mipmaps = false) { clear(); - fs::path resolved = path.is_absolute() ? path : fs::path(getDataPath(path.string())); + fs::path resolved = getDataPath(path); // absolute paths pass through if (!pixels_.load(resolved)) { return false; } diff --git a/core/include/tc/graphics/tcPixels.cpp b/core/include/tc/graphics/tcPixels.cpp index 04c5a196..62b41d8d 100644 --- a/core/include/tc/graphics/tcPixels.cpp +++ b/core/include/tc/graphics/tcPixels.cpp @@ -12,11 +12,12 @@ bool Pixels::save(const fs::path& path) const { // Convert relative paths to data path fs::path savePath = path; if (path.is_relative()) { - savePath = getDataPath(path.string()); + savePath = getDataPath(path); } auto ext = savePath.extension().string(); - auto pathStr = savePath.string(); + // UTF-8 for stb (STBIW_WINDOWS_UTF8 makes stb wide-open it on Windows) + auto pathStr = internal::pathToUtf8(savePath); int result = 0; if (ext == ".png" || ext == ".PNG") { diff --git a/core/include/tc/graphics/tcPixels.h b/core/include/tc/graphics/tcPixels.h index 3fe3f71d..0a376b22 100644 --- a/core/include/tc/graphics/tcPixels.h +++ b/core/include/tc/graphics/tcPixels.h @@ -9,6 +9,7 @@ #include #include "stb/stb_image.h" #include "stb/stb_image_write.h" +#include "tc/utils/tcFileIO.h" // internal::pathToUtf8 namespace trussc { @@ -215,7 +216,7 @@ class Pixels { clear(); int w, h, channels; - unsigned char* loaded = stbi_load(path.string().c_str(), &w, &h, &channels, 4); + unsigned char* loaded = stbi_load(internal::pathToUtf8(path).c_str(), &w, &h, &channels, 4); if (!loaded) { // Fallback to platform-specific loader (ImageIO on macOS) return loadPlatform(path); @@ -242,7 +243,7 @@ class Pixels { clear(); int w, h, channels; - float* loaded = stbi_loadf(path.string().c_str(), &w, &h, &channels, 3); + float* loaded = stbi_loadf(internal::pathToUtf8(path).c_str(), &w, &h, &channels, 3); if (!loaded) { return false; } diff --git a/core/include/tc/sound/tcAudio_impl.cpp b/core/include/tc/sound/tcAudio_impl.cpp index e09a6121..dba5339b 100644 --- a/core/include/tc/sound/tcAudio_impl.cpp +++ b/core/include/tc/sound/tcAudio_impl.cpp @@ -39,6 +39,20 @@ namespace trussc { +namespace { +// Open a ma_decoder from a path. Wide entry point on Windows so non-ASCII +// paths survive (same helper as tcSound_impl.cpp). +ma_result maDecoderInitPathA(const fs::path& path, + const ma_decoder_config* cfg, ma_decoder* dec) { +#ifdef _WIN32 + return ma_decoder_init_file_w(path.c_str(), cfg, dec); +#else + return ma_decoder_init_file(path.c_str(), cfg, dec); +#endif +} +} // namespace + + // ============================================================================= // Streaming audio playback // @@ -260,19 +274,16 @@ class StreamWorker { // query duration/channels/sampleRate. Per-voice decoders are opened // lazily by AudioEngine::play(). // --------------------------------------------------------------------------- -bool SoundStream::loadStream(const std::string& path, int maxPolyphony) { +bool SoundStream::loadStream(const fs::path& path, int maxPolyphony) { if (maxPolyphony < 1) maxPolyphony = 1; // Detect format by extension. We don't go through stb_vorbis here — // OGG support for streaming would need a separate code path. WAV / // MP3 / FLAC are routed through ma_decoder, which handles all three // with the same API. - std::string ext; - auto dot = path.find_last_of('.'); - if (dot != std::string::npos) { - ext = path.substr(dot + 1); - for (auto& c : ext) c = (char)std::tolower((unsigned char)c); - } + std::string ext = path.extension().string(); + if (!ext.empty() && ext[0] == '.') ext.erase(0, 1); + for (auto& c : ext) c = (char)std::tolower((unsigned char)c); ma_encoding_format fmt = ma_encoding_format_unknown; if (ext == "wav") fmt = ma_encoding_format_wav; @@ -300,9 +311,10 @@ bool SoundStream::loadStream(const std::string& path, int maxPolyphony) { StreamInstance::CHANNELS, AudioEngine::getInstance().getSampleRate()); cfg.encodingFormat = fmt; - ma_result r = ma_decoder_init_file(path.c_str(), &cfg, &probe); + ma_result r = maDecoderInitPathA(path, &cfg, &probe); if (r != MA_SUCCESS) { - printf("SoundStream: failed to open %s (result=%d)\n", path.c_str(), (int)r); + printf("SoundStream: failed to open %s (result=%d)\n", + internal::pathToUtf8(path).c_str(), (int)r); return false; } @@ -320,7 +332,7 @@ bool SoundStream::loadStream(const std::string& path, int maxPolyphony) { encodingFormatHint_ = (int)fmt; printf("SoundStream: ready %s (%d ch, %d Hz, %.2f s, maxPolyphony=%d)\n", - path.c_str(), channels, sampleRate, duration_, maxPolyphony); + internal::pathToUtf8(path).c_str(), channels, sampleRate, duration_, maxPolyphony); return true; } @@ -360,7 +372,7 @@ std::shared_ptr AudioEngine::play(std::shared_ptr sou ma_decoder_config cfg = ma_decoder_config_init( ma_format_f32, StreamInstance::CHANNELS, sampleRate_); cfg.encodingFormat = (ma_encoding_format)s->encodingFormatHint_; - ma_result r = ma_decoder_init_file(s->getPath().c_str(), &cfg, &stream->decoder); + ma_result r = maDecoderInitPathA(s->path_, &cfg, &stream->decoder); if (r != MA_SUCCESS) { printf("SoundStream: per-voice decoder init failed for %s (result=%d)\n", s->getPath().c_str(), (int)r); @@ -799,7 +811,7 @@ void AudioEngine::migrateVoicesToNewRate(int oldRate, int newRate) { ma_decoder_config cfg = ma_decoder_config_init( ma_format_f32, StreamInstance::CHANNELS, newRate); cfg.encodingFormat = (ma_encoding_format)src->encodingFormatHint_; - ma_result r = ma_decoder_init_file(src->getPath().c_str(), &cfg, &newStream->decoder); + ma_result r = maDecoderInitPathA(src->path_, &cfg, &newStream->decoder); if (r != MA_SUCCESS) { printf("AudioEngine: stream voice migration failed for %s (result=%d) — stopping voice\n", src->getPath().c_str(), (int)r); diff --git a/core/include/tc/sound/tcSound.h b/core/include/tc/sound/tcSound.h index 905715a1..324f0361 100644 --- a/core/include/tc/sound/tcSound.h +++ b/core/include/tc/sound/tcSound.h @@ -1,5 +1,6 @@ #pragma once #include "tc/utils/tcAnnotations.h" +#include "tc/utils/tcFileIO.h" // fs alias + path boundary helpers // ============================================================================= // TrussC Sound @@ -84,15 +85,15 @@ class SoundBuffer : public SoundSource { // File-based decoders (implemented in tcSound_impl.cpp). // WAV / MP3 / FLAC go through ma_decoder (miniaudio); OGG goes through // stb_vorbis directly because miniaudio does not bundle a Vorbis decoder. - bool loadOgg(const std::string& path); - bool loadWav(const std::string& path); - bool loadMp3(const std::string& path); - bool loadFlac(const std::string& path); + bool loadOgg(const fs::path& path); + bool loadWav(const fs::path& path); + bool loadMp3(const fs::path& path); + bool loadFlac(const fs::path& path); // Auto-detect entry point. Dispatches to the format-specific loader // based on the file's extension (.wav / .mp3 / .ogg / .flac / .aac / // .m4a — case-insensitive). - bool load(const std::string& path); + bool load(const fs::path& path); // Memory-based decoders. Format must be known (no extension to sniff). bool loadWavFromMemory(const void* data, size_t dataSize); @@ -102,7 +103,7 @@ class SoundBuffer : public SoundSource { // Load AAC/M4A file (platform-specific implementation) // Returns false on unsupported platforms - TC_PLATFORMS("macos,windows,linux,ios,web") bool loadAac(const std::string& path); + TC_PLATFORMS("macos,windows,linux,ios,web") bool loadAac(const fs::path& path); // Load AAC data from memory (platform-specific implementation) // Returns false on unsupported platforms @@ -408,15 +409,15 @@ class SoundStream : public SoundSource { // engine when play() is called. Returns false if the file can't be // opened or the format is unsupported. Format is detected from // extension (.wav .mp3 .flac .ogg — same as SoundBuffer::load). - bool loadStream(const std::string& path, int maxPolyphony = 1); + bool loadStream(const fs::path& path, int maxPolyphony = 1); float getDuration() const override { return duration_; } - const std::string& getPath() const { return path_; } + std::string getPath() const { return internal::pathToUtf8(path_); } int getMaxPolyphony() const { return maxPolyphony_; } private: - std::string path_; + fs::path path_; int maxPolyphony_ = 1; int encodingFormatHint_ = 0; // ma_encoding_format value, stored as int // to avoid pulling miniaudio.h into the header. @@ -1051,7 +1052,7 @@ class Sound { // can overlap. Default 1 = single-instance // (typical BGM); raise for cross-fade or // layered ambient tracks. - bool load(const std::string& path) { + bool load(const fs::path& path) { // Initialize AudioEngine (only once) AudioEngine::getInstance().init(); @@ -1059,7 +1060,8 @@ class Sound { auto buf = std::make_shared(); // Determine format by extension - std::string ext = path.substr(path.find_last_of('.') + 1); + std::string ext = path.extension().string(); + if (!ext.empty() && ext[0] == '.') ext.erase(0, 1); bool success = false; if (ext == "ogg" || ext == "OGG") { @@ -1097,7 +1099,7 @@ class Sound { // user apps portable we fall back to eager load() and log a warning so // the developer can branch explicitly with isStreaming() / #ifdef // __EMSCRIPTEN__ if they need to know. - TC_PLATFORMS("macos,windows,linux,android,ios") bool loadStream(const std::string& path, int maxPolyphony = 1) { + TC_PLATFORMS("macos,windows,linux,android,ios") bool loadStream(const fs::path& path, int maxPolyphony = 1) { AudioEngine::getInstance().init(); #ifdef __EMSCRIPTEN__ (void)maxPolyphony; diff --git a/core/include/tc/sound/tcSound_impl.cpp b/core/include/tc/sound/tcSound_impl.cpp index 8373a8fd..f4751f0c 100644 --- a/core/include/tc/sound/tcSound_impl.cpp +++ b/core/include/tc/sound/tcSound_impl.cpp @@ -90,21 +90,33 @@ ma_decoder_config makeFloat32Config(ma_encoding_format hint) { return cfg; } -bool decodeFileWithMiniaudio(const std::string& path, +// Open a ma_decoder from a path. Uses the wide-path entry point on Windows so +// non-ASCII paths survive (ma_fopen would take the narrow ACP route). +ma_result maDecoderInitPath(const fs::path& path, + const ma_decoder_config* cfg, ma_decoder* dec) { +#ifdef _WIN32 + return ma_decoder_init_file_w(path.c_str(), cfg, dec); +#else + return ma_decoder_init_file(path.c_str(), cfg, dec); +#endif +} + +bool decodeFileWithMiniaudio(const fs::path& path, ma_encoding_format hint, const char* label, SoundBuffer& out) { ma_decoder decoder; ma_decoder_config cfg = makeFloat32Config(hint); - ma_result result = ma_decoder_init_file(path.c_str(), &cfg, &decoder); + std::string pathStr = internal::pathToUtf8(path); + ma_result result = maDecoderInitPath(path, &cfg, &decoder); if (result != MA_SUCCESS) { printf("SoundBuffer: failed to open %s %s (result=%d)\n", - label, path.c_str(), (int)result); + label, pathStr.c_str(), (int)result); return false; } - if (!drainDecoder(decoder, out, path.c_str())) return false; + if (!drainDecoder(decoder, out, pathStr.c_str())) return false; printf("SoundBuffer: loaded %s %s (%d ch, %d Hz, %zu samples)\n", - label, path.c_str(), out.channels, out.sampleRate, out.numSamples); + label, pathStr.c_str(), out.channels, out.sampleRate, out.numSamples); return true; } @@ -132,11 +144,20 @@ bool decodeMemoryWithMiniaudio(const void* data, size_t dataSize, // OGG Vorbis: stb_vorbis (miniaudio does not bundle a Vorbis decoder) // ----------------------------------------------------------------------------- -bool SoundBuffer::loadOgg(const std::string& path) { +bool SoundBuffer::loadOgg(const fs::path& path) { + std::string pathStr = internal::pathToUtf8(path); + // stb_vorbis has no UTF-8 filename mode on Windows — open the FILE* + // ourselves (wide API) and hand it over (close_handle_on_close=TRUE). + FILE* f = internal::openFile(path, "rb"); + if (!f) { + printf("SoundBuffer: failed to open %s\n", pathStr.c_str()); + return false; + } int error = 0; - stb_vorbis* vorbis = stb_vorbis_open_filename(path.c_str(), &error, nullptr); + stb_vorbis* vorbis = stb_vorbis_open_file(f, 1, &error, nullptr); if (!vorbis) { - printf("SoundBuffer: failed to open %s (error=%d)\n", path.c_str(), error); + fclose(f); + printf("SoundBuffer: failed to open %s (error=%d)\n", pathStr.c_str(), error); return false; } @@ -153,7 +174,7 @@ bool SoundBuffer::loadOgg(const std::string& path) { stb_vorbis_close(vorbis); printf("SoundBuffer: loaded %s (%d ch, %d Hz, %zu samples)\n", - path.c_str(), channels, sampleRate, numSamples); + pathStr.c_str(), channels, sampleRate, numSamples); return decoded > 0; } @@ -162,15 +183,15 @@ bool SoundBuffer::loadOgg(const std::string& path) { // WAV / MP3 / FLAC: routed through ma_decoder // ----------------------------------------------------------------------------- -bool SoundBuffer::loadWav(const std::string& path) { +bool SoundBuffer::loadWav(const fs::path& path) { return decodeFileWithMiniaudio(path, ma_encoding_format_wav, "WAV", *this); } -bool SoundBuffer::loadMp3(const std::string& path) { +bool SoundBuffer::loadMp3(const fs::path& path) { return decodeFileWithMiniaudio(path, ma_encoding_format_mp3, "MP3", *this); } -bool SoundBuffer::loadFlac(const std::string& path) { +bool SoundBuffer::loadFlac(const fs::path& path) { return decodeFileWithMiniaudio(path, ma_encoding_format_flac, "FLAC", *this); } @@ -221,10 +242,10 @@ bool SoundBuffer::loadOggFromMemory(const void* data, size_t dataSize) { // can use it directly. // ----------------------------------------------------------------------------- -bool SoundBuffer::load(const std::string& path) { +bool SoundBuffer::load(const fs::path& path) { // Lowercase the extension once - const auto dot = path.find_last_of('.'); - std::string ext = (dot == std::string::npos) ? "" : path.substr(dot + 1); + std::string ext = path.extension().string(); + if (!ext.empty() && ext[0] == '.') ext.erase(0, 1); for (auto& c : ext) c = (char)std::tolower((unsigned char)c); if (ext == "wav") return loadWav(path); @@ -234,7 +255,7 @@ bool SoundBuffer::load(const std::string& path) { if (ext == "aac" || ext == "m4a") return loadAac(path); printf("SoundBuffer: unsupported extension '.%s' for %s\n", - ext.c_str(), path.c_str()); + ext.c_str(), internal::pathToUtf8(path).c_str()); return false; } diff --git a/core/include/tc/utils/tcFile.h b/core/include/tc/utils/tcFile.h index 01548e9c..69059af3 100644 --- a/core/include/tc/utils/tcFile.h +++ b/core/include/tc/utils/tcFile.h @@ -18,18 +18,18 @@ namespace trussc { // ============================================================================= // Get filename from path: "dir/test.txt" -> "test.txt" -inline std::string getFileName(const std::string& path) { - return std::filesystem::path(path).filename().string(); +inline std::string getFileName(const fs::path& path) { + return path.filename().string(); } // Get filename without extension: "dir/test.txt" -> "test" -inline std::string getBaseName(const std::string& path) { - return std::filesystem::path(path).stem().string(); +inline std::string getBaseName(const fs::path& path) { + return path.stem().string(); } // Get file extension without dot: "dir/test.txt" -> "txt" -inline std::string getFileExtension(const std::string& path) { - std::string ext = std::filesystem::path(path).extension().string(); +inline std::string getFileExtension(const fs::path& path) { + std::string ext = path.extension().string(); if (!ext.empty() && ext[0] == '.') { ext = ext.substr(1); } @@ -37,17 +37,17 @@ inline std::string getFileExtension(const std::string& path) { } // Get parent directory: "dir/test.txt" -> "dir" -inline std::string getParentDirectory(const std::string& path) { - return std::filesystem::path(path).parent_path().string(); +inline std::string getParentDirectory(const fs::path& path) { + return path.parent_path().string(); } // Join paths: ("dir", "file.txt") -> "dir/file.txt" -inline std::string joinPath(const std::string& dir, const std::string& file) { - return (std::filesystem::path(dir) / file).string(); +inline std::string joinPath(const fs::path& dir, const fs::path& file) { + return (dir / file).string(); } // Get absolute path -inline std::string getAbsolutePath(const std::string& path) { +inline std::string getAbsolutePath(const fs::path& path) { return std::filesystem::absolute(path).string(); } @@ -56,21 +56,21 @@ inline std::string getAbsolutePath(const std::string& path) { // ============================================================================= // Check if file exists -inline bool fileExists(const std::string& path) { - std::string fullPath = getDataPath(path); +inline bool fileExists(const fs::path& path) { + fs::path fullPath = getDataPath(path); return std::filesystem::exists(fullPath) && std::filesystem::is_regular_file(fullPath); } // Check if directory exists -inline bool directoryExists(const std::string& path) { - std::string fullPath = getDataPath(path); +inline bool directoryExists(const fs::path& path) { + fs::path fullPath = getDataPath(path); return std::filesystem::exists(fullPath) && std::filesystem::is_directory(fullPath); } // Create directory (and parent directories if needed) // Returns true if directory was created or already exists -inline bool createDirectory(const std::string& path) { - std::string fullPath = getDataPath(path); +inline bool createDirectory(const fs::path& path) { + fs::path fullPath = getDataPath(path); try { if (std::filesystem::exists(fullPath)) { return std::filesystem::is_directory(fullPath); @@ -84,9 +84,9 @@ inline bool createDirectory(const std::string& path) { // List files and directories in a directory // Returns vector of filenames (not full paths) -inline std::vector listDirectory(const std::string& path) { +inline std::vector listDirectory(const fs::path& path) { std::vector result; - std::string fullPath = getDataPath(path); + fs::path fullPath = getDataPath(path); if (!std::filesystem::exists(fullPath) || !std::filesystem::is_directory(fullPath)) { return result; @@ -104,8 +104,8 @@ inline std::vector listDirectory(const std::string& path) { } // Remove file -inline bool removeFile(const std::string& path) { - std::string fullPath = getDataPath(path); +inline bool removeFile(const fs::path& path) { + fs::path fullPath = getDataPath(path); try { return std::filesystem::remove(fullPath); } catch (const std::exception& e) { @@ -115,8 +115,8 @@ inline bool removeFile(const std::string& path) { } // Get file size in bytes (-1 on error) -inline int64_t getFileSize(const std::string& path) { - std::string fullPath = getDataPath(path); +inline int64_t getFileSize(const fs::path& path) { + fs::path fullPath = getDataPath(path); try { if (!std::filesystem::exists(fullPath)) return -1; return static_cast(std::filesystem::file_size(fullPath)); @@ -130,8 +130,8 @@ inline int64_t getFileSize(const std::string& path) { // ============================================================================= // Load entire text file into string -inline std::string loadTextFile(const std::string& path) { - std::string fullPath = getDataPath(path); +inline std::string loadTextFile(const fs::path& path) { + fs::path fullPath = getDataPath(path); std::ifstream file(fullPath); if (!file.is_open()) { logError() << "Cannot open file: " << path; @@ -144,8 +144,8 @@ inline std::string loadTextFile(const std::string& path) { } // Save string to text file -inline bool saveTextFile(const std::string& path, const std::string& content) { - std::string fullPath = getDataPath(path); +inline bool saveTextFile(const fs::path& path, const std::string& content) { + fs::path fullPath = getDataPath(path); std::ofstream file(fullPath); if (!file.is_open()) { logError() << "Cannot create file: " << path; @@ -157,8 +157,8 @@ inline bool saveTextFile(const std::string& path, const std::string& content) { } // Append string to text file -inline bool appendToFile(const std::string& path, const std::string& content) { - std::string fullPath = getDataPath(path); +inline bool appendToFile(const fs::path& path, const std::string& content) { + fs::path fullPath = getDataPath(path); std::ofstream file(fullPath, std::ios::app); if (!file.is_open()) { logError() << "Cannot open file for append: " << path; @@ -196,9 +196,9 @@ class FileWriter { } // Open file (append = true to append to existing file) - bool open(const std::string& path, bool append = false) { + bool open(const fs::path& path, bool append = false) { close(); - std::string fullPath = getDataPath(path); + fs::path fullPath = getDataPath(path); auto mode = std::ios::out | std::ios::binary; if (append) mode |= std::ios::app; @@ -304,9 +304,9 @@ class FileReader { } // Open file for reading - bool open(const std::string& path) { + bool open(const fs::path& path) { close(); - std::string fullPath = getDataPath(path); + fs::path fullPath = getDataPath(path); file_.open(fullPath, std::ios::in | std::ios::binary); if (!file_.is_open()) { logError() << "FileReader: Cannot open file: " << path; diff --git a/core/include/tc/utils/tcJson.h b/core/include/tc/utils/tcJson.h index 1d70c3c4..4b8459ee 100644 --- a/core/include/tc/utils/tcJson.h +++ b/core/include/tc/utils/tcJson.h @@ -20,8 +20,8 @@ using Json = nlohmann::json; // JSON file loading // Relative paths are resolved via getDataPath (like oF) // --------------------------------------------------------------------------- -inline Json loadJson(const std::string& path) { - std::string fullPath = getDataPath(path); +inline Json loadJson(const fs::path& path) { + fs::path fullPath = getDataPath(path); std::ifstream file(fullPath); if (!file.is_open()) { logError() << "Cannot open JSON file: " << path; @@ -42,8 +42,8 @@ inline Json loadJson(const std::string& path) { // JSON file writing // Relative paths are resolved via getDataPath (like oF) // --------------------------------------------------------------------------- -inline bool saveJson(const Json& j, const std::string& path, int indent = 2) { - std::string fullPath = getDataPath(path); +inline bool saveJson(const Json& j, const fs::path& path, int indent = 2) { + fs::path fullPath = getDataPath(path); std::ofstream file(fullPath); if (!file.is_open()) { logError() << "Cannot create JSON file: " << path; diff --git a/core/include/tc/utils/tcLog.h b/core/include/tc/utils/tcLog.h index 24172403..c450d114 100644 --- a/core/include/tc/utils/tcLog.h +++ b/core/include/tc/utils/tcLog.h @@ -16,6 +16,7 @@ // Uses Event system #include "../events/tcEvent.h" #include "../events/tcEventListener.h" +#include "tcFileIO.h" // fs alias + pathToUtf8 namespace trussc { @@ -133,16 +134,16 @@ class Logger { // === File settings === - bool setLogFile(const std::string& path) { + bool setLogFile(const fs::path& path) { closeFile(); fileStream_.open(path, std::ios::app); if (!fileStream_.is_open()) { - log(LogLevel::Error, "Failed to open log file: " + path); + log(LogLevel::Error, "Failed to open log file: " + internal::pathToUtf8(path)); return false; } - filePath_ = path; + filePath_ = internal::pathToUtf8(path); // Register file listener fileListener_ = onLog.listen([this](LogEventArgs& e) { @@ -214,7 +215,7 @@ inline void setFileLogLevel(LogLevel level) { getLogger().setFileLogLevel(level); } -inline bool setLogFile(const std::string& path) { +inline bool setLogFile(const fs::path& path) { return getLogger().setLogFile(path); } @@ -236,7 +237,7 @@ inline void tcSetConsoleLogLevel(LogLevel level) { setConsoleLogLevel(level); } inline void tcSetFileLogLevel(LogLevel level) { setFileLogLevel(level); } [[deprecated("Use setLogFile() instead. Will be removed in v1.0.0")]] -inline bool tcSetLogFile(const std::string& path) { return setLogFile(path); } +inline bool tcSetLogFile(const fs::path& path) { return setLogFile(path); } [[deprecated("Use closeLogFile() instead. Will be removed in v1.0.0")]] inline void tcCloseLogFile() { closeLogFile(); } diff --git a/core/include/tc/utils/tcStandardTools.h b/core/include/tc/utils/tcStandardTools.h index 1eb38c59..ed4a37d4 100644 --- a/core/include/tc/utils/tcStandardTools.h +++ b/core/include/tc/utils/tcStandardTools.h @@ -112,7 +112,9 @@ inline void registerInspectionTools() { tool("save_screenshot", "Save screenshot to file") .arg("path", "File path") .bind([](std::string path) { - if (trussc::saveScreenshot(path)) { + // JSON strings are UTF-8 — convert explicitly (fs::path(string) + // would interpret them in the ACP on Windows) + if (trussc::saveScreenshot(trussc::internal::utf8ToPath(path))) { return json{{"status", "ok"}, {"path", path}}; } else { return json{{"status", "error"}, {"message", "Failed to save screenshot"}}; @@ -157,7 +159,7 @@ inline void registerInspectionTools() { path = "recording-" + trussc::getTimestampString("%Y-%m-%d-%H-%M-%S") + (isProRes ? ".mov" : ".mp4"); } - bool ok = trussc::startRecording(path, settings); + bool ok = trussc::startRecording(trussc::internal::utf8ToPath(path), settings); json r{{"status", ok ? "ok" : "error"}, {"path", trussc::recordingPath()}, {"fps", settings.fps}, diff --git a/core/include/tc/utils/tcUtils.h b/core/include/tc/utils/tcUtils.h index e5d0e306..6a622921 100644 --- a/core/include/tc/utils/tcUtils.h +++ b/core/include/tc/utils/tcUtils.h @@ -11,6 +11,7 @@ #include #include #include +#include "tc/utils/tcFileIO.h" // fs alias + path boundary helpers #include "../sound/tcSound.h" // Forward declaration for tcPlatform.h (avoid circular include) @@ -30,50 +31,40 @@ namespace internal { // --------------------------------------------------------------------------- namespace internal { - // Default is "data/" relative to executable directory - // On macOS, executable is in xxx.app/Contents/MacOS/, so "../../../data/" points to bin/data/ + // Default is "data" relative to executable directory + // On macOS, executable is in xxx.app/Contents/MacOS/, so "../../../data" points to bin/data #ifdef __APPLE__ - inline std::string dataPathRoot = "../../../data/"; + inline fs::path dataPathRoot = "../../../data"; #else - inline std::string dataPathRoot = "data/"; + inline fs::path dataPathRoot = "data"; #endif - inline bool dataPathRootIsAbsolute = false; } // Set the data path root -// If relative path, resolved relative to executable directory -// If absolute path (starts with /), used as-is -inline void setDataPathRoot(const std::string& path) { +// If relative, resolved relative to executable directory +// If absolute, used as-is (fs::path::is_absolute — handles "C:/..." on Windows too) +inline void setDataPathRoot(const fs::path& path) { internal::dataPathRoot = path; - // Add trailing slash if missing - if (!internal::dataPathRoot.empty() && internal::dataPathRoot.back() != '/') { - internal::dataPathRoot += '/'; - } - // Record whether path is absolute - internal::dataPathRootIsAbsolute = (!path.empty() && path[0] == '/'); } // Get the data path root -inline std::string getDataPathRoot() { +inline fs::path getDataPathRoot() { return internal::dataPathRoot; } // Get data path for a filename -// - If filename is absolute, return as-is +// - If filename is absolute, return as-is (like oF) // - Otherwise, resolved relative to executable directory + dataPathRoot -inline std::string getDataPath(const std::string& filename) { - // If filename is absolute, return as-is (like oF) - // Uses std::filesystem for cross-platform support (Unix: /path, Windows: C:\path) - if (!filename.empty() && std::filesystem::path(filename).is_absolute()) { +inline fs::path getDataPath(const fs::path& filename) { + if (!filename.empty() && filename.is_absolute()) { return filename; } - if (internal::dataPathRootIsAbsolute) { - // Absolute dataPathRoot: use as-is - return internal::dataPathRoot + filename; + if (internal::dataPathRoot.is_absolute()) { + return internal::dataPathRoot / filename; } else { - // Relative path: resolve relative to executable directory - return getExecutableDir() + internal::dataPathRoot + filename; + // Relative root: resolve relative to executable directory + return fs::path(getExecutableDir()) / internal::dataPathRoot / filename; } } diff --git a/core/include/tc/utils/tcXml.h b/core/include/tc/utils/tcXml.h index 82bcc8fc..671ef879 100644 --- a/core/include/tc/utils/tcXml.h +++ b/core/include/tc/utils/tcXml.h @@ -27,8 +27,10 @@ class Xml { Xml() = default; // Load from file (relative paths resolved via getDataPath, like loadJson) - bool load(const std::string& path) { - std::string fullPath = getDataPath(path); + bool load(const fs::path& path) { + fs::path fullPath = getDataPath(path); + // fullPath.c_str() is wchar_t* on Windows — pugixml has a wide + // load_file overload there, so non-ASCII paths survive. XmlParseResult result = doc_.load_file(fullPath.c_str()); if (!result) { logError() << "XML load error: " << path @@ -52,8 +54,9 @@ class Xml { } // Save to file (relative paths resolved via getDataPath, like saveJson) - bool save(const std::string& path, const std::string& indent = " ") const { - std::string fullPath = getDataPath(path); + bool save(const fs::path& path, const std::string& indent = " ") const { + fs::path fullPath = getDataPath(path); + // Wide save_file overload on Windows (see load) bool success = doc_.save_file(fullPath.c_str(), indent.c_str()); if (!success) { logError() << "XML write error: " << path; @@ -114,7 +117,7 @@ class Xml { // --------------------------------------------------------------------------- // Load XML from file -inline Xml loadXml(const std::string& path) { +inline Xml loadXml(const fs::path& path) { Xml xml; xml.load(path); return xml; diff --git a/core/include/tc/video/tcVideoPlayer.h b/core/include/tc/video/tcVideoPlayer.h index 9956b738..ed44176b 100644 --- a/core/include/tc/video/tcVideoPlayer.h +++ b/core/include/tc/video/tcVideoPlayer.h @@ -53,13 +53,13 @@ class TC_PLATFORMS("macos,windows,linux,ios,web") VideoPlayer : public VideoPlay // Load / Close // ========================================================================= - bool load(const std::string& path) override { + bool load(const fs::path& path) override { if (initialized_) { close(); } // Resolve relative paths via getDataPath - std::string resolvedPath = getDataPath(path); + fs::path resolvedPath = getDataPath(path); // Platform-specific load if (!loadPlatform(resolvedPath)) { @@ -279,8 +279,8 @@ class TC_PLATFORMS("macos,windows,linux,ios,web") VideoPlayer : public VideoPlay /// Path of the currently loaded video file (empty string when not loaded). /// This is the resolved path (relative paths passed to load() are resolved - /// via getDataPath). - const std::string& getPath() const { return sourcePath_; } + /// via getDataPath), as UTF-8. + std::string getPath() const { return internal::pathToUtf8(sourcePath_); } protected: // ------------------------------------------------------------------------- @@ -345,7 +345,7 @@ class TC_PLATFORMS("macos,windows,linux,ios,web") VideoPlayer : public VideoPlay // Resolved path of the currently loaded video (empty when not loaded). // Used by getPath() and the instance-level frame-extraction overloads. - std::string sourcePath_; + fs::path sourcePath_; // ------------------------------------------------------------------------- // Internal methods @@ -397,7 +397,7 @@ class TC_PLATFORMS("macos,windows,linux,ios,web") VideoPlayer : public VideoPlay // ------------------------------------------------------------------------- // Platform-specific methods (implemented in tcVideoPlayer_mac.mm, etc.) // ------------------------------------------------------------------------- - bool loadPlatform(const std::string& path); + bool loadPlatform(const fs::path& path); void closePlatform(); void playPlatform(); void stopPlatform(); @@ -454,7 +454,7 @@ class TC_PLATFORMS("macos,windows,linux,ios,web") VideoPlayer : public VideoPlay /// @param timeSec Time in seconds to extract from (default 1.0) /// @param outDuration If non-null, receives video duration in seconds /// @return true on success - static bool extractFrame(const std::string& path, Pixels& outPixels, + static bool extractFrame(const fs::path& path, Pixels& outPixels, float timeSec = 1.0f, float* outDuration = nullptr) { return extractFramePlatform(path, outPixels, timeSec, outDuration); } @@ -466,7 +466,7 @@ class TC_PLATFORMS("macos,windows,linux,ios,web") VideoPlayer : public VideoPlay /// @param timeSec Upper-bound time in seconds (default 1.0) /// @param outDuration If non-null, receives video duration in seconds /// @return true on success - static bool extractKeyFrame(const std::string& path, Pixels& outPixels, + static bool extractKeyFrame(const fs::path& path, Pixels& outPixels, float timeSec = 1.0f, float* outDuration = nullptr) { return extractKeyFramePlatform(path, outPixels, timeSec, outDuration); } @@ -488,9 +488,9 @@ class TC_PLATFORMS("macos,windows,linux,ios,web") VideoPlayer : public VideoPlay } private: - static bool extractFramePlatform(const std::string& path, Pixels& outPixels, + static bool extractFramePlatform(const fs::path& path, Pixels& outPixels, float timeSec, float* outDuration); - static bool extractKeyFramePlatform(const std::string& path, Pixels& outPixels, + static bool extractKeyFramePlatform(const fs::path& path, Pixels& outPixels, float timeSec, float* outDuration); // Allow platform implementations to access internals diff --git a/core/include/tc/video/tcVideoPlayerBase.h b/core/include/tc/video/tcVideoPlayerBase.h index 52e58753..edc3dea9 100644 --- a/core/include/tc/video/tcVideoPlayerBase.h +++ b/core/include/tc/video/tcVideoPlayerBase.h @@ -8,6 +8,7 @@ #include #include #include +#include #include "tc/gpu/tcHasTexture.h" namespace trussc { @@ -28,7 +29,7 @@ class VideoPlayerBase : public HasTexture { // Load / Close (must be implemented by derived class) // ========================================================================= - virtual bool load(const std::string& path) = 0; + virtual bool load(const fs::path& path) = 0; virtual void close() = 0; virtual bool isLoaded() const { return initialized_; } diff --git a/core/include/tc/video/tcVideoRecorder.h b/core/include/tc/video/tcVideoRecorder.h index 8c44a919..5c625d32 100644 --- a/core/include/tc/video/tcVideoRecorder.h +++ b/core/include/tc/video/tcVideoRecorder.h @@ -108,7 +108,7 @@ class TC_PLATFORMS("macos,windows,linux,android,ios") VideoWriter { // Open the encoder. `path` is resolved via getDataPath() when relative; the // parent directory is created if missing. Returns false if the encoder (or // the requested codec on this platform) could not be created. - bool open(const std::string& path, int width, int height, + bool open(const fs::path& path, int width, int height, const VideoRecordSettings& settings = {}) { close(); if (width <= 0 || height <= 0) { @@ -123,8 +123,7 @@ class TC_PLATFORMS("macos,windows,linux,android,ios") VideoWriter { path_ = getDataPath(path); { // native encoders won't create intermediate directories std::error_code ec; - std::filesystem::path parent = - std::filesystem::path(path_).parent_path(); + fs::path parent = path_.parent_path(); if (!parent.empty()) std::filesystem::create_directories(parent, ec); } width_ = width; @@ -134,7 +133,8 @@ class TC_PLATFORMS("macos,windows,linux,android,ios") VideoWriter { frameCount_ = 0; scratch_.resize((size_t)width_ * height_ * 4); - if (!openPlatform(path_, width_, height_, fps_, settings_)) { + // Platform encoders take UTF-8 (converted to native inside) + if (!openPlatform(internal::pathToUtf8(path_), width_, height_, fps_, settings_)) { logError("VideoWriter") << "failed to open " << videoCodecName(settings_.codec) << " encoder for " << path_; @@ -162,7 +162,7 @@ class TC_PLATFORMS("macos,windows,linux,android,ios") VideoWriter { int getWidth() const { return width_; } int getHeight() const { return height_; } float getFps() const { return fps_; } - const std::string& getPath() const { return path_; } + std::string getPath() const { return internal::pathToUtf8(path_); } const VideoRecordSettings& getSettings() const { return settings_; } // Append one frame at the fixed-rate clock (frameIndex / fps) — deterministic @@ -266,7 +266,7 @@ class TC_PLATFORMS("macos,windows,linux,android,ios") VideoWriter { #endif internal::VideoWriterPlatformData* platform_ = nullptr; - std::string path_; + fs::path path_; std::vector scratch_; int width_ = 0; int height_ = 0; @@ -288,13 +288,13 @@ class TC_PLATFORMS("macos,windows,linux,android,ios") ScreenRecorder { // Record the whole window (swapchain) — the same fully-composited image // get_screenshot returns. Size is taken from the framebuffer. - bool start(const std::string& path, const VideoRecordSettings& settings = {}) { + bool start(const fs::path& path, const VideoRecordSettings& settings = {}) { return startSource(Source::Swapchain, nullptr, sapp_width(), sapp_height(), path, settings); } // Record an offscreen Fbo every frame (clean output, GUI-free). - bool start(const Fbo& fbo, const std::string& path, + bool start(const Fbo& fbo, const fs::path& path, const VideoRecordSettings& settings = {}) { return startSource(Source::Fbo, &fbo, fbo.getWidth(), fbo.getHeight(), path, settings); @@ -337,14 +337,14 @@ class TC_PLATFORMS("macos,windows,linux,android,ios") ScreenRecorder { bool isRecording() const { return writer_.isOpen(); } int getFrameCount() const { return writer_.getFrameCount(); } - const std::string& getPath() const { return writer_.getPath(); } + std::string getPath() const { return writer_.getPath(); } VideoWriter& writer() { return writer_; } // for advanced introspection private: enum class Source { None, Swapchain, Fbo }; bool startSource(Source src, const Fbo* fbo, int w, int h, - const std::string& path, + const fs::path& path, const VideoRecordSettings& settings) { stop(); if (!writer_.open(path, w, h, settings)) return false; @@ -459,7 +459,7 @@ namespace internal { // Start recording the whole window to a video file. `path` is required; // relative paths resolve via getDataPath(). Auto-finalizes on app exit. -TC_PLATFORMS("macos,windows,linux,android,ios") inline bool startRecording(const std::string& path, +TC_PLATFORMS("macos,windows,linux,android,ios") inline bool startRecording(const fs::path& path, const VideoRecordSettings& settings = {}) { return internal::globalScreenRecorder().start(path, settings); } @@ -475,6 +475,6 @@ TC_PLATFORMS("macos,windows,linux,android,ios") inline bool startRecording(const inline void stopRecording() { internal::globalScreenRecorder().stop(); } inline bool isRecording() { return internal::globalScreenRecorder().isRecording(); } inline int recordingFrameCount() { return internal::globalScreenRecorder().getFrameCount(); } -inline const std::string& recordingPath() { return internal::globalScreenRecorder().getPath(); } +inline std::string recordingPath() { return internal::globalScreenRecorder().getPath(); } } // namespace trussc diff --git a/core/platform/android/tcPlatform_android.cpp b/core/platform/android/tcPlatform_android.cpp index 7e57ace0..2c7e1eb2 100644 --- a/core/platform/android/tcPlatform_android.cpp +++ b/core/platform/android/tcPlatform_android.cpp @@ -258,7 +258,7 @@ bool captureWindow(Pixels& outPixels) { bool internal::captureWindowToFile(const std::filesystem::path& path) { if (path.is_relative()) { - return internal::captureWindowToFile(getDataPath(path.string())); + return internal::captureWindowToFile(getDataPath(path)); } Pixels pixels; if (!captureWindow(pixels)) { @@ -266,7 +266,7 @@ bool internal::captureWindowToFile(const std::filesystem::path& path) { } std::string ext = path.extension().string(); - std::string pathStr = path.string(); + std::string pathStr = internal::pathToUtf8(path); // UTF-8 for stb (STBIW_WINDOWS_UTF8) int width = pixels.getWidth(); int height = pixels.getHeight(); diff --git a/core/platform/android/tcSound_android.cpp b/core/platform/android/tcSound_android.cpp index 8a96e2c1..50c3454c 100644 --- a/core/platform/android/tcSound_android.cpp +++ b/core/platform/android/tcSound_android.cpp @@ -13,7 +13,7 @@ namespace trussc { -bool SoundBuffer::loadAac(const std::string& path) { +bool SoundBuffer::loadAac(const fs::path& path) { logWarning("SoundBuffer") << "AAC loading not yet implemented on Android"; return false; } diff --git a/core/platform/android/tcVideoPlayer_android.cpp b/core/platform/android/tcVideoPlayer_android.cpp index ec136485..2e2634b5 100644 --- a/core/platform/android/tcVideoPlayer_android.cpp +++ b/core/platform/android/tcVideoPlayer_android.cpp @@ -12,7 +12,7 @@ namespace trussc { -bool VideoPlayer::loadPlatform(const std::string& path) { +bool VideoPlayer::loadPlatform(const fs::path& path) { logWarning("VideoPlayer") << "Not yet implemented on Android"; return false; } @@ -46,13 +46,13 @@ int VideoPlayer::getAudioChannelsPlatform() const { return 0; } bool VideoPlayer::isUsingHwAccelPlatform() const { return false; } std::string VideoPlayer::getHwAccelNamePlatform() const { return "none"; } -bool VideoPlayer::extractFramePlatform(const std::string& path, Pixels& outPixels, +bool VideoPlayer::extractFramePlatform(const fs::path& path, Pixels& outPixels, float timeSec, float* outDuration) { (void)path; (void)outPixels; (void)timeSec; (void)outDuration; return false; } -bool VideoPlayer::extractKeyFramePlatform(const std::string& path, Pixels& outPixels, +bool VideoPlayer::extractKeyFramePlatform(const fs::path& path, Pixels& outPixels, float timeSec, float* outDuration) { (void)path; (void)outPixels; (void)timeSec; (void)outDuration; return false; diff --git a/core/platform/ios/tcPlatform_ios.mm b/core/platform/ios/tcPlatform_ios.mm index d8e979a9..41635980 100644 --- a/core/platform/ios/tcPlatform_ios.mm +++ b/core/platform/ios/tcPlatform_ios.mm @@ -227,7 +227,7 @@ bool captureWindow(Pixels& outPixels) { bool internal::captureWindowToFile(const std::filesystem::path& path) { if (path.is_relative()) { - return internal::captureWindowToFile(getDataPath(path.string())); + return internal::captureWindowToFile(getDataPath(path)); } Pixels pixels; if (!captureWindow(pixels)) { diff --git a/core/platform/ios/tcSound_ios.mm b/core/platform/ios/tcSound_ios.mm index 16b6d839..452149b7 100644 --- a/core/platform/ios/tcSound_ios.mm +++ b/core/platform/ios/tcSound_ios.mm @@ -48,13 +48,14 @@ static SInt64 MemoryAudioFile_GetSizeProc(void* inClientData) { // ----------------------------------------------------------------------------- // SoundBuffer::loadAac - macOS implementation (file-based) // ----------------------------------------------------------------------------- -bool SoundBuffer::loadAac(const std::string& path) { +bool SoundBuffer::loadAac(const fs::path& path) { // Convert path to URL - NSString* nsPath = [NSString stringWithUTF8String:path.c_str()]; + std::string pathStr = internal::pathToUtf8(path); + NSString* nsPath = [NSString stringWithUTF8String:pathStr.c_str()]; NSURL* fileURL = [NSURL fileURLWithPath:nsPath]; if (!fileURL) { - printf("SoundBuffer: invalid path: %s\n", path.c_str()); + printf("SoundBuffer: invalid path: %s\n", pathStr.c_str()); return false; } diff --git a/core/platform/ios/tcVideoPlayer_ios.mm b/core/platform/ios/tcVideoPlayer_ios.mm index 83a2b5fa..24aba61d 100644 --- a/core/platform/ios/tcVideoPlayer_ios.mm +++ b/core/platform/ios/tcVideoPlayer_ios.mm @@ -618,11 +618,11 @@ - (NSData*)extractAudioData { namespace trussc { -bool VideoPlayer::loadPlatform(const std::string& path) { +bool VideoPlayer::loadPlatform(const fs::path& path) { // Create Objective-C implementation TCVideoPlayerImpl* impl = [[TCVideoPlayerImpl alloc] init]; - NSString* nsPath = [NSString stringWithUTF8String:path.c_str()]; + NSString* nsPath = [NSString stringWithUTF8String:internal::pathToUtf8(path).c_str()]; if (![impl loadWithPath:nsPath]) { return false; diff --git a/core/platform/linux/tcPlatform_linux.cpp b/core/platform/linux/tcPlatform_linux.cpp index be0ac9ee..54af34d9 100644 --- a/core/platform/linux/tcPlatform_linux.cpp +++ b/core/platform/linux/tcPlatform_linux.cpp @@ -164,7 +164,7 @@ bool captureWindow(Pixels& outPixels) { bool internal::captureWindowToFile(const std::filesystem::path& path) { if (path.is_relative()) { - return internal::captureWindowToFile(getDataPath(path.string())); + return internal::captureWindowToFile(getDataPath(path)); } Pixels pixels; if (!captureWindow(pixels)) { @@ -173,7 +173,7 @@ bool internal::captureWindowToFile(const std::filesystem::path& path) { // Use stb_image_write to save std::string ext = path.extension().string(); - std::string pathStr = path.string(); + std::string pathStr = internal::pathToUtf8(path); // UTF-8 for stb (STBIW_WINDOWS_UTF8) int width = pixels.getWidth(); int height = pixels.getHeight(); diff --git a/core/platform/linux/tcSound_linux.cpp b/core/platform/linux/tcSound_linux.cpp index 54a08f0a..aeb17e41 100644 --- a/core/platform/linux/tcSound_linux.cpp +++ b/core/platform/linux/tcSound_linux.cpp @@ -251,9 +251,9 @@ class GstAacDecoder { size_t memoryPos_ = 0; }; -bool SoundBuffer::loadAac(const std::string& path) { +bool SoundBuffer::loadAac(const fs::path& path) { GstAacDecoder decoder; - return decoder.decodeFile(path, *this); + return decoder.decodeFile(internal::pathToUtf8(path), *this); } bool SoundBuffer::loadAacFromMemory(const void* data, size_t dataSize) { diff --git a/core/platform/linux/tcVideoPlayer_linux.cpp b/core/platform/linux/tcVideoPlayer_linux.cpp index 28b5dd64..2b580ec7 100644 --- a/core/platform/linux/tcVideoPlayer_linux.cpp +++ b/core/platform/linux/tcVideoPlayer_linux.cpp @@ -1212,10 +1212,11 @@ class NV12VideoShader : public Shader { namespace trussc { -bool VideoPlayer::loadPlatform(const std::string& path) { +bool VideoPlayer::loadPlatform(const fs::path& path) { auto impl = new TCVideoPlayerImpl(); - if (!impl->load(path, this)) { + // FFmpeg takes narrow UTF-8 (Linux native) + if (!impl->load(internal::pathToUtf8(path), this)) { delete impl; return false; } @@ -1539,18 +1540,19 @@ static bool tcv_extract_frame_linux(const std::string& path, Pixels& outPixels, return ok; } -bool VideoPlayer::extractFramePlatform(const std::string& path, Pixels& outPixels, +bool VideoPlayer::extractFramePlatform(const fs::path& path, Pixels& outPixels, float timeSec, float* outDuration) { - return tcv_extract_frame_linux(path, outPixels, timeSec, outDuration, /*exact=*/true); + return tcv_extract_frame_linux(internal::pathToUtf8(path), outPixels, timeSec, outDuration, /*exact=*/true); } -bool VideoPlayer::extractKeyFramePlatform(const std::string& path, Pixels& outPixels, +bool VideoPlayer::extractKeyFramePlatform(const fs::path& path, Pixels& outPixels, float timeSec, float* outDuration) { - if (tcv_extract_frame_linux(path, outPixels, timeSec, outDuration, /*exact=*/false)) { + std::string p = internal::pathToUtf8(path); + if (tcv_extract_frame_linux(p, outPixels, timeSec, outDuration, /*exact=*/false)) { return true; } // No keyframe reachable — fall back to an exact decode. - return tcv_extract_frame_linux(path, outPixels, timeSec, outDuration, /*exact=*/true); + return tcv_extract_frame_linux(p, outPixels, timeSec, outDuration, /*exact=*/true); } } // namespace trussc diff --git a/core/platform/mac/tcPixels_mac.mm b/core/platform/mac/tcPixels_mac.mm index 9a076712..e9e28b07 100644 --- a/core/platform/mac/tcPixels_mac.mm +++ b/core/platform/mac/tcPixels_mac.mm @@ -15,7 +15,7 @@ bool Pixels::loadPlatform(const fs::path& path) { @autoreleasepool { - NSString* nsPath = [NSString stringWithUTF8String:path.string().c_str()]; + NSString* nsPath = [NSString stringWithUTF8String:internal::pathToUtf8(path).c_str()]; NSURL* url = [NSURL fileURLWithPath:nsPath]; // Create image source diff --git a/core/platform/mac/tcPlatform_mac.mm b/core/platform/mac/tcPlatform_mac.mm index 8364eb01..9eeca2af 100644 --- a/core/platform/mac/tcPlatform_mac.mm +++ b/core/platform/mac/tcPlatform_mac.mm @@ -357,7 +357,7 @@ bool captureWindow(Pixels& outPixels) { bool internal::captureWindowToFile(const std::filesystem::path& path) { // Resolve relative paths if (path.is_relative()) { - return internal::captureWindowToFile(getDataPath(path.string())); + return internal::captureWindowToFile(getDataPath(path)); } // Capture to Pixels Pixels pixels; diff --git a/core/platform/mac/tcSound_mac.mm b/core/platform/mac/tcSound_mac.mm index 14446c44..11679aab 100644 --- a/core/platform/mac/tcSound_mac.mm +++ b/core/platform/mac/tcSound_mac.mm @@ -47,13 +47,14 @@ static SInt64 MemoryAudioFile_GetSizeProc(void* inClientData) { // ----------------------------------------------------------------------------- // SoundBuffer::loadAac - macOS implementation (file-based) // ----------------------------------------------------------------------------- -bool SoundBuffer::loadAac(const std::string& path) { +bool SoundBuffer::loadAac(const fs::path& path) { // Convert path to URL - NSString* nsPath = [NSString stringWithUTF8String:path.c_str()]; + std::string pathStr = internal::pathToUtf8(path); + NSString* nsPath = [NSString stringWithUTF8String:pathStr.c_str()]; NSURL* fileURL = [NSURL fileURLWithPath:nsPath]; if (!fileURL) { - printf("SoundBuffer: invalid path: %s\n", path.c_str()); + printf("SoundBuffer: invalid path: %s\n", pathStr.c_str()); return false; } diff --git a/core/platform/mac/tcVideoPlayer_mac.mm b/core/platform/mac/tcVideoPlayer_mac.mm index 72c43fa1..259997ad 100644 --- a/core/platform/mac/tcVideoPlayer_mac.mm +++ b/core/platform/mac/tcVideoPlayer_mac.mm @@ -643,11 +643,11 @@ - (NSData*)extractAudioData { namespace trussc { -bool VideoPlayer::loadPlatform(const std::string& path) { +bool VideoPlayer::loadPlatform(const fs::path& path) { // Create Objective-C implementation TCVideoPlayerImpl* impl = [[TCVideoPlayerImpl alloc] init]; - NSString* nsPath = [NSString stringWithUTF8String:path.c_str()]; + NSString* nsPath = [NSString stringWithUTF8String:internal::pathToUtf8(path).c_str()]; if (![impl loadWithPath:nsPath]) { return false; @@ -936,18 +936,19 @@ static bool tcv_extract_frame_mac(const std::string& path, Pixels& outPixels, } } -bool VideoPlayer::extractFramePlatform(const std::string& path, Pixels& outPixels, +bool VideoPlayer::extractFramePlatform(const fs::path& path, Pixels& outPixels, float timeSec, float* outDuration) { - return tcv_extract_frame_mac(path, outPixels, timeSec, outDuration, /*exact=*/true); + return tcv_extract_frame_mac(internal::pathToUtf8(path), outPixels, timeSec, outDuration, /*exact=*/true); } -bool VideoPlayer::extractKeyFramePlatform(const std::string& path, Pixels& outPixels, +bool VideoPlayer::extractKeyFramePlatform(const fs::path& path, Pixels& outPixels, float timeSec, float* outDuration) { - if (tcv_extract_frame_mac(path, outPixels, timeSec, outDuration, /*exact=*/false)) { + std::string p = internal::pathToUtf8(path); + if (tcv_extract_frame_mac(p, outPixels, timeSec, outDuration, /*exact=*/false)) { return true; } // No keyframe reachable — fall back to an exact decode. - return tcv_extract_frame_mac(path, outPixels, timeSec, outDuration, /*exact=*/true); + return tcv_extract_frame_mac(p, outPixels, timeSec, outDuration, /*exact=*/true); } } // namespace trussc diff --git a/core/platform/web/tcSound_web.cpp b/core/platform/web/tcSound_web.cpp index ee51aa6c..2ad68a0a 100644 --- a/core/platform/web/tcSound_web.cpp +++ b/core/platform/web/tcSound_web.cpp @@ -119,11 +119,11 @@ EM_JS(void, copyAacData, (float* outPtr, int totalSamples), { // ----------------------------------------------------------------------------- // SoundBuffer::loadAac - Web implementation (deferred loading) // ----------------------------------------------------------------------------- -bool SoundBuffer::loadAac(const std::string& path) { +bool SoundBuffer::loadAac(const fs::path& path) { printf("SoundBuffer: deferring AAC load: %s [Web]\n", path.c_str()); - // Save path for deferred loading - deferredAacPath_ = path; + // Save path for deferred loading (web paths are plain UTF-8 strings) + deferredAacPath_ = path.string(); // Set placeholder values so the app doesn't crash channels = 2; diff --git a/core/platform/web/tcVideoPlayer_web.cpp b/core/platform/web/tcVideoPlayer_web.cpp index 4c8257a3..68bd587a 100644 --- a/core/platform/web/tcVideoPlayer_web.cpp +++ b/core/platform/web/tcVideoPlayer_web.cpp @@ -16,7 +16,8 @@ namespace trussc { // VideoPlayer Web implementation // --------------------------------------------------------------------------- -bool VideoPlayer::loadPlatform(const std::string& path) { +bool VideoPlayer::loadPlatform(const fs::path& path) { + std::string pathStr = internal::pathToUtf8(path); // Create video element in JavaScript char script[8192]; snprintf(script, sizeof(script), R"JS( @@ -98,7 +99,7 @@ bool VideoPlayer::loadPlatform(const std::string& path) { return 1; })(); - )JS", path.c_str()); + )JS", pathStr.c_str()); int result = emscripten_run_script_int(script); if (result <= 0) { @@ -114,7 +115,7 @@ bool VideoPlayer::loadPlatform(const std::string& path) { pixels_ = new unsigned char[width_ * height_ * 4]; std::memset(pixels_, 0, width_ * height_ * 4); - printf("VideoPlayer: loading '%s' [Web]\n", path.c_str()); + printf("VideoPlayer: loading '%s' [Web]\n", pathStr.c_str()); return true; } diff --git a/core/platform/win/tcPlatform_win.cpp b/core/platform/win/tcPlatform_win.cpp index ce91c181..5dd05c88 100644 --- a/core/platform/win/tcPlatform_win.cpp +++ b/core/platform/win/tcPlatform_win.cpp @@ -364,7 +364,7 @@ bool captureWindow(Pixels& outPixels) { // --------------------------------------------------------------------------- bool internal::captureWindowToFile(const std::filesystem::path& path) { if (path.is_relative()) { - return internal::captureWindowToFile(getDataPath(path.string())); + return internal::captureWindowToFile(getDataPath(path)); } // Capture to Pixels Pixels pixels; @@ -385,7 +385,7 @@ bool internal::captureWindowToFile(const std::filesystem::path& path) { } int result = 0; - std::string pathStr = path.string(); + std::string pathStr = internal::pathToUtf8(path); // UTF-8 for stb (STBIW_WINDOWS_UTF8) if (ext == ".png") { result = stbi_write_png(pathStr.c_str(), width, height, 4, data, width * 4); diff --git a/core/platform/win/tcSound_win.cpp b/core/platform/win/tcSound_win.cpp index 8d9efc67..8a9b15fe 100644 --- a/core/platform/win/tcSound_win.cpp +++ b/core/platform/win/tcSound_win.cpp @@ -132,13 +132,11 @@ namespace { } } -bool SoundBuffer::loadAac(const std::string& path) { +bool SoundBuffer::loadAac(const fs::path& path) { EnsureMFStartup(); - // Convert path to wide string - int size_needed = MultiByteToWideChar(CP_UTF8, 0, path.c_str(), (int)path.size(), NULL, 0); - std::wstring wpath(size_needed, 0); - MultiByteToWideChar(CP_UTF8, 0, path.c_str(), (int)path.size(), &wpath[0], size_needed); + // fs::path is already wide on Windows + std::wstring wpath = path.wstring(); IMFSourceReader* pReader = NULL; HRESULT hr = MFCreateSourceReaderFromURL(wpath.c_str(), NULL, &pReader); diff --git a/core/platform/win/tcVideoPlayer_win.cpp b/core/platform/win/tcVideoPlayer_win.cpp index 2f941f1b..e4534b8e 100644 --- a/core/platform/win/tcVideoPlayer_win.cpp +++ b/core/platform/win/tcVideoPlayer_win.cpp @@ -845,10 +845,11 @@ void TCVideoPlayerImpl::onMediaEvent(DWORD event, DWORD_PTR param1, DWORD param2 namespace trussc { -bool VideoPlayer::loadPlatform(const std::string& path) { +bool VideoPlayer::loadPlatform(const fs::path& path) { auto impl = new TCVideoPlayerImpl(); - if (!impl->load(path, this)) { + // Impl-side strings are UTF-8 (converted to wide via CP_UTF8 internally) + if (!impl->load(internal::pathToUtf8(path), this)) { delete impl; return false; } @@ -1200,18 +1201,19 @@ static bool tcv_extract_frame_win(const std::string& path, Pixels& outPixels, return ok; } -bool VideoPlayer::extractFramePlatform(const std::string& path, Pixels& outPixels, +bool VideoPlayer::extractFramePlatform(const fs::path& path, Pixels& outPixels, float timeSec, float* outDuration) { - return tcv_extract_frame_win(path, outPixels, timeSec, outDuration, /*exact=*/true); + return tcv_extract_frame_win(internal::pathToUtf8(path), outPixels, timeSec, outDuration, /*exact=*/true); } -bool VideoPlayer::extractKeyFramePlatform(const std::string& path, Pixels& outPixels, +bool VideoPlayer::extractKeyFramePlatform(const fs::path& path, Pixels& outPixels, float timeSec, float* outDuration) { - if (tcv_extract_frame_win(path, outPixels, timeSec, outDuration, /*exact=*/false)) { + std::string p = internal::pathToUtf8(path); + if (tcv_extract_frame_win(p, outPixels, timeSec, outDuration, /*exact=*/false)) { return true; } // No keyframe reachable — fall back to an exact decode. - return tcv_extract_frame_win(path, outPixels, timeSec, outDuration, /*exact=*/true); + return tcv_extract_frame_win(p, outPixels, timeSec, outDuration, /*exact=*/true); } } // namespace trussc diff --git a/core/tests/filePath/.gitignore b/core/tests/filePath/.gitignore new file mode 100644 index 00000000..f16d06dc --- /dev/null +++ b/core/tests/filePath/.gitignore @@ -0,0 +1,40 @@ +# ============================================================================= +# TrussC Project .gitignore +# ============================================================================= + +# Generated by projectGenerator (regenerate with projectGenerator update) +CMakeLists.txt +CMakePresets.json + +# TrussC local config (path override, generated by projectGenerator) +.trussc + +# Build directories +build/ +build-*/ +emscripten/ +xcode*/ +vs/ + +# Build scripts (generated, OS dependent) +build-web.* + +# Binary output (keep data folder) +bin/* +!bin/data/ + +# IDE specific +.vscode/ +.vs/ +.cache/ + +# Generated shader headers (rebuilt by CMake) +*.glsl.h + +# OS specific +.DS_Store +Thumbs.db + +# Secrets (don't commit these!) +.env +secrets.* diff --git a/core/tests/filePath/addons.make b/core/tests/filePath/addons.make new file mode 100644 index 00000000..dc9fc4ff --- /dev/null +++ b/core/tests/filePath/addons.make @@ -0,0 +1 @@ +# TrussC addons - one addon per line diff --git a/core/tests/filePath/src/main.cpp b/core/tests/filePath/src/main.cpp new file mode 100644 index 00000000..3f925bab --- /dev/null +++ b/core/tests/filePath/src/main.cpp @@ -0,0 +1,193 @@ +// ============================================================================= +// core/tests/filePath — behavioral regression test for fs::path unification. +// +// Headless, console, exit code = pass/fail (build_all.py runs it in CI on +// macOS / Linux / Windows). +// +// Guards the invariant: file paths survive end-to-end as fs::path — through +// getDataPath composition, the tc file utilities, and the C-library +// boundaries (stb, miniaudio, nlohmann, pugixml) — including non-ASCII +// (Japanese) names, spaces/parentheses, and absolute-path passthrough. +// On Windows this exercises the UTF-8 → wide conversions; a regression to +// ACP-narrow file IO fails here with Japanese names. +// ============================================================================= + +#include + +#include +#include +#include +#include +#include + +using namespace std; +using namespace tc; + +static int g_fail = 0; +static void check(const char* name, bool ok) { + std::printf("%-64s %s\n", name, ok ? "PASS" : "FAIL"); + std::fflush(stdout); // flush per line so CI logs survive a later crash + if (!ok) ++g_fail; +} + +// Minimal 16-bit mono PCM WAV (440 Hz-ish square, ~0.1 s @ 44100) +static vector makeWavBytes() { + const uint32_t sampleRate = 44100; + const uint32_t numSamples = 4410; + const uint32_t dataSize = numSamples * 2; + vector b; + auto push32 = [&](uint32_t v) { for (int i = 0; i < 4; i++) b.push_back(uint8_t(v >> (8 * i))); }; + auto push16 = [&](uint16_t v) { for (int i = 0; i < 2; i++) b.push_back(uint8_t(v >> (8 * i))); }; + auto pushStr = [&](const char* s) { while (*s) b.push_back(uint8_t(*s++)); }; + pushStr("RIFF"); push32(36 + dataSize); pushStr("WAVE"); + pushStr("fmt "); push32(16); push16(1); push16(1); + push32(sampleRate); push32(sampleRate * 2); push16(2); push16(16); + pushStr("data"); push32(dataSize); + for (uint32_t i = 0; i < numSamples; i++) { + push16(uint16_t(((i / 50) % 2) ? 12000 : -12000)); + } + return b; +} + +int main() { + // Sandbox under the OS temp dir. On Windows this is an absolute path like + // C:\Users\...\Temp — using it as the data-path root exercises the + // fs::is_absolute() root detection (the old path[0]=='/' check failed it). + const fs::path sandbox = fs::temp_directory_path() / "tc_filePath_test"; + std::error_code ec; + fs::remove_all(sandbox, ec); + fs::create_directories(sandbox); + + // --- 1. data-path root + getDataPath composition --- + { + setDataPathRoot(sandbox); + check("setDataPathRoot(absolute): root round-trips", + fs::path(getDataPathRoot()) == sandbox || + fs::path(getDataPathRoot()) == sandbox / ""); + + fs::path p = getDataPath("sub/file.txt"); + check("getDataPath(relative): resolves under the absolute root", + p.is_absolute() && p == sandbox / "sub" / "file.txt"); + + const fs::path abs = sandbox / "already_absolute.bin"; + check("getDataPath(absolute): passthrough unchanged", + fs::path(getDataPath(abs)) == abs); + } + + // --- 2. text round-trip: ASCII / Japanese name / Japanese content --- + { + check("saveTextFile: ASCII name", saveTextFile("ascii.txt", "hello")); + check("loadTextFile: ASCII round-trip", loadTextFile("ascii.txt") == "hello"); + + const string jpName = "日本語ファイル名.txt"; + const string jpBody = "こんにちはTrussC。改行\nもある。"; + check("saveTextFile: Japanese name", saveTextFile(jpName, jpBody)); + check("loadTextFile: Japanese name + content round-trip", + loadTextFile(jpName) == jpBody); + check("fileExists: Japanese name", fileExists(jpName)); + check("getFileSize: Japanese name", getFileSize(jpName) == (int64_t)jpBody.size()); + } + + // --- 3. spaces / parentheses / Japanese directory (exhibition-PC case) --- + { + const string dir = "新しいフォルダー (2)"; + check("createDirectory: Japanese + space + parens", createDirectory(dir)); + const string f = dir + "/テスト ファイル.txt"; + check("saveTextFile: file inside that directory", saveTextFile(f, "data")); + check("loadTextFile: reads it back", loadTextFile(f) == "data"); + + auto listing = listDirectory(dir); + bool found = false; + for (auto& e : listing) { + if (fs::path(e).filename() == fs::path("テスト ファイル.txt")) found = true; + } + check("listDirectory: finds the Japanese-named file", found); + + check("removeFile: Japanese path", removeFile(f) && !fileExists(f)); + } + + // --- 4. binary round-trip through the stb boundary (PNG) --- + { + Pixels px; + px.allocate(4, 4, 4); + for (int y = 0; y < 4; y++) + for (int x = 0; x < 4; x++) + px.setColor(x, y, Color(x / 3.0f, y / 3.0f, 0.5f, 1.0f)); + + const string imgPath = "画像/テスト画像.png"; + createDirectory("画像"); + check("Pixels::save: PNG with Japanese path", px.save(imgPath)); + + Pixels loaded; + bool ok = loaded.load(getDataPath(imgPath)); + check("Pixels::load: PNG back from Japanese path", ok); + bool same = ok && loaded.getWidth() == 4 && loaded.getHeight() == 4; + if (same) { + Color a = loaded.getColor(3, 0); + same = a.r > 0.9f && a.g < 0.1f; // (1, 0, 0.5) corner survived + } + check("Pixels round-trip: pixel data intact", same); + } + + // --- 5. sound decode through the miniaudio boundary (WAV) --- + { + const auto wav = makeWavBytes(); + const fs::path wavPath = sandbox / "音" / "テスト音声.wav"; + fs::create_directories(wavPath.parent_path()); + { + std::ofstream out(wavPath, std::ios::binary); + out.write(reinterpret_cast(wav.data()), wav.size()); + } + Sound snd; + check("Sound::load: WAV with Japanese absolute path", snd.load(wavPath)); + check("Sound::load: decoded duration > 0", snd.getDuration() > 0.05f); + } + + // --- 6. JSON / XML round-trip with Japanese paths and values --- + { + Json j; + j["名前"] = "トラス"; + j["value"] = 42; + createDirectory("設定"); // save helpers don't create parent dirs + check("saveJson: Japanese path", saveJson(j, "設定/データ.json")); + Json k = loadJson("設定/データ.json"); + check("loadJson: values round-trip", + k.value("value", 0) == 42 && k.value("名前", string()) == "トラス"); + + Xml xml; + XmlNode root = xml.addRoot("root"); + root.append_attribute("名") = "値"; + check("Xml::save: Japanese path", xml.save("設定/データ.xml")); + Xml xml2; + check("Xml::load: back from Japanese path", xml2.load("設定/データ.xml")); + check("Xml round-trip: root + Japanese attribute survive", + string(xml2.root().name()) == "root" && + string(xml2.root().attribute("名").value()) == "値"); + } + + // --- 7. FileWriter / FileReader line round-trip --- + { + const string txtPath = "書き込み/ログ.txt"; + createDirectory("書き込み"); + { + FileWriter w; + check("FileWriter::open: Japanese path", w.open(txtPath)); + w.writeLine("一行目"); + w.writeLine("second line"); + } + { + FileReader r; + bool ok = r.open(txtPath); // relative: resolved via getDataPath + check("FileReader::open: Japanese path", ok); + check("FileReader: Japanese content line survives", + ok && r.readLine() == "一行目" && r.readLine() == "second line"); + } + } + + fs::remove_all(sandbox, ec); + + std::printf("\n%s (%d failure%s)\n", g_fail ? "FAILED" : "PASSED", + g_fail, g_fail == 1 ? "" : "s"); + std::fflush(stdout); + return g_fail ? 1 : 0; +} diff --git a/docs/FOR_AI_ASSISTANT.md b/docs/FOR_AI_ASSISTANT.md index 00dd9d7b..925ef810 100644 --- a/docs/FOR_AI_ASSISTANT.md +++ b/docs/FOR_AI_ASSISTANT.md @@ -1328,6 +1328,10 @@ If you want an app that runs without a window (update loop only, no `draw()`), u - Log to a file: `getLogger().setLogFile(path)` (unique name: `getTimestampString()`; hook all lines: `onLog.listen`) - CLI / console app: handle `argc`/`argv` in `main()` (no framework arg API); no args → `runApp()`; headless loop → `runHeadlessApp()` +### How are file paths handled? Japanese / non-ASCII filenames on Windows? + +All file-path parameters take `fs::path` (`std::filesystem::path`) — string literals and `std::string` convert implicitly, so just write `img.load("photo.png")` as always. `getDataPath()` also returns `fs::path`; join paths with `/` (`getDataPath("save") / "shot.png"`), not string concatenation. Non-ASCII paths (Japanese filenames, `新しいフォルダー (2)`, spaces) work on every platform including Windows: TrussC converts to the OS-native encoding at the C-library boundary internally, so there is nothing to configure. `setDataPathRoot()` accepts absolute roots on Windows (`C:/data`) too. If you need a narrow string from a path, use `path.string()` on macOS/Linux; avoid it for file IO on Windows (pass the `fs::path` through instead). + ### "Window / media / basics" → which API? - Window: `setWindowTitle()` / `setWindowSize()` / `setFullscreen(bool)` / `toggleFullscreen()` diff --git a/docs/reference/api-reference.toml b/docs/reference/api-reference.toml index cecfb8f2..58ddaaaa 100644 --- a/docs/reference/api-reference.toml +++ b/docs/reference/api-reference.toml @@ -12207,17 +12207,17 @@ related = ["setCurveTolerance"] category = "file" keywords = ["resource", "asset", "resolve"] of = ["ofToDataPath"] -description.en = "Get full path relative to data directory" -description.ja = "データディレクトリからの相対パスを取得" -description.ko = "데이터 디렉토리 기준의 전체 경로를 얻음" +description.en = "Resolve a relative path against the data directory and return it as fs::path. An absolute input is returned unchanged." +description.ja = "相対パスをデータディレクトリ基準で解決して fs::path で返す。絶対パスはそのまま返る" +description.ko = "상대 경로를 데이터 디렉터리 기준으로 해석해 fs::path로 반환. 절대 경로는 그대로 반환됨" related = ["getDataPathRoot", "getAbsolutePath", "joinPath"] ["getDataPathRoot"] category = "file" keywords = ["resource", "directory", "base"] -description.en = "Get the current data path root (with trailing slash)." -description.ja = "現在のデータパスルートを取得(末尾スラッシュ付き)" -description.ko = "현재 데이터 경로 루트를 얻음(끝 슬래시 포함)" +description.en = "Get the current data path root as fs::path." +description.ja = "現在のデータパスルートを fs::path で取得" +description.ko = "현재 데이터 경로 루트를 fs::path로 얻음" related = ["setDataPathRoot", "getDataPath"] ["getDay"] @@ -13968,9 +13968,9 @@ related = ["getCurveTolerance", "setCurveResolution", "drawBezier", "drawCurve"] ["setDataPathRoot"] category = "file" keywords = ["resource", "directory", "base"] -description.en = "Set the root directory used to resolve relative data paths. A relative path is resolved against the executable directory; an absolute path (starting with /) is used as-is. A trailing slash is added automatically." -description.ja = "相対データパスの解決に使うルートディレクトリを設定。相対パスは実行ファイルのディレクトリ基準で解決され、絶対パス(/始まり)はそのまま使われる。末尾のスラッシュは自動で付加される" -description.ko = "상대 데이터 경로 해석에 사용할 루트 디렉터리를 설정. 상대 경로는 실행 파일 디렉터리 기준으로 해석되고, 절대 경로(/로 시작)는 그대로 사용됨. 끝의 슬래시는 자동으로 추가됨" +description.en = "Set the root directory used to resolve relative data paths. A relative root is resolved against the executable directory; an absolute root (fs::path::is_absolute, e.g. C:/ on Windows) is used as-is." +description.ja = "相対データパスの解決に使うルートディレクトリを設定。相対ルートは実行ファイルのディレクトリ基準で解決され、絶対ルート(fs::path::is_absolute 判定、Windows の C:/ も可)はそのまま使われる" +description.ko = "상대 데이터 경로 해석에 사용할 루트 디렉터리를 설정. 상대 루트는 실행 파일 디렉터리 기준으로 해석되고, 절대 루트(fs::path::is_absolute 판정, Windows의 C:/ 포함)는 그대로 사용됨" related = ["getDataPathRoot", "setDataPathToResources", "getDataPath"] ["setDataPathToResources"] diff --git a/examples/sound/soundPlayerExample/src/tcApp.cpp b/examples/sound/soundPlayerExample/src/tcApp.cpp index 690f3957..cb956bdc 100644 --- a/examples/sound/soundPlayerExample/src/tcApp.cpp +++ b/examples/sound/soundPlayerExample/src/tcApp.cpp @@ -115,7 +115,7 @@ void tcApp::draw() { y += 40; } else { setColor(colors::red); - drawBitmapString("Music file not found: " + musicPath, 50, y); + drawBitmapString("Music file not found: " + musicPath.string(), 50, y); y += 40; } diff --git a/examples/sound/soundPlayerExample/src/tcApp.h b/examples/sound/soundPlayerExample/src/tcApp.h index 618d704b..145cf0aa 100644 --- a/examples/sound/soundPlayerExample/src/tcApp.h +++ b/examples/sound/soundPlayerExample/src/tcApp.h @@ -14,7 +14,7 @@ class tcApp : public App { Sound music; Sound sfx; - std::string musicPath; + std::filesystem::path musicPath; bool musicLoaded = false; bool sfxLoaded = false; }; diff --git a/examples/sound/soundPlayerFFTExample/src/tcApp.cpp b/examples/sound/soundPlayerFFTExample/src/tcApp.cpp index 9e861120..771d62e5 100644 --- a/examples/sound/soundPlayerFFTExample/src/tcApp.cpp +++ b/examples/sound/soundPlayerFFTExample/src/tcApp.cpp @@ -23,7 +23,7 @@ void tcApp::setup() { spectrumSmooth.resize(FFT_SIZE / 2, 0.0f); // Load music - std::string musicPath = getDataPath("beat_loop.wav"); + fs::path musicPath = getDataPath("beat_loop.wav"); if (music.load(musicPath)) { musicLoaded = true; music.setLoop(true); diff --git a/examples/sound/soundStreamExample/src/tcApp.cpp b/examples/sound/soundStreamExample/src/tcApp.cpp index 0c983bc1..9c966d52 100644 --- a/examples/sound/soundStreamExample/src/tcApp.cpp +++ b/examples/sound/soundStreamExample/src/tcApp.cpp @@ -134,7 +134,7 @@ void tcApp::draw() { y += 40; } else { setColor(colors::red); - drawBitmapString("Music file not found: " + musicPath, 50, y); + drawBitmapString("Music file not found: " + musicPath.string(), 50, y); y += 40; } diff --git a/examples/sound/soundStreamExample/src/tcApp.h b/examples/sound/soundStreamExample/src/tcApp.h index cb859b5d..27742665 100644 --- a/examples/sound/soundStreamExample/src/tcApp.h +++ b/examples/sound/soundStreamExample/src/tcApp.h @@ -16,7 +16,7 @@ class tcApp : public App { // sfx = eager voice (PCM in RAM) — coexists with the stream in the mixer Sound sfx; - std::string musicPath; + std::filesystem::path musicPath; bool musicLoaded = false; bool sfxLoaded = false; }; From 57f0af2c542f3cd7889adb655428a3e1e997b1db Mon Sep 17 00:00:00 2001 From: tettou771 Date: Fri, 3 Jul 2026 23:34:27 +0900 Subject: [PATCH 02/87] doc: fs::path path-handling docs (reference prose + FOR_AI corpus) --- docs/FOR_AI_ASSISTANT.md | 104 +++++++++++++++++++-------------------- 1 file changed, 52 insertions(+), 52 deletions(-) diff --git a/docs/FOR_AI_ASSISTANT.md b/docs/FOR_AI_ASSISTANT.md index 925ef810..792cef64 100644 --- a/docs/FOR_AI_ASSISTANT.md +++ b/docs/FOR_AI_ASSISTANT.md @@ -1714,7 +1714,7 @@ bool grabScreen(Pixels & outPixels) // Capture current screen to Pixels bool isFullscreen() // Check if window is fullscreen bool isRecording() // Check whether a recording is in progress int recordingFrameCount() // Number of frames captured so far in the current recording -const std::string & recordingPath() // Output file path of the current recording +std::string recordingPath() // Output file path of the current recording void redraw(int count = 1) // Request extra redraws (useful for event-driven rendering) int runHeadlessApp(const HeadlessSettings & settings = HeadlessSettings()) // Run an app class without a window or graphics context (update loop only). Template on the app type; returns the process exit code bool saveScreenshot(const std::filesystem::path & path) // Save a screenshot of the rendered frame (png/jpg/bmp). Safe to call from anywhere; capture is deferred to after present(). Returns true when the destination was prepared and the capture queued (parent dir created/writable), not that the file is already written. @@ -1728,7 +1728,7 @@ void setWindowPosition(int x, int y) [macos,windows] // Set window position in void setWindowSize(int width, int height) // Set window size void setWindowSizeLogical(int width, int height) // Resize the window to the given logical size (logical pixels) void setWindowTitle(const std::string & title) // Set window title -bool startRecording(const std::string & path, const VideoRecordSettings & settings = {}) [macos,windows,linux,android,ios] // Start recording the window to a video file (native encoder, no ffmpeg) +bool startRecording(const fs::path & path, const VideoRecordSettings & settings = {}) [+1] [macos,windows,linux,android,ios] // Start recording the window to a video file (native encoder, no ffmpeg) void stopRecording() // Stop the current recording and finalize the file void toggleFullscreen() // Toggle fullscreen mode ``` @@ -1765,7 +1765,7 @@ Json reflectToJson(T & obj) // Return all reflected (TC_REFLECT) members of obj void runOnMainThread(std::function fn) // Run a callback on the main (scene) thread; immediately if already on it, otherwise queued to the next frame void setConsoleLogLevel(LogLevel level) // Set the minimum log level printed to the console void setFileLogLevel(LogLevel level) // Set the minimum log level written to the log file -bool setLogFile(const std::string & path) // Open a file to receive log output +bool setLogFile(const fs::path & path) // Open a file to receive log output const std::string & shortTypeName(const std::type_info & ti) // Short (unqualified) readable name for a type, cached per type std::vector splitString(const std::string & source, const std::string & delimiter, bool ignoreEmpty = false, bool trim = false) // Split string by delimiter void stringReplace(std::string & input, const std::string & searchStr, const std::string & replaceStr) // Replace substring in place @@ -1780,7 +1780,7 @@ LogStream tcLogVerbose(const std::string & module = std::string("")) ⚠️depre LogStream tcLogWarning(const std::string & module = std::string("")) ⚠️deprecated // Deprecated alias for logWarning() void tcSetConsoleLogLevel(LogLevel level) ⚠️deprecated // Deprecated alias for setConsoleLogLevel() void tcSetFileLogLevel(LogLevel level) ⚠️deprecated // Deprecated alias for setFileLogLevel() -bool tcSetLogFile(const std::string & path) ⚠️deprecated // Deprecated alias for setLogFile() +bool tcSetLogFile(const fs::path & path) ⚠️deprecated // Deprecated alias for setLogFile() std::string toBase64(const unsigned char * bytes, size_t len) [+2] // Encode raw bytes as a Base64 string std::string toBinary(int value) [+4] // Convert an integer to a binary string bool toBool(const std::string & str) // Parse a string into a bool @@ -1802,29 +1802,29 @@ const std::string & typeName(const std::type_info & ti) [+1] // Readable (deman ### File ```cpp -bool appendToFile(const std::string & path, const std::string & content) // Append string to file -bool createDirectory(const std::string & path) // Create directory (and parents) -bool directoryExists(const std::string & path) // Check if directory exists -bool fileExists(const std::string & path) // Check if file exists -std::string getAbsolutePath(const std::string & path) // Get absolute path -std::string getBaseName(const std::string & path) // Get filename without extension -std::string getDataPath(const std::string & filename) // Get full path relative to data directory -std::string getDataPathRoot() // Get the current data path root (with trailing slash). +bool appendToFile(const fs::path & path, const std::string & content) // Append string to file +bool createDirectory(const fs::path & path) // Create directory (and parents) +bool directoryExists(const fs::path & path) // Check if directory exists +bool fileExists(const fs::path & path) // Check if file exists +std::string getAbsolutePath(const fs::path & path) // Get absolute path +std::string getBaseName(const fs::path & path) // Get filename without extension +fs::path getDataPath(const fs::path & filename) // Resolve a relative path against the data directory and return it as fs::path. An absolute input is returned unchanged. +fs::path getDataPathRoot() // Get the current data path root as fs::path. std::string getExecutableDir() // Get the directory containing the running executable (with trailing slash). std::string getExecutablePath() // Get the absolute path of the running executable. -std::string getFileExtension(const std::string & path) // Get file extension without dot -std::string getFileName(const std::string & path) // Get filename from path -int64_t getFileSize(const std::string & path) // Get file size in bytes -std::string getParentDirectory(const std::string & path) // Get parent directory -std::string joinPath(const std::string & dir, const std::string & file) // Join directory and filename -std::vector listDirectory(const std::string & path) // List files in directory -Json loadJson(const std::string & path) // Load a JSON file and return it as a Json object. Relative paths are resolved via getDataPath; returns an empty Json on error. -std::string loadTextFile(const std::string & path) // Load entire text file -Xml loadXml(const std::string & path) // Load an XML file and return it as an Xml object. Relative paths are resolved via getDataPath. -bool removeFile(const std::string & path) // Remove file -bool saveJson(const Json & j, const std::string & path, int indent = 2) // Write a Json object to a file. Relative paths are resolved via getDataPath. indent sets the pretty-print width (negative for compact). Returns true on success. -bool saveTextFile(const std::string & path, const std::string & content) // Save string to text file -void setDataPathRoot(const std::string & path) // Set the root directory used to resolve relative data paths. A relative path is resolved against the executable directory; an absolute path (starting with /) is used as-is. A trailing slash is added automatically. +std::string getFileExtension(const fs::path & path) // Get file extension without dot +std::string getFileName(const fs::path & path) // Get filename from path +int64_t getFileSize(const fs::path & path) // Get file size in bytes +std::string getParentDirectory(const fs::path & path) // Get parent directory +std::string joinPath(const fs::path & dir, const fs::path & file) // Join directory and filename +std::vector listDirectory(const fs::path & path) // List files in directory +Json loadJson(const fs::path & path) // Load a JSON file and return it as a Json object. Relative paths are resolved via getDataPath; returns an empty Json on error. +std::string loadTextFile(const fs::path & path) // Load entire text file +Xml loadXml(const fs::path & path) // Load an XML file and return it as an Xml object. Relative paths are resolved via getDataPath. +bool removeFile(const fs::path & path) // Remove file +bool saveJson(const Json & j, const fs::path & path, int indent = 2) // Write a Json object to a file. Relative paths are resolved via getDataPath. indent sets the pretty-print width (negative for compact). Returns true on success. +bool saveTextFile(const fs::path & path, const std::string & content) // Save string to text file +void setDataPathRoot(const fs::path & path) // Set the root directory used to resolve relative data paths. A relative root is resolved against the executable directory; an absolute root (fs::path::is_absolute, e.g. C:/ on Windows) is used as-is. void setDataPathToResources() [macos,ios] // Point the data path root at the macOS app bundle's Contents/Resources/data folder for distribution. No-op on non-macOS platforms. ``` @@ -2291,7 +2291,7 @@ const Texture & Environment::getIrradianceMap() const // Get irradiance cubemap const Texture & Environment::getPrefilterMap() const // Get prefiltered environment cubemap for specular IBL int Environment::getPrefilterMipLevels() const // Get number of mip levels in the prefilter map bool Environment::isLoaded() const // Check if environment is loaded -bool Environment::loadFromHDR(const std::string & path) [+1] // Load environment from HDR image file +bool Environment::loadFromHDR(const fs::path & path) [+1] // Load environment from HDR image file bool Environment::loadProcedural() // Generate a simple procedural sky environment void Environment::release() // Release GPU resources ``` @@ -2353,7 +2353,7 @@ bool Fbo::save(const fs::path & path) const // Save FBO contents to file void FileReader::close() // Close file bool FileReader::eof() const // Check if at end of file bool FileReader::isOpen() const // Check if file is open -bool FileReader::open(const std::string & path) // Open file for reading +bool FileReader::open(const fs::path & path) // Open file for reading size_t FileReader::read(void * buffer, size_t size) // Read binary data int FileReader::readChar() // Read one character (-1 at EOF) std::string FileReader::readLine() [+1] // Read one line @@ -2368,7 +2368,7 @@ size_t FileReader::tell() // Get current position void FileWriter::close() // Close file void FileWriter::flush() // Flush buffer to disk bool FileWriter::isOpen() const // Check if file is open -bool FileWriter::open(const std::string & path, bool append = false) // Open file for writing +bool FileWriter::open(const fs::path & path, bool append = false) // Open file for writing FileWriter & FileWriter::write(const std::string & text) [+2] // Write data to file FileWriter & FileWriter::writeLine(const std::string & text = std::string("")) // Write line with newline ``` @@ -2416,7 +2416,7 @@ bool Font::isLoaded() const // Check if loaded bool Font::isWrapEnabled() const // Check if line wrapping is enabled bool Font::kinsokuLineEnd(uint32_t cp) const // Return whether a codepoint is forbidden at the end of a line (kinsoku rule). bool Font::kinsokuLineStart(uint32_t cp) const // Return whether a codepoint is forbidden at the start of a line (kinsoku rule). -bool Font::load(const std::string & nameOrPath, int size) // Load font file +bool Font::load(const fs::path & nameOrPath, int size) // Load font file void Font::resetLineHeight() // Reset line height to the font default void Font::setAlign(Direction h, Direction v) [+1] // Set horizontal (and optional vertical) text alignment void Font::setHangingPunctuation(bool enabled) // Let prohibited line-start CJK punctuation hang past the line edge instead of wrapping (default off) @@ -2511,7 +2511,7 @@ sg_sampler IesProfile::getSampler() const // Return the sokol-gfx sampler of th int IesProfile::getTextureWidth() const // Get width of the generated 1D lookup texture sg_view IesProfile::getView() const // Return the sokol-gfx texture view of the IES profile for pipeline binding (advanced interop). bool IesProfile::isLoaded() const // Check if profile is loaded -bool IesProfile::load(const std::string & path) // Load IES profile from file +bool IesProfile::load(const fs::path & path) // Load IES profile from file bool IesProfile::loadFromString(const std::string & data) // Load IES profile from inline string data ``` @@ -2664,7 +2664,7 @@ bool Logger::isFileOpen() const // Check whether a log file is currently open void Logger::log(LogLevel level, const std::string & message) // Emit a log message at the given level void Logger::setConsoleLogLevel(LogLevel level) // Set the minimum console log level void Logger::setFileLogLevel(LogLevel level) // Set the minimum file log level -bool Logger::setLogFile(const std::string & path) // Open a file to receive log output +bool Logger::setLogFile(const fs::path & path) // Open a file to receive log output ``` ### Mat3 — 3x3 matrix for 2D affine / homography transforms (row-major). Includes static factories and a homography solver @@ -3188,9 +3188,9 @@ bool Reflector::visit(const char * name, float & v) [+7] // Handle one reflecte ```cpp int ScreenRecorder::getFrameCount() const // Number of frames captured so far -const std::string & ScreenRecorder::getPath() const // Output file path of the current recording +std::string ScreenRecorder::getPath() const // Output file path of the current recording bool ScreenRecorder::isRecording() const // Check if the screen recorder is currently capturing -bool ScreenRecorder::start(const std::string & path, const VideoRecordSettings & settings = {}) [+1] // Start live capture (window, or an Fbo for clean GUI-free output); size is taken automatically +bool ScreenRecorder::start(const fs::path & path, const VideoRecordSettings & settings = {}) [+3] // Start live capture (window, or an Fbo for clean GUI-free output); size is taken automatically void ScreenRecorder::stop() // Stop live capture and finalize the file VideoWriter & ScreenRecorder::writer() // Access the underlying VideoWriter for advanced introspection ``` @@ -3321,9 +3321,9 @@ bool Sound::isLoop() const // Check if loop mode is enabled bool Sound::isPaused() const // Check if paused bool Sound::isPlaying() const // Check if playing bool Sound::isStreaming() const // True if this Sound was loaded via loadStream() (vs eager load()) -bool Sound::load(const std::string & path) // Load audio file. Format auto-detected by extension: .wav .mp3 .ogg .flac .aac .m4a +bool Sound::load(const fs::path & path) // Load audio file. Format auto-detected by extension: .wav .mp3 .ogg .flac .aac .m4a void Sound::loadFromBuffer(const SoundBuffer & buf) [+1] // Load PCM directly from a pre-generated SoundBuffer (e.g. from ChipSound or a procedural waveform), copying it or adopting the shared_ptr. -bool Sound::loadStream(const std::string & path, int maxPolyphony = 1) [macos,windows,linux,android,ios] // Stream sound from disk (WAV/MP3/FLAC). Best for long files; cuts memory. maxPolyphony = simultaneous play() count. +bool Sound::loadStream(const fs::path & path, int maxPolyphony = 1) [macos,windows,linux,android,ios] // Stream sound from disk (WAV/MP3/FLAC). Best for long files; cuts memory. maxPolyphony = simultaneous play() count. void Sound::loadTestTone(float frequency = 440.0, float duration = 1.0) // Load a generated sine test tone (no file needed). Handy for verifying audio output. void Sound::pause() // Pause playback void Sound::play() // Play audio @@ -3354,17 +3354,17 @@ void SoundBuffer::generateSquareWave(float frequency, float duration, float volu void SoundBuffer::generateTriangleWave(float frequency, float duration, float volume = 0.5, int sr = 44100) // Fill the buffer with a mono triangle wave. int SoundBuffer::getAdtsSampleRateIndex(int sampleRate) // ADTS sample-rate index for the given rate (AAC-in-MOV container helper). float SoundBuffer::getDuration() const // Duration in seconds (numSamples / sampleRate). -bool SoundBuffer::load(const std::string & path) // Decode a file into PCM, auto-detecting format from the extension (.wav .mp3 .ogg .flac .aac .m4a, case-insensitive). Returns false on failure. -bool SoundBuffer::loadAac(const std::string & path) [macos,windows,linux,ios,web] // Decode an AAC / M4A file into PCM (platform-specific; returns false on unsupported platforms). +bool SoundBuffer::load(const fs::path & path) // Decode a file into PCM, auto-detecting format from the extension (.wav .mp3 .ogg .flac .aac .m4a, case-insensitive). Returns false on failure. +bool SoundBuffer::loadAac(const fs::path & path) [macos,windows,linux,ios,web] // Decode an AAC / M4A file into PCM (platform-specific; returns false on unsupported platforms). bool SoundBuffer::loadAacFromMemory(const void * data, size_t dataSize) [macos,windows,linux,ios,web] // Decode AAC data from a memory buffer (platform-specific; returns false on unsupported platforms). -bool SoundBuffer::loadFlac(const std::string & path) // Decode a FLAC file into PCM. +bool SoundBuffer::loadFlac(const fs::path & path) // Decode a FLAC file into PCM. bool SoundBuffer::loadFlacFromMemory(const void * data, size_t dataSize) // Decode FLAC data from a memory buffer. -bool SoundBuffer::loadMp3(const std::string & path) // Decode an MP3 file into PCM. +bool SoundBuffer::loadMp3(const fs::path & path) // Decode an MP3 file into PCM. bool SoundBuffer::loadMp3FromMemory(const void * data, size_t dataSize) // Decode MP3 data from a memory buffer. -bool SoundBuffer::loadOgg(const std::string & path) // Decode an OGG Vorbis file into PCM (via stb_vorbis). +bool SoundBuffer::loadOgg(const fs::path & path) // Decode an OGG Vorbis file into PCM (via stb_vorbis). bool SoundBuffer::loadOggFromMemory(const void * data, size_t dataSize) // Decode OGG Vorbis data from a memory buffer. bool SoundBuffer::loadPcmFromMemory(const void * data, size_t dataSize, int numChannels, int rate, int bitsPerSample = 16, bool bigEndian = false) // Load raw interleaved PCM (16-bit signed or 32-bit float) from memory with explicit format. Returns false for unsupported bit depths. -bool SoundBuffer::loadWav(const std::string & path) // Decode a WAV file into PCM. +bool SoundBuffer::loadWav(const fs::path & path) // Decode a WAV file into PCM. bool SoundBuffer::loadWavFromMemory(const void * data, size_t dataSize) // Decode WAV data from a memory buffer. void SoundBuffer::mixFrom(const SoundBuffer & other, size_t offsetSamples, float volume = 1.0) // Additively mix another buffer into this one starting at offsetSamples, growing this buffer if needed. ``` @@ -3381,8 +3381,8 @@ Kind SoundSource::kind() const // Source kind (Eager for SoundBuffer, Stream fo ```cpp float SoundStream::getDuration() const // Decoded file duration in seconds. int SoundStream::getMaxPolyphony() const // Number of concurrent decoder slots reserved at loadStream(). -const std::string & SoundStream::getPath() const // Path the stream was opened from. -bool SoundStream::loadStream(const std::string & path, int maxPolyphony = 1) // Open the file, validate format (.wav .mp3 .flac .ogg), and populate channels / sampleRate / duration. maxPolyphony reserves that many concurrent decoder slots. Returns false if the file can't be opened or the format is unsupported. +std::string SoundStream::getPath() const // Path the stream was opened from. +bool SoundStream::loadStream(const fs::path & path, int maxPolyphony = 1) // Open the file, validate format (.wav .mp3 .flac .ogg), and populate channels / sampleRate / duration. maxPolyphony reserves that many concurrent decoder slots. Returns false if the file can't be opened or the format is unsupported. ``` ### StrokeMesh — Variable-width polyline stroke geometry with caps, joins and miter limit; build it from points or a Path, then update() and draw() @@ -3775,8 +3775,8 @@ void VideoGrabber::update() // Poll for a new frame and upload it to the textur ```cpp void VideoPlayer::close() // Close the video and release resources void VideoPlayer::draw(float x, float y) const [+1] // Draw the current video frame at (x, y), optionally scaled to w x h -bool VideoPlayer::extractFrame(const std::string & path, Pixels & outPixels, float timeSec = 1.0, float * outDuration = nullptr) [+1] // Extract the exact frame at a given time from a video file. Frame-accurate on every platform -bool VideoPlayer::extractKeyFrame(const std::string & path, Pixels & outPixels, float timeSec = 1.0, float * outDuration = nullptr) [+1] // Extract the nearest keyframe at or before a given time. Faster than extractFrame but time-approximate +bool VideoPlayer::extractFrame(const fs::path & path, Pixels & outPixels, float timeSec = 1.0, float * outDuration = nullptr) [+1] // Extract the exact frame at a given time from a video file. Frame-accurate on every platform +bool VideoPlayer::extractKeyFrame(const fs::path & path, Pixels & outPixels, float timeSec = 1.0, float * outDuration = nullptr) [+1] // Extract the nearest keyframe at or before a given time. Faster than extractFrame but time-approximate int VideoPlayer::getAudioChannels() const [macos,windows,linux,ios] // Number of audio channels (0 if no audio) uint32_t VideoPlayer::getAudioCodec() const [macos,windows,linux,ios] // FourCC of the audio codec in the loaded video (0 if none) std::vector VideoPlayer::getAudioData() const [macos,windows,linux,ios] // Raw decoded audio data for the loaded video @@ -3785,7 +3785,7 @@ int VideoPlayer::getCurrentFrame() const // Get current frame number float VideoPlayer::getDuration() const // Get total duration in seconds float VideoPlayer::getGammaCorrection() const // Get current gamma correction value std::string VideoPlayer::getHwAccelName() const // Get the name of the active decode backend. Returns 'vaapi', 'v4l2m2m', 'cuda', 'videotoolbox', 'mediafoundation', 'software', or 'none' -const std::string & VideoPlayer::getPath() const // Path of the currently loaded video file (resolved via getDataPath); empty string when nothing is loaded +std::string VideoPlayer::getPath() const // Path of the currently loaded video file (resolved via getDataPath); empty string when nothing is loaded unsigned char * VideoPlayer::getPixels() [+1] // Pointer to the current RGBA pixel buffer (mutable) unsigned char * VideoPlayer::getPixelsUV() // Pointer to the interleaved UV (chroma) plane when decoding NV12; null otherwise unsigned char * VideoPlayer::getPixelsY() // Pointer to the Y (luma) plane when decoding NV12/YUV; null otherwise @@ -3794,7 +3794,7 @@ int VideoPlayer::getTotalFrames() const // Get total number of frames bool VideoPlayer::getUseHwAccel() const // Get HW accel preference (not the actual backend — use isUsingHwAccel() for that) bool VideoPlayer::hasAudio() const // Check if the loaded video has an audio track bool VideoPlayer::isUsingHwAccel() const // Check if hardware decoding is currently active (after load) -bool VideoPlayer::load(const std::string & path) // Load a video file +bool VideoPlayer::load(const fs::path & path) // Load a video file void VideoPlayer::nextFrame() // Advance to the next frame void VideoPlayer::playImpl() // Backend implementation of playImpl for this platform's video player. void VideoPlayer::previousFrame() // Go back to the previous frame @@ -3842,7 +3842,7 @@ bool VideoPlayerBase::isLoop() const // Check if looping is enabled bool VideoPlayerBase::isPaused() const // Check if video is paused bool VideoPlayerBase::isPlaying() const // Check if video is currently playing (not paused) bool VideoPlayerBase::isUsingHwAccel() const // Return true if hardware-accelerated decoding is currently active. -bool VideoPlayerBase::load(const std::string & path) // Load a video from the given file path; return true on success. +bool VideoPlayerBase::load(const fs::path & path) // Load a video from the given file path; return true on success. void VideoPlayerBase::markDone() // Mark playback as done, clearing playing unless looping. void VideoPlayerBase::markFrameNew() // Mark that a new frame has arrived (sets frameNew and firstFrameReceived). void VideoPlayerBase::nextFrame() // Advance to the next frame. @@ -3884,12 +3884,12 @@ void VideoWriter::close() // Finalize and flush the video file float VideoWriter::getFps() const // Fixed encoding frame rate int VideoWriter::getFrameCount() const // Number of frames written so far int VideoWriter::getHeight() const // Encoder output height in pixels -const std::string & VideoWriter::getPath() const // Resolved output file path +std::string VideoWriter::getPath() const // Resolved output file path const VideoRecordSettings & VideoWriter::getSettings() const // Encoder settings the writer was opened with int VideoWriter::getWidth() const // Encoder output width in pixels bool VideoWriter::isOpen() const // Check if the encoder is open and accepting frames unsigned char * VideoWriter::lockFrame(int & strideOut) // Lock and return the encoder's frame buffer for zero-copy fills; strideOut receives the row stride. Pair with submitFrame -bool VideoWriter::open(const std::string & path, int width, int height, const VideoRecordSettings & settings = {}) // Open the encoder at the given size (path resolved via getDataPath) +bool VideoWriter::open(const fs::path & path, int width, int height, const VideoRecordSettings & settings = {}) // Open the encoder at the given size (path resolved via getDataPath) bool VideoWriter::submitFrame(double timeSec) // Append the previously locked frame at the given presentation time (seconds) ``` @@ -3915,10 +3915,10 @@ XmlNode Xml::addRoot(const std::string & name) // Append a new root element wit XmlNode Xml::child(const std::string & name) // Find a direct child node of the document by name. XmlDocument & Xml::document() [+1] // Access the underlying pugixml document for advanced operations. bool Xml::empty() const // Return true if the document has no content. -bool Xml::load(const std::string & path) // Load an XML document from a file. Relative paths are resolved via getDataPath. Returns true on success. +bool Xml::load(const fs::path & path) // Load an XML document from a file. Relative paths are resolved via getDataPath. Returns true on success. bool Xml::parse(const std::string & str) // Parse an XML document from a string. Returns true on success. XmlNode Xml::root() [+1] // Get the document's root element node. -bool Xml::save(const std::string & path, const std::string & indent = std::string(" ")) const // Save the document to a file. Relative paths are resolved via getDataPath. indent sets the per-level indentation string. Returns true on success. +bool Xml::save(const fs::path & path, const std::string & indent = std::string(" ")) const // Save the document to a file. Relative paths are resolved via getDataPath. indent sets the per-level indentation string. Returns true on success. std::string Xml::toString(const std::string & indent = std::string(" ")) const // Serialize the document to an XML string. indent sets the per-level indentation string. ``` From e9dcb952267297da28f09cd2ee59d514065cdfdd Mon Sep 17 00:00:00 2001 From: tettou771 Date: Fri, 3 Jul 2026 23:34:27 +0900 Subject: [PATCH 03/87] feat: carry file paths as fs::path end to end (UTF-8 safe on Windows) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit All file-path parameters take const fs::path& (string literals and std::string still convert implicitly). getDataPath/getDataPathRoot are fs::path in/out: joining uses operator/, and the absolute-root check is fs::path::is_absolute(), fixing the old path[0]=='/' test that treated C:/... roots as relative on Windows. Paths cross into C libraries through new internal helpers (tcFileIO.h: pathToUtf8/utf8ToPath/openFile) at the last moment: - stb: STBI_WINDOWS_UTF8 / STBIW_WINDOWS_UTF8 + UTF-8 bytes at all call sites (incl. platform screenshot writers) - miniaudio: ma_decoder_init_file_w on Windows - stb_vorbis: openFile() + stb_vorbis_open_file (no UTF-8 mode exists) - pugixml: wide-path overloads on Windows - Media Foundation: path.wstring(); NSString/FFmpeg/GStreamer: UTF-8 Non-ASCII paths (Japanese names, spaces, parens) now work on Windows; covered by the new core/tests/filePath in CI. BREAKING: getDataPath()/getDataPathRoot() return fs::path (on Windows, assigning to std::string no longer compiles — use auto or .string()); string concatenation on the result must become operator/. --- core/include/tc/utils/tcFileIO.h | 67 ++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 core/include/tc/utils/tcFileIO.h diff --git a/core/include/tc/utils/tcFileIO.h b/core/include/tc/utils/tcFileIO.h new file mode 100644 index 00000000..4726cf9b --- /dev/null +++ b/core/include/tc/utils/tcFileIO.h @@ -0,0 +1,67 @@ +#pragma once + +// ============================================================================= +// tcFileIO.h - fs::path <-> C-library boundary helpers (internal) +// ============================================================================= +// +// TrussC carries file paths as fs::path end to end. C libraries (stb, +// miniaudio, fopen) only take narrow char* strings, which on Windows are +// interpreted in the active code page — non-ASCII paths get mangled. These +// helpers do the conversion at the last moment, right at the library call: +// +// - pathToUtf8() : path -> UTF-8 bytes (for UTF-8-aware sinks such as +// stb with STBI(W)_WINDOWS_UTF8, NSString, FFmpeg) +// - utf8ToPath() : UTF-8 bytes -> path (for UTF-8 sources such as JSON) +// - openFile() : fopen that takes fs::path (wide API on Windows) +// +// Everything here is internal plumbing, not public API. + +#include +#include +#include +#include + +namespace trussc { + +namespace fs = std::filesystem; + +namespace internal { + +// Convert a path to UTF-8 bytes. On POSIX the native encoding already is +// UTF-8; on Windows the native encoding is UTF-16, so go through u8string(). +inline std::string pathToUtf8(const fs::path& p) { +#ifdef _WIN32 + auto u8 = p.u8string(); // std::u8string (char8_t) in C++20 + return std::string(u8.begin(), u8.end()); +#else + return p.string(); +#endif +} + +// Convert UTF-8 bytes to a path. fs::path(std::string) would interpret the +// bytes in the active code page on Windows — this never does. +inline fs::path utf8ToPath(std::string_view utf8) { +#ifdef _WIN32 + return fs::path(std::u8string(utf8.begin(), utf8.end())); +#else + return fs::path(std::string(utf8)); +#endif +} + +// fopen that accepts fs::path. Uses _wfopen on Windows so non-ASCII paths +// survive; mode strings are short ASCII ("rb", "wb", ...). +inline std::FILE* openFile(const fs::path& p, const char* mode) { +#ifdef _WIN32 + wchar_t wmode[8]; + size_t i = 0; + for (; mode[i] != '\0' && i < 7; ++i) wmode[i] = static_cast(mode[i]); + wmode[i] = L'\0'; + return _wfopen(p.c_str(), wmode); +#else + return std::fopen(p.c_str(), mode); +#endif +} + +} // namespace internal + +} // namespace trussc From 20014004d87e99bfd1e9e8adbf05ed1bc686ae3d Mon Sep 17 00:00:00 2001 From: tettou771 Date: Sat, 4 Jul 2026 02:59:40 +0900 Subject: [PATCH 04/87] ci: fail the build on deprecated TrussC API use in the repo's own sources MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New opt-in TC_DEPRECATED_ERRORS (cache var or env, read by core CMake and trussc_app.cmake) adds -Werror=deprecated-declarations PRIVATE to the core lib, bundled examples and tests. CI sets it workflow-wide (mac/linux/web/ android); user builds never see it, and without the flag deprecated use stays a plain warning. GCC/Clang only: MSVC folds CRT 'unsafe function' warnings into the same C4996, so /we4996 would misfire. Sweep fixes so the gate starts green (verified: all 107 example builds + 4 core tests pass with the flag; a deliberate deprecated call fails with it and warns without it): - tcxTls + tcxLua exampleFileReload: tcLogX -> logX (23 call sites) - tcxBox2d example-collision: old listen(listener, cb) -> listener = listen(cb) - tcxLua: PI binding value -> HALF_TAU; generated bindings TU is exempted via pragma (the Lua API deliberately keeps deprecated compat names) — luagen.js template updated so regeneration keeps the exemption --- .github/workflows/build.yml | 7 +++++ .../tcxBox2d/example-collision/src/tcApp.cpp | 5 ++-- addons/tcxLua/exampleFileReload/src/tcApp.cpp | 4 +-- .../tcxLua/src/generated/trussc_generated.cpp | 5 ++++ addons/tcxLua/src/tcxLua.cpp | 2 +- addons/tcxLua/tools/luagen/luagen.js | 5 ++++ addons/tcxTls/example-tls/src/tcApp.cpp | 16 ++++++------ addons/tcxTls/src/tcTlsClient.cpp | 26 +++++++++---------- core/CMakeLists.txt | 14 ++++++++++ core/cmake/trussc_app.cmake | 17 ++++++++++++ 10 files changed, 74 insertions(+), 27 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index faa8e2ca..9b93cc4f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -16,6 +16,13 @@ concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true +env: + # Uses of [[deprecated]] TrussC API in the repo's own sources + # (core/examples/tests, all jobs incl. web/android) fail the build + # (GCC/Clang only; see core/CMakeLists.txt + trussc_app.cmake). + # User projects never set this. + TC_DEPRECATED_ERRORS: 1 + jobs: # Path-based change detector. Emits docs_only=true when EVERY file in the diff # is documentation (any *.md, or anything under docs/ — prose + doc tooling). diff --git a/addons/tcxBox2d/example-collision/src/tcApp.cpp b/addons/tcxBox2d/example-collision/src/tcApp.cpp index 9cd853b2..2c67ec76 100644 --- a/addons/tcxBox2d/example-collision/src/tcApp.cpp +++ b/addons/tcxBox2d/example-collision/src/tcApp.cpp @@ -54,15 +54,14 @@ void tcApp::addBall(float x, float y) { // Add collision callback to ball auto* collider = ball->getCollider(); if (collider) { - ballListeners.emplace_back(); - collider->onCollisionEnter.listen(ballListeners.back(), [this](box2d::CollisionEvent& e) { + ballListeners.push_back(collider->onCollisionEnter.listen([this](box2d::CollisionEvent& e) { enterCount++; collisionMarkers.push_back({ e.contactPoint, 0.3f, Color(0.2f, 0.8f, 1.0f) // cyan for ball collisions }); - }); + })); } addChild(ball); diff --git a/addons/tcxLua/exampleFileReload/src/tcApp.cpp b/addons/tcxLua/exampleFileReload/src/tcApp.cpp index c1478178..9b3588ee 100644 --- a/addons/tcxLua/exampleFileReload/src/tcApp.cpp +++ b/addons/tcxLua/exampleFileReload/src/tcApp.cpp @@ -34,10 +34,10 @@ void tcApp::reloadLuaFile(){ << result.value().what() << std::endl; } }else{ - tcLogError("tcApp") << "Lua file not found at: " << luaScriptPath; + logError("tcApp") << "Lua file not found at: " << luaScriptPath; } #else - tcLogWarning("tcApp") << "[file reload] sorry, this platform currently not supported"; + logWarning("tcApp") << "[file reload] sorry, this platform currently not supported"; #endif // FILE_RELOAD_SUPPORTED } diff --git a/addons/tcxLua/src/generated/trussc_generated.cpp b/addons/tcxLua/src/generated/trussc_generated.cpp index fe2b970b..3505f42d 100644 --- a/addons/tcxLua/src/generated/trussc_generated.cpp +++ b/addons/tcxLua/src/generated/trussc_generated.cpp @@ -5,9 +5,14 @@ using namespace trussc; using namespace std; +// The Lua API deliberately keeps binding deprecated C++ compat names +// (colorFromHSB, tcLog*, ...) so existing Lua scripts stay stable — this shim +// layer is exempt from the CI deprecation gate (-Werror=deprecated-declarations). #ifndef _MSC_VER #pragma GCC diagnostic push #pragma clang diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" +#pragma clang diagnostic ignored "-Wdeprecated-declarations" #endif void tcxLua::setTrussCGeneratedBindings(const std::shared_ptr& lua) { diff --git a/addons/tcxLua/src/tcxLua.cpp b/addons/tcxLua/src/tcxLua.cpp index ab72e09e..d9ed7084 100644 --- a/addons/tcxLua/src/tcxLua.cpp +++ b/addons/tcxLua/src/tcxLua.cpp @@ -142,7 +142,7 @@ void tcxLua::setBindings(const std::shared_ptr& lua){ void tcxLua::setConstBindings(const std::shared_ptr& lua){ auto&& l = *lua; l["TAU"] = TAU; - l["PI"] = PI; + l["PI"] = HALF_TAU; // Lua keeps PI; HALF_TAU avoids the deprecated C++ constant l["HALF_TAU"] = HALF_TAU; l["QUARTER_TAU"] = QUARTER_TAU; } diff --git a/addons/tcxLua/tools/luagen/luagen.js b/addons/tcxLua/tools/luagen/luagen.js index 13925eb7..67e6f35e 100644 --- a/addons/tcxLua/tools/luagen/luagen.js +++ b/addons/tcxLua/tools/luagen/luagen.js @@ -119,9 +119,14 @@ process.stdout.write(`// AUTO-GENERATED from reference-data.json by luagen.js using namespace trussc; using namespace std; +// The Lua API deliberately keeps binding deprecated C++ compat names +// (colorFromHSB, tcLog*, ...) so existing Lua scripts stay stable — this shim +// layer is exempt from the CI deprecation gate (-Werror=deprecated-declarations). #ifndef _MSC_VER #pragma GCC diagnostic push #pragma clang diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" +#pragma clang diagnostic ignored "-Wdeprecated-declarations" #endif void tcxLua::setTrussCGeneratedBindings(const std::shared_ptr& lua) { diff --git a/addons/tcxTls/example-tls/src/tcApp.cpp b/addons/tcxTls/example-tls/src/tcApp.cpp index 7ea8b9e2..eefa52be 100644 --- a/addons/tcxTls/example-tls/src/tcApp.cpp +++ b/addons/tcxTls/example-tls/src/tcApp.cpp @@ -8,12 +8,12 @@ using namespace std; void tcApp::setup() { - tcLogNotice() << "=== TLS (HTTPS) Client Example ==="; - tcLogNotice() << "Press C to connect to httpbin.org (HTTPS)"; - tcLogNotice() << "Press SPACE to send HTTP GET request"; - tcLogNotice() << "Press D to disconnect"; - tcLogNotice() << "Press X to clear log"; - tcLogNotice() << "=================================="; + logNotice() << "=== TLS (HTTPS) Client Example ==="; + logNotice() << "Press C to connect to httpbin.org (HTTPS)"; + logNotice() << "Press SPACE to send HTTP GET request"; + logNotice() << "Press D to disconnect"; + logNotice() << "Press X to clear log"; + logNotice() << "=================================="; // TLS configuration (no certificate verification - for testing) client.setVerifyNone(); @@ -190,7 +190,7 @@ void tcApp::cleanup() { } void tcApp::addSent(const string& msg) { - tcLogNotice() << "[SENT] " << msg; + logNotice() << "[SENT] " << msg; lock_guard lock(sentMutex); sentMessages.push_back(msg); if (sentMessages.size() > 30) { @@ -199,7 +199,7 @@ void tcApp::addSent(const string& msg) { } void tcApp::addReceived(const string& msg) { - tcLogNotice() << "[RECV] " << msg; + logNotice() << "[RECV] " << msg; lock_guard lock(receivedMutex); receivedMessages.push_back(msg); if (receivedMessages.size() > 30) { diff --git a/addons/tcxTls/src/tcTlsClient.cpp b/addons/tcxTls/src/tcTlsClient.cpp index 862162c3..29c743b9 100644 --- a/addons/tcxTls/src/tcTlsClient.cpp +++ b/addons/tcxTls/src/tcTlsClient.cpp @@ -138,7 +138,7 @@ TlsClient::TlsClient() { reinterpret_cast(pers), strlen(pers)); if (ret != 0) { - tcLogError() << "TlsClient: Failed to seed random generator: " << ret; + logError() << "TlsClient: Failed to seed random generator: " << ret; } } @@ -157,7 +157,7 @@ bool TlsClient::setCACertificate(const std::string& pemData) { if (ret < 0) { char errBuf[256]; mbedtls_strerror(ret, errBuf, sizeof(errBuf)); - tcLogError() << "TlsClient: Failed to parse CA certificate: " << errBuf; + logError() << "TlsClient: Failed to parse CA certificate: " << errBuf; return false; } // Explicit user intent: skip the default-CA auto-load entirely. @@ -170,16 +170,16 @@ bool TlsClient::setCACertificateFile(const std::string& path) { // call — works portably on POSIX and Windows. std::ifstream file(path, std::ios::binary | std::ios::ate); if (!file) { - tcLogError() << "TlsClient: Failed to open CA certificate file: " << path; + logError() << "TlsClient: Failed to open CA certificate file: " << path; return false; } std::streamsize size = file.tellg(); if (size < 0) { - tcLogError() << "TlsClient: Failed to size CA certificate file: " << path; + logError() << "TlsClient: Failed to size CA certificate file: " << path; return false; } if (size > kMaxCaPemBytes) { - tcLogError() << "TlsClient: CA certificate file too large (" + logError() << "TlsClient: CA certificate file too large (" << size << " bytes, limit " << kMaxCaPemBytes << "): " << path; return false; @@ -220,7 +220,7 @@ void TlsClient::ensureDefaultCAsLoaded() { } CertCloseStore(store, 0); if (parsed > 0) { - tcLogNotice() << "TlsClient: loaded " << parsed + logNotice() << "TlsClient: loaded " << parsed << " CAs from Windows Cert Store (ROOT)"; return; } @@ -240,7 +240,7 @@ void TlsClient::ensureDefaultCAsLoaded() { struct stat st; if (stat(*p, &st) != 0) continue; if (st.st_size > kMaxCaPemBytes) { - tcLogWarning() << "TlsClient: skipping " << *p + logWarning() << "TlsClient: skipping " << *p << " (size " << st.st_size << " > limit " << kMaxCaPemBytes << ")"; continue; @@ -257,7 +257,7 @@ void TlsClient::ensureDefaultCAsLoaded() { if (ret < 0) continue; // try the next path const size_t loaded = countCerts(&ctx_->cacert) - before; if (loaded > 0) { - tcLogNotice() << "TlsClient: loaded " << loaded + logNotice() << "TlsClient: loaded " << loaded << " CAs from " << *p; return; } @@ -273,14 +273,14 @@ void TlsClient::ensureDefaultCAsLoaded() { if (ret >= 0) { const size_t loaded = countCerts(&ctx_->cacert) - before; if (loaded > 0) { - tcLogNotice() << "TlsClient: loaded " << loaded + logNotice() << "TlsClient: loaded " << loaded << " CAs from bundled cacert.pem (" << tls_internal::bundledCaBundleDate() << ")"; return; } } - tcLogError() << "TlsClient: failed to load any default CA certificates. " + logError() << "TlsClient: failed to load any default CA certificates. " << "TLS handshakes will fail unless setCACertificate() or " << "setVerifyNone() is called explicitly."; } @@ -377,7 +377,7 @@ bool TlsClient::connect(const std::string& host, int port) { // TCP Connected immediately running_ = true; handshakePending_ = true; - tcLogNotice() << "TCP connected to " << host << ":" << port << ", starting TLS handshake..."; + logNotice() << "TCP connected to " << host << ":" << port << ", starting TLS handshake..."; } if (running_) { @@ -497,7 +497,7 @@ void TlsClient::processNetwork() { #endif connectPending_ = false; handshakePending_ = true; - tcLogNotice() << "TCP connected (async), starting TLS handshake..."; + logNotice() << "TCP connected (async), starting TLS handshake..."; } else { return; // Still connecting } @@ -509,7 +509,7 @@ void TlsClient::processNetwork() { handshakePending_ = false; connected_ = true; - tcLogNotice() << "TLS connected to " << remoteHost_ << ":" << remotePort_ + logNotice() << "TLS connected to " << remoteHost_ << ":" << remotePort_ << " [" << getTlsVersion() << ", " << getCipherSuite() << "]"; tc::TcpConnectEventArgs args; diff --git a/core/CMakeLists.txt b/core/CMakeLists.txt index 2c99e5f8..920a137d 100644 --- a/core/CMakeLists.txt +++ b/core/CMakeLists.txt @@ -152,6 +152,20 @@ add_library(tc::TrussC ALIAS TrussC) # PUBLIC so user apps and addons see the same value. target_compile_definitions(TrussC PUBLIC TC_VERSION_STRING="${TC_VERSION}") +# CI gate: promote uses of [[deprecated]] TrussC API to errors in the +# framework's OWN sources (core lib TUs here; bundled examples/tests get the +# same flag via trussc_app.cmake). Opt-in through -DTC_DEPRECATED_ERRORS=ON or +# the TC_DEPRECATED_ERRORS env var (set in CI) — never on by default, so user +# builds are unaffected. GCC/Clang only: MSVC folds CRT "unsafe function" +# warnings into the same C4996, so /we4996 would misfire on fopen etc. +if(NOT DEFINED TC_DEPRECATED_ERRORS AND DEFINED ENV{TC_DEPRECATED_ERRORS}) + set(TC_DEPRECATED_ERRORS "$ENV{TC_DEPRECATED_ERRORS}") +endif() +if(TC_DEPRECATED_ERRORS) + target_compile_options(TrussC PRIVATE + $<$>:-Werror=deprecated-declarations>) +endif() + # iOS: sound sources include ObjC headers (AVFoundation) via miniaudio, # so they must be compiled as Objective-C++ if(TC_PLATFORM_IOS AND TC_SOUND_SOURCES) diff --git a/core/cmake/trussc_app.cmake b/core/cmake/trussc_app.cmake index 8098e673..b41508ac 100644 --- a/core/cmake/trussc_app.cmake +++ b/core/cmake/trussc_app.cmake @@ -489,6 +489,23 @@ message(\" [HotReload] Generated \${DEF_FILE} with \${SYM_COUNT} symbols\") endif() endif() + # CI gate for the repo's OWN bundled projects (examples/tests): uses of + # [[deprecated]] TrussC API become errors so stale API can't ship in + # examples. Opt-in via -DTC_DEPRECATED_ERRORS=ON or the + # TC_DEPRECATED_ERRORS env var (set in CI); never on for user builds. + # GCC/Clang only — MSVC's C4996 also covers CRT "unsafe" warnings. + if(NOT DEFINED TC_DEPRECATED_ERRORS AND DEFINED ENV{TC_DEPRECATED_ERRORS}) + set(TC_DEPRECATED_ERRORS "$ENV{TC_DEPRECATED_ERRORS}") + endif() + if(TC_DEPRECATED_ERRORS) + set(_TC_DEPR_FLAG + $<$>:-Werror=deprecated-declarations>) + target_compile_options(${PROJECT_NAME} PRIVATE ${_TC_DEPR_FLAG}) + if(TARGET guest) + target_compile_options(guest PRIVATE ${_TC_DEPR_FLAG}) + endif() + endif() + # Silence the macOS ld-prime "ignoring duplicate libraries" warning. It # fires on the legitimate diamond dependency (app -> TrussC and # app -> addon -> TrussC), where libTrussC.a appears twice in the link From 97b8f9f1997703eb1d7d8b98a2def5e0f1c51cb9 Mon Sep 17 00:00:00 2001 From: tettou771 Date: Sat, 4 Jul 2026 17:10:43 +0900 Subject: [PATCH 05/87] refactor: gather per-window state into internal::WindowContext (multi-window phase 0) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every piece of state that is conceptually per-window — mouse/keyboard input, hover/grab/selection and the root node, camera and view/projection tracking, swapchain/FBO pass state, scissor stack, blend mode, clipboard size, and the RenderContext / CoreEvents instances — moves from scattered process globals into one internal::WindowContext (new tc/app/tcWindowContext.h). The active*() pipeline helpers and restoreCurrentPipeline() move with the state they select on; registerCameraContext() is declared in tcCameraContext.h and defined where the context is complete. internal::currentWindowContext() resolves the active context (a plain inline pointer, null = main window); mainWindowContext() is non-inline in tcGlobal.cpp so a hot-reload guest binds the host's instance — the same sharing pattern as events()/getDefaultContext(), whose accessors now wire their singletons into the context lazily, preserving the exact pre-context construction timing. Zero behavior change and no public API change: the app is still single-window and all free functions keep their signatures, now delegating through the current context. This is the seam Phase 1 uses to instantiate additional contexts (one per native secondary window) and switch them while dispatching each window's events and draw. Verified: all 107 examples + 4 core tests + HotReloadExample build green with TC_DEPRECATED_ERRORS=1 (macOS); threadSafety/filePath suites pass; graphicsExample renders and serves get_node_tree/save_screenshot over MCP. --- core/include/TrussC.h | 219 +++++++------------ core/include/tc/3d/tcEasyCam.h | 8 +- core/include/tc/3d/tcMeshPbrPipeline.h | 10 +- core/include/tc/3d/tcMeshPointPipeline.h | 6 +- core/include/tc/app/tcGlobal.cpp | 66 +++--- core/include/tc/app/tcHotReloadHost.h | 4 +- core/include/tc/app/tcMouseGlobal.h | 26 +-- core/include/tc/app/tcWindowContext.h | 158 +++++++++++++ core/include/tc/gpu/tcFbo.h | 38 ++-- core/include/tc/gpu/tcShader.h | 2 +- core/include/tc/gpu/tcTexture.h | 2 +- core/include/tc/graphics/tcCameraContext.h | 23 +- core/include/tc/graphics/tcRenderContext.cpp | 21 +- core/include/tc/graphics/tcRenderContext.h | 25 +-- core/include/tc/graphics/tcRenderTarget.h | 28 +-- core/include/tc/utils/tcStandardTools.h | 14 +- core/include/tcBaseApp.h | 4 +- core/include/tcNode.h | 75 +++---- core/platform/ios/tcPlatform_ios.mm | 2 +- core/platform/mac/tcPlatform_mac.mm | 2 +- 20 files changed, 410 insertions(+), 323 deletions(-) create mode 100644 core/include/tc/app/tcWindowContext.h diff --git a/core/include/TrussC.h b/core/include/TrussC.h index 737d5c40..1692212e 100644 --- a/core/include/TrussC.h +++ b/core/include/TrussC.h @@ -171,43 +171,24 @@ namespace internal { inline float nearClipOverride = 0.0f; inline float farClipOverride = 0.0f; - // Current screen setup state (for 2D drawing in perspective mode) - inline float currentScreenFov = 45.0f; - inline float currentViewW = 0.0f; - inline float currentViewH = 0.0f; - inline float currentCameraDist = 0.0f; // Distance from camera to Z=0 plane - - // View/Projection matrix tracking (for worldToScreen/screenToWorld) - inline Mat4 currentViewMatrix = Mat4::identity(); - inline Mat4 currentProjectionMatrix = Mat4::identity(); + // Screen setup / view-projection tracking state (currentScreenFov, + // currentViewW/H, currentCameraDist, currentView/ProjectionMatrix) and the + // current 2D blend mode moved to WindowContext (tc/app/tcWindowContext.h). // Forward declaration of setupScreenFov functions (defined later, after TAU is available) // `pickable` flows into the registered CameraContext (false for FBO scopes). inline void setupScreenFovWithSize(float fovDeg, float viewW, float viewH, float nearDist = 0.0f, float farDist = 0.0f, bool pickable = true); inline void setupScreenFov(float fovDeg, float nearDist = 0.0f, float farDist = 0.0f); - - - // Current 2D blend mode (the "role"). The actual sgl pipeline for it lives in - // the active RenderTarget's lazy cache — see tcRenderTarget.h active2D(). - inline BlendMode currentBlendMode = BlendMode::Alpha; } } // namespace trussc (temporarily closed) // RenderTarget: single source of truth for sgl pipeline selection (swapchain/FBO). -// Included before restoreCurrentPipeline so it can use the active*() helpers. #include "tc/graphics/tcRenderTarget.h" -// Forward declarations for FBO pipeline switching (used in tcRenderContext.h) -namespace trussc { namespace internal { - inline bool inFboPass = false; - - // Restore the current blend pipeline after temporary pipeline changes. - // FBO uses its accumulating Fill2D; swapchain honors the current blend mode. - inline void restoreCurrentPipeline() { - loadPipeline(inFboPass ? activeFill2D() : active2D(currentBlendMode)); - } -}} +// Per-window state container (input/hover/camera/pass state) + the active*() +// pipeline helpers and restoreCurrentPipeline() (used in tcRenderContext.h). +#include "tc/app/tcWindowContext.h" // VertexWriter abstraction (for shader integration) #include "tc/graphics/tcVertexWriter.h" @@ -234,24 +215,14 @@ namespace trussc { // Drawing state has been moved to RenderContext // --------------------------------------------------------------------------- namespace internal { - // Clipboard buffer size (for overflow check) - inline int clipboardSize = 65536; + // clipboardSize / ScissorRect / scissorStack / currentScissor moved to + // WindowContext (tc/app/tcWindowContext.h). // sokol_gl vertex buffer management (auto-grows on overflow) inline int sglMaxVertices = 65536; inline int sglMaxCommands = 16384; inline int sglPendingResize = 0; // non-zero = need resize next frame - // --------------------------------------------------------------------------- - // Scissor Clipping stack - // --------------------------------------------------------------------------- - struct ScissorRect { - float x, y, w, h; - bool active; // Whether a valid range exists in the stack - }; - inline std::vector scissorStack; - inline ScissorRect currentScissor = {0, 0, 0, 0, false}; - // --------------------------------------------------------------------------- // Loop Architecture (Decoupled Update/Draw) // --------------------------------------------------------------------------- @@ -276,11 +247,8 @@ namespace internal { inline bool lastDrawTimeInitialized = false; inline double drawAccumulator = 0.0; - // Mouse position state (mouseX/Y, pmouseX/Y) + window-space getters now live - // in tc/app/tcMouseGlobal.h (included above) so lower-level headers can use - // them directly. Button/pressed state stays here. - inline int mouseButton = -1; // Currently pressed button (-1 = none) - inline bool mousePressed = false; + // Mouse position/button state + keyboard state moved to WindowContext + // (tc/app/tcWindowContext.h); window-space getters in tc/app/tcMouseGlobal.h. // Touch-as-mouse mapping // Default ON everywhere — the first touch synthesizes mouse press/drag, so @@ -293,9 +261,6 @@ namespace internal { inline EventListener touchMovedListener; inline EventListener touchReleasedListener; - // Keyboard state - inline std::unordered_set keysPressed; - // Delta time (actual elapsed time since last update call) inline double updateDeltaTime = 0.0; inline std::chrono::high_resolution_clock::time_point lastUpdateCallTime; @@ -313,22 +278,8 @@ namespace internal { inline std::chrono::high_resolution_clock::time_point startTime; inline bool startTimeInitialized = false; - // Pass state (for suspending swapchain pass for FBO) - inline bool inSwapchainPass = false; - - // Saved clear color for resume after FBO suspend (set by clear()) - inline sg_color swapchainClearValue = { 0.0f, 0.0f, 0.0f, 1.0f }; - - // The Metal CAMetalDrawable acquired for THIS frame's swapchain pass. sokol's - // sapp_get_swapchain() advances to the next drawable on every call (it must be - // called exactly once per frame), so end-of-frame capture must NOT call it - // again — it would grab a different, unrendered drawable. Instead the swapchain - // pass setup records the drawable it actually rendered into here, and - // captureWindow() reads back from this one. (Metal-only; on D3D11/GL the - // swapchain handle is a stable backbuffer/FBO so this stays null and is unused.) - inline const void* lastSwapchainDrawable = nullptr; - - // inFboPass is declared earlier (before tcRenderContext.h) + // Pass state (inSwapchainPass / swapchainClearValue / lastSwapchainDrawable / + // inFboPass) moved to WindowContext (tc/app/tcWindowContext.h). // FBO clearColor function pointer (set in tcFbo.h) inline void (*fboClearColorFunc)(float, float, float, float) = nullptr; @@ -547,7 +498,7 @@ inline void intersectRect(float x1, float y1, float w1, float h1, // Set scissor rectangle (screen coordinates) inline void setScissor(float x, float y, float w, float h) { - internal::currentScissor = {x, y, w, h, true}; + internal::currentWindowContext().currentScissor = {x, y, w, h, true}; sgl_scissor_rectf(x, y, w, h, true); // origin_top_left = true } @@ -558,20 +509,20 @@ inline void setScissor(int x, int y, int w, int h) { // Reset scissor to entire window inline void resetScissor() { - internal::currentScissor.active = false; + internal::currentWindowContext().currentScissor.active = false; sgl_scissor_rect(0, 0, sapp_width(), sapp_height(), true); } // Save scissor to stack and set new range (intersection with current range) inline void pushScissor(float x, float y, float w, float h) { // Save current state to stack - internal::scissorStack.push_back(internal::currentScissor); + internal::currentWindowContext().scissorStack.push_back(internal::currentWindowContext().currentScissor); // Calculate new range (intersection with current range) - if (internal::currentScissor.active) { + if (internal::currentWindowContext().currentScissor.active) { float nx, ny, nw, nh; - intersectRect(internal::currentScissor.x, internal::currentScissor.y, - internal::currentScissor.w, internal::currentScissor.h, + intersectRect(internal::currentWindowContext().currentScissor.x, internal::currentWindowContext().currentScissor.y, + internal::currentWindowContext().currentScissor.w, internal::currentWindowContext().currentScissor.h, x, y, w, h, nx, ny, nw, nh); setScissor(nx, ny, nw, nh); @@ -582,17 +533,17 @@ inline void pushScissor(float x, float y, float w, float h) { // Restore scissor from stack inline void popScissor() { - if (internal::scissorStack.empty()) { + if (internal::currentWindowContext().scissorStack.empty()) { resetScissor(); return; } - internal::currentScissor = internal::scissorStack.back(); - internal::scissorStack.pop_back(); + internal::currentWindowContext().currentScissor = internal::currentWindowContext().scissorStack.back(); + internal::currentWindowContext().scissorStack.pop_back(); - if (internal::currentScissor.active) { - sgl_scissor_rectf(internal::currentScissor.x, internal::currentScissor.y, - internal::currentScissor.w, internal::currentScissor.h, true); + if (internal::currentWindowContext().currentScissor.active) { + sgl_scissor_rectf(internal::currentWindowContext().currentScissor.x, internal::currentWindowContext().currentScissor.y, + internal::currentWindowContext().currentScissor.w, internal::currentWindowContext().currentScissor.h, true); } else { sgl_scissor_rect(0, 0, sapp_width(), sapp_height(), true); } @@ -608,16 +559,16 @@ inline void popScissor() { // Set blend mode // Alpha channel is additive in all modes (to prevent transparency when drawing to FBO) inline void setBlendMode(BlendMode mode) { - if (internal::swapchainTarget.context.id == 0) return; // renderer not set up yet + if (internal::currentWindowContext().swapchainTarget.context.id == 0) return; // renderer not set up yet // Skip in FBO - FBO uses its own pipeline - if (internal::inFboPass) return; - internal::currentBlendMode = mode; + if (internal::currentWindowContext().inFboPass) return; + internal::currentWindowContext().currentBlendMode = mode; internal::loadPipeline(internal::active2D(mode)); } // Get current blend mode inline BlendMode getBlendMode() { - return internal::currentBlendMode; + return internal::currentWindowContext().currentBlendMode; } // Reset to default blend mode (Alpha) @@ -627,7 +578,7 @@ inline void resetBlendMode() { // Restore current blend mode pipeline (use after temporary pipeline changes) inline void restoreBlendPipeline() { - internal::loadPipeline(internal::active2D(internal::currentBlendMode)); + internal::loadPipeline(internal::active2D(internal::currentWindowContext().currentBlendMode)); } // --------------------------------------------------------------------------- @@ -676,10 +627,10 @@ namespace internal { float dist = viewH / (2.0f * theTan); // Save current screen state for 2D drawing - currentScreenFov = fovDeg; - currentViewW = viewW; - currentViewH = viewH; - currentCameraDist = dist; + internal::currentWindowContext().currentScreenFov = fovDeg; + internal::currentWindowContext().currentViewW = viewW; + internal::currentWindowContext().currentViewH = viewH; + internal::currentWindowContext().currentCameraDist = dist; // Apply clip overrides or auto-calculate if (nearDist == 0.0f) nearDist = (nearClipOverride > 0.0f) ? nearClipOverride : dist / 10.0f; @@ -709,8 +660,8 @@ namespace internal { ); // Save matrices for worldToScreen/screenToWorld - currentProjectionMatrix = Mat4::ortho(-viewW / 2.0f, viewW / 2.0f, viewH / 2.0f, -viewH / 2.0f, -farDist, farDist); - currentViewMatrix = Mat4::lookAt( + internal::currentWindowContext().currentProjectionMatrix = Mat4::ortho(-viewW / 2.0f, viewW / 2.0f, viewH / 2.0f, -viewH / 2.0f, -farDist, farDist); + internal::currentWindowContext().currentViewMatrix = Mat4::lookAt( Vec3(eyeX, eyeY, dist), Vec3(eyeX, eyeY, 0.0f), Vec3(0.0f, 1.0f, 0.0f) @@ -742,8 +693,8 @@ namespace internal { ); // Save matrices for worldToScreen/screenToWorld - currentProjectionMatrix = Mat4::frustum(left, right, top, bottom, nearDist, farDist); - currentViewMatrix = Mat4::lookAt( + internal::currentWindowContext().currentProjectionMatrix = Mat4::frustum(left, right, top, bottom, nearDist, farDist); + internal::currentWindowContext().currentViewMatrix = Mat4::lookAt( Vec3(eyeX, eyeY, dist), Vec3(eyeX, eyeY, 0.0f), Vec3(0.0f, 1.0f, 0.0f) @@ -752,7 +703,7 @@ namespace internal { // Register this camera scope so nodes drawn from here on stamp it // (draw-time stamping → per-context pick rays; see tcCameraContext.h). - registerCameraContext(currentViewMatrix, currentProjectionMatrix, viewW, viewH, pickable); + registerCameraContext(internal::currentWindowContext().currentViewMatrix, internal::currentWindowContext().currentProjectionMatrix, viewW, viewH, pickable); } // Wrapper that uses main screen size @@ -817,13 +768,13 @@ inline float getFarClip() { /// Returns Vec3: x, y = screen position (pixels from top-left), z = normalized depth [0, 1] inline Vec3 worldToScreen(const Vec3& worldPos) { // Get current viewport dimensions - float viewW = internal::currentViewW; - float viewH = internal::currentViewH; + float viewW = internal::currentWindowContext().currentViewW; + float viewH = internal::currentWindowContext().currentViewH; if (viewW == 0) viewW = (float)sapp_width() / sapp_dpi_scale(); if (viewH == 0) viewH = (float)sapp_height() / sapp_dpi_scale(); // Transform world -> clip space - Mat4 mvp = internal::currentProjectionMatrix * internal::currentViewMatrix; + Mat4 mvp = internal::currentWindowContext().currentProjectionMatrix * internal::currentWindowContext().currentViewMatrix; Vec4 clip = mvp * Vec4(worldPos.x, worldPos.y, worldPos.z, 1.0f); // Perspective division @@ -846,8 +797,8 @@ inline Vec3 worldToScreen(const Vec3& worldPos) { /// worldZ: target Z plane (default = 0) inline Vec3 screenToWorld(const Vec2& screenPos, float worldZ = 0.0f) { // Get current viewport dimensions - float viewW = internal::currentViewW; - float viewH = internal::currentViewH; + float viewW = internal::currentWindowContext().currentViewW; + float viewH = internal::currentWindowContext().currentViewH; if (viewW == 0) viewW = (float)sapp_width() / sapp_dpi_scale(); if (viewH == 0) viewH = (float)sapp_height() / sapp_dpi_scale(); @@ -856,7 +807,7 @@ inline Vec3 screenToWorld(const Vec2& screenPos, float worldZ = 0.0f) { float ndcY = 1.0f - (screenPos.y / viewH) * 2.0f; // Flip Y // Create inverse MVP matrix - Mat4 mvp = internal::currentProjectionMatrix * internal::currentViewMatrix; + Mat4 mvp = internal::currentWindowContext().currentProjectionMatrix * internal::currentWindowContext().currentViewMatrix; Mat4 invMvp = mvp.inverted(); // Unproject two points: near plane (z=-1) and a middle point (z=0) @@ -1306,7 +1257,7 @@ inline void drawBitmapStringHighlight(const std::string& text, float x, float y, sgl_matrix_mode_projection(); sgl_push_matrix(); sgl_load_identity(); - sgl_ortho(0.0f, internal::currentViewW, internal::currentViewH, 0.0f, -10000.0f, 10000.0f); + sgl_ortho(0.0f, internal::currentWindowContext().currentViewW, internal::currentWindowContext().currentViewH, 0.0f, -10000.0f, 10000.0f); sgl_matrix_mode_modelview(); sgl_push_matrix(); sgl_load_identity(); @@ -1356,8 +1307,8 @@ inline void drawBitmapStringHighlight(const std::string& text, float x, float y, // Restore current blend pipeline (not default, which has blend disabled). // FBO uses its accumulating Fill2D; swapchain honors the current blend mode. - internal::loadPipeline(internal::inFboPass ? internal::activeFill2D() - : internal::active2D(internal::currentBlendMode)); + internal::loadPipeline(internal::currentWindowContext().inFboPass ? internal::activeFill2D() + : internal::active2D(internal::currentWindowContext().currentBlendMode)); // Restore matrices sgl_pop_matrix(); @@ -1461,9 +1412,9 @@ inline void unbindCursorImage(Cursor cursor) { // Copy string to clipboard inline void setClipboardString(const std::string& text) { - if (static_cast(text.size()) >= internal::clipboardSize) { + if (static_cast(text.size()) >= internal::currentWindowContext().clipboardSize) { logWarning("Clipboard") << "Text truncated (" << text.size() << " bytes > " - << internal::clipboardSize << " buffer). Use WindowSettings::setClipboardSize() to increase."; + << internal::currentWindowContext().clipboardSize << " buffer). Use WindowSettings::setClipboardSize() to increase."; } sapp_set_clipboard_string(text.c_str()); } @@ -1599,17 +1550,17 @@ inline void releaseSglBuffers() { // Is mouse button pressed inline bool isMousePressed() { - return internal::mousePressed; + return internal::currentWindowContext().mousePressed; } // Currently pressed mouse button (-1 = none) inline int getMouseButton() { - return internal::mouseButton; + return internal::currentWindowContext().mouseButton; } // Is specific key currently pressed inline bool isKeyPressed(int key) { - return internal::keysPressed.count(key) > 0; + return internal::currentWindowContext().keysPressed.count(key) > 0; } // "Either-side" modifier checks. Return true while either the left or @@ -2260,8 +2211,8 @@ namespace internal { } // Save previous frame's mouse position - pmouseX = mouseX; - pmouseY = mouseY; + internal::currentWindowContext().pmouseX = internal::currentWindowContext().mouseX; + internal::currentWindowContext().pmouseY = internal::currentWindowContext().mouseY; frameReentryGuard = false; } @@ -2306,7 +2257,7 @@ namespace internal { // Track key state (only on first press, not auto-repeat) if (!ev->key_repeat) { - keysPressed.insert(ev->key_code); + internal::currentWindowContext().keysPressed.insert(ev->key_code); } // Forward to App / Node tree. Fire on auto-repeat too @@ -2325,21 +2276,21 @@ namespace internal { events().keyReleased.notify(args); // Track key state - keysPressed.erase(ev->key_code); + internal::currentWindowContext().keysPressed.erase(ev->key_code); if (appKeyReleasedFunc) appKeyReleasedFunc(args); break; } case SAPP_EVENTTYPE_MOUSE_DOWN: { currentMouseButton = ev->mouse_button; - mouseX = ev->mouse_x * scale; - mouseY = ev->mouse_y * scale; - mouseButton = ev->mouse_button; - mousePressed = true; + internal::currentWindowContext().mouseX = ev->mouse_x * scale; + internal::currentWindowContext().mouseY = ev->mouse_y * scale; + internal::currentWindowContext().mouseButton = ev->mouse_button; + internal::currentWindowContext().mousePressed = true; // App-level args: no node transform, so pos == globalPos. MouseEventArgs args; - args.pos = args.globalPos = Vec2(mouseX, mouseY); + args.pos = args.globalPos = Vec2(internal::currentWindowContext().mouseX, internal::currentWindowContext().mouseY); args.button = ev->mouse_button; args.shift = hasModShift; args.ctrl = hasModCtrl; @@ -2353,13 +2304,13 @@ namespace internal { } case SAPP_EVENTTYPE_MOUSE_UP: { currentMouseButton = -1; - mouseX = ev->mouse_x * scale; - mouseY = ev->mouse_y * scale; - mouseButton = -1; - mousePressed = false; + internal::currentWindowContext().mouseX = ev->mouse_x * scale; + internal::currentWindowContext().mouseY = ev->mouse_y * scale; + internal::currentWindowContext().mouseButton = -1; + internal::currentWindowContext().mousePressed = false; MouseEventArgs args; - args.pos = args.globalPos = Vec2(mouseX, mouseY); + args.pos = args.globalPos = Vec2(internal::currentWindowContext().mouseX, internal::currentWindowContext().mouseY); args.button = ev->mouse_button; args.shift = hasModShift; args.ctrl = hasModCtrl; @@ -2372,16 +2323,16 @@ namespace internal { break; } case SAPP_EVENTTYPE_MOUSE_MOVE: { - float prevX = mouseX; - float prevY = mouseY; - mouseX = ev->mouse_x * scale; - mouseY = ev->mouse_y * scale; + float prevX = internal::currentWindowContext().mouseX; + float prevY = internal::currentWindowContext().mouseY; + internal::currentWindowContext().mouseX = ev->mouse_x * scale; + internal::currentWindowContext().mouseY = ev->mouse_y * scale; // Rich carrier (internal::MouseEventRaw); the per-kind public type is // built from it at the boundary (internal::toDragArgs / internal::toMoveArgs). internal::MouseEventRaw args; - args.pos = args.globalPos = Vec2(mouseX, mouseY); - args.delta = args.globalDelta = Vec2(mouseX - prevX, mouseY - prevY); + args.pos = args.globalPos = Vec2(internal::currentWindowContext().mouseX, internal::currentWindowContext().mouseY); + args.delta = args.globalDelta = Vec2(internal::currentWindowContext().mouseX - prevX, internal::currentWindowContext().mouseY - prevY); args.shift = hasModShift; args.ctrl = hasModCtrl; args.alt = hasModAlt; @@ -2405,7 +2356,7 @@ namespace internal { } case SAPP_EVENTTYPE_MOUSE_SCROLL: { ScrollEventArgs args; - args.pos = args.globalPos = Vec2(mouseX, mouseY); + args.pos = args.globalPos = Vec2(internal::currentWindowContext().mouseX, internal::currentWindowContext().mouseY); args.scroll = Vec2(ev->scroll_x, ev->scroll_y); args.shift = hasModShift; args.ctrl = hasModCtrl; @@ -2451,9 +2402,9 @@ namespace internal { if (ev->type == SAPP_EVENTTYPE_TOUCHES_BEGAN) { currentMouseButton = 0; - mouseX = tx; mouseY = ty; - mouseButton = 0; - mousePressed = true; + internal::currentWindowContext().mouseX = tx; internal::currentWindowContext().mouseY = ty; + internal::currentWindowContext().mouseButton = 0; + internal::currentWindowContext().mousePressed = true; MouseEventArgs margs; margs.pos = margs.globalPos = Vec2(tx, ty); @@ -2462,8 +2413,8 @@ namespace internal { events().mousePressed.notify(margs); if (appMousePressedFunc) appMousePressedFunc(margs); } else if (ev->type == SAPP_EVENTTYPE_TOUCHES_MOVED) { - float prevX = mouseX, prevY = mouseY; - mouseX = tx; mouseY = ty; + float prevX = internal::currentWindowContext().mouseX, prevY = internal::currentWindowContext().mouseY; + internal::currentWindowContext().mouseX = tx; internal::currentWindowContext().mouseY = ty; internal::MouseEventRaw margs; margs.pos = margs.globalPos = Vec2(tx, ty); @@ -2475,9 +2426,9 @@ namespace internal { if (appMouseDraggedFunc) appMouseDraggedFunc(margs); } else { currentMouseButton = -1; - mouseX = tx; mouseY = ty; - mouseButton = -1; - mousePressed = false; + internal::currentWindowContext().mouseX = tx; internal::currentWindowContext().mouseY = ty; + internal::currentWindowContext().mouseButton = -1; + internal::currentWindowContext().mousePressed = false; MouseEventArgs margs; margs.pos = margs.globalPos = Vec2(tx, ty); @@ -2508,8 +2459,8 @@ namespace internal { } case SAPP_EVENTTYPE_FILES_DROPPED: { DragDropEventArgs args; - args.x = mouseX; // Last mouse position - args.y = mouseY; + args.x = internal::currentWindowContext().mouseX; // Last mouse position + args.y = internal::currentWindowContext().mouseY; int numFiles = sapp_get_num_dropped_files(); for (int i = 0; i < numFiles; i++) { args.files.push_back(sapp_get_dropped_file_path(i)); @@ -2569,7 +2520,7 @@ sapp_desc buildAppDescriptor(const WindowSettings& settings = WindowSettings()) internal::updateFrameCount++; // Update frame count events().update.notify(); if (app) { - app->handleUpdate(internal::mouseX, internal::mouseY); + app->handleUpdate(internal::currentWindowContext().mouseX, internal::currentWindowContext().mouseY); } }; internal::appDrawFunc = []() { @@ -2657,7 +2608,7 @@ sapp_desc buildAppDescriptor(const WindowSettings& settings = WindowSettings()) // Enable clipboard desc.enable_clipboard = true; desc.clipboard_size = settings.clipboardSize; - internal::clipboardSize = settings.clipboardSize; + internal::currentWindowContext().clipboardSize = settings.clipboardSize; return desc; } diff --git a/core/include/tc/3d/tcEasyCam.h b/core/include/tc/3d/tcEasyCam.h index 86e24859..7526f44f 100644 --- a/core/include/tc/3d/tcEasyCam.h +++ b/core/include/tc/3d/tcEasyCam.h @@ -99,10 +99,10 @@ class EasyCam { Mat4 view = Mat4::lookAt(eye, target_, camUp); // Save for worldToScreen/screenToWorld - internal::currentProjectionMatrix = projection; - internal::currentViewMatrix = view; - internal::currentViewW = w; - internal::currentViewH = h; + internal::currentWindowContext().currentProjectionMatrix = projection; + internal::currentWindowContext().currentViewMatrix = view; + internal::currentWindowContext().currentViewW = w; + internal::currentWindowContext().currentViewH = h; // Register this camera scope: nodes drawn between begin()/end() stamp // it, so mouse picking unprojects through THIS camera for them. diff --git a/core/include/tc/3d/tcMeshPbrPipeline.h b/core/include/tc/3d/tcMeshPbrPipeline.h index 964c4e63..912afab2 100644 --- a/core/include/tc/3d/tcMeshPbrPipeline.h +++ b/core/include/tc/3d/tcMeshPbrPipeline.h @@ -111,7 +111,7 @@ class PbrPipeline { // SG_PIXELFORMAT_NONE (1) は「カラーアタッチメントなし」なので使わない sg_pixel_format colorFmt; int sampleCount; - if (internal::inFboPass) { + if (internal::currentWindowContext().inFboPass) { colorFmt = internal::currentFboColorFormat; sampleCount = internal::currentFboSampleCount; } else { @@ -234,7 +234,7 @@ class PbrPipeline { // TrussC Mat4 is row-major; GLSL mat4 is column-major. Transpose the // storage before upload so shader can use the conventional `model * v`. Mat4 modelT = getDefaultContext().getMatrix().transposed(); - Mat4 viewProj = internal::currentProjectionMatrix * internal::currentViewMatrix; + Mat4 viewProj = internal::currentWindowContext().currentProjectionMatrix * internal::currentWindowContext().currentViewMatrix; Mat4 viewProjT = viewProj.transposed(); // For now, normal matrix is just the model matrix. This is correct for // rotations and uniform scale; non-uniform scale would need @@ -366,7 +366,7 @@ class PbrPipeline { // shaders use). flushDeferredShaderDraws() replays it per layer. PbrDrawCommand cmd{ pip, bind, vsp, fsp, mesh.getGpuIndexCount(), mesh.getGpuVertexCount() }; - if (internal::inFboPass) { + if (internal::currentWindowContext().inFboPass) { // Defer (like the swapchain path) into the per-FBO list; flushed // per-layer in Fbo::end(). Bump the FBO layer so 2D drawn after this // mesh composites on top of it, matching the swapchain ordering. @@ -487,10 +487,10 @@ class PbrPipeline { ensureShadowTexture(light.getShadowResolution()); // Suspend swapchain pass if active - if (internal::inSwapchainPass) { + if (internal::currentWindowContext().inSwapchainPass) { sgl_draw(); sg_end_pass(); - internal::inSwapchainPass = false; + internal::currentWindowContext().inSwapchainPass = false; } // Compute light VP matrix (reuse spot projector VP) diff --git a/core/include/tc/3d/tcMeshPointPipeline.h b/core/include/tc/3d/tcMeshPointPipeline.h index 78f8172d..0297adfc 100644 --- a/core/include/tc/3d/tcMeshPointPipeline.h +++ b/core/include/tc/3d/tcMeshPointPipeline.h @@ -152,7 +152,7 @@ class PointPipeline { sg_pixel_format colorFmt; int sampleCount; - if (internal::inFboPass) { + if (internal::currentWindowContext().inFboPass) { colorFmt = internal::currentFboColorFormat; sampleCount = internal::currentFboSampleCount; } else { @@ -181,7 +181,7 @@ class PointPipeline { // after this mesh composites on top. Inside an FBO pass we defer into the // per-FBO list (flushed in flushFboDeferredPbr); otherwise into the // swapchain list (flushed in flushDeferredShaderDraws). - if (internal::inFboPass) { + if (internal::currentWindowContext().inFboPass) { internal::fboPointDraws.push_back({ internal::fboLayerNext, cmd }); internal::fboLayerNext++; sgl_layer(internal::fboLayerNext); @@ -201,7 +201,7 @@ class PointPipeline { // TrussC Mat4 is row-major; GLSL mat4 is column-major. Transpose before // upload so the shader can use the conventional `viewProj * v`. - Mat4 viewProj = (internal::currentProjectionMatrix * internal::currentViewMatrix).transposed(); + Mat4 viewProj = (internal::currentWindowContext().currentProjectionMatrix * internal::currentWindowContext().currentViewMatrix).transposed(); std::memcpy(vsp.viewProj, viewProj.m, sizeof(vsp.viewProj)); const int w = getWindowWidth(); diff --git a/core/include/tc/app/tcGlobal.cpp b/core/include/tc/app/tcGlobal.cpp index e8d5d1c6..57e460df 100644 --- a/core/include/tc/app/tcGlobal.cpp +++ b/core/include/tc/app/tcGlobal.cpp @@ -59,8 +59,8 @@ void setup() { // RenderTarget: the swapchain target uses the default sgl context (carries the // swapchain color/depth format + sample count). Pipelines build lazily on first use. - internal::swapchainTarget.context = sgl_default_context(); - internal::swapchainTarget.isFbo = false; + internal::currentWindowContext().swapchainTarget.context = sgl_default_context(); + internal::currentWindowContext().swapchainTarget.isFbo = false; // Initialize bitmap font sampler + pipeline. The atlas TEXTURE itself is // allocated lazily on first drawBitmapString call (see ensureFontAtlas in @@ -171,10 +171,10 @@ void resizeSgl(int newMaxVertices, int newMaxCommands) { // corrupts the frame (the same reason we pre-warm at setup). Warm against the // swapchain target explicitly in case a resize ever fires outside a swapchain // context. - swapchainTarget.context = sgl_default_context(); - swapchainTarget.cache.clear(); - RenderTarget* prevTarget = currentTarget; - currentTarget = &swapchainTarget; + internal::currentWindowContext().swapchainTarget.context = sgl_default_context(); + internal::currentWindowContext().swapchainTarget.cache.clear(); + RenderTarget* prevTarget = internal::currentWindowContext().currentTarget; + internal::currentWindowContext().currentTarget = &internal::currentWindowContext().swapchainTarget; active2D(BlendMode::Alpha); active2D(BlendMode::Add); active2D(BlendMode::Multiply); @@ -184,7 +184,7 @@ void resizeSgl(int newMaxVertices, int newMaxCommands) { activePremult(); activeClear(); active3D(); - currentTarget = prevTarget; + internal::currentWindowContext().currentTarget = prevTarget; // NOTE: each FBO's RenderTarget cache (and its sgl context) is also invalidated // by sgl_shutdown but is NOT rebuilt here — FBOs surviving an sgl buffer resize // is a pre-existing limitation, out of scope for this refactor. @@ -201,12 +201,12 @@ void clear(float r, float g, float b, float a /* = 1.0f */) { // Skip in headless mode (no graphics context) if (headless::isActive()) return; - if (internal::inFboPass && internal::fboClearColorFunc) { + if (internal::currentWindowContext().inFboPass && internal::fboClearColorFunc) { // During FBO pass, call FBO's clearColor() to restart pass internal::fboClearColorFunc(r, g, b, a); - } else if (internal::inSwapchainPass) { + } else if (internal::currentWindowContext().inSwapchainPass) { // During swapchain pass - BlendMode prevBlendMode = internal::currentBlendMode; + BlendMode prevBlendMode = internal::currentWindowContext().currentBlendMode; sgl_push_matrix(); sgl_matrix_mode_projection(); @@ -238,7 +238,7 @@ void clear(float r, float g, float b, float a /* = 1.0f */) { // Save clear color — the pass will start in present() with this value. // sgl commands accumulate without an active pass, so FBOs can render // in their own passes without interrupting the swapchain pass. - internal::swapchainClearValue = { r, g, b, a }; + internal::currentWindowContext().swapchainClearValue = { r, g, b, a }; } } @@ -246,18 +246,18 @@ void clear(float r, float g, float b, float a /* = 1.0f */) { // パス管理関数(non-inline: Hot Reload時にHost/Guest間で同じ状態を参照するため) // --------------------------------------------------------------------------- void ensureSwapchainPass() { - if (!internal::inSwapchainPass && !internal::inFboPass) { + if (!internal::currentWindowContext().inSwapchainPass && !internal::currentWindowContext().inFboPass) { sg_pass pass = {}; pass.action.colors[0].load_action = SG_LOADACTION_CLEAR; - pass.action.colors[0].clear_value = internal::swapchainClearValue; + pass.action.colors[0].clear_value = internal::currentWindowContext().swapchainClearValue; pass.action.depth.load_action = SG_LOADACTION_CLEAR; pass.action.depth.clear_value = 1.0f; pass.swapchain = sglue_swapchain(); // Record the drawable we render into so end-of-frame capture reads back // THIS one (sapp_get_swapchain() advances the Metal drawable per call). - internal::lastSwapchainDrawable = pass.swapchain.metal.current_drawable; + internal::currentWindowContext().lastSwapchainDrawable = pass.swapchain.metal.current_drawable; sg_begin_pass(&pass); - internal::inSwapchainPass = true; + internal::currentWindowContext().inSwapchainPass = true; } } @@ -283,32 +283,32 @@ void present() { } sg_end_pass(); - internal::inSwapchainPass = false; + internal::currentWindowContext().inSwapchainPass = false; sg_commit(); } bool isInSwapchainPass() { - return internal::inSwapchainPass; + return internal::currentWindowContext().inSwapchainPass; } void suspendSwapchainPass() { - if (internal::inSwapchainPass) { + if (internal::currentWindowContext().inSwapchainPass) { sg_end_pass(); - internal::inSwapchainPass = false; + internal::currentWindowContext().inSwapchainPass = false; } } void resumeSwapchainPass() { - if (!internal::inSwapchainPass) { + if (!internal::currentWindowContext().inSwapchainPass) { sg_pass pass = {}; pass.action.colors[0].load_action = SG_LOADACTION_CLEAR; - pass.action.colors[0].clear_value = internal::swapchainClearValue; + pass.action.colors[0].clear_value = internal::currentWindowContext().swapchainClearValue; pass.action.depth.load_action = SG_LOADACTION_CLEAR; pass.action.depth.clear_value = 1.0f; pass.swapchain = sglue_swapchain(); - internal::lastSwapchainDrawable = pass.swapchain.metal.current_drawable; + internal::currentWindowContext().lastSwapchainDrawable = pass.swapchain.metal.current_drawable; sg_begin_pass(&pass); - internal::inSwapchainPass = true; + internal::currentWindowContext().inSwapchainPass = true; } } @@ -316,9 +316,25 @@ void resumeSwapchainPass() { // Non-inline singletons / accessors (Hot Reload: Host/Guest share state) // --------------------------------------------------------------------------- +namespace internal { +// The main window's state container. Non-inline so a hot-reload guest binds +// to the host's instance (same pattern as events()/getDefaultContext()). +WindowContext& mainWindowContext() { + static WindowContext ctx; + return ctx; +} +} // namespace internal + CoreEvents& events() { - static CoreEvents instance; - return instance; + // The CoreEvents instance is owned per window context. The main window's + // is wired lazily here (preserving the pre-context construction timing); + // Phase 1 pre-wires the pointer when constructing secondary contexts. + auto& ctx = internal::currentWindowContext(); + if (!ctx.coreEvents) { + static CoreEvents instance; // main-window storage + ctx.coreEvents = &instance; + } + return *ctx.coreEvents; } double getElapsedTime() { diff --git a/core/include/tc/app/tcHotReloadHost.h b/core/include/tc/app/tcHotReloadHost.h index d3dd7429..e6dab668 100644 --- a/core/include/tc/app/tcHotReloadHost.h +++ b/core/include/tc/app/tcHotReloadHost.h @@ -554,7 +554,7 @@ inline int runHotReloadApp(const WindowSettings& settings) { events().update.notify(); App* app = g_host.getApp(); if (app) { - app->handleUpdate(internal::mouseX, internal::mouseY); + app->handleUpdate(internal::currentWindowContext().mouseX, internal::currentWindowContext().mouseY); } }; @@ -638,7 +638,7 @@ inline int runHotReloadApp(const WindowSettings& settings) { desc.max_dropped_file_path_length = 2048; desc.enable_clipboard = true; desc.clipboard_size = settings.clipboardSize; - internal::clipboardSize = settings.clipboardSize; + internal::currentWindowContext().clipboardSize = settings.clipboardSize; sapp_run(&desc); return 0; diff --git a/core/include/tc/app/tcMouseGlobal.h b/core/include/tc/app/tcMouseGlobal.h index f85cd113..0a82f3df 100644 --- a/core/include/tc/app/tcMouseGlobal.h +++ b/core/include/tc/app/tcMouseGlobal.h @@ -7,20 +7,16 @@ // TrussC.h so lower-level headers (e.g. tcNode.h, which converts global mouse // coords to node-local) can depend on them directly, in dependency order, // instead of pulling the whole umbrella. Apps include ; the framework -// event loop writes internal::mouseX/Y each frame. This is an internal piece. +// event loop writes internal::currentWindowContext().mouseX/Y each frame. This is an internal piece. // ============================================================================= #include "tcMath.h" // Vec2 namespace trussc { -namespace internal { - // Mouse state (window coordinates), written by the framework event loop. - inline float mouseX = 0.0f; - inline float mouseY = 0.0f; - inline float pmouseX = 0.0f; // Previous frame mouse position - inline float pmouseY = 0.0f; -} +// Mouse position state (mouseX/Y, pmouseX/Y) lives in WindowContext +// (tc/app/tcWindowContext.h, included from TrussC.h before this header); +// the framework event loop writes the current window's members each frame. // --------------------------------------------------------------------------- // Mouse state (global / window coordinates) @@ -28,28 +24,28 @@ namespace internal { // Current mouse X coordinate (window coordinates) inline float getGlobalMouseX() { - return internal::mouseX; + return internal::currentWindowContext().mouseX; } // Current mouse Y coordinate (window coordinates) inline float getGlobalMouseY() { - return internal::mouseY; + return internal::currentWindowContext().mouseY; } // Previous frame mouse X coordinate (window coordinates) inline float getGlobalPMouseX() { - return internal::pmouseX; + return internal::currentWindowContext().pmouseX; } // Previous frame mouse Y coordinate (window coordinates) inline float getGlobalPMouseY() { - return internal::pmouseY; + return internal::currentWindowContext().pmouseY; } // Alias for getGlobalMouseX/Y (for tcDebugInput) -inline float getMouseX() { return internal::mouseX; } -inline float getMouseY() { return internal::mouseY; } -inline Vec2 getMousePos() { return Vec2(internal::mouseX, internal::mouseY); } +inline float getMouseX() { return getGlobalMouseX(); } +inline float getMouseY() { return getGlobalMouseY(); } +inline Vec2 getMousePos() { return Vec2(getGlobalMouseX(), getGlobalMouseY()); } inline Vec2 getGlobalMousePos() { return Vec2(getGlobalMouseX(), getGlobalMouseY()); } } // namespace trussc diff --git a/core/include/tc/app/tcWindowContext.h b/core/include/tc/app/tcWindowContext.h new file mode 100644 index 00000000..0647e938 --- /dev/null +++ b/core/include/tc/app/tcWindowContext.h @@ -0,0 +1,158 @@ +#pragma once + +// ============================================================================= +// tcWindowContext.h - per-window state container (internal) +// ============================================================================= +// +// Phase 0 of multi-window support: every piece of state that is conceptually +// "per window" — input, hover/scene, camera/projection, render-pass — lives in +// one WindowContext instead of scattered process globals. The app is still +// single-window: currentWindowContext() == mainWindowContext() always. Phase 1 +// creates additional contexts (one per native window) and switches +// internal::currentWindowCtx while dispatching each window's events and draw. +// +// This file is included from TrussC.h AFTER tcRenderTarget.h (RenderTarget by +// value) and tcCameraContext.h / tcCoreEvents.h; it is not self-contained. +// +// Hot-reload note: mainWindowContext() is NON-inline (defined in tcGlobal.cpp) +// so Host and Guest share one instance — same pattern as events() and +// getDefaultContext(). The RenderContext / CoreEvents pointers below are wired +// lazily by those accessors, preserving their existing construction timing. + +namespace trussc { + +class Node; +class CoreEvents; + +namespace internal { + +class RenderContext; // the real class lives in internal:: (tcRenderContext.h) + +// --------------------------------------------------------------------------- +// Scissor clipping stack (moved from TrussC.h) +// --------------------------------------------------------------------------- +struct ScissorRect { + float x, y, w, h; + bool active; // Whether a valid range exists in the stack +}; + +struct WindowContext { + WindowContext() = default; + // currentTarget points into this object — copying/moving would dangle it. + WindowContext(const WindowContext&) = delete; + WindowContext& operator=(const WindowContext&) = delete; + + // --- input state (written by the framework event loop) --- + float mouseX = 0.0f; + float mouseY = 0.0f; + float pmouseX = 0.0f; // Previous frame mouse position + float pmouseY = 0.0f; + int mouseButton = -1; // Currently pressed button (-1 = none) + bool mousePressed = false; + std::unordered_set keysPressed; + + // --- scene / hover state (per window's node tree) --- + Node* rootNode = nullptr; // The running App (set by the framework) + Node* hoveredNode = nullptr; // Currently hovered node + Node* prevHoveredNode = nullptr; // Previously hovered node + Node* grabbedNode = nullptr; // Node grabbed by mouse press + int grabbedButton = -1; // Mouse button that caused the grab + Node* selectedNode = nullptr; // Last node clicked (selection) + + // --- camera / projection state --- + std::shared_ptr currentCameraContext; + Mat4 currentViewMatrix = Mat4::identity(); + Mat4 currentProjectionMatrix = Mat4::identity(); + float currentScreenFov = 45.0f; + float currentViewW = 0.0f; + float currentViewH = 0.0f; + float currentCameraDist = 0.0f; // Distance from camera to Z=0 plane + + // --- render-pass state --- + RenderTarget swapchainTarget; // .context set after sgl_setup() + RenderTarget* currentTarget = &swapchainTarget; // Fbo::begin/end retargets this + sg_color swapchainClearValue = { 0.0f, 0.0f, 0.0f, 1.0f }; + bool inSwapchainPass = false; + bool inFboPass = false; + // Metal drawable actually rendered into this frame (see tcGlobal.cpp notes) + const void* lastSwapchainDrawable = nullptr; + std::vector scissorStack; + ScissorRect currentScissor = {0, 0, 0, 0, false}; + // Current 2D blend mode (the "role"); actual sgl pipeline in currentTarget + BlendMode currentBlendMode = BlendMode::Alpha; + + // --- misc per-window --- + int clipboardSize = 65536; // Clipboard buffer size (for overflow check) + + // Wired lazily by getDefaultContext() / events() for the main window + // (preserves their pre-context construction timing); Phase 1 pre-wires + // these when constructing secondary-window contexts. + RenderContext* render = nullptr; + CoreEvents* coreEvents = nullptr; +}; + +// Main window context. Non-inline (tcGlobal.cpp) — Host/Guest share one. +WindowContext& mainWindowContext(); + +// Active context while dispatching a window's events / draw. Null = main. +// (An inline variable: a hot-reload guest may hold its own copy, which stays +// null there and falls through to the shared mainWindowContext() — identical +// behavior while single-window.) +inline WindowContext* currentWindowCtx = nullptr; + +inline WindowContext& currentWindowContext() { + return currentWindowCtx ? *currentWindowCtx : mainWindowContext(); +} + +// --------------------------------------------------------------------------- +// Active render-target pipeline helpers (moved from tcRenderTarget.h — they +// resolve through the current window's target now) +// --------------------------------------------------------------------------- + +// Role keys: high nibble = role, low byte = blend mode (2D only). +inline sgl_pipeline active2D(BlendMode m) { return currentWindowContext().currentTarget->pipeline(0x000u | (uint32_t)m, pipeDesc2D(m)); } +inline sgl_pipeline activeFill2D() { return active2D(BlendMode::Alpha); } +inline sgl_pipeline activePremult() { return currentWindowContext().currentTarget->pipeline(0x100u, pipeDescPremult()); } +inline sgl_pipeline activeClear() { return currentWindowContext().currentTarget->pipeline(0x200u, pipeDesc2D(BlendMode::Disabled)); } +inline sgl_pipeline active3D() { return currentWindowContext().currentTarget->pipeline(0x300u, pipeDesc3D()); } + +// Single chokepoint for loading an sgl pipeline by role. No-op if the target isn't +// ready (id 0). In debug builds it asserts the pipeline was built for the active +// target's context — the runtime safety net against loading a pipeline meant for a +// different target (the bug class this refactor removes). +inline void loadPipeline(sgl_pipeline p) { + if (p.id == 0) return; +#ifndef NDEBUG + auto& owner = pipelineOwnerCtx(); + auto it = owner.find(p.id); + // Only check pipelines WE built (others, e.g. sgl's built-in default, are unknown). + assert((it == owner.end() || it->second == currentWindowContext().currentTarget->context.id) + && "loadPipeline: sgl pipeline built for a different render target than the active one"); +#endif + sgl_load_pipeline(p); +} + +// Restore the current blend pipeline after temporary pipeline changes. +// FBO uses its accumulating Fill2D; swapchain honors the current blend mode. +// (moved from TrussC.h) +inline void restoreCurrentPipeline() { + auto& ctx = currentWindowContext(); + loadPipeline(ctx.inFboPass ? activeFill2D() : active2D(ctx.currentBlendMode)); +} + +// Register a new camera scope (declared in tcCameraContext.h, defined here +// where WindowContext is complete). Always allocates a fresh context — see +// the immutability note in tcCameraContext.h. +inline void registerCameraContext(const Mat4& view, const Mat4& projection, + float viewW, float viewH, bool pickable) { + auto ctx = std::make_shared(); + ctx->view = view; + ctx->projection = projection; + ctx->viewW = viewW; + ctx->viewH = viewH; + ctx->pickable = pickable; + currentWindowContext().currentCameraContext = std::move(ctx); +} + +} // namespace internal +} // namespace trussc diff --git a/core/include/tc/gpu/tcFbo.h b/core/include/tc/gpu/tcFbo.h index 8d2f748e..248d1a65 100644 --- a/core/include/tc/gpu/tcFbo.h +++ b/core/include/tc/gpu/tcFbo.h @@ -247,23 +247,23 @@ class Fbo : public HasTexture { // Switch back to default context sgl_set_context(sgl_default_context()); active_ = false; - internal::inFboPass = false; + internal::currentWindowContext().inFboPass = false; internal::currentFbo = nullptr; internal::currentFboColorFormat = SG_PIXELFORMAT_RGBA8; internal::currentFboSampleCount = 1; - internal::currentTarget = &internal::swapchainTarget; // RenderTarget: back to swapchain + internal::currentWindowContext().currentTarget = &internal::currentWindowContext().swapchainTarget; // RenderTarget: back to swapchain internal::fboClearColorFunc = nullptr; // Restore the screen camera state saved in beginInternal() so // worldToScreen / camera-context stamping after end() see the screen // camera again instead of this FBO's projection. - internal::currentScreenFov = savedScreenFov_; - internal::currentViewW = savedViewW_; - internal::currentViewH = savedViewH_; - internal::currentCameraDist = savedCameraDist_; - internal::currentProjectionMatrix = savedProjectionMatrix_; - internal::currentViewMatrix = savedViewMatrix_; - internal::currentCameraContext = savedCameraContext_; + internal::currentWindowContext().currentScreenFov = savedScreenFov_; + internal::currentWindowContext().currentViewW = savedViewW_; + internal::currentWindowContext().currentViewH = savedViewH_; + internal::currentWindowContext().currentCameraDist = savedCameraDist_; + internal::currentWindowContext().currentProjectionMatrix = savedProjectionMatrix_; + internal::currentWindowContext().currentViewMatrix = savedViewMatrix_; + internal::currentWindowContext().currentCameraContext = savedCameraContext_; savedCameraContext_.reset(); // Resume swapchain pass (if we were in one before) @@ -589,7 +589,7 @@ class Fbo : public HasTexture { if (!allocated_) return; // Guard: nested FBO begin is not supported - if (internal::inFboPass) { + if (internal::currentWindowContext().inFboPass) { logWarning("Fbo") << "Nested fbo.begin() is not supported. " << "Call end() on the current FBO first."; return; @@ -645,13 +645,13 @@ class Fbo : public HasTexture { // projection setup below overwrites these globals, and anything drawn // after end() (worldToScreen, node camera-context stamping) must see // the screen camera again, not the FBO's. - savedScreenFov_ = internal::currentScreenFov; - savedViewW_ = internal::currentViewW; - savedViewH_ = internal::currentViewH; - savedCameraDist_ = internal::currentCameraDist; - savedProjectionMatrix_ = internal::currentProjectionMatrix; - savedViewMatrix_ = internal::currentViewMatrix; - savedCameraContext_ = internal::currentCameraContext; + savedScreenFov_ = internal::currentWindowContext().currentScreenFov; + savedViewW_ = internal::currentWindowContext().currentViewW; + savedViewH_ = internal::currentWindowContext().currentViewH; + savedCameraDist_ = internal::currentWindowContext().currentCameraDist; + savedProjectionMatrix_ = internal::currentWindowContext().currentProjectionMatrix; + savedViewMatrix_ = internal::currentWindowContext().currentViewMatrix; + savedCameraContext_ = internal::currentWindowContext().currentCameraContext; // Setup screen projection using defaultScreenFov (like main screen). // pickable=false: geometry drawn into an offscreen target must not be @@ -659,11 +659,11 @@ class Fbo : public HasTexture { internal::setupScreenFovWithSize(internal::defaultScreenFov, (float)width_, (float)height_, 0.0f, 0.0f, false); active_ = true; - internal::inFboPass = true; + internal::currentWindowContext().inFboPass = true; // Retarget to this FBO BEFORE loading its fill pipeline so activeFill2D() // resolves in the FBO's context/format (setupScreenFov above still ran on // the previous target, as before). - internal::currentTarget = &shared.target; + internal::currentWindowContext().currentTarget = &shared.target; // Use the FBO's alpha-blend Fill2D pipeline (Porter-Duff over); the result // is stored as premultiplied alpha in the FBO. diff --git a/core/include/tc/gpu/tcShader.h b/core/include/tc/gpu/tcShader.h index 5a3cbf1c..035f7964 100644 --- a/core/include/tc/gpu/tcShader.h +++ b/core/include/tc/gpu/tcShader.h @@ -438,7 +438,7 @@ class Shader { // sample count and depth match the FBO, so one is built lazily (from the same // createPipelineDesc()) and cached per distinct (format, sampleCount) target. sg_pipeline pipelineForCurrentTarget() { - if (!internal::inFboPass) return pipeline; + if (!internal::currentWindowContext().inFboPass) return pipeline; uint64_t key = ((uint64_t)internal::currentFboColorFormat << 8) | (uint64_t)(internal::currentFboSampleCount & 0xff); auto it = targetPipelines_.find(key); diff --git a/core/include/tc/gpu/tcTexture.h b/core/include/tc/gpu/tcTexture.h index e3663187..b1d3747c 100644 --- a/core/include/tc/gpu/tcTexture.h +++ b/core/include/tc/gpu/tcTexture.h @@ -904,7 +904,7 @@ class Texture { float u0, float v0, float u1, float v1) const { // Blend pipeline for the active target (behavior preserved: FBO uses Fill2D // even for premultiplied sources, as before). - if (internal::inFboPass) { + if (internal::currentWindowContext().inFboPass) { internal::loadPipeline(internal::activeFill2D()); } else if (premultipliedAlpha_) { internal::loadPipeline(internal::activePremult()); diff --git a/core/include/tc/graphics/tcCameraContext.h b/core/include/tc/graphics/tcCameraContext.h index 9de5771a..b7e3fd52 100644 --- a/core/include/tc/graphics/tcCameraContext.h +++ b/core/include/tc/graphics/tcCameraContext.h @@ -72,24 +72,17 @@ class CameraContext { namespace internal { -// The context the current drawing happens under (most recent registration). -// Nodes stamp this during drawTree(); null until the first camera setup of -// the process (e.g. headless mode), in which case picking falls back to the -// plain vertical screen ray. -inline std::shared_ptr currentCameraContext; +// The context the current drawing happens under (most recent registration) +// lives in WindowContext (tc/app/tcWindowContext.h): reach it via +// internal::currentWindowContext().currentCameraContext. Null until the first +// camera setup of the process (e.g. headless mode), in which case picking +// falls back to the plain vertical screen ray. // Register a new camera scope. Always allocates a fresh context — see the -// immutability note in the header comment. +// immutability note in the header comment. Defined in tcWindowContext.h +// (needs the complete WindowContext). inline void registerCameraContext(const Mat4& view, const Mat4& projection, - float viewW, float viewH, bool pickable = true) { - auto ctx = std::make_shared(); - ctx->view = view; - ctx->projection = projection; - ctx->viewW = viewW; - ctx->viewH = viewH; - ctx->pickable = pickable; - currentCameraContext = std::move(ctx); -} + float viewW, float viewH, bool pickable = true); // Per-pick ray cache used by one hit-test traversal: the cursor position plus // one lazily-built ray per camera context encountered (a frame typically has diff --git a/core/include/tc/graphics/tcRenderContext.cpp b/core/include/tc/graphics/tcRenderContext.cpp index 0ee0583a..1f55f902 100644 --- a/core/include/tc/graphics/tcRenderContext.cpp +++ b/core/include/tc/graphics/tcRenderContext.cpp @@ -187,7 +187,7 @@ bool internal::RenderContext::drawBitmapStringBillboard(const std::string& text, // Absence-of-context handling only (not a semantic gate): with no camera // context yet (headless / before the first camera setup) or a degenerate // viewport there is nothing to project through. - std::shared_ptr ctx = internal::currentCameraContext; + std::shared_ptr ctx = internal::currentWindowContext().currentCameraContext; if (!ctx || ctx->viewW <= 0.0f || ctx->viewH <= 0.0f) return false; if (text.empty()) return true; // handled: nothing to draw @@ -226,7 +226,7 @@ bool internal::RenderContext::drawBitmapStringBillboard(const std::string& text, sgl_matrix_mode_projection(); sgl_push_matrix(); sgl_load_identity(); - sgl_ortho(0.0f, internal::currentViewW, internal::currentViewH, 0.0f, -10000.0f, 10000.0f); + sgl_ortho(0.0f, internal::currentWindowContext().currentViewW, internal::currentWindowContext().currentViewH, 0.0f, -10000.0f, 10000.0f); sgl_matrix_mode_modelview(); sgl_push_matrix(); @@ -311,7 +311,7 @@ void internal::RenderContext::drawBitmapString(const std::string& text, float x, sgl_matrix_mode_projection(); sgl_push_matrix(); sgl_load_identity(); - sgl_ortho(0.0f, internal::currentViewW, internal::currentViewH, 0.0f, -10000.0f, 10000.0f); + sgl_ortho(0.0f, internal::currentWindowContext().currentViewW, internal::currentWindowContext().currentViewH, 0.0f, -10000.0f, 10000.0f); sgl_matrix_mode_modelview(); sgl_push_matrix(); @@ -484,7 +484,7 @@ void internal::RenderContext::drawBitmapString(const std::string& text, float x, sgl_matrix_mode_projection(); sgl_push_matrix(); sgl_load_identity(); - sgl_ortho(0.0f, internal::currentViewW, internal::currentViewH, 0.0f, -10000.0f, 10000.0f); + sgl_ortho(0.0f, internal::currentWindowContext().currentViewW, internal::currentWindowContext().currentViewH, 0.0f, -10000.0f, 10000.0f); sgl_matrix_mode_modelview(); sgl_push_matrix(); @@ -580,12 +580,19 @@ void internal::RenderContext::drawBitmapString(const std::string& text, float x, } // --------------------------------------------------------------------------- -// Default context singleton (non-inline — see tcRenderContext.h comment) +// Default context accessor (non-inline — see tcRenderContext.h comment) // --------------------------------------------------------------------------- namespace internal { RenderContext& getDefaultContext() { - static RenderContext instance; - return instance; + // The RenderContext is owned per window context. The main window's is + // wired lazily here (preserving the pre-context construction timing); + // Phase 1 pre-wires the pointer when constructing secondary contexts. + auto& ctx = currentWindowContext(); + if (!ctx.render) { + static RenderContext instance; // main-window storage + ctx.render = &instance; + } + return *ctx.render; } } // namespace internal diff --git a/core/include/tc/graphics/tcRenderContext.h b/core/include/tc/graphics/tcRenderContext.h index dae59dd0..3b02187b 100644 --- a/core/include/tc/graphics/tcRenderContext.h +++ b/core/include/tc/graphics/tcRenderContext.h @@ -59,15 +59,8 @@ namespace internal { extern sg_sampler fontSampler; extern bool fontInitialized; extern bool pixelPerfectMode; - // Current screen setup state (for 2D drawing in perspective mode) - extern float currentScreenFov; - extern float currentViewW; - extern float currentViewH; - extern float currentCameraDist; - - // View/Projection matrix tracking (for worldToScreen/screenToWorld) - extern Mat4 currentViewMatrix; - extern Mat4 currentProjectionMatrix; + // Screen setup / view-projection tracking state lives in WindowContext + // (tc/app/tcWindowContext.h, included before this header from TrussC.h). } // --------------------------------------------------------------------------- @@ -275,9 +268,9 @@ class RenderContext { // ----------------------------------------------------------------------- void pushStyle() { - // Capture the live blend mode (it lives in internal::currentBlendMode, not in + // Capture the live blend mode (it lives in internal::currentWindowContext().currentBlendMode, not in // style_) so it is saved/restored alongside color/fill/stroke. - style_.blend = internal::currentBlendMode; + style_.blend = internal::currentWindowContext().currentBlendMode; styleStack_.push_back(style_); } @@ -300,9 +293,9 @@ class RenderContext { // load is otherwise a no-op that sokol_gl batches away, but we skip it anyway). // No-op inside an FBO pass, which uses its own accumulating pipeline. void applyBlend(BlendMode mode) { - if (mode == internal::currentBlendMode) return; - internal::currentBlendMode = mode; - if (!internal::inFboPass) { + if (mode == internal::currentWindowContext().currentBlendMode) return; + internal::currentWindowContext().currentBlendMode = mode; + if (!internal::currentWindowContext().inFboPass) { internal::loadPipeline(internal::active2D(mode)); } } @@ -380,7 +373,7 @@ class RenderContext { // Restore default view matrix (camera lookat) set by setupScreenFovWithSize // This works correctly both in main screen and FBO contexts // Note: Mat4 is row-major, sokol_gl expects column-major, so transpose - Mat4 t = internal::currentViewMatrix.transposed(); + Mat4 t = internal::currentWindowContext().currentViewMatrix.transposed(); sgl_load_matrix(t.m); } @@ -964,7 +957,7 @@ class RenderContext { Direction textAlignH = Direction::Left; Direction textAlignV = Direction::Top; float bitmapLineHeight = bitmapfont::CHAR_TEX_HEIGHT; - // Mirror of internal::currentBlendMode. blend state itself lives outside the + // Mirror of internal::currentWindowContext().currentBlendMode. blend state itself lives outside the // RenderContext (it maps to an sgl pipeline), so push/popStyle capture and // restore it explicitly rather than via this field's value alone. BlendMode blend = BlendMode::Alpha; diff --git a/core/include/tc/graphics/tcRenderTarget.h b/core/include/tc/graphics/tcRenderTarget.h index c61b9fdc..d9fdf38a 100644 --- a/core/include/tc/graphics/tcRenderTarget.h +++ b/core/include/tc/graphics/tcRenderTarget.h @@ -138,30 +138,8 @@ struct RenderTarget { } }; -inline RenderTarget swapchainTarget; // .context set after sgl_setup() -inline RenderTarget* currentTarget = &swapchainTarget; // Fbo::begin/end retargets this - -// Role keys: high nibble = role, low byte = blend mode (2D only). -inline sgl_pipeline active2D(BlendMode m) { return currentTarget->pipeline(0x000u | (uint32_t)m, pipeDesc2D(m)); } -inline sgl_pipeline activeFill2D() { return active2D(BlendMode::Alpha); } -inline sgl_pipeline activePremult() { return currentTarget->pipeline(0x100u, pipeDescPremult()); } -inline sgl_pipeline activeClear() { return currentTarget->pipeline(0x200u, pipeDesc2D(BlendMode::Disabled)); } -inline sgl_pipeline active3D() { return currentTarget->pipeline(0x300u, pipeDesc3D()); } - -// Single chokepoint for loading an sgl pipeline by role. No-op if the target isn't -// ready (id 0). In debug builds it asserts the pipeline was built for the active -// target's context — the runtime safety net against loading a pipeline meant for a -// different target (the bug class this refactor removes). -inline void loadPipeline(sgl_pipeline p) { - if (p.id == 0) return; -#ifndef NDEBUG - auto& owner = pipelineOwnerCtx(); - auto it = owner.find(p.id); - // Only check pipelines WE built (others, e.g. sgl's built-in default, are unknown). - assert((it == owner.end() || it->second == currentTarget->context.id) - && "loadPipeline: sgl pipeline built for a different render target than the active one"); -#endif - sgl_load_pipeline(p); -} +// The swapchain target, the active-target pointer, and the active*() pipeline +// helpers moved to the per-window WindowContext — see tc/app/tcWindowContext.h +// (included right after this header from TrussC.h). }} // namespace trussc::internal diff --git a/core/include/tc/utils/tcStandardTools.h b/core/include/tc/utils/tcStandardTools.h index ed4a37d4..38c47416 100644 --- a/core/include/tc/utils/tcStandardTools.h +++ b/core/include/tc/utils/tcStandardTools.h @@ -263,8 +263,8 @@ inline void registerDebuggerTools() { } // Update global mouse state - ::trussc::internal::mouseX = x; - ::trussc::internal::mouseY = y; + ::trussc::internal::currentWindowContext().mouseX = x; + ::trussc::internal::currentWindowContext().mouseY = y; return json{{"status", "ok"}}; }); @@ -283,8 +283,8 @@ inline void registerDebuggerTools() { args.button = a.value("button", 0); args.syncLegacy(); - ::trussc::internal::mouseX = args.pos.x; - ::trussc::internal::mouseY = args.pos.y; + ::trussc::internal::currentWindowContext().mouseX = args.pos.x; + ::trussc::internal::currentWindowContext().mouseY = args.pos.y; events().mousePressed.notify(args); if (::trussc::internal::appMousePressedFunc) @@ -303,8 +303,8 @@ inline void registerDebuggerTools() { args.button = a.value("button", 0); args.syncLegacy(); - ::trussc::internal::mouseX = args.pos.x; - ::trussc::internal::mouseY = args.pos.y; + ::trussc::internal::currentWindowContext().mouseX = args.pos.x; + ::trussc::internal::currentWindowContext().mouseY = args.pos.y; events().mouseReleased.notify(args); if (::trussc::internal::appMouseReleasedFunc) @@ -351,7 +351,7 @@ inline void registerDebuggerTools() { .arg("dy", "Vertical scroll delta") .bind([](float dx, float dy) { ScrollEventArgs args; - args.pos = args.globalPos = Vec2(::trussc::internal::mouseX, ::trussc::internal::mouseY); + args.pos = args.globalPos = Vec2(::trussc::internal::currentWindowContext().mouseX, ::trussc::internal::currentWindowContext().mouseY); args.scroll = Vec2(dx, dy); args.syncLegacy(); events().mouseScrolled.notify(args); diff --git a/core/include/tcBaseApp.h b/core/include/tcBaseApp.h index 06a6319a..8648533c 100644 --- a/core/include/tcBaseApp.h +++ b/core/include/tcBaseApp.h @@ -42,11 +42,11 @@ class App : public RectNode { // The App is the scene-graph root; expose it via getRootNode() so // tools (e.g. the MCP node tools) can walk the tree. - internal::rootNode = this; + internal::currentWindowContext().rootNode = this; } virtual ~App() { - if (internal::rootNode == this) internal::rootNode = nullptr; + if (internal::currentWindowContext().rootNode == this) internal::currentWindowContext().rootNode = nullptr; } // ------------------------------------------------------------------------- diff --git a/core/include/tcNode.h b/core/include/tcNode.h index 51e9a0b7..fc057b26 100644 --- a/core/include/tcNode.h +++ b/core/include/tcNode.h @@ -65,15 +65,10 @@ class Mod; using NodePtr = std::shared_ptr; using NodeWeakPtr = std::weak_ptr; -// Hover state cache (updated once per frame) +// Hover state cache (updated once per frame): hoveredNode / prevHoveredNode / +// grabbedNode / grabbedButton / selectedNode / rootNode moved to WindowContext +// (tc/app/tcWindowContext.h) — each window's node tree has its own. namespace internal { - inline Node* hoveredNode = nullptr; // Currently hovered node - inline Node* prevHoveredNode = nullptr; // Previously hovered node - inline Node* grabbedNode = nullptr; // Node grabbed by mouse press - inline int grabbedButton = -1; // Mouse button that caused the grab - inline Node* selectedNode = nullptr; // Last node clicked (selection) - inline Node* rootNode = nullptr; // The running App (set by the framework) - // Overlay (e.g. tcxImGui) capture queries. An overlay registers these so the // framework knows when the pointer is over it / it owns keyboard focus. Null // when no overlay is present, so plain apps are unaffected. @@ -340,7 +335,7 @@ class Node : public std::enable_shared_from_this { bool isEventsEnabled() const { return eventsEnabled_; } // Whether mouse is over this node (auto-updated each frame, O(1)) - bool isMouseOver() const { return internal::hoveredNode == this; } + bool isMouseOver() const { return internal::currentWindowContext().hoveredNode == this; } // ------------------------------------------------------------------------- // Identity / Name @@ -846,13 +841,13 @@ class Node : public std::enable_shared_from_this { } // Clear global references to this node (prevent dangling pointers) - if (internal::hoveredNode == this) internal::hoveredNode = nullptr; - if (internal::prevHoveredNode == this) internal::prevHoveredNode = nullptr; - if (internal::grabbedNode == this) { - internal::grabbedNode = nullptr; - internal::grabbedButton = -1; + if (internal::currentWindowContext().hoveredNode == this) internal::currentWindowContext().hoveredNode = nullptr; + if (internal::currentWindowContext().prevHoveredNode == this) internal::currentWindowContext().prevHoveredNode = nullptr; + if (internal::currentWindowContext().grabbedNode == this) { + internal::currentWindowContext().grabbedNode = nullptr; + internal::currentWindowContext().grabbedButton = -1; } - if (internal::selectedNode == this) internal::selectedNode = nullptr; + if (internal::currentWindowContext().selectedNode == this) internal::currentWindowContext().selectedNode = nullptr; dead_ = true; cleanup(); @@ -870,8 +865,8 @@ class Node : public std::enable_shared_from_this { // Stamp the camera scope this node is being drawn under (pointer // compare first — steady state is one assignment skip per frame). - if (internal::currentCameraContext && cameraContext_ != internal::currentCameraContext) { - cameraContext_ = internal::currentCameraContext; + if (internal::currentWindowContext().currentCameraContext && cameraContext_ != internal::currentWindowContext().currentCameraContext) { + cameraContext_ = internal::currentWindowContext().currentCameraContext; } pushMatrix(); @@ -946,14 +941,14 @@ class Node : public std::enable_shared_from_this { HitResult result = findHitNodeFromScreen(e.globalPos.x, e.globalPos.y); // Selection: clicking a node selects it; clicking empty space clears it. - internal::selectedNode = result.hit() ? result.node.get() : nullptr; + internal::currentWindowContext().selectedNode = result.hit() ? result.node.get() : nullptr; if (result.hit()) { MouseEventArgs local = result.node->localizeMouse(e); if (result.node->fireMousePress(local)) { // Set grabbed node for drag tracking - internal::grabbedNode = result.node.get(); - internal::grabbedButton = e.button; + internal::currentWindowContext().grabbedNode = result.node.get(); + internal::currentWindowContext().grabbedButton = e.button; return result.node; } } @@ -963,16 +958,16 @@ class Node : public std::enable_shared_from_this { Ptr dispatchMouseRelease(const MouseEventArgs& e) { // Send release to grabbed node if it exists - if (internal::grabbedNode && internal::grabbedButton == e.button) { - MouseEventArgs local = internal::grabbedNode->localizeMouse(e); - internal::grabbedNode->fireMouseRelease(local); + if (internal::currentWindowContext().grabbedNode && internal::currentWindowContext().grabbedButton == e.button) { + MouseEventArgs local = internal::currentWindowContext().grabbedNode->localizeMouse(e); + internal::currentWindowContext().grabbedNode->fireMouseRelease(local); Ptr result = std::dynamic_pointer_cast( - internal::grabbedNode->shared_from_this()); + internal::currentWindowContext().grabbedNode->shared_from_this()); // Clear grabbed state - internal::grabbedNode = nullptr; - internal::grabbedButton = -1; + internal::currentWindowContext().grabbedNode = nullptr; + internal::currentWindowContext().grabbedButton = -1; return result; } @@ -992,10 +987,10 @@ class Node : public std::enable_shared_from_this { Ptr dispatchMouseMove(const internal::MouseEventRaw& e) { // Send drag event to grabbed node - if (internal::grabbedNode) { - internal::MouseEventRaw local = internal::grabbedNode->localizeMouse(e); - local.button = internal::grabbedButton; - internal::grabbedNode->fireMouseDrag(internal::toDragArgs(local)); + if (internal::currentWindowContext().grabbedNode) { + internal::MouseEventRaw local = internal::currentWindowContext().grabbedNode->localizeMouse(e); + local.button = internal::currentWindowContext().grabbedButton; + internal::currentWindowContext().grabbedNode->fireMouseDrag(internal::toDragArgs(local)); } // Also send move event to hit node (for hover, etc.) @@ -1046,7 +1041,7 @@ class Node : public std::enable_shared_from_this { // Update hover state (call once per frame) void updateHoverState(float screenX, float screenY) { // Save previous frame's hovered node - internal::prevHoveredNode = internal::hoveredNode; + internal::currentWindowContext().prevHoveredNode = internal::currentWindowContext().hoveredNode; // Search for new hovered node. When an overlay (e.g. a tcxImGui panel) // has the pointer, the tree hovers nothing — so a node under the panel @@ -1057,15 +1052,15 @@ class Node : public std::enable_shared_from_this { HitResult result = findHitNodeFromScreen(screenX, screenY); hit = result.hit() ? result.node.get() : nullptr; } - internal::hoveredNode = hit; + internal::currentWindowContext().hoveredNode = hit; // Fire Enter/Leave events - if (internal::prevHoveredNode != internal::hoveredNode) { - if (internal::prevHoveredNode) { - internal::prevHoveredNode->fireMouseLeave(); + if (internal::currentWindowContext().prevHoveredNode != internal::currentWindowContext().hoveredNode) { + if (internal::currentWindowContext().prevHoveredNode) { + internal::currentWindowContext().prevHoveredNode->fireMouseLeave(); } - if (internal::hoveredNode) { - internal::hoveredNode->fireMouseEnter(); + if (internal::currentWindowContext().hoveredNode) { + internal::currentWindowContext().hoveredNode->fireMouseEnter(); } } } @@ -1464,12 +1459,12 @@ inline void Mod::removeSelf() { // Selection — the last-clicked node, held by the Node system (set in // dispatchMousePress, cleared when the node is destroyed). A tool such as an // inspector can both read it and drive it via setSelectedNode(). -inline Node* getSelectedNode() { return internal::selectedNode; } -inline void setSelectedNode(Node* n) { internal::selectedNode = n; } +inline Node* getSelectedNode() { return internal::currentWindowContext().selectedNode; } +inline void setSelectedNode(Node* n) { internal::currentWindowContext().selectedNode = n; } // The running App as the root of the node tree (set by the framework while the // app is alive, null otherwise). Lets tools — e.g. the MCP node tools — walk // the whole tree without the app passing itself around. -inline Node* getRootNode() { return internal::rootNode; } +inline Node* getRootNode() { return internal::currentWindowContext().rootNode; } } // namespace trussc diff --git a/core/platform/ios/tcPlatform_ios.mm b/core/platform/ios/tcPlatform_ios.mm index 41635980..8f79588c 100644 --- a/core/platform/ios/tcPlatform_ios.mm +++ b/core/platform/ios/tcPlatform_ios.mm @@ -138,7 +138,7 @@ bool captureWindow(Pixels& outPixels) { // swapchain pass). NOT sapp_get_swapchain() — that advances to the next, // unrendered drawable, scrambling captures on GPU-heavy frames. Fall back // only if no pass ran yet. (Mirrors the macOS fix.) - const void* drawablePtr = internal::lastSwapchainDrawable; + const void* drawablePtr = internal::currentWindowContext().lastSwapchainDrawable; if (!drawablePtr) drawablePtr = sapp_get_swapchain().metal.current_drawable; id drawable = (__bridge id)drawablePtr; if (!drawable) { diff --git a/core/platform/mac/tcPlatform_mac.mm b/core/platform/mac/tcPlatform_mac.mm index 9eeca2af..1d152175 100644 --- a/core/platform/mac/tcPlatform_mac.mm +++ b/core/platform/mac/tcPlatform_mac.mm @@ -252,7 +252,7 @@ void setWindowSizeLogical(int width, int height) { // Read back the drawable this frame ACTUALLY rendered into (recorded by the // swapchain pass). NOT sapp_get_swapchain() — that advances to the next, // unrendered drawable. Fall back only if no pass ran yet. - const void* drawablePtr = internal::lastSwapchainDrawable; + const void* drawablePtr = internal::currentWindowContext().lastSwapchainDrawable; if (!drawablePtr) drawablePtr = sapp_get_swapchain().metal.current_drawable; id drawable = (__bridge id)drawablePtr; if (!drawable) return false; From 1add9f328bb4dd8a96dd30e74c7b1fca0e7561da Mon Sep 17 00:00:00 2001 From: tettou771 Date: Sun, 5 Jul 2026 00:49:15 +0900 Subject: [PATCH 06/87] feat: per-window swapchain + dimension seams in the window context MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit WindowContext gains isMain/fbWidth/fbHeight/dpiScale and an acquireSwapchain provider; ensureSwapchainPass/resumeSwapchainPass use it (main window falls back to sglue_swapchain as before). All dimension-dependent code paths that can run under a secondary window's context (getWindowWidth/getFramebufferWidth/getDpiScale, internal::setupScreenFov, EasyCam, fullscreen-shader ortho, RectNode dpi, scissor reset) now read the ACTIVE context instead of sapp directly — a secondary window on a display with a different size or scale gets its own projection and mouse coordinate space. Node befriends Window for tree dispatch. Zero behavior change for single-window apps (main context still reads sapp). --- core/include/TrussC.h | 33 +++++++++++++++++---------- core/include/tc/3d/tcEasyCam.h | 6 ++--- core/include/tc/app/tcGlobal.cpp | 18 ++++++++++----- core/include/tc/app/tcWindowContext.h | 14 ++++++++++++ core/include/tc/gpu/tcShader.h | 2 +- core/include/tc/types/tcRectNode.h | 2 +- core/include/tcNode.h | 3 ++- 7 files changed, 54 insertions(+), 24 deletions(-) diff --git a/core/include/TrussC.h b/core/include/TrussC.h index 1692212e..29b168e9 100644 --- a/core/include/TrussC.h +++ b/core/include/TrussC.h @@ -311,17 +311,21 @@ void cleanup(); // --------------------------------------------------------------------------- // Get DPI scale (e.g., 2.0 for Retina displays) +// Routed through the active window context (secondary windows keep their own). inline float getDpiScale() { - return sapp_dpi_scale(); + auto& ctx = internal::currentWindowContext(); + return ctx.isMain ? sapp_dpi_scale() : ctx.dpiScale; } // Get actual framebuffer size (in pixels) inline int getFramebufferWidth() { - return sapp_width(); + auto& ctx = internal::currentWindowContext(); + return ctx.isMain ? sapp_width() : ctx.fbWidth; } inline int getFramebufferHeight() { - return sapp_height(); + auto& ctx = internal::currentWindowContext(); + return ctx.isMain ? sapp_height() : ctx.fbHeight; } // Call at frame start (before clear) @@ -510,7 +514,7 @@ inline void setScissor(int x, int y, int w, int h) { // Reset scissor to entire window inline void resetScissor() { internal::currentWindowContext().currentScissor.active = false; - sgl_scissor_rect(0, 0, sapp_width(), sapp_height(), true); + sgl_scissor_rect(0, 0, getFramebufferWidth(), getFramebufferHeight(), true); } // Save scissor to stack and set new range (intersection with current range) @@ -545,7 +549,7 @@ inline void popScissor() { sgl_scissor_rectf(internal::currentWindowContext().currentScissor.x, internal::currentWindowContext().currentScissor.y, internal::currentWindowContext().currentScissor.w, internal::currentWindowContext().currentScissor.h, true); } else { - sgl_scissor_rect(0, 0, sapp_width(), sapp_height(), true); + sgl_scissor_rect(0, 0, getFramebufferWidth(), getFramebufferHeight(), true); } } @@ -708,9 +712,11 @@ namespace internal { // Wrapper that uses main screen size inline void setupScreenFov(float fovDeg, float nearDist, float farDist) { - float dpiScale = sapp_dpi_scale(); - float viewW = pixelPerfectMode ? (float)sapp_width() : (float)sapp_width() / dpiScale; - float viewH = pixelPerfectMode ? (float)sapp_height() : (float)sapp_height() / dpiScale; + // Per-window: framebuffer size and dpi come from the ACTIVE window + // context (a secondary window on another display has its own scale). + float dpiScale = getDpiScale(); + float viewW = pixelPerfectMode ? (float)getFramebufferWidth() : getFramebufferWidth() / dpiScale; + float viewH = pixelPerfectMode ? (float)getFramebufferHeight() : getFramebufferHeight() / dpiScale; setupScreenFovWithSize(fovDeg, viewW, viewH, nearDist, farDist); } } // namespace internal @@ -1491,17 +1497,17 @@ TC_PLATFORMS("android,ios") void setOrientation(Orientation mask); // Get window width (size corresponding to coordinate system) inline int getWindowWidth() { if (internal::pixelPerfectMode) { - return sapp_width(); // Framebuffer size + return getFramebufferWidth(); // Framebuffer size } - return static_cast(sapp_width() / sapp_dpi_scale()); // Logical size + return static_cast(getFramebufferWidth() / getDpiScale()); // Logical size } // Get window height (size corresponding to coordinate system) inline int getWindowHeight() { if (internal::pixelPerfectMode) { - return sapp_height(); // Framebuffer size + return getFramebufferHeight(); // Framebuffer size } - return static_cast(sapp_height() / sapp_dpi_scale()); // Logical size + return static_cast(getFramebufferHeight() / getDpiScale()); // Logical size } // Get window size as Vec2 @@ -2885,5 +2891,8 @@ namespace tc = trussc; // using namespace std; // using namespace tc; +// Secondary windows (multi-window; needs Node/CoreEvents/WindowSettings above) +#include "tc/app/tcWindow.h" + // TrussC standard MCP tools (included last to resolve dependencies) #include "tc/utils/tcStandardTools.h" diff --git a/core/include/tc/3d/tcEasyCam.h b/core/include/tc/3d/tcEasyCam.h index 7526f44f..3fcbe286 100644 --- a/core/include/tc/3d/tcEasyCam.h +++ b/core/include/tc/3d/tcEasyCam.h @@ -43,9 +43,9 @@ class EasyCam { // the format-correct one; no per-site swapchain/FBO branch. internal::loadPipeline(internal::active3D()); - float dpiScale = sapp_dpi_scale(); - float w = (float)sapp_width() / dpiScale; - float h = (float)sapp_height() / dpiScale; + float dpiScale = getDpiScale(); + float w = (float)getFramebufferWidth() / dpiScale; + float h = (float)getFramebufferHeight() / dpiScale; float aspect = w / h; // Camera basis from orientation quaternion (no gimbal lock / no pole diff --git a/core/include/tc/app/tcGlobal.cpp b/core/include/tc/app/tcGlobal.cpp index 57e460df..4336ceb0 100644 --- a/core/include/tc/app/tcGlobal.cpp +++ b/core/include/tc/app/tcGlobal.cpp @@ -252,12 +252,16 @@ void ensureSwapchainPass() { pass.action.colors[0].clear_value = internal::currentWindowContext().swapchainClearValue; pass.action.depth.load_action = SG_LOADACTION_CLEAR; pass.action.depth.clear_value = 1.0f; - pass.swapchain = sglue_swapchain(); + auto& ctx = internal::currentWindowContext(); + // Secondary windows provide their own swapchain (same drawable for the + // whole window frame); the main window uses sokol_glue as before. + pass.swapchain = ctx.acquireSwapchain ? ctx.acquireSwapchain(ctx.acquireSwapchainUser) + : sglue_swapchain(); // Record the drawable we render into so end-of-frame capture reads back // THIS one (sapp_get_swapchain() advances the Metal drawable per call). - internal::currentWindowContext().lastSwapchainDrawable = pass.swapchain.metal.current_drawable; + ctx.lastSwapchainDrawable = pass.swapchain.metal.current_drawable; sg_begin_pass(&pass); - internal::currentWindowContext().inSwapchainPass = true; + ctx.inSwapchainPass = true; } } @@ -305,10 +309,12 @@ void resumeSwapchainPass() { pass.action.colors[0].clear_value = internal::currentWindowContext().swapchainClearValue; pass.action.depth.load_action = SG_LOADACTION_CLEAR; pass.action.depth.clear_value = 1.0f; - pass.swapchain = sglue_swapchain(); - internal::currentWindowContext().lastSwapchainDrawable = pass.swapchain.metal.current_drawable; + auto& ctx = internal::currentWindowContext(); + pass.swapchain = ctx.acquireSwapchain ? ctx.acquireSwapchain(ctx.acquireSwapchainUser) + : sglue_swapchain(); + ctx.lastSwapchainDrawable = pass.swapchain.metal.current_drawable; sg_begin_pass(&pass); - internal::currentWindowContext().inSwapchainPass = true; + ctx.inSwapchainPass = true; } } diff --git a/core/include/tc/app/tcWindowContext.h b/core/include/tc/app/tcWindowContext.h index 0647e938..c76f0e96 100644 --- a/core/include/tc/app/tcWindowContext.h +++ b/core/include/tc/app/tcWindowContext.h @@ -81,6 +81,20 @@ struct WindowContext { // Current 2D blend mode (the "role"); actual sgl pipeline in currentTarget BlendMode currentBlendMode = BlendMode::Alpha; + // --- window identity / swapchain source (Phase 1 seam) --- + // Main window: isMain=true, swapchain comes from sglue_swapchain() and + // dimensions from sapp. Secondary windows: the native layer sets isMain + // false, keeps fbWidth/fbHeight/dpiScale up to date, and provides the + // frame's swapchain through acquireSwapchain (called by + // ensureSwapchainPass/resumeSwapchainPass; must return the SAME drawable + // for the duration of one window frame). + bool isMain = true; + int fbWidth = 0; + int fbHeight = 0; + float dpiScale = 1.0f; + sg_swapchain (*acquireSwapchain)(void* user) = nullptr; + void* acquireSwapchainUser = nullptr; + // --- misc per-window --- int clipboardSize = 65536; // Clipboard buffer size (for overflow check) diff --git a/core/include/tc/gpu/tcShader.h b/core/include/tc/gpu/tcShader.h index 035f7964..055accda 100644 --- a/core/include/tc/gpu/tcShader.h +++ b/core/include/tc/gpu/tcShader.h @@ -615,7 +615,7 @@ class FullscreenShader : public Shader { sg_reset_state_cache(); sgl_defaults(); sgl_matrix_mode_projection(); - sgl_ortho(0.0f, (float)sapp_width(), (float)sapp_height(), 0.0f, -10000.0f, 10000.0f); + sgl_ortho(0.0f, (float)getFramebufferWidth(), (float)getFramebufferHeight(), 0.0f, -10000.0f, 10000.0f); sgl_matrix_mode_modelview(); sgl_load_identity(); } diff --git a/core/include/tc/types/tcRectNode.h b/core/include/tc/types/tcRectNode.h index abe89945..1d2ea028 100644 --- a/core/include/tc/types/tcRectNode.h +++ b/core/include/tc/types/tcRectNode.h @@ -190,7 +190,7 @@ class RectNode : public Node { float gx1 = g1.x, gy1 = g1.y, gx2 = g2.x, gy2 = g2.y; // Calculate rectangle in screen coordinates (considering DPI scale) - float dpi = sapp_dpi_scale(); + float dpi = getDpiScale(); float sx = std::min(gx1, gx2) * dpi; float sy = std::min(gy1, gy2) * dpi; float sw = std::abs(gx2 - gx1) * dpi; diff --git a/core/include/tcNode.h b/core/include/tcNode.h index fc057b26..3243522c 100644 --- a/core/include/tcNode.h +++ b/core/include/tcNode.h @@ -89,7 +89,8 @@ inline bool isOverlayFocused() { return internal::overlayFocusedQuery && interna // ============================================================================= class Node : public std::enable_shared_from_this { - friend class App; // Allow App to call dispatch methods + friend class App; // Allow App to call dispatch methods + friend class Window; // Secondary windows drive their own tree (tcWindow.h) friend class Mod; // Allow Mod to access owner_ public: From 47f93c117cbdd675f23a195d4796ff6e45e312e8 Mon Sep 17 00:00:00 2001 From: tettou771 Date: Sun, 5 Jul 2026 00:49:15 +0900 Subject: [PATCH 07/87] feat: secondary windows on macOS (createWindow, per-display tick) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Public API: createWindow(WindowSettings) -> shared_ptr with setRoot/events/close/setTitle; macOS only for now (stub elsewhere). Native glue: one NSWindow + CAMetalLayer + CADisplayLink per window, ticking on the main run loop at that display's own rate (120/60 mixed refresh works by construction). A fully occluded window skips drawable acquisition — the standalone PoC measured the alternative: acquiring while occluded blocks ~1s and drags the whole main thread to 9 fps (the known upstream sokol_app multi-window stall). Each window has its own sokol_gl context, MSAA + depth textures, mouse/key forwarding into its WindowContext, and draws its own Node tree; GPU resources are shared zero-copy through the single sokol_gfx context. Closing a secondary window (user or close()) leaves the app running. --- core/include/tc/app/tcWindow.h | 106 +++++++++ core/platform/mac/tcWindowMac.mm | 386 +++++++++++++++++++++++++++++++ 2 files changed, 492 insertions(+) create mode 100644 core/include/tc/app/tcWindow.h create mode 100644 core/platform/mac/tcWindowMac.mm diff --git a/core/include/tc/app/tcWindow.h b/core/include/tc/app/tcWindow.h new file mode 100644 index 00000000..78b19a77 --- /dev/null +++ b/core/include/tc/app/tcWindow.h @@ -0,0 +1,106 @@ +#pragma once + +// ============================================================================= +// tcWindow.h - Secondary application windows (multi-window, Phase 1) +// ============================================================================= +// +// One App, many windows: the main window keeps the classic implicit lifecycle +// (runApp / update / draw), a secondary Window owns its own Node tree, events +// and render state (WindowContext) and is driven by its display's own vsync +// (macOS: one CADisplayLink per window, delivered on the main run loop). +// Everything runs synchronously on the main thread; a fully occluded window +// simply skips drawable acquisition (fps 0), so it can never stall the others. +// +// GPU resources (Texture / Fbo / Mesh / Font) live in the single shared +// sokol_gfx context and can be used freely from any window. +// +// Platform support: macOS only for now. On other platforms createWindow() +// logs an error and returns nullptr. +// +// This file is included from TrussC.h (after Node / CoreEvents / WindowSettings). + +namespace trussc { + +class TC_PLATFORMS("macos") Window { +public: + ~Window(); + + // Attach the Node tree shown in this window (setup runs on first tick). + void setRoot(std::shared_ptr root); + std::shared_ptr getRoot() const { return root_; } + + // This window's event stream (mousePressed / keyPressed / draw / ...). + CoreEvents& events() { return events_; } + + // Close the native window. The main window and other windows keep running. + void close(); + bool isOpen() const { return native_ != nullptr; } + + void setTitle(const std::string& title); + int getWidth() const; // logical size (matches the window's coordinates) + int getHeight() const; + + void setClearColor(const Color& c) { clearColor_ = c; } + + internal::WindowContext& context() { return ctx_; } + + // --- tree driving (called by the platform glue; friend access to Node) --- + void dispatchMousePressToTree(const MouseEventArgs& e) { + if (ctx_.rootNode) ctx_.rootNode->dispatchMousePress(e); + } + void dispatchMouseReleaseToTree(const MouseEventArgs& e) { + if (ctx_.rootNode) ctx_.rootNode->dispatchMouseRelease(e); + } + void tickTree() { + if (!ctx_.rootNode) return; + ctx_.rootNode->updateTree(); + ctx_.rootNode->updateHoverState(ctx_.mouseX, ctx_.mouseY); + } + void drawTreeNow() { + if (ctx_.rootNode) ctx_.rootNode->drawTree(); + } + + // --- internal (used by the platform glue; not user API) --- + Window(); + void* native_ = nullptr; // platform-side state + internal::WindowContext ctx_; + CoreEvents events_; + internal::RenderContext render_; + std::shared_ptr root_; + bool rootSetupDone_ = false; + Color clearColor_ = Color(0.05f, 0.05f, 0.08f, 1.0f); +}; + +// Create a secondary window. macOS only for now (nullptr elsewhere). +// The returned shared_ptr keeps the window alive; closing the window (user or +// close()) destroys the native side while the handle stays valid (isOpen()). +TC_PLATFORMS("macos") std::shared_ptr createWindow(const WindowSettings& settings = {}); + +inline Window::Window() { + ctx_.isMain = false; + ctx_.render = &render_; + ctx_.coreEvents = &events_; +} + +inline void Window::setRoot(std::shared_ptr root) { + root_ = std::move(root); + ctx_.rootNode = root_.get(); + rootSetupDone_ = false; +} + +#if !defined(__APPLE__) +// Non-macOS stubs. The real implementations live in platform/mac/tcWindowMac.mm. +// (iOS also defines __APPLE__ but has no window glue — iOS is not in the CI +// build matrix; revisit if an iOS app ever links these symbols.) +inline Window::~Window() {} +inline void Window::close() {} +inline void Window::setTitle(const std::string&) {} +inline int Window::getWidth() const { return 0; } +inline int Window::getHeight() const { return 0; } +inline std::shared_ptr createWindow(const WindowSettings&) { + logError("Window") << "createWindow(): secondary windows are only supported on macOS for now"; + return nullptr; +} +#endif + +} // namespace trussc diff --git a/core/platform/mac/tcWindowMac.mm b/core/platform/mac/tcWindowMac.mm new file mode 100644 index 00000000..7b506558 --- /dev/null +++ b/core/platform/mac/tcWindowMac.mm @@ -0,0 +1,386 @@ +// ============================================================================= +// tcWindowMac.mm - Secondary window native glue (macOS) +// ============================================================================= +// One NSWindow + CAMetalLayer + CADisplayLink per secondary window. The display +// link fires on the main run loop at the window's own display rate, so a +// window on a 60 Hz display ticks at 60 while the main window runs at 120. +// A fully occluded window skips drawable acquisition entirely (the PoC showed +// acquiring while occluded blocks ~1s and stalls the main thread). +// All rendering shares the one sokol_gfx context; each window gets its own +// sokol_gl context (same pattern as Fbo). + +#include "TrussC.h" + +#if defined(__APPLE__) +#import +#import +#import +#import + +using namespace trussc; + +// --------------------------------------------------------------------------- +// Native per-window state +// --------------------------------------------------------------------------- +@class TCWindowView; +@class TCWindowDelegate; + +namespace { + +struct NativeWindow { + Window* owner = nullptr; // back-pointer (owner outlives native) + NSWindow* window = nil; + TCWindowView* view = nil; + TCWindowDelegate* delegate = nil; + CAMetalLayer* layer = nil; + CADisplayLink* link = nil; + id depthTex = nil; // depth-stencil, drawable-sized + id msaaTex = nil; // MSAA color (when sampleCount > 1) + int sampleCount = 1; + int texW = 0, texH = 0; // size the aux textures were made for + id frameDrawable = nil; // valid during one tick only + bool loggedOccluded = false; + bool loggedGeometry = false; +}; + +// The swapchain provider handed to WindowContext::acquireSwapchain. Returns +// the drawable acquired at the top of the current tick (never blocks here). +sg_swapchain acquireSecondarySwapchain(void* user) { + auto* nw = static_cast(user); + sg_swapchain sc = {}; + if (!nw || nw->frameDrawable == nil) return sc; + sc.width = nw->texW; + sc.height = nw->texH; + sc.sample_count = nw->sampleCount; + sc.color_format = SG_PIXELFORMAT_BGRA8; + sc.depth_format = SG_PIXELFORMAT_DEPTH_STENCIL; + sc.metal.current_drawable = (__bridge const void*)nw->frameDrawable; + sc.metal.depth_stencil_texture = (__bridge const void*)nw->depthTex; + if (nw->sampleCount > 1) { + sc.metal.msaa_color_texture = (__bridge const void*)nw->msaaTex; + } + return sc; +} + +void ensureAuxTextures(NativeWindow* nw, int w, int h) { + if (nw->texW == w && nw->texH == h && nw->depthTex != nil && + (nw->sampleCount <= 1 || nw->msaaTex != nil)) return; + id dev = nw->layer.device; + MTLTextureDescriptor* dd = [MTLTextureDescriptor + texture2DDescriptorWithPixelFormat:MTLPixelFormatDepth32Float_Stencil8 + width:w height:h mipmapped:NO]; + dd.usage = MTLTextureUsageRenderTarget; + dd.storageMode = MTLStorageModePrivate; + dd.sampleCount = nw->sampleCount; + dd.textureType = nw->sampleCount > 1 ? MTLTextureType2DMultisample : MTLTextureType2D; + nw->depthTex = [dev newTextureWithDescriptor:dd]; + if (nw->sampleCount > 1) { + MTLTextureDescriptor* md = [MTLTextureDescriptor + texture2DDescriptorWithPixelFormat:MTLPixelFormatBGRA8Unorm + width:w height:h mipmapped:NO]; + md.usage = MTLTextureUsageRenderTarget; + md.storageMode = MTLStorageModePrivate; + md.sampleCount = nw->sampleCount; + md.textureType = MTLTextureType2DMultisample; + nw->msaaTex = [dev newTextureWithDescriptor:md]; + } + nw->texW = w; nw->texH = h; +} + +} // namespace + +// --------------------------------------------------------------------------- +// Content view: input forwarding into the window's WindowContext / CoreEvents +// --------------------------------------------------------------------------- +@interface TCWindowView : NSView +@property (assign) NativeWindow* nw; +@end + +@implementation TCWindowView + +- (BOOL)acceptsFirstResponder { return YES; } +- (BOOL)isFlipped { return YES; } // top-left origin, matches TrussC coords + +- (void)updateTrackingAreas { + for (NSTrackingArea* a in self.trackingAreas) [self removeTrackingArea:a]; + NSTrackingArea* area = [[NSTrackingArea alloc] initWithRect:self.bounds + options:(NSTrackingMouseMoved | NSTrackingMouseEnteredAndExited | + NSTrackingActiveInKeyWindow | NSTrackingInVisibleRect) + owner:self userInfo:nil]; + [self addTrackingArea:area]; + [super updateTrackingAreas]; +} + +- (Vec2)localPos:(NSEvent*)ev { + NSPoint p = [self convertPoint:ev.locationInWindow fromView:nil]; + return Vec2((float)p.x, (float)p.y); // isFlipped => already top-left +} + +- (void)forwardMods:(NSEvent*)ev shift:(bool*)s ctrl:(bool*)c alt:(bool*)a super:(bool*)sp { + NSEventModifierFlags f = ev.modifierFlags; + *s = (f & NSEventModifierFlagShift) != 0; + *c = (f & NSEventModifierFlagControl) != 0; + *a = (f & NSEventModifierFlagOption) != 0; + *sp = (f & NSEventModifierFlagCommand) != 0; +} + +- (void)mouse:(NSEvent*)ev button:(int)btn pressed:(bool)down { + if (!self.nw || !self.nw->owner) return; + Window& win = *self.nw->owner; + auto& ctx = win.context(); + Vec2 p = [self localPos:ev]; + auto* prev = internal::currentWindowCtx; + internal::currentWindowCtx = &ctx; + ctx.mouseX = p.x; ctx.mouseY = p.y; + ctx.mousePressed = down; + ctx.mouseButton = down ? btn : -1; + MouseEventArgs e; + e.x = p.x; e.y = p.y; e.pos = p; e.globalPos = p; e.button = btn; + [self forwardMods:ev shift:&e.shift ctrl:&e.ctrl alt:&e.alt super:&e.super]; + if (down) { + win.events().mousePressed.notify(e); + win.dispatchMousePressToTree(e); + } else { + win.events().mouseReleased.notify(e); + win.dispatchMouseReleaseToTree(e); + } + internal::currentWindowCtx = prev; +} + +- (void)mouseDown:(NSEvent*)ev { [self mouse:ev button:0 pressed:true]; } +- (void)mouseUp:(NSEvent*)ev { [self mouse:ev button:0 pressed:false]; } +- (void)rightMouseDown:(NSEvent*)ev { [self mouse:ev button:1 pressed:true]; } +- (void)rightMouseUp:(NSEvent*)ev { [self mouse:ev button:1 pressed:false]; } + +- (void)moveWith:(NSEvent*)ev { + if (!self.nw || !self.nw->owner) return; + Window& win = *self.nw->owner; + auto& ctx = win.context(); + Vec2 p = [self localPos:ev]; + ctx.pmouseX = ctx.mouseX; ctx.pmouseY = ctx.mouseY; + ctx.mouseX = p.x; ctx.mouseY = p.y; + MouseMoveEventArgs e; + e.x = p.x; e.y = p.y; + e.deltaX = (float)ev.deltaX; e.deltaY = (float)ev.deltaY; + [self forwardMods:ev shift:&e.shift ctrl:&e.ctrl alt:&e.alt super:&e.super]; + win.events().mouseMoved.notify(e); +} +- (void)mouseMoved:(NSEvent*)ev { [self moveWith:ev]; } +- (void)mouseDragged:(NSEvent*)ev { [self moveWith:ev]; } + +- (void)keyEvt:(NSEvent*)ev pressed:(bool)down { + if (!self.nw || !self.nw->owner) return; + Window& win = *self.nw->owner; + auto& ctx = win.context(); + // NSEvent keyCode is a hardware code; map letters/digits via characters + // for the common case (full keycode table is Phase 2 polish). + int key = 0; + NSString* chars = ev.charactersIgnoringModifiers; + if (chars.length > 0) key = toupper([chars characterAtIndex:0]); + if (down) ctx.keysPressed.insert(key); else ctx.keysPressed.erase(key); + KeyEventArgs e; + e.key = key; e.isRepeat = ev.isARepeat; + [self forwardMods:ev shift:&e.shift ctrl:&e.ctrl alt:&e.alt super:&e.super]; + if (down) win.events().keyPressed.notify(e); + else win.events().keyReleased.notify(e); +} +- (void)keyDown:(NSEvent*)ev { [self keyEvt:ev pressed:true]; } +- (void)keyUp:(NSEvent*)ev { [self keyEvt:ev pressed:false]; } + +// -- per-frame tick (CADisplayLink target) -- +- (void)tick:(CADisplayLink*)link { + NativeWindow* nw = self.nw; + if (!nw || !nw->owner) return; + Window& win = *nw->owner; + auto& ctx = win.context(); + + // Occluded => not due. This is what prevents the ~1s nextDrawable stall. + if ((nw->window.occlusionState & NSWindowOcclusionStateVisible) == 0) { + if (!nw->loggedOccluded) { + nw->loggedOccluded = true; + logNotice("Window") << "second window occluded - skipping frames (no stall)"; + } + return; + } + nw->loggedOccluded = false; + + // Keep layer size in sync with the view + CGFloat scale = nw->window.backingScaleFactor; + NSSize sz = self.bounds.size; + int fbw = (int)(sz.width * scale), fbh = (int)(sz.height * scale); + if (fbw <= 0 || fbh <= 0) return; + if ((int)nw->layer.drawableSize.width != fbw || (int)nw->layer.drawableSize.height != fbh) { + nw->layer.contentsScale = scale; + nw->layer.drawableSize = CGSizeMake(fbw, fbh); + } + ctx.fbWidth = fbw; ctx.fbHeight = fbh; ctx.dpiScale = (float)scale; + if (!nw->loggedGeometry) { + nw->loggedGeometry = true; + logNotice("Window") << "geometry: bounds=" << sz.width << "x" << sz.height + << " backingScale=" << (float)nw->window.backingScaleFactor + << " contentsScale=" << (float)nw->layer.contentsScale + << " drawable=" << fbw << "x" << fbh + << " screen=" << nw->window.screen.frame.size.width << "x" << nw->window.screen.frame.size.height; + } + + nw->frameDrawable = [nw->layer nextDrawable]; + if (nw->frameDrawable == nil) return; + ensureAuxTextures(nw, fbw, fbh); + + // --- render this window's tree with its context active --- + auto* prev = internal::currentWindowCtx; + internal::currentWindowCtx = &ctx; + + // Own sokol_gl context, created lazily on the first tick (sgl is set up + // by then). Same lifecycle caveats as Fbo contexts on sgl resize. + if (ctx.swapchainTarget.context.id == SG_INVALID_ID) { + sgl_context_desc_t cdesc = {}; + cdesc.max_vertices = 65536; + cdesc.max_commands = 16384; + cdesc.color_format = SG_PIXELFORMAT_BGRA8; + cdesc.depth_format = SG_PIXELFORMAT_DEPTH_STENCIL; + cdesc.sample_count = nw->sampleCount; + ctx.swapchainTarget.context = sgl_make_context(&cdesc); + } + sgl_set_context(ctx.swapchainTarget.context); + sgl_defaults(); + + beginFrame(); + + if (win.root_ && !win.rootSetupDone_) { + win.root_->setup(); + win.rootSetupDone_ = true; + } + + win.events().update.notify(); + win.tickTree(); + + clear(win.clearColor_.r, win.clearColor_.g, win.clearColor_.b, win.clearColor_.a); + win.events().draw.notify(); + win.drawTreeNow(); + + present(); + win.events().afterFrame.notify(); + + sgl_set_context(sgl_default_context()); + internal::currentWindowCtx = prev; + nw->frameDrawable = nil; +} + +@end + +// --------------------------------------------------------------------------- +// Window delegate: user closes the window +// --------------------------------------------------------------------------- +@interface TCWindowDelegate : NSObject +@property (assign) NativeWindow* nw; +@end + +@implementation TCWindowDelegate +- (void)windowWillClose:(NSNotification*)n { + if (self.nw && self.nw->owner) { + self.nw->owner->close(); // tears down link + native state + } +} +@end + +// --------------------------------------------------------------------------- +// Window methods (macOS implementations) +// --------------------------------------------------------------------------- +namespace trussc { + +Window::~Window() { close(); } + +void Window::close() { + auto* nw = static_cast(native_); + if (!nw) return; + native_ = nullptr; // re-entrancy guard (windowWillClose) + ctx_.acquireSwapchain = nullptr; + ctx_.acquireSwapchainUser = nullptr; + if (nw->link) { [nw->link invalidate]; nw->link = nil; } + nw->view.nw = nullptr; + nw->delegate.nw = nullptr; + if (nw->window) { + nw->window.delegate = nil; + [nw->window orderOut:nil]; + nw->window = nil; + } + if (ctx_.swapchainTarget.context.id != SG_INVALID_ID) { + sgl_destroy_context(ctx_.swapchainTarget.context); + ctx_.swapchainTarget.context.id = SG_INVALID_ID; + ctx_.swapchainTarget.cache.clear(); + } + nw->owner = nullptr; + delete nw; +} + +void Window::setTitle(const std::string& title) { + auto* nw = static_cast(native_); + if (nw && nw->window) nw->window.title = [NSString stringWithUTF8String:title.c_str()]; +} + +int Window::getWidth() const { + auto* nw = static_cast(native_); + if (!nw || !nw->view) return 0; + return (int)nw->view.bounds.size.width; +} + +int Window::getHeight() const { + auto* nw = static_cast(native_); + if (!nw || !nw->view) return 0; + return (int)nw->view.bounds.size.height; +} + +std::shared_ptr createWindow(const WindowSettings& settings) { + if (headless::isActive()) { + logError("Window") << "createWindow(): not available in headless mode"; + return nullptr; + } + auto win = std::shared_ptr(new Window()); + auto* nw = new NativeWindow(); + nw->owner = win.get(); + nw->sampleCount = settings.sampleCount; + + NSRect rect = NSMakeRect(120, 120, settings.width, settings.height); + NSWindowStyleMask style = NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | + NSWindowStyleMaskMiniaturizable | NSWindowStyleMaskResizable; + if (!settings.decorated) style = NSWindowStyleMaskBorderless; + nw->window = [[NSWindow alloc] initWithContentRect:rect styleMask:style + backing:NSBackingStoreBuffered defer:NO]; + nw->window.title = [NSString stringWithUTF8String:settings.title.c_str()]; + nw->window.releasedWhenClosed = NO; + + TCWindowView* view = [[TCWindowView alloc] initWithFrame:rect]; + view.nw = nw; + nw->view = view; + nw->layer = [CAMetalLayer layer]; + // Share the device sokol_gfx renders with (sokol_app created it) + nw->layer.device = (__bridge id)sglue_environment().metal.device; + nw->layer.pixelFormat = MTLPixelFormatBGRA8Unorm; + nw->layer.opaque = YES; + view.wantsLayer = YES; + view.layer = nw->layer; + nw->window.contentView = view; + + TCWindowDelegate* del = [TCWindowDelegate new]; + del.nw = nw; + nw->delegate = del; + nw->window.delegate = del; + + win->native_ = nw; + win->ctx_.acquireSwapchain = &acquireSecondarySwapchain; + win->ctx_.acquireSwapchainUser = nw; + + [nw->window makeKeyAndOrderFront:nil]; + + // Per-window display link: fires on the main run loop at THIS window's + // display rate (macOS 14+). + nw->link = [view displayLinkWithTarget:view selector:@selector(tick:)]; + [nw->link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes]; + + return win; +} + +} // namespace trussc + +#endif // __APPLE__ From b5a5b7624c0fc336deeb6737c60adbff6ebe422d Mon Sep 17 00:00:00 2001 From: tettou771 Date: Sun, 5 Jul 2026 00:49:15 +0900 Subject: [PATCH 08/87] add: multiWindowExample (second window, own mouse, shared FBO) --- .../multiWindowExample/CMakeLists.txt | 12 ++++ .../windowing/multiWindowExample/addons.make | 1 + .../windowing/multiWindowExample/src/main.cpp | 13 ++++ .../multiWindowExample/src/tcApp.cpp | 64 +++++++++++++++++++ .../windowing/multiWindowExample/src/tcApp.h | 50 +++++++++++++++ 5 files changed, 140 insertions(+) create mode 100644 examples/windowing/multiWindowExample/CMakeLists.txt create mode 100644 examples/windowing/multiWindowExample/addons.make create mode 100644 examples/windowing/multiWindowExample/src/main.cpp create mode 100644 examples/windowing/multiWindowExample/src/tcApp.cpp create mode 100644 examples/windowing/multiWindowExample/src/tcApp.h diff --git a/examples/windowing/multiWindowExample/CMakeLists.txt b/examples/windowing/multiWindowExample/CMakeLists.txt new file mode 100644 index 00000000..f64927e1 --- /dev/null +++ b/examples/windowing/multiWindowExample/CMakeLists.txt @@ -0,0 +1,12 @@ +cmake_minimum_required(VERSION 3.20) +project(multiWindowExample) + +# TRUSSC_DIR is provided by CMakePresets.json (generated by trusscli) +# For CI/manual builds without presets, fallback to relative path +if(NOT DEFINED TRUSSC_DIR) + set(TRUSSC_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../../../core") +endif() + +include(${TRUSSC_DIR}/cmake/trussc_app.cmake) + +trussc_app() diff --git a/examples/windowing/multiWindowExample/addons.make b/examples/windowing/multiWindowExample/addons.make new file mode 100644 index 00000000..dc9fc4ff --- /dev/null +++ b/examples/windowing/multiWindowExample/addons.make @@ -0,0 +1 @@ +# TrussC addons - one addon per line diff --git a/examples/windowing/multiWindowExample/src/main.cpp b/examples/windowing/multiWindowExample/src/main.cpp new file mode 100644 index 00000000..f55751e3 --- /dev/null +++ b/examples/windowing/multiWindowExample/src/main.cpp @@ -0,0 +1,13 @@ +// ============================================================================= +// main.cpp - Entry point +// ============================================================================= + +#include "tcApp.h" + +int main() { + tc::WindowSettings settings; + settings.setSize(800, 600); + settings.setTitle("multiWindowExample - main"); + + return TC_RUN_APP(tcApp, settings); +} diff --git a/examples/windowing/multiWindowExample/src/tcApp.cpp b/examples/windowing/multiWindowExample/src/tcApp.cpp new file mode 100644 index 00000000..7279e65a --- /dev/null +++ b/examples/windowing/multiWindowExample/src/tcApp.cpp @@ -0,0 +1,64 @@ +// ============================================================================= +// tcApp.cpp - Multi-window example +// ============================================================================= +// Press W to open a second window. It runs on its own display link (its +// display's refresh rate), has its own Node tree, events and mouse, and can +// draw GPU resources from the main window directly (shared sokol_gfx context). +// Closing the second window leaves the main window running. +// Secondary windows are macOS-only for now; on other platforms W logs an error. + +#include "tcApp.h" + +void tcApp::setup() { + logNotice("tcApp") << "Press W to open/close the second window"; + fbo.allocate(320, 240); +} + +void tcApp::update() { + t += getDeltaTime(); + + // Animate something into the shared FBO every main-window frame + fbo.begin(); + clear(0.1f, 0.1f, 0.15f); + setColor(Color::fromHSB(fmodf(t * 0.1f, 1.0f), 0.7f, 1.0f)); + float cx = 160 + cosf(t * TAU * 0.25f) * 100; + float cy = 120 + sinf(t * TAU * 0.4f) * 60; + drawCircle(cx, cy, 40); + fbo.end(); +} + +void tcApp::draw() { + clear(0.12f); + + setColor(1.0f); + drawBitmapString("MAIN window (W: toggle second window)", 20, 30); + drawBitmapString(second && second->isOpen() ? "second window: OPEN" : "second window: closed", 20, 50); + + // The same FBO also drawn here + setColor(1.0f); + fbo.draw(20, 80, 320, 240); + + // Main window's own mouse + setColor(0.2f, 1.0f, 0.5f); + drawCircle(getMouseX(), getMouseY(), 12); +} + +void tcApp::keyPressed(int key) { + if (key == 'W') { + if (second && second->isOpen()) { + logNotice("tcApp") << "closing second window"; + second->close(); + return; + } + WindowSettings ws; + ws.setSize(480, 320); + ws.setTitle("multiWindowExample - second"); + second = createWindow(ws); + logNotice("tcApp") << (second ? "second window created" : "createWindow failed"); + if (second) { + subScene = make_shared(); + subScene->sharedFbo = &fbo; + second->setRoot(subScene); + } + } +} diff --git a/examples/windowing/multiWindowExample/src/tcApp.h b/examples/windowing/multiWindowExample/src/tcApp.h new file mode 100644 index 00000000..3ab57b89 --- /dev/null +++ b/examples/windowing/multiWindowExample/src/tcApp.h @@ -0,0 +1,50 @@ +#pragma once + +#include +using namespace std; +using namespace tc; + +// Scene shown in the SECOND window. It has its own mouse state: getMouseX() +// etc. resolve to the window this node is drawn in, not the main window. +class SubScene : public Node { +public: + Fbo* sharedFbo = nullptr; // rendered by the MAIN window, drawn here too + + bool loggedFirstDraw = false; + void draw() override { + if (!loggedFirstDraw) { + loggedFirstDraw = true; + logNotice("SubScene") << "first draw in second window: " + << getWindowWidth() << "x" << getWindowHeight() + << " dpi=" << getDpiScale(); + } + // The main window's FBO texture, used directly — one shared GPU + // context means zero-copy sharing across windows. + if (sharedFbo && sharedFbo->isAllocated()) { + setColor(1.0f); + sharedFbo->draw(20, 60, 200, 150); + setColor(0.7f); + drawBitmapString("live FBO from the MAIN window", 20, 230); + } + setColor(0.6f, 0.9f, 1.0f); + drawBitmapString("second window: move the mouse here", 20, 30); + + // This window's own mouse + setColor(1.0f, 0.6f, 0.2f); + drawCircle(getMouseX(), getMouseY(), 16); + } +}; + +class tcApp : public App { +public: + void setup() override; + void update() override; + void draw() override; + void keyPressed(int key) override; + +private: + Fbo fbo; + float t = 0.0f; + shared_ptr second; + shared_ptr subScene; +}; From 178cae1324e4ed529014a4176c6e6402a9728403 Mon Sep 17 00:00:00 2001 From: tettou771 Date: Sun, 5 Jul 2026 00:50:49 +0900 Subject: [PATCH 09/87] doc: reference prose for Window / createWindow (glue helpers hidden) --- docs/FOR_AI_ASSISTANT.md | 15 ++++++ docs/reference/api-reference.toml | 79 +++++++++++++++++++++++++++++++ 2 files changed, 94 insertions(+) diff --git a/docs/FOR_AI_ASSISTANT.md b/docs/FOR_AI_ASSISTANT.md index 9750c95b..68d017dc 100644 --- a/docs/FOR_AI_ASSISTANT.md +++ b/docs/FOR_AI_ASSISTANT.md @@ -2013,6 +2013,7 @@ float atanh(float x) [std] // Inverse hyperbolic tangent Baseline // Direction shorthand for Direction::Baseline (text baseline) Bottom // Direction shorthand for Direction::Bottom Center // Direction shorthand for Direction::Center +std::shared_ptr createWindow(const WindowSettings & settings = {}) [macos] // Create a secondary window (macOS only for now; returns nullptr elsewhere). It runs on its own display link; closing it leaves the app running const char * enumLabel(E value) // Return the display string for one enum value (TC_ENUM_LABELS override, else reflected name). const std::array enumNames() // Return a compile-time array of all valid enumerator names of E. EnumLabelSpan enumReflectedSpan() // Return an EnumLabelSpan synthesized from reflection (valid for contiguous zero-based enums). @@ -3936,6 +3937,20 @@ bool VideoWriter::open(const fs::path & path, int width, int height, const Video bool VideoWriter::submitFrame(double timeSec) // Append the previously locked frame at the given presentation time (seconds) ``` +### Window — A secondary application window (macOS only for now). Owns its own Node tree, events, mouse state and render context; ticks at its display's refresh rate. GPU resources are shared with every other window + +```cpp +void Window::close() // Close the native window; the main window and other windows keep running +CoreEvents & Window::events() // This window's own event stream (mousePressed / keyPressed / draw / ...) +int Window::getHeight() const // Window height in logical points (matches its coordinate system) +std::shared_ptr Window::getRoot() const // Get the Node tree attached to this window +int Window::getWidth() const // Window width in logical points (matches its coordinate system) +bool Window::isOpen() const // Whether the native window is still open +void Window::setClearColor(const Color & c) // Background clear color for this window +void Window::setRoot(std::shared_ptr root) // Attach the Node tree shown in this window (setup runs on its first frame) +void Window::setTitle(const std::string & title) // Set the window title +``` + ### WindowSettings — Window configuration passed to the app at startup (size, title, DPI, MSAA, fullscreen, decoration, VSync). Setters chain ```cpp diff --git a/docs/reference/api-reference.toml b/docs/reference/api-reference.toml index ca1dba5e..0d62f2ff 100644 --- a/docs/reference/api-reference.toml +++ b/docs/reference/api-reference.toml @@ -10691,6 +10691,85 @@ value_desc.Silent.en = "Silent (no sound)" value_desc.Silent.ja = "無音" value_desc.Silent.ko = "무음 (사운드 없음)" +["Window"] +keywords = ["multi window", "second window", "secondary", "display"] +description.en = "A secondary application window (macOS only for now). Owns its own Node tree, events, mouse state and render context; ticks at its display's refresh rate. GPU resources are shared with every other window" +description.ja = "セカンダリウィンドウ (現状macOSのみ)。自分のNodeツリー・イベント・マウス状態・描画コンテキストを持ち、所属ディスプレイのリフレッシュレートで駆動される。GPUリソースは全ウィンドウで共有" +description.ko = "보조 윈도우 (현재 macOS 전용). 자체 Node 트리·이벤트·마우스 상태·렌더 컨텍스트를 갖고, 소속 디스플레이의 주사율로 구동됨. GPU 리소스는 모든 윈도우와 공유" +related = ["createWindow", "WindowSettings"] + +["Window::setRoot"] +description.en = "Attach the Node tree shown in this window (setup runs on its first frame)" +description.ja = "このウィンドウに表示するNodeツリーを設定 (setupは最初のフレームで実行)" +description.ko = "이 윈도우에 표시할 Node 트리를 설정 (setup은 첫 프레임에 실행)" + +["Window::getRoot"] +description.en = "Get the Node tree attached to this window" +description.ja = "このウィンドウのNodeツリーを取得" +description.ko = "이 윈도우의 Node 트리를 얻음" + +["Window::events"] +description.en = "This window's own event stream (mousePressed / keyPressed / draw / ...)" +description.ja = "このウィンドウ専用のイベントストリーム (mousePressed / keyPressed / draw など)" +description.ko = "이 윈도우 전용 이벤트 스트림 (mousePressed / keyPressed / draw 등)" + +["Window::close"] +description.en = "Close the native window; the main window and other windows keep running" +description.ja = "ウィンドウを閉じる。メインウィンドウや他のウィンドウは動き続ける" +description.ko = "윈도우를 닫음. 메인 윈도우와 다른 윈도우는 계속 실행됨" + +["Window::isOpen"] +description.en = "Whether the native window is still open" +description.ja = "ウィンドウが開いているか" +description.ko = "윈도우가 열려 있는지" + +["Window::setTitle"] +description.en = "Set the window title" +description.ja = "ウィンドウタイトルを設定" +description.ko = "윈도우 제목을 설정" + +["Window::getWidth"] +description.en = "Window width in logical points (matches its coordinate system)" +description.ja = "ウィンドウ幅 (論理ポイント、座標系と一致)" +description.ko = "윈도우 너비 (논리 포인트, 좌표계와 일치)" + +["Window::getHeight"] +description.en = "Window height in logical points (matches its coordinate system)" +description.ja = "ウィンドウ高さ (論理ポイント、座標系と一致)" +description.ko = "윈도우 높이 (논리 포인트, 좌표계와 일치)" + +["Window::setClearColor"] +description.en = "Background clear color for this window" +description.ja = "このウィンドウの背景クリア色" +description.ko = "이 윈도우의 배경 클리어 색" + +["Window::context"] +hide = true +keywords = ["internal"] + +["Window::dispatchMousePressToTree"] +hide = true +keywords = ["internal"] + +["Window::dispatchMouseReleaseToTree"] +hide = true +keywords = ["internal"] + +["Window::tickTree"] +hide = true +keywords = ["internal"] + +["Window::drawTreeNow"] +hide = true +keywords = ["internal"] + +["createWindow"] +keywords = ["multi window", "second window", "open window", "display"] +description.en = "Create a secondary window (macOS only for now; returns nullptr elsewhere). It runs on its own display link; closing it leaves the app running" +description.ja = "セカンダリウィンドウを作成 (現状macOSのみ、他プラットフォームはnullptr)。専用のdisplay linkで駆動され、閉じてもアプリは動き続ける" +description.ko = "보조 윈도우를 생성 (현재 macOS 전용, 다른 플랫폼은 nullptr). 전용 display link로 구동되며 닫아도 앱은 계속 실행됨" +related = ["Window", "WindowSettings"] + ["WindowSettings"] keywords = ["config", "size", "title", "fullscreen", "startup"] description.en = "Window configuration passed to the app at startup (size, title, DPI, MSAA, fullscreen, decoration, VSync). Setters chain" From 89ad7bc4c8ca4be00674992e9869d4a37d906e41 Mon Sep 17 00:00:00 2001 From: tettou771 Date: Sun, 5 Jul 2026 01:19:48 +0900 Subject: [PATCH 10/87] fix: secondary window tracks hover without key focus NSTrackingActiveAlways + acceptsMouseMovedEvents (the default tracking only delivers mouseMoved to the key window, so an unfocused control-panel window froze its pointer). mouseExited parks the cursor offscreen so the window's own hoveredNode clears on the next tick. --- core/platform/mac/tcWindowMac.mm | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/core/platform/mac/tcWindowMac.mm b/core/platform/mac/tcWindowMac.mm index 7b506558..00298efc 100644 --- a/core/platform/mac/tcWindowMac.mm +++ b/core/platform/mac/tcWindowMac.mm @@ -103,9 +103,12 @@ - (BOOL)isFlipped { return YES; } // top-left origin, matches TrussC coords - (void)updateTrackingAreas { for (NSTrackingArea* a in self.trackingAreas) [self removeTrackingArea:a]; + // ActiveAlways: hover must work while the window is NOT key (the main + // window keeps focus; a control-panel window still tracks the pointer). NSTrackingArea* area = [[NSTrackingArea alloc] initWithRect:self.bounds options:(NSTrackingMouseMoved | NSTrackingMouseEnteredAndExited | - NSTrackingActiveInKeyWindow | NSTrackingInVisibleRect) + NSTrackingActiveAlways | NSTrackingEnabledDuringMouseDrag | + NSTrackingInVisibleRect) owner:self userInfo:nil]; [self addTrackingArea:area]; [super updateTrackingAreas]; @@ -167,6 +170,15 @@ - (void)moveWith:(NSEvent*)ev { } - (void)mouseMoved:(NSEvent*)ev { [self moveWith:ev]; } - (void)mouseDragged:(NSEvent*)ev { [self moveWith:ev]; } +- (void)mouseEntered:(NSEvent*)ev { [self moveWith:ev]; } +- (void)mouseExited:(NSEvent*)ev { + // Park the cursor offscreen so the next tick's updateHoverState clears + // this window's hover (hoveredNode lives in ITS WindowContext). + if (!self.nw || !self.nw->owner) return; + auto& ctx = self.nw->owner->context(); + ctx.pmouseX = ctx.mouseX; ctx.pmouseY = ctx.mouseY; + ctx.mouseX = -1.0f; ctx.mouseY = -1.0f; +} - (void)keyEvt:(NSEvent*)ev pressed:(bool)down { if (!self.nw || !self.nw->owner) return; @@ -349,6 +361,7 @@ - (void)windowWillClose:(NSNotification*)n { backing:NSBackingStoreBuffered defer:NO]; nw->window.title = [NSString stringWithUTF8String:settings.title.c_str()]; nw->window.releasedWhenClosed = NO; + nw->window.acceptsMouseMovedEvents = YES; // hover without key status TCWindowView* view = [[TCWindowView alloc] initWithFrame:rect]; view.nw = nw; From 81a1c71d73e3d9555a0b009392385d774400a87a Mon Sep 17 00:00:00 2001 From: tettou771 Date: Sun, 5 Jul 2026 10:44:25 +0900 Subject: [PATCH 11/87] feat: sync a RectNode root to the secondary window's logical size Mirrors the main App contract (App is a RectNode resized on SAPP_EVENTTYPE_RESIZED): a RectNode root attached to a secondary window is resized at attach and on every resize/display change; a plain Node root is left untouched (documented convention). Example shows the synced bounds; resize the second window to see them follow. --- core/include/tc/app/tcWindow.h | 11 +++++++++++ core/platform/mac/tcWindowMac.mm | 3 +++ examples/windowing/multiWindowExample/src/tcApp.h | 10 +++++++++- 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/core/include/tc/app/tcWindow.h b/core/include/tc/app/tcWindow.h index 78b19a77..65e5030e 100644 --- a/core/include/tc/app/tcWindow.h +++ b/core/include/tc/app/tcWindow.h @@ -59,6 +59,17 @@ class TC_PLATFORMS("macos") Window { void drawTreeNow() { if (ctx_.rootNode) ctx_.rootNode->drawTree(); } + // Size-sync convention (mirrors the main App, which is a RectNode kept in + // sync with the window): if the root IS a RectNode it is resized to the + // window's logical size at attach time and on every resize/display change. + // A plain Node root is left untouched. + void syncRootSize(float wPts, float hPts) { + if (auto* rect = dynamic_cast(ctx_.rootNode)) { + if (rect->getWidth() != wPts || rect->getHeight() != hPts) { + rect->setSize(wPts, hPts); + } + } + } // --- internal (used by the platform glue; not user API) --- Window(); diff --git a/core/platform/mac/tcWindowMac.mm b/core/platform/mac/tcWindowMac.mm index 00298efc..4fc8625e 100644 --- a/core/platform/mac/tcWindowMac.mm +++ b/core/platform/mac/tcWindowMac.mm @@ -226,6 +226,9 @@ - (void)tick:(CADisplayLink*)link { nw->layer.drawableSize = CGSizeMake(fbw, fbh); } ctx.fbWidth = fbw; ctx.fbHeight = fbh; ctx.dpiScale = (float)scale; + // Keep a RectNode root in sync with the window's logical size (same + // contract as the main App on SAPP_EVENTTYPE_RESIZED). + win.syncRootSize((float)sz.width, (float)sz.height); if (!nw->loggedGeometry) { nw->loggedGeometry = true; logNotice("Window") << "geometry: bounds=" << sz.width << "x" << sz.height diff --git a/examples/windowing/multiWindowExample/src/tcApp.h b/examples/windowing/multiWindowExample/src/tcApp.h index 3ab57b89..3b3eb61b 100644 --- a/examples/windowing/multiWindowExample/src/tcApp.h +++ b/examples/windowing/multiWindowExample/src/tcApp.h @@ -6,7 +6,7 @@ using namespace tc; // Scene shown in the SECOND window. It has its own mouse state: getMouseX() // etc. resolve to the window this node is drawn in, not the main window. -class SubScene : public Node { +class SubScene : public RectNode { public: Fbo* sharedFbo = nullptr; // rendered by the MAIN window, drawn here too @@ -29,6 +29,14 @@ class SubScene : public Node { setColor(0.6f, 0.9f, 1.0f); drawBitmapString("second window: move the mouse here", 20, 30); + // Bounds synced to the window size (resize the window to see it follow) + setColor(0.3f, 0.5f, 0.3f); + noFill(); + drawRect(1, 1, getWidth() - 2, getHeight() - 2); + fill(); + drawBitmapString("root RectNode: " + toString((int)getWidth()) + "x" + toString((int)getHeight()), + 20, getHeight() - 16); + // This window's own mouse setColor(1.0f, 0.6f, 0.2f); drawCircle(getMouseX(), getMouseY(), 16); From e338d8569ed0e774409b430bdbe83179d2858b31 Mon Sep 17 00:00:00 2001 From: tettou771 Date: Sun, 5 Jul 2026 10:56:33 +0900 Subject: [PATCH 12/87] =?UTF-8?q?feat:=20Window::setApp=20=E2=80=94=20an?= =?UTF-8?q?=20App=20drives=20each=20window=20(setRoot=20removed)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit setApp(shared_ptr) is the only way to give a window content: the App's familiar lifecycle runs against THAT window — setup() once on the first tree update (standard Node lifecycle), update()/draw() on the window's tick, key/mouse virtuals from the window's event stream, and handleWindowResized() on resize (RectNode size sync, same path as the main window's SAPP_EVENTTYPE_RESIZED). One App per window, double-attach and main-App reuse rejected; close() runs exit()/cleanup() then releases. App's constructor now registers itself as the scene root only when the active window has none (previously constructing ANY App clobbered the main window's rootNode — a secondary-window App hijacked the main tree). setRoot(bare Node) is deleted: one way to do it; a minimal view is a trivial App subclass. runApp untouched — 'runApp = create main window + setApp' unification is a future refactor. multiWindowExample: second window is now a SubApp demonstrating setup/update/draw/keyPressed/windowResized, keeping the shared-FBO proof. Verified live: setup x1, resize sync x1, per-window key routing, close teardown with the main window surviving. --- core/include/tc/app/tcWindow.h | 60 +++++++++++++++---- core/include/tcBaseApp.h | 11 +++- core/platform/mac/tcWindowMac.mm | 24 +++++--- docs/FOR_AI_ASSISTANT.md | 4 +- docs/reference/api-reference.toml | 21 ++++--- .../multiWindowExample/src/tcApp.cpp | 6 +- .../windowing/multiWindowExample/src/tcApp.h | 56 +++++++++++------ 7 files changed, 127 insertions(+), 55 deletions(-) diff --git a/core/include/tc/app/tcWindow.h b/core/include/tc/app/tcWindow.h index 65e5030e..d2260fc4 100644 --- a/core/include/tc/app/tcWindow.h +++ b/core/include/tc/app/tcWindow.h @@ -25,9 +25,21 @@ class TC_PLATFORMS("macos") Window { public: ~Window(); - // Attach the Node tree shown in this window (setup runs on first tick). - void setRoot(std::shared_ptr root); - std::shared_ptr getRoot() const { return root_; } + // Attach an App to this window — the ONLY way to give a window content. + // The App gets its familiar lifecycle wired to THIS window: setup() once + // on the first tick, update()/draw() on the window's own tick, + // keyPressed/mousePressed/... from this window's event stream, and + // windowResized() on resize (whose base impl keeps the App's RectNode + // size in sync, exactly like the main window). App IS a Node — attach + // children as usual for scene content. + // One App per window; attaching an App that is already driving another + // window (or the main one) is an error. + // Caveat (Phase 2): App::setSize / window-control helpers still target + // the MAIN window; use this Window handle to control this one. + // Note: the App's setup() runs once on the window's first tree update + // (standard Node lifecycle), i.e. on the window's first tick. + void setApp(std::shared_ptr app); + std::shared_ptr getApp() const { return app_; } // This window's event stream (mousePressed / keyPressed / draw / ...). CoreEvents& events() { return events_; } @@ -64,21 +76,24 @@ class TC_PLATFORMS("macos") Window { // window's logical size at attach time and on every resize/display change. // A plain Node root is left untouched. void syncRootSize(float wPts, float hPts) { - if (auto* rect = dynamic_cast(ctx_.rootNode)) { - if (rect->getWidth() != wPts || rect->getHeight() != hPts) { - rect->setSize(wPts, hPts); - } + if (!app_) return; + // handleWindowResized() = RectNode::setSize (non-virtual; App::setSize + // is overridden to resize the OS window) + the windowResized() hook — + // the same path the main window uses on SAPP_EVENTTYPE_RESIZED. + if (app_->getWidth() != wPts || app_->getHeight() != hPts) { + app_->handleWindowResized((int)wPts, (int)hPts); + ResizeEventArgs args; args.width = (int)wPts; args.height = (int)hPts; + events_.windowResized.notify(args); } } // --- internal (used by the platform glue; not user API) --- Window(); + std::shared_ptr app_; // set via setApp (may be null) void* native_ = nullptr; // platform-side state internal::WindowContext ctx_; CoreEvents events_; internal::RenderContext render_; - std::shared_ptr root_; - bool rootSetupDone_ = false; Color clearColor_ = Color(0.05f, 0.05f, 0.08f, 1.0f); }; @@ -93,10 +108,29 @@ inline Window::Window() { ctx_.coreEvents = &events_; } -inline void Window::setRoot(std::shared_ptr root) { - root_ = std::move(root); - ctx_.rootNode = root_.get(); - rootSetupDone_ = false; +namespace internal { +// Apps currently driving a window (double-attach guard). An inline variable: +// a hot-reload guest gets its own copy, so the guard is per-binary — fine for +// a misuse check. (runApp unification — "runApp = create main window + +// setApp" — is a future refactor; the main App is guarded via rootNode.) +inline std::unordered_set attachedApps; +} + +inline void Window::setApp(std::shared_ptr app) { + if (app) { + if (app.get() == internal::mainWindowContext().rootNode) { + logError("Window") << "setApp(): this App is the running main App"; + return; + } + if (internal::attachedApps.count(app.get())) { + logError("Window") << "setApp(): this App already drives another window"; + return; + } + } + if (app_) internal::attachedApps.erase(app_.get()); + if (app) internal::attachedApps.insert(app.get()); + app_ = std::move(app); + ctx_.rootNode = app_.get(); } #if !defined(__APPLE__) diff --git a/core/include/tcBaseApp.h b/core/include/tcBaseApp.h index 8648533c..2f030149 100644 --- a/core/include/tcBaseApp.h +++ b/core/include/tcBaseApp.h @@ -40,9 +40,14 @@ class App : public RectNode { audioInListener_ = AudioEngine::getInstance().audioIn.listen( [this](AudioInBuffer& b) { audioIn(b); }); - // The App is the scene-graph root; expose it via getRootNode() so - // tools (e.g. the MCP node tools) can walk the tree. - internal::currentWindowContext().rootNode = this; + // The FIRST App becomes the scene-graph root of the active window + // (normally the main App created by runApp) — exposed via + // getRootNode() so tools (e.g. the MCP node tools) can walk the tree. + // Later App instances don't clobber it: they are secondary-window + // content, registered explicitly by Window::setApp(). + if (internal::currentWindowContext().rootNode == nullptr) { + internal::currentWindowContext().rootNode = this; + } } virtual ~App() { diff --git a/core/platform/mac/tcWindowMac.mm b/core/platform/mac/tcWindowMac.mm index 4fc8625e..0d069d03 100644 --- a/core/platform/mac/tcWindowMac.mm +++ b/core/platform/mac/tcWindowMac.mm @@ -142,9 +142,11 @@ - (void)mouse:(NSEvent*)ev button:(int)btn pressed:(bool)down { [self forwardMods:ev shift:&e.shift ctrl:&e.ctrl alt:&e.alt super:&e.super]; if (down) { win.events().mousePressed.notify(e); + if (win.app_) win.app_->mousePressed(e); win.dispatchMousePressToTree(e); } else { win.events().mouseReleased.notify(e); + if (win.app_) win.app_->mouseReleased(e); win.dispatchMouseReleaseToTree(e); } internal::currentWindowCtx = prev; @@ -167,6 +169,7 @@ - (void)moveWith:(NSEvent*)ev { e.deltaX = (float)ev.deltaX; e.deltaY = (float)ev.deltaY; [self forwardMods:ev shift:&e.shift ctrl:&e.ctrl alt:&e.alt super:&e.super]; win.events().mouseMoved.notify(e); + if (self.nw->owner->app_) self.nw->owner->app_->mouseMoved(e); } - (void)mouseMoved:(NSEvent*)ev { [self moveWith:ev]; } - (void)mouseDragged:(NSEvent*)ev { [self moveWith:ev]; } @@ -193,8 +196,13 @@ - (void)keyEvt:(NSEvent*)ev pressed:(bool)down { KeyEventArgs e; e.key = key; e.isRepeat = ev.isARepeat; [self forwardMods:ev shift:&e.shift ctrl:&e.ctrl alt:&e.alt super:&e.super]; - if (down) win.events().keyPressed.notify(e); - else win.events().keyReleased.notify(e); + if (down) { + win.events().keyPressed.notify(e); + if (win.app_) win.app_->keyPressed(e); + } else { + win.events().keyReleased.notify(e); + if (win.app_) win.app_->keyReleased(e); + } } - (void)keyDown:(NSEvent*)ev { [self keyEvt:ev pressed:true]; } - (void)keyUp:(NSEvent*)ev { [self keyEvt:ev pressed:false]; } @@ -262,11 +270,6 @@ - (void)tick:(CADisplayLink*)link { beginFrame(); - if (win.root_ && !win.rootSetupDone_) { - win.root_->setup(); - win.rootSetupDone_ = true; - } - win.events().update.notify(); win.tickTree(); @@ -327,6 +330,13 @@ - (void)windowWillClose:(NSNotification*)n { } nw->owner = nullptr; delete nw; + if (app_) { + app_->exit(); + app_->cleanup(); + internal::attachedApps.erase(app_.get()); + app_.reset(); + ctx_.rootNode = nullptr; + } } void Window::setTitle(const std::string& title) { diff --git a/docs/FOR_AI_ASSISTANT.md b/docs/FOR_AI_ASSISTANT.md index 68d017dc..aacd81e1 100644 --- a/docs/FOR_AI_ASSISTANT.md +++ b/docs/FOR_AI_ASSISTANT.md @@ -3942,12 +3942,12 @@ bool VideoWriter::submitFrame(double timeSec) // Append the previously locked f ```cpp void Window::close() // Close the native window; the main window and other windows keep running CoreEvents & Window::events() // This window's own event stream (mousePressed / keyPressed / draw / ...) +std::shared_ptr Window::getApp() const // Get the App attached to this window int Window::getHeight() const // Window height in logical points (matches its coordinate system) -std::shared_ptr Window::getRoot() const // Get the Node tree attached to this window int Window::getWidth() const // Window width in logical points (matches its coordinate system) bool Window::isOpen() const // Whether the native window is still open +void Window::setApp(std::shared_ptr app) // Attach an App to this window — the only way to give a window content. The App's full lifecycle (setup/update/draw/key/mouse/windowResized + RectNode size sync) runs against this window. One App per window void Window::setClearColor(const Color & c) // Background clear color for this window -void Window::setRoot(std::shared_ptr root) // Attach the Node tree shown in this window (setup runs on its first frame) void Window::setTitle(const std::string & title) // Set the window title ``` diff --git a/docs/reference/api-reference.toml b/docs/reference/api-reference.toml index 0d62f2ff..cadfeccf 100644 --- a/docs/reference/api-reference.toml +++ b/docs/reference/api-reference.toml @@ -10698,15 +10698,16 @@ description.ja = "セカンダリウィンドウ (現状macOSのみ)。自分の description.ko = "보조 윈도우 (현재 macOS 전용). 자체 Node 트리·이벤트·마우스 상태·렌더 컨텍스트를 갖고, 소속 디스플레이의 주사율로 구동됨. GPU 리소스는 모든 윈도우와 공유" related = ["createWindow", "WindowSettings"] -["Window::setRoot"] -description.en = "Attach the Node tree shown in this window (setup runs on its first frame)" -description.ja = "このウィンドウに表示するNodeツリーを設定 (setupは最初のフレームで実行)" -description.ko = "이 윈도우에 표시할 Node 트리를 설정 (setup은 첫 프레임에 실행)" +["Window::setApp"] +keywords = ["attach", "lifecycle", "content"] +description.en = "Attach an App to this window — the only way to give a window content. The App's full lifecycle (setup/update/draw/key/mouse/windowResized + RectNode size sync) runs against this window. One App per window" +description.ja = "このウィンドウにAppを設定 (ウィンドウにコンテンツを与える唯一の方法)。Appのライフサイクル一式 (setup/update/draw/キー/マウス/windowResized + RectNodeサイズ同期) がこのウィンドウで動く。1ウィンドウ1App" +description.ko = "이 윈도우에 App을 연결 (윈도우에 콘텐츠를 주는 유일한 방법). App의 전체 라이프사이클이 이 윈도우에서 실행됨. 윈도우당 App 하나" -["Window::getRoot"] -description.en = "Get the Node tree attached to this window" -description.ja = "このウィンドウのNodeツリーを取得" -description.ko = "이 윈도우의 Node 트리를 얻음" +["Window::getApp"] +description.en = "Get the App attached to this window" +description.ja = "このウィンドウのAppを取得" +description.ko = "이 윈도우의 App을 얻음" ["Window::events"] description.en = "This window's own event stream (mousePressed / keyPressed / draw / ...)" @@ -10755,6 +10756,10 @@ keywords = ["internal"] hide = true keywords = ["internal"] +["Window::syncRootSize"] +hide = true +keywords = ["internal"] + ["Window::tickTree"] hide = true keywords = ["internal"] diff --git a/examples/windowing/multiWindowExample/src/tcApp.cpp b/examples/windowing/multiWindowExample/src/tcApp.cpp index 7279e65a..2c450440 100644 --- a/examples/windowing/multiWindowExample/src/tcApp.cpp +++ b/examples/windowing/multiWindowExample/src/tcApp.cpp @@ -56,9 +56,9 @@ void tcApp::keyPressed(int key) { second = createWindow(ws); logNotice("tcApp") << (second ? "second window created" : "createWindow failed"); if (second) { - subScene = make_shared(); - subScene->sharedFbo = &fbo; - second->setRoot(subScene); + subApp = make_shared(); + subApp->sharedFbo = &fbo; + second->setApp(subApp); } } } diff --git a/examples/windowing/multiWindowExample/src/tcApp.h b/examples/windowing/multiWindowExample/src/tcApp.h index 3b3eb61b..c24150c5 100644 --- a/examples/windowing/multiWindowExample/src/tcApp.h +++ b/examples/windowing/multiWindowExample/src/tcApp.h @@ -4,22 +4,25 @@ using namespace std; using namespace tc; -// Scene shown in the SECOND window. It has its own mouse state: getMouseX() -// etc. resolve to the window this node is drawn in, not the main window. -class SubScene : public RectNode { +// The SECOND window's content is a normal App subclass: setup/update/draw/ +// keyPressed/windowResized all run against that window (its own mouse, its +// own size, its own event stream). GPU resources from the main window are +// usable directly — one shared sokol_gfx context. +class SubApp : public App { public: - Fbo* sharedFbo = nullptr; // rendered by the MAIN window, drawn here too + Fbo* sharedFbo = nullptr; // rendered by the MAIN window's app + + void setup() override { + logNotice("SubApp") << "setup in second window: " + << getWindowWidth() << "x" << getWindowHeight() + << " dpi=" << getDpiScale(); + } + + void update() override { + wobble += getDeltaTime() * TAU * 0.5f; + } - bool loggedFirstDraw = false; void draw() override { - if (!loggedFirstDraw) { - loggedFirstDraw = true; - logNotice("SubScene") << "first draw in second window: " - << getWindowWidth() << "x" << getWindowHeight() - << " dpi=" << getDpiScale(); - } - // The main window's FBO texture, used directly — one shared GPU - // context means zero-copy sharing across windows. if (sharedFbo && sharedFbo->isAllocated()) { setColor(1.0f); sharedFbo->draw(20, 60, 200, 150); @@ -27,20 +30,35 @@ class SubScene : public RectNode { drawBitmapString("live FBO from the MAIN window", 20, 230); } setColor(0.6f, 0.9f, 1.0f); - drawBitmapString("second window: move the mouse here", 20, 30); + drawBitmapString("second window (App lifecycle)", 20, 30); + drawBitmapString("keys typed here: " + toString(keyCount), 20, 46); - // Bounds synced to the window size (resize the window to see it follow) + // App is a RectNode kept in sync with THIS window's size setColor(0.3f, 0.5f, 0.3f); noFill(); drawRect(1, 1, getWidth() - 2, getHeight() - 2); fill(); - drawBitmapString("root RectNode: " + toString((int)getWidth()) + "x" + toString((int)getHeight()), + drawBitmapString("app size: " + toString((int)getWidth()) + "x" + toString((int)getHeight()), 20, getHeight() - 16); - // This window's own mouse + // This window's own mouse (wobbles from update()) setColor(1.0f, 0.6f, 0.2f); - drawCircle(getMouseX(), getMouseY(), 16); + drawCircle(getMouseX(), getMouseY() + sinf(wobble) * 6.0f, 16); } + + void keyPressed(int key) override { + keyCount++; + logNotice("SubApp") << "key in second window: " << key; + } + + void windowResized(int w, int h) override { + // RectNode size is already synced by the framework before this hook + logNotice("SubApp") << "second window resized: " << w << "x" << h; + } + +private: + float wobble = 0.0f; + int keyCount = 0; }; class tcApp : public App { @@ -54,5 +72,5 @@ class tcApp : public App { Fbo fbo; float t = 0.0f; shared_ptr second; - shared_ptr subScene; + shared_ptr subApp; }; From dd498b0c7040de3668dcaf26118f1c4a7c186624 Mon Sep 17 00:00:00 2001 From: tettou771 Date: Tue, 7 Jul 2026 10:27:44 +0900 Subject: [PATCH 13/87] =?UTF-8?q?fix:=20per-window=20delta=20time=20?= =?UTF-8?q?=E2=80=94=20getDeltaTime/getFrameRate=20resolve=20through=20Win?= =?UTF-8?q?dowContext?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move updateDeltaTime / lastUpdateCallTime / frame-rate buffer from process globals into WindowContext. The main loop writes the main window's context; a secondary window measures its own wall-clock delta per processed tick (first tick estimated from the display link interval). Fixes the latent slow-motion bug where a secondary window on a display with a different refresh rate (e.g. 60 Hz projector next to a 120 Hz main) read the MAIN window's delta. Verified: main pinned to setFps(10) reports avg dt 0.100 while the second window on a 120 Hz display reports 0.00833. Tween/TweenMod go through getDeltaTime() and are fixed by the same move. --- core/include/TrussC.h | 27 +++++++++++---------------- core/include/tc/app/tcGlobal.cpp | 19 +++++++++++-------- core/include/tc/app/tcWindowContext.h | 12 ++++++++++++ core/platform/mac/tcWindowMac.mm | 17 +++++++++++++++++ 4 files changed, 51 insertions(+), 24 deletions(-) diff --git a/core/include/TrussC.h b/core/include/TrussC.h index 29b168e9..2e784f29 100644 --- a/core/include/TrussC.h +++ b/core/include/TrussC.h @@ -261,15 +261,7 @@ namespace internal { inline EventListener touchMovedListener; inline EventListener touchReleasedListener; - // Delta time (actual elapsed time since last update call) - inline double updateDeltaTime = 0.0; - inline std::chrono::high_resolution_clock::time_point lastUpdateCallTime; - inline bool lastUpdateCallTimeInitialized = false; - - // Frame rate measurement (10-frame moving average) - inline double frameTimeBuffer[10] = {}; - inline int frameTimeIndex = 0; - inline bool frameTimeBufferFilled = false; + // Delta time / frame-rate state lives in WindowContext (per window). // Frame count (number of update calls) inline uint64_t updateFrameCount = 0; @@ -2113,16 +2105,19 @@ namespace internal { mcp::processHttpQueue(); #endif - // Compute update delta time (actual elapsed since last update call) + // Compute update delta time (actual elapsed since last update call). + // Written into the main window's context; secondary windows measure + // their own delta in their tick (tcWindowMac.mm). auto computeUpdateDelta = [&]() { - if (!lastUpdateCallTimeInitialized) { - lastUpdateCallTimeInitialized = true; - lastUpdateCallTime = now; - updateDeltaTime = sapp_frame_duration(); // first frame: use sokol's estimate + auto& wctx = internal::currentWindowContext(); + if (!wctx.lastUpdateCallTimeInitialized) { + wctx.lastUpdateCallTimeInitialized = true; + wctx.lastUpdateCallTime = now; + wctx.updateDeltaTime = sapp_frame_duration(); // first frame: use sokol's estimate } else { auto callNow = std::chrono::high_resolution_clock::now(); - updateDeltaTime = std::chrono::duration(callNow - lastUpdateCallTime).count(); - lastUpdateCallTime = callNow; + wctx.updateDeltaTime = std::chrono::duration(callNow - wctx.lastUpdateCallTime).count(); + wctx.lastUpdateCallTime = callNow; } }; diff --git a/core/include/tc/app/tcGlobal.cpp b/core/include/tc/app/tcGlobal.cpp index 4336ceb0..f5e2b417 100644 --- a/core/include/tc/app/tcGlobal.cpp +++ b/core/include/tc/app/tcGlobal.cpp @@ -367,24 +367,27 @@ uint64_t getFrameCount() { } double getDeltaTime() { - return internal::updateDeltaTime; + // Per-window: inside a secondary window's update/draw this is that + // window's own tick delta, not the main window's. + return internal::currentWindowContext().updateDeltaTime; } double getFrameRate() { - double dt = internal::updateDeltaTime; + auto& ctx = internal::currentWindowContext(); + double dt = ctx.updateDeltaTime; if (dt <= 0.0) return 0.0; - internal::frameTimeBuffer[internal::frameTimeIndex] = dt; - internal::frameTimeIndex = (internal::frameTimeIndex + 1) % 10; - if (internal::frameTimeIndex == 0) { - internal::frameTimeBufferFilled = true; + ctx.frameTimeBuffer[ctx.frameTimeIndex] = dt; + ctx.frameTimeIndex = (ctx.frameTimeIndex + 1) % 10; + if (ctx.frameTimeIndex == 0) { + ctx.frameTimeBufferFilled = true; } - int count = internal::frameTimeBufferFilled ? 10 : internal::frameTimeIndex; + int count = ctx.frameTimeBufferFilled ? 10 : ctx.frameTimeIndex; if (count == 0) return 0.0; double sum = 0.0; for (int i = 0; i < count; i++) { - sum += internal::frameTimeBuffer[i]; + sum += ctx.frameTimeBuffer[i]; } double avgDt = sum / count; return avgDt > 0.0 ? 1.0 / avgDt : 0.0; diff --git a/core/include/tc/app/tcWindowContext.h b/core/include/tc/app/tcWindowContext.h index c76f0e96..7b382865 100644 --- a/core/include/tc/app/tcWindowContext.h +++ b/core/include/tc/app/tcWindowContext.h @@ -95,6 +95,18 @@ struct WindowContext { sg_swapchain (*acquireSwapchain)(void* user) = nullptr; void* acquireSwapchainUser = nullptr; + // --- frame timing (per window) --- + // getDeltaTime()/getFrameRate() resolve through the current context, so a + // window ticking at 60 Hz next to a 120 Hz main window sees its own real + // per-tick delta (measured wall-clock between THIS window's update calls). + double updateDeltaTime = 0.0; + std::chrono::high_resolution_clock::time_point lastUpdateCallTime; + bool lastUpdateCallTimeInitialized = false; + // Frame rate measurement (10-frame moving average) + double frameTimeBuffer[10] = {}; + int frameTimeIndex = 0; + bool frameTimeBufferFilled = false; + // --- misc per-window --- int clipboardSize = 65536; // Clipboard buffer size (for overflow check) diff --git a/core/platform/mac/tcWindowMac.mm b/core/platform/mac/tcWindowMac.mm index 0d069d03..156d3962 100644 --- a/core/platform/mac/tcWindowMac.mm +++ b/core/platform/mac/tcWindowMac.mm @@ -250,6 +250,23 @@ - (void)tick:(CADisplayLink*)link { if (nw->frameDrawable == nil) return; ensureAuxTextures(nw, fbw, fbh); + // Per-window delta time: wall-clock between THIS window's processed ticks, + // so getDeltaTime() inside this window's update/draw is correct even when + // its display runs at a different refresh rate than the main window's. + // Same semantics as the main loop: a long gap (occlusion) yields one large + // delta, exactly like an event-driven main window. + { + auto callNow = std::chrono::high_resolution_clock::now(); + if (!ctx.lastUpdateCallTimeInitialized) { + ctx.lastUpdateCallTimeInitialized = true; + // First tick: estimate from the display link's frame interval. + ctx.updateDeltaTime = link.targetTimestamp - link.timestamp; + } else { + ctx.updateDeltaTime = std::chrono::duration(callNow - ctx.lastUpdateCallTime).count(); + } + ctx.lastUpdateCallTime = callNow; + } + // --- render this window's tree with its context active --- auto* prev = internal::currentWindowCtx; internal::currentWindowCtx = &ctx; From f5101e664810b8568ac3566d1bd2844221623037 Mon Sep 17 00:00:00 2001 From: tettou771 Date: Tue, 7 Jul 2026 10:27:44 +0900 Subject: [PATCH 14/87] doc: sokol_app_tc.h design + sapp_* usage inventory Design for graduating from sokol_app: standalone sokol-style C header (fork lineage, zlib) with floooh's PR #437 API shape (sapp_window handles, window field on events, _window fn variants) revived on post-2024 per-pass sg_swapchain sokol_gfx with an event-driven per-window vsync tick loop. Inventory: 27 distinct sapp_* identifiers in core, tcxImGui the only addon consumer, 10 fork patches catalogued with carry-over flags. --- docs/dev/sapp-inventory.md | 239 ++++++++++++++++++++++++++++++++ docs/dev/sokol_app_tc-design.md | 163 ++++++++++++++++++++++ 2 files changed, 402 insertions(+) create mode 100644 docs/dev/sapp-inventory.md create mode 100644 docs/dev/sokol_app_tc-design.md diff --git a/docs/dev/sapp-inventory.md b/docs/dev/sapp-inventory.md new file mode 100644 index 00000000..ae7d2f93 --- /dev/null +++ b/docs/dev/sapp-inventory.md @@ -0,0 +1,239 @@ +# sokol_app (sapp_) usage inventory + +Purpose: complete inventory of everything TrussC consumes from `sokol_app.h`, +to prepare a drop-in replacement header (`sokol_app_tc.h`). Scope: `core/`, +`addons/`, a quick sweep of `examples/`/`apps/`/`tools/`, plus the existing +TrussC patches already applied to `sokol_app.h` (these must carry over). + +Method: `grep -rohE 'sapp_[A-Za-z0-9_]*' --exclude-dir=sokol | sort | uniq -c` +per file, read call sites for context. Counts are per-identifier occurrence +(not unique call sites — a line using the same identifier twice counts twice). + +Headline numbers: **core/ (excluding `sokol/`) uses 27 distinct `sapp_*` +identifiers** across 10 files, overwhelmingly concentrated in `TrussC.h` +(52 of ~75 occurrences). Addons add 3 more identifiers (all via +`tcxImGui/src/sokol_imgui.h`, which is the single largest consumer outside +core). Only 1 identifier (`sapp_request_quit`) leaks into example/app user +code, and only in the trivial "Esc to quit" idiom (9 sites). Nothing in +`tools/` (trusscli) touches sapp at all. + +--- + +## Table 1: API usage + +### Lifecycle / frame + +| sapp API | call sites | files | what it provides | per-window / app-global | platforms | +|---|---|---|---|---|---| +| `sapp_run` | 3 | TrussC.h (`runApp`), tcHotReloadHost.h (`runHotReloadApp`) x2 | Starts the sokol_app event loop from a built `sapp_desc` | app-global (one loop) | all desktop (not called on Android — see notes) | +| `sapp_desc` (type) | 8 | TrussC.h, tcHotReloadHost.h | Descriptor struct: width/height/title/high_dpi/sample_count/fullscreen/swap_interval, `init_cb`/`frame_cb`/`cleanup_cb`/`event_cb`, `logger.func`, `enable_dragndrop`+`max_dropped_files`+`max_dropped_file_path_length`, `enable_clipboard`+`clipboard_size` | app-global | all | +| `sapp_quit` | 2 | TrussC.h (`exitApp`), tcHotReloadHost.h (host init failure) | Immediate, non-cancellable exit | app-global | all | +| `sapp_request_quit` | 2 (core) + 9 (examples) | TrussC.h (`requestExitApp`), tcStandardTools.h; examples/\*/tcApp.cpp (9 files: coordinateConversionExample, 3DPrimitivesExample, vectorMathExample, loopModeExample, hitTestExample, nodeExample, graphicsExample, roundedRectExample, colorExample) | Cancellable exit; triggers `SAPP_EVENTTYPE_QUIT_REQUESTED` | app-global | all except Android (patched out — see Table 2 item "Android BACK key") | +| `sapp_cancel_quit` | 1 | TrussC.h (`_event_cb`, `SAPP_EVENTTYPE_QUIT_REQUESTED` handler) | Cancels a pending quit if `exitRequested` listener sets `args.cancel` | app-global | all | +| `sapp_frame_duration` | 3 | TrussC.h (x2, update-loop dt + first-frame estimate), tcxImGui.h/sokol_imgui.h (`simgui_desc.delta_time`) | Smoothed per-frame delta time from sokol's internal timer | app-global | all | +| `sapp_frame_count` | 5 | tcTexture.h x2 (`loadData` once-per-frame guard), tcFont.h x2 (dpi + `flushPendingDestroys` once-per-frame guard), tcGlobal.cpp (`getDrawCount()`) | Monotonic frame counter | app-global | all | +| `sapp_skip_present` | 1 | TrussC.h (`_frame_cb`, after `endSwapchainPass`) | Skips the next present/swap call (event-driven low-power redraw without visible flicker) | app-global | all (TrussC-added function; see Table 2) | +| `sapp_event` (type) | 3 | TrussC.h (`_event_cb` signature x1), tcCoreEvents.h (`rawEvent` listener signature x2) | Raw input event struct forwarded to `events().rawEvent` for addons (tcxImGui) | app-global | all | +| `sapp_event_type` enum consts (`SAPP_EVENTTYPE_*`) | ~14 distinct + counts | TrussC.h `_event_cb` switch | Dispatch table: KEY_DOWN/UP, MOUSE_DOWN/UP/MOVE/SCROLL, TOUCHES_BEGAN/MOVED/ENDED/CANCELLED, RESIZED, FILES_DROPPED, CLIPBOARD_PASTED, QUIT_REQUESTED | app-global (delivered per active window in multi-window future) | all (touch: Android/iOS/web only) | + +### Window geometry / DPI + +| sapp API | call sites | files | what it provides | per-window / app-global | platforms | +|---|---|---|---|---|---| +| `sapp_width` | 11 | TrussC.h (getWidth/getViewport x4, resize handler, aspect ratio, screenshot helpers), tcVideoRecorder.h x2, tcHotReloadHost.h, tcxLut/tcLut.h, tcxImGui.h/sokol_imgui.h, tcPlatform_linux.cpp, tcPlatform_android.cpp, tcVideoPlayer_linux.cpp | Framebuffer width in pixels | **app-global only for the main window** — `getWidth()` branches on `ctx.isMain` and uses `ctx.fbWidth` for secondary windows (see Notes) | all | +| `sapp_height` | 9 | same set minus one | Framebuffer height in pixels | same caveat as above | all | +| `sapp_dpi_scale` | 11 | TrussC.h (getDisplayScaleFactor x1 + 6 more sites), tcFont.h (glyph physical-size scaling), tcHotReloadHost.h, tcxLut/tcLut.h, tcxImGui.h/sokol_imgui.h, tcPlatform_android.cpp (`getDisplayScaleFactor`), tcPlatform_win.cpp (indirectly via setWindowSize), tcVideoPlayer_linux.cpp | Display scale factor (Retina/HiDPI); on Android patched to use real `AConfiguration` density instead of fb/win ratio (Table 2) | same `isMain` caveat | all | +| `sapp_sample_count` | 2 | tcMeshPointPipeline.h, tcMeshPbrPipeline.h | MSAA sample count for the default swapchain, used to pick/build the right pipeline when not inside an FBO pass | app-global | all | + +### Events / input + +| sapp API | call sites | files | what it provides | per-window / app-global | platforms | +|---|---|---|---|---|---| +| `sapp_event` fields (`type`, `modifiers`, `key_code`, `key_repeat`, `mouse_x/y`, `mouse_button`, `scroll_x/y`, `num_touches`, `touches[]`) | many (read, not counted separately) | TrussC.h `_event_cb` | Source data for TrussC's `KeyEventArgs`/`MouseEventArgs`/`ScrollEventArgs`/`TouchEventArgs` and legacy `App::handle*` callbacks | app-global input → written into `WindowContext` (mouseX/Y, keysPressed) | all | +| `SAPP_MODIFIER_SHIFT/CTRL/ALT/SUPER` | 4 | TrussC.h `_event_cb` | Modifier bitmask decode | app-global | all | +| `SAPP_KEYCODE_*` (30 distinct constants) | 35 | TrussC.h: `constexpr int KEY_*` alias table ~L1827-1866 (SPACE/ESCAPE/ENTER/TAB/BACKSPACE/DELETE, arrows, F1-F12) + `isShiftPressed`/`isControlPressed`/`isAltPressed`/`isSuperPressed` modifier-check helpers ~L1567-1572 (LEFT/RIGHT_SHIFT/CONTROL/ALT/SUPER) | Named key constants re-exported as TrussC's own `KEY_*` constants, so user code never needs to spell `SAPP_KEYCODE_*` | app-global | all | +| `sapp_get_num_dropped_files` | 1 | TrussC.h (`SAPP_EVENTTYPE_FILES_DROPPED` handler) | Count of files in a drag-and-drop event | app-global | desktop (macOS/Windows/Linux) + web | +| `sapp_get_dropped_file_path` | 1 | TrussC.h (same handler, loop) | Path of the i-th dropped file | app-global | desktop + web | +| `sapp_consume_event` | 4 | addons/tcxImGui/src/sokol_imgui.h (`simgui_handle_event`, key down/up when ImGui wants keyboard) | Marks the current event as consumed so sokol_app / TrussC's own dispatch doesn't double-handle it | app-global | all | +| `sapp_keyboard_shown` / `sapp_show_keyboard` | 1 each | addons/tcxImGui/src/sokol_imgui.h (`simgui_new_frame`, toggles on-screen keyboard based on `io->WantTextInput`) | On-screen (mobile) keyboard visibility control | app-global | Android/iOS (no-op elsewhere) | +| `sapp_ios_set_supported_orientations` | 1 | tcPlatform_ios.mm | Runtime orientation mask control | per-app (iOS has one screen) | iOS (TrussC-patched API — Table 2) | + +### Mouse cursor + +| sapp API | call sites | files | what it provides | per-window / app-global | platforms | +|---|---|---|---|---|---| +| `sapp_show_mouse` | 2 | TrussC.h (`showCursor`/`hideCursor`) | Show/hide system cursor | app-global | all | +| `sapp_set_mouse_cursor` / `sapp_get_mouse_cursor` | 1 / 1 (core) + 2 (sokol_imgui.h ImGui-driven cursor sync) | TrussC.h (`setCursor`/`getCursor`), sokol_imgui.h `simgui_new_frame` (maps `ImGuiMouseCursor` → `sapp_mouse_cursor` each frame unless `disable_set_mouse_cursor`) | Set/read current cursor shape (system cursor enum or custom slot) | app-global | all | +| `sapp_mouse_cursor` (type) + `SAPP_MOUSECURSOR_*` (11 system + 16 `CUSTOM_0..15` slots) | 40 + many enum refs | TrussC.h `enum class Cursor` (1:1 mirror of the sokol enum) | Cursor shape enum, including 16 custom-image slots | app-global | all | +| `sapp_bind_mouse_cursor_image` / `sapp_unbind_mouse_cursor_image` | 1 / 1 | TrussC.h (`bindCursorImage`/`unbindCursorImage`) | Upload/release a custom RGBA cursor image + hotspot into a `CUSTOM_n` slot | app-global | all (custom cursor images not exercised much beyond this wrapper — no example uses it) | +| `sapp_image_desc` (type) | 1 | TrussC.h (`bindCursorImage`) | Struct for `{width, height, pixels, cursor_hotspot_x/y}` passed to bind | app-global | all | + +### Clipboard + +| sapp API | call sites | files | what it provides | per-window / app-global | platforms | +|---|---|---|---|---|---| +| `sapp_set_clipboard_string` | 1 (core) + 1 (sokol_imgui.h) | TrussC.h (`setClipboardString`), sokol_imgui.h (ImGui `SetClipboardTextFn`) | Write OS clipboard | app-global | all | +| `sapp_get_clipboard_string` | 1 (core) + 1 (sokol_imgui.h) | TrussC.h (`getClipboardString`), sokol_imgui.h (ImGui `GetClipboardTextFn`) | Read OS clipboard | app-global | all | +| `SAPP_EVENTTYPE_CLIPBOARD_PASTED` | 1 | TrussC.h `_event_cb` | Notifies when web clipboard content becomes available (web needs the paste event; desktop can read clipboard synchronously) | app-global | primarily web | +| `desc.enable_clipboard` / `desc.clipboard_size` | (part of `sapp_desc` count above) | TrussC.h `buildAppDescriptor`, tcHotReloadHost.h | Enables clipboard subsystem + buffer size (mirrored into `WindowContext::clipboardSize` for overflow warnings) | app-global | all | + +### Swapchain / native handles + +| sapp API | call sites | files | what it provides | per-window / app-global | platforms | +|---|---|---|---|---|---| +| `sapp_get_swapchain` | 1 (core) + 1 (win) + 1 (mac) | tcGlobal.cpp (fallback when `ctx.acquireSwapchain` is null), tcPlatform_win.cpp (`captureWindow` — reads `sc.d3d11.render_view`), tcPlatform_mac.mm (`captureWindowAsync` fallback — reads `.metal.current_drawable`) | Current-frame `sg_swapchain` handle (backend-specific texture/view/drawable) | **app-global / main-window only** — secondary windows supply their own via `ctx.acquireSwapchain` hook, bypassing sapp entirely | all (fields vary: d3d11/.metal/.gl/.wgpu) | +| `sglue_swapchain()` (sokol_glue, wraps `sapp_get_environment()` + `sapp_get_swapchain()`) | 1 | tcGlobal.cpp (`beginSwapchainPass`, main-window default) | Bridges sapp's swapchain/environment into an `sg_pass.swapchain` | app-global (main window) | all | +| `sapp_macos_get_window` | 3 | tcPlatform_mac.mm (window-level ops: always-on-top, position, etc.) | Native `NSWindow*` for the sokol-owned main window | per-window (main only) | macOS | +| `sapp_win32_get_hwnd` | 6 | tcPlatform_win.cpp | Native `HWND` for the sokol-owned main window | per-window (main only) | Windows | +| `sapp_x11_get_window` | 2 | tcPlatform_linux.cpp | Native `Window` (X11) for the sokol-owned main window | per-window (main only) | Linux | +| `sapp_ios_get_window` | 1 | tcFileDialog_ios.mm | Native `UIWindow*` (for presenting a document picker) | per-window | iOS | +| `sapp_android_get_native_activity` | 6 | tcPlatform_android.cpp (x4), tcSerial_android.cpp (x2), tcxCurl_android.cpp (x2 — wait, counted separately below) | `ANativeActivity*` — JNI bridge for file dialogs, serial permissions, network state | app-global (Android is single-activity) | Android | + +### Window control / fullscreen + +| sapp API | call sites | files | what it provides | per-window / app-global | platforms | +|---|---|---|---|---|---| +| `sapp_set_window_title` | 1 | TrussC.h (`setWindowTitle`) | Set OS window title | main window only | all (no-op/limited on web) | +| `sapp_is_fullscreen` | 2 | TrussC.h (`setFullscreen`, `isFullscreen`) | Query fullscreen state | main window only | all | +| `sapp_toggle_fullscreen` | 2 | TrussC.h (`setFullscreen`, `toggleFullscreen`) | Toggle fullscreen (sokol has no "set" — only toggle) | main window only | all | + +Note: `setWindowSize`/`setWindowSizeLogical` do **not** go through sapp at +all — sokol_app has no runtime desktop resize API, so TrussC calls native +platform code directly (`SetWindowPos`/`NSWindow setFrame`/`XResizeWindow`) +using the native handle obtained via `sapp_*_get_*window` above. `sapp_dpi_scale()` +is used only to convert between pixel-perfect and logical size requests. + +### Misc / android-serial / curl (indirect, via native activity handle) + +| sapp API | call sites | files | what it provides | per-window / app-global | platforms | +|---|---|---|---|---|---| +| `sapp_android_get_native_activity` (addon) | 2 | addons/tcxCurl/src/tcxCurl_android.cpp | Same JNI bridge, used to query Android network connectivity for libcurl | app-global | Android | + +--- + +## Table 2: TrussC's existing sokol_app.h patches + +(Source: `core/include/sokol/TRUSSC_MODIFICATIONS.md`, cross-checked against +`grep -n "tettou771\|\[TrussC" core/include/sokol/sokol_app.h`.) + +| patch | location (anchor) | why it exists | must carry over? | +|---|---|---|---| +| **Skip Present** (adds `sapp_skip_present()`) | Declaration ~L2258; `skip_present` field in `_sapp_t` ~L3276; checked in `_sapp_wgpu_frame` ~L4262, `_sapp_vk_frame` ~L5022, `_sapp_d3d11_present` ~L8751, `_sapp_wgl_swap_buffers` ~L9059, Android EGL swap ~L10530, Linux GLX swap ~L12566, Linux EGL swap ~L13829, public impl `sapp_skip_present()` ~L14596 | Fixes D3D11 flickering in event-driven (non-continuous) rendering by letting TrussC skip the next present call after a frame that shouldn't visibly swap | **Yes — used by `TrussC.h::_frame_cb`** (1 call site in core); needed on all backends it patches, but D3D11 is the motivating case | +| **Emscripten: keyboard events on canvas, not window** | Register ~L7963–7972 (`emscripten_set_keydown/keyup/keypress_callback` target changed from `EMSCRIPTEN_EVENT_TARGET_WINDOW` to `_sapp.html5_canvas_selector`); unregister ~L7998 | Lets other page elements (e.g. Monaco editor embedded alongside a TrussSketch canvas) receive keyboard input independently; canvas needs a `tabindex` to be focusable | Yes — required for TrussSketch web embedding use case | +| **10-bit color output (RGB10A2)** | Enum `SAPP_PIXELFORMAT_RGB10A2` ~L1865; macOS Metal layer + MSAA tex ~L5095/L5217; iOS Metal layer + MSAA tex ~L6407/L6488; D3D11 swapchain format + resize ~L8592/L8717/L8745; `sapp_color_format()` report ~L14041; plus 1-line mapping in `sokol_glue.h` (`SAPP_PIXELFORMAT_RGB10A2 → SG_PIXELFORMAT_RGB10A2`) | Reduces color banding on 10-bit displays at no bandwidth cost (same 32-bit/px as BGRA8); Metal + D3D11 only, WebGL unchanged | Yes — visual quality feature, no known regressions | +| **iOS custom view controller + runtime orientation control** | `_sapp_ios_view_ctrl` @interface/@impl ~L6917–6918 area; `sapp_ios_set_supported_orientations()` decl ~L14508 region | Upstream `UIViewController` doesn't support changing supported orientations at runtime; TrussC apps need this (e.g. rotate-to-landscape features) | Yes — `tcPlatform_ios.mm` calls `sapp_ios_set_supported_orientations()` directly (1 call site) | +| **iOS immersive mode** | Non-static `_sapp_ios_immersive_mode` global + `prefersStatusBarHidden`/`prefersHomeIndicatorAutoHidden` overrides on `_sapp_ios_view_ctrl` | Hide status bar / home indicator for fullscreen iOS experiences; `tcPlatform_ios.mm` reads/writes the extern global directly (not a function call) | Yes — used by `tcPlatform_ios.mm` (`getImmersiveMode`/set) | +| **iOS: use view bounds instead of `UIScreen.mainScreen.bounds`** | `_sapp_ios_update_dimensions()` ~L6658 | Avoids a timing bug where screen bounds don't yet match actual view layout; falls back to screen bounds if view isn't laid out yet | Yes — correctness fix for iOS sizing | +| **iOS: read back actual Metal drawable dimensions** | `_sapp_ios_mtl_update_framebuffer_dimensions()` ~L6512 | After setting drawable size, re-reads `layer.drawableSize` so `sapp_width()`/`sapp_height()` always matches what Metal will actually render, preventing scissor-rect-exceeds-render-pass errors | Yes — correctness fix | +| **Android: BACK key no longer triggers shutdown** | `_sapp_android_key_event()` ~L10594 | Upstream calls `_sapp_android_shutdown()` on BACK, destroying the EGL surface + firing `cleanup_cb`; under screen-pinning (lock-task) `ANativeActivity_finish()` is blocked by the OS, so the process survives but EGL is gone → frozen app. Event is consumed without shutdown instead. Documented long-term fix: implement `SAPP_EVENTTYPE_QUIT_REQUESTED` for Android (already implemented for macOS/Windows/Linux) | Yes — kiosk/lock-task deployments depend on this | +| **Android: real display density for `sapp_dpi_scale()`** | `_sapp_android_update_dimensions()` ~L10482 (`#include ` + `AConfiguration_getDensity()` / 160.0f) | Upstream computes dpi_scale from framebuffer/window pixel ratio, which is unreliable; using actual density makes Android's `sapp_dpi_scale()` consistent with macOS/Windows semantics | Yes — `tcFont.h` and others rely on `sapp_dpi_scale()` being meaningful cross-platform | +| **CGBitmapInfo enum-cast (silence C++20 warning)** | `_sapp_macos_set_dock_tile()` CGImage creation ~L5833 | C++20 flags `-Wdeprecated-enum-enum-conversion` on the original `CGImageAlphaInfo | CGImageByteOrderInfo` OR; explicit cast to the destination `CGBitmapInfo` (`uint32_t`) type, no behavior change | Yes, trivially (or simply avoid the warning in the rewritten code) | +| **(Removed) SetForegroundWindow (Win32)** | N/A — historical | Was in sokol_app.h; moved to `trussc::bringWindowToFront()` in `tcPlatform.h`/platform files, made cross-platform | N/A — already lives outside sokol_app; nothing to port from sokol_app.h itself | + +Also documented but **not sokol_app.h** (carries over to the `sokol_gl_tc.h` +fork, out of scope for this inventory but noted since it's in the same +"sokol modifications" family): `sg_append_buffer` multi-draw-per-frame, +auto-grow CPU/GPU buffers, `sgl_tc_*` public API additions, float vertex +colors (UBYTE4N→FLOAT4). These live in `util/sokol_gl_tc.h`, not +`sokol_app.h`, and are unaffected by a sokol_app replacement. + +--- + +## Table 3: Unused sokol_app features + +| feature | evidence it's unused | port now / defer / never | +|---|---|---| +| Mouse lock / pointer lock (`sapp_lock_mouse`, `sapp_mouse_locked`) | Zero hits anywhere in core/addons/examples (`grep sapp_lock_mouse\|sapp_mouse_locked` → empty) | Defer — no current feature needs it, but cheap to keep if the replacement supports it structurally | +| Window icon API (`sapp_set_icon`, `sapp_icon_desc`) | Zero hits outside `sokol_app.h` itself; TrussC's app icons are handled by CMake/platform resource embedding (`.ico`/`Info.plist`/`resources/`), not sapp | Never (for the sapp path) — keep using native resource-embedded icons | +| Vulkan backend (`sapp_vk_*`, `SOKOL_VULKAN`) | No `SOKOL_VULKAN` define anywhere in `core/CMakeLists.txt`; backends selected are Metal (mac/iOS), D3D11 (Windows), GLCORE (Linux desktop/Raspbian via GLES3), GLES3 (Android, Raspbian, web default), optional WGPU (web only) | Never (unless a future Vulkan backend is added — not currently planned) | +| WGPU backend (`sapp_wgpu_*`) | Only reachable via `TC_GRAPHICS_BACKEND STREQUAL "WGPU"` opt-in on Emscripten (`core/CMakeLists.txt` L188-193); default web build uses GLES3 | Defer — keep as an optional path if the replacement wants to support it, but it's not exercised by default CI/build | +| GL desktop path on macOS/iOS (`sapp_gl_*`, `sapp_macos_gl_*`) | macOS/iOS always build `SOKOL_METAL` (CMakeLists.txt), never `SOKOL_GLCORE`/`SOKOL_GLES3` on Apple platforms | Never for Apple; GLCORE path IS used on Linux desktop/Raspbian, GLES3 on Android — so the GL backend overall is NOT dead, just dead specifically on Apple | +| Custom mouse cursor images beyond the wrapper (`sapp_bind/unbind_mouse_cursor_image`) | `TrussC.h` exposes `bindCursorImage`/`unbindCursorImage` wrappers (1 call site each) but no example/app in the repo calls them | Port now (thin, already wired) but low priority to verify beyond compiling — no exerciser exists | +| Gamepad input | sokol_app has no gamepad API at all (not a TrussC omission — upstream doesn't expose this) | N/A — not a sokol_app feature to begin with | +| `sapp_html5_*` fetch / drag&drop / ask-leave-site internals | Only the public surface (`enable_dragndrop`, `SAPP_EVENTTYPE_FILES_DROPPED`, `SAPP_EVENTTYPE_CLIPBOARD_PASTED`) is touched by TrussC; the `sapp_html5_fetch_*`/`sapp_js_*` symbols found in the earlier broad grep are sokol_app's own internal implementation, not called by TrussC code | N/A (internal to sokol_app, not a TrussC dependency) | +| iOS/Android/Web-specific internals (`sapp_ios_mtl_*`, `sapp_android_egl_*`, `sapp_emsc_*`, `sapp_x11_*`, `sapp_win32_*`, `sapp_wgl_*`, `sapp_glx_*`, `sapp_egl_*`, `sapp_dxgi_*`, `sapp_d3d11_*` private helpers) | These are sokol_app's own backend implementation details (all inside `core/include/sokol/sokol_app.h`, prefixed `_sapp_` in the source even though the broad grep strips the leading underscore) — TrussC never calls them directly except through the small public surface documented in Table 1 | N/A — these must exist in the replacement's own backend implementations, but they are not a "TrussC consumes X" dependency; they're implementation, not API surface | +| Fullscreen: TrussC never calls a "set fullscreen(bool)" sapp function directly | sokol_app only exposes `sapp_toggle_fullscreen()` (no direct setter); TrussC's `setFullscreen(bool)` wrapper compares against `sapp_is_fullscreen()` first, then toggles | Port now — trivial, already wrapped this way | +| Runtime desktop window resize | No `sapp_set_window_size` — doesn't exist upstream. TrussC's `setWindowSizeLogical` bypasses sapp entirely via native handles (`sapp_macos_get_window`, `sapp_win32_get_hwnd`, `sapp_x11_get_window`) | Port now — but note the native-handle escape hatch (`sapp_*_get_*window`) is exactly how TrussC already works around this sokol_app gap; the replacement should keep exposing an equivalent native handle getter | + +--- + +## Notes + +- **Entry point / `TC_RUN_APP` — there is no macro; it's a template function.** + `runApp(settings)` (in `TrussC.h`, ~L2622–2644) builds a `sapp_desc` + via `buildAppDescriptor()` and calls `sapp_run(&desc)`. All the + `App` lifecycle glue (`internal::appSetupFunc`, `appUpdateFunc`, + `appDrawFunc`, `appCleanupFunc`, plus the per-input-type forwarding lambdas) + is wired inside `buildAppDescriptor` before the descriptor is returned. + `main.cpp` in every example just calls `return trussc::runApp(settings);`. + **On Android**, `sapp_run` is never called — `runApp()` just stores + the built descriptor into `internal::g_androidDesc`, and + `core/platform/android/sokol_impl.cpp`'s `sokol_main(argc, argv)` calls the + app's `main()` (which populates that global via `runApp`) then returns the + stored descriptor, because `ANativeActivity_onCreate` (provided by + sokol_app.h without `SOKOL_NO_ENTRY` on Android) is the real entry point and + calls `sokol_main()` to get it. + +- **There are two independent places that build a `sapp_desc` and call `sapp_run`**: + `TrussC.h::buildAppDescriptor`/`runApp` (normal path) and + `tcHotReloadHost.h::runHotReloadApp` (hot-reload path, `internal::` namespace). + They duplicate almost the entire descriptor-building logic (width/height/ + pixel-perfect conversion, high_dpi, sample_count, fullscreen, swap_interval, + the four core callbacks, drag&drop + clipboard enable/size). A sokol_app + replacement should ideally let both paths share one builder function instead + of hand-duplicating the field list twice (currently they're just two copies + kept in sync by hand). + +- **`sokol_glue.h` is the sapp↔sgfx bridge.** `sglue_swapchain()` (used once, + in `tcGlobal.cpp::beginSwapchainPass`) internally calls + `sapp_get_environment()` + `sapp_get_swapchain()` and converts + `sapp_pixel_format` → `sg_pixel_format` (including the TrussC-added + `SAPP_PIXELFORMAT_RGB10A2` → `SG_PIXELFORMAT_RGB10A2` mapping, the file's + only patch). This is the *only* place `sapp_get_environment` is reached from + TrussC code (transitively) — TrussC itself never calls it directly. + +- **Multi-window architecture already treats sapp as "main-window-only."** + `core/include/tc/app/tcWindowContext.h`'s `WindowContext` has an `isMain` + flag: when true, `getWidth()`/`getHeight()`/`getDisplayScaleFactor()` + (TrussC.h ~L317-328) read `sapp_width()`/`sapp_height()`/`sapp_dpi_scale()` + directly; when false (a Phase-1 secondary native window), they read + `ctx.fbWidth`/`ctx.fbHeight`/`ctx.dpiScale`, which the native window layer + keeps up to date itself, and the frame's `sg_swapchain` comes from + `ctx.acquireSwapchain(ctx.acquireSwapchainUser)` instead of + `sapp_get_swapchain()`. In other words: **the multi-window seam already + exists and sapp's job is scoped to exactly one (the main) window** — a + sokol_app replacement only needs to serve that single main-window role; + secondary windows are handled entirely outside sapp already (see + `core/platform/mac/tcWindowMac.mm`, which has zero `sapp_` references). + +- **Emscripten/web build differs only in backend selection, not in the sapp + API surface consumed.** `core/CMakeLists.txt` picks `SOKOL_GLES3` by default + or `SOKOL_WGPU` if `TC_GRAPHICS_BACKEND=WGPU`; the emscripten keyboard-on-canvas + patch (Table 2) is the only web-specific sokol_app.h modification. No + TrussC/addon code calls `sapp_emsc_*`/`sapp_js_*`/`sapp_html5_*` directly — + those are all sokol_app's own internal Emscripten implementation. + +- **`tcxImGui/src/sokol_imgui.h`'s sapp dependency is fully self-contained + and small.** It needs exactly: `sapp_event`/`sapp_keycode` (types, for + `simgui_handle_event`/`simgui_map_keycode`), `sapp_consume_event` (x4, key + down/up when ImGui wants keyboard focus), `sapp_width`/`sapp_height`/ + `sapp_frame_duration`/`sapp_dpi_scale` (feeding `simgui_new_frame`'s + `simgui_frame_desc_t`), `sapp_set_clipboard_string`/`sapp_get_clipboard_string` + (ImGui clipboard callbacks), `sapp_keyboard_shown`/`sapp_show_keyboard` + (mobile on-screen keyboard sync with `io->WantTextInput`), and + `sapp_get_mouse_cursor`/`sapp_set_mouse_cursor` (ImGui-driven cursor shape + sync, gated by `disable_set_mouse_cursor`). `tcxImGui.h` (the TrussC-side + wrapper) itself only touches `sapp_event` (raw-event listener signature) and + `sapp_width`/`sapp_height`/`sapp_frame_duration`/`sapp_dpi_scale` (building + the per-frame `simgui_frame_desc_t`) — i.e. it needs the exact same small + surface as sokol_imgui.h itself, nothing extra. + +- **User-facing code essentially never touches sapp directly.** Of ~30 + example projects, only 9 call `sapp_request_quit()` (the "Esc quits" idiom), + and none call anything else. This means the replacement header's public API + compatibility surface for *user* code is trivially small — the real + compatibility burden is entirely inside `TrussC.h`/`tcHotReloadHost.h` + (core lifecycle + input dispatch) and `tcxImGui`'s vendored `sokol_imgui.h`. + +- **`tools/` (trusscli) has zero sapp references** — it's a project-generator/ + build tool, not a sokol_app consumer at all. diff --git a/docs/dev/sokol_app_tc-design.md b/docs/dev/sokol_app_tc-design.md new file mode 100644 index 00000000..c8b4146b --- /dev/null +++ b/docs/dev/sokol_app_tc-design.md @@ -0,0 +1,163 @@ +# sokol_app_tc.h — design + +Full replacement of `sokol_app.h` for TrussC ("graduation"), written as a +standalone sokol-style C header with zero TrussC dependencies. End state: +`sokol_app.h` is deleted from the tree and `sokol_app_tc.h` drives every +platform. The header should be readable/adoptable by upstream sokol users — +same license (zlib), same conventions, modifications and lineage documented. + +## Why this exists (and why it can work now) + +sokol_app is single-window by design. floooh's own multi-window attempt +(PR #437, ~2020) implemented `sapp_window` handles on all desktop platforms +but was abandoned on two blockers: + +1. **Old sokol_gfx context model** — one `sg_context` per window with + `sg_activate_context()` juggling and a single `sg_commit()`; multiple + default-passes per frame "required rewriting the entire garbage + collection code". +2. **Metal occlusion stall** — `[CAMetalLayer nextDrawable]` blocks ~1s for + a fully occluded window, freezing the shared frame loop. Called + "unfixable" at the time. + +Both are dead in 2026: + +1. The 2024 sokol_gfx rework made swapchains **per-pass parameters** + (`sg_begin_pass{ .swapchain = ... }`). One sg instance, N windows, + multiple commits per frame — officially supported (multiwindow-glfw + sample). +2. TrussC's multi-window branch proved a **per-window vsync tick** model on + macOS: each window's CADisplayLink posts ticks to the main run loop; an + occluded window's link suspends (plus an explicit occlusionState gate + before drawable acquisition), so the stalling call is simply never made. + Verified live: close / fully cover / other virtual desktop — the other + window keeps running at full rate, mixed 120/60 Hz works. + +So the pitch is: **PR #437's API shape, on post-2024 sokol_gfx, with an +event-driven per-window tick loop instead of a pull-everything frame loop.** + +## Lineage, naming, license + +- Fork lineage of `sokol_app.h` (zlib). Keep the `sapp_` prefix and the + existing single-window API as a compatibility surface so diffs against + upstream stay legible and the ~130 existing `sapp_*` call sites in TrussC + keep compiling during migration. +- Multi-window API aligned with #437 where it had one: `sapp_window` + handle (sokol-gfx-style `{uint32_t id}`), `sapp_window_desc`, + `sapp_event.window` field, `sapp_window_*()` variants of geometry/dpi + queries. Handle-less legacy calls mean "the main window". +- File lives at `core/include/sokol/sokol_app_tc.h`, implementation via + `SOKOL_APP_TC_IMPL` in per-platform TUs (ObjC for mac/iOS, C++ allowed + for win, C for linux/web/android) — same compilation model as today's + `sokol_impl.mm` / `sokol_impl.cpp`. +- The three existing TrussC patches to sokol_app.h (D3D11 skip_present, + emscripten canvas keyboard, RGB10A2) carry over as native features. + +## Core model + +**The app owns the OS run loop.** `sapp_run(&desc)` enters `[NSApp run]` / +Win32 `GetMessage` loop / X11 poll loop / emscripten RAF registration. +There is no polled frame loop. + +**Every window is a `sapp_window`, including the main one.** `sapp_run` +creates window #0 from `sapp_desc` (compat); more via +`sapp_create_window(&(sapp_window_desc){...})`. Single-window platforms +(web/iOS/Android) are the degenerate case: window #0 only, +`sapp_create_window` returns an invalid handle + log. Same header, same +API, explicit platform gap. + +**Per-window vsync tick source** (the load-bearing idea): + +| platform | tick source | notes | +|---|---|---| +| macOS | one CADisplayLink per window (NSView-based, macOS 14+) | auto-suspends on occlusion; explicit occlusionState gate as belt-and-braces | +| Windows | DXGI frame-latency waitable object per swapchain | real vsync pacing (fixes the WM_TIMER ~64 Hz cap in the current win branch); waits serviced via MsgWaitForMultipleObjectsEx in the message loop | +| Linux | timer paced to RandR refresh rate (clock_nanosleep) | best-effort; GLX has no per-window vsync callback | +| web | requestAnimationFrame | inherently the single window's tick | +| iOS | CADisplayLink | same as macOS | +| Android | Choreographer | last to port; EGL lifecycle is the hard part | + +**Callbacks.** App-wide, like sokol_app (not per-window like early #437 — +floooh himself pivoted away from that): + +- `frame_cb()` — legacy, fires on the MAIN window's tick (compat). +- `window_frame_cb(sapp_window)` — fires on any window's tick, including + the main one. TrussC migrates to this everywhere; frame_cb is sugar. +- `event_cb(const sapp_event*)` — unchanged signature, `ev->window` added. +- Tick coalescing is depth-1 per window by construction (run loop is the + queue; CADisplayLink / waitable object / WM paced sources never stack). + +**Occlusion / lifecycle events:** occluded window = ticks stop = fps 0. +`SAPP_EVENTTYPE_SUSPENDED/RESUMED` (or a new OCCLUDED/UNOCCLUDED pair) +notify the app so it can drop work; no API needed to "handle" the stall — +it cannot occur. + +**Swapchain bridging:** `sokol_glue_tc.h` gains +`sg_swapchain sglue_window_swapchain(sapp_window)`; backend-specific +handle getters (`sapp_window_metal_next_drawable()`, +`sapp_window_d3d11_render_view()`, ...) mirror today's main-window ones. +Drawable acquisition happens lazily inside the window's tick, after the +visibility gate — never for a non-ticking window. + +**Threading:** everything on the main thread, same as sokol_app. A slow +window's draw delays others (accepted; documented). + +## What stays OUT of the header (TrussC-side) + +- loop modes / fps gating / update-vs-draw split — TrussC decides what to + do with each tick (including skipping present via the existing + skip_present mechanism, generalized per window). +- WindowContext switching, CoreEvents fan-out, App/Node lifecycle, dt. + **Per-window dt** is a TrussC fix: move `updateDeltaTime` / + `lastUpdateCallTime` into WindowContext, measured per tick. (This also + fixes the projector-slow-motion bug currently latent in secondary + windows.) +- IME / text editing (TrussC's tcxIME is native and stays so); the header + exposes raw native handles (`sapp_window_macos_get_window()` etc.) as + escape hatches, same as sokol_app does today. + +sokol_imgui note: upstream supports `SOKOL_IMGUI_NO_SOKOL_APP`; tcxImGui +can feed events manually, so imgui does not chain us to sokol_app. + +## Migration phases (each independently shippable; breakage between phases OK) + +- **P0 — header skeleton + macOS driver.** New run loop owns the app; main + window becomes window #0; the existing tcWindowMac.mm secondary-window + code moves INTO the header impl (de-TrussC-ified: C API out, ObjC in); + tcWindowMac.mm shrinks to a thin adapter (Window ↔ sapp_window, events → + CoreEvents). TrussC.h main loop consumes window ticks. Per-window dt. + Gate: all examples + multiWindowExample + hot-reload run; occlusion gate + holds; mixed-Hz holds. +- **P1 — Windows driver.** Cannibalize feat/multi-window-win (DXGI flip + swapchain, WndProc, DPI v2 — audited good) + sokol_app's win32 keyboard + tables; DXGI waitable ticks replace WM_TIMER (kills the 64 Hz cap); main + window unified. The win branch is reference source, not merged as-is. +- **P2 — Linux driver.** X11 + GLX shared context (multiwindow-glfw + pattern), timer-paced ticks. +- **P3 — web driver.** RAF tick, HTML5 event callbacks, WebGL context; + port the canvas-keyboard patch natively. +- **P4 — iOS driver.** UIWindow + CAMetalLayer + CADisplayLink + touch. +- **P5 — Android driver.** NativeActivity + ALooper + Choreographer + EGL + surface lifecycle. Last on purpose; needs a real-device verification + plan (rotation, pause/resume, surface recreate). +- **P6 — delete sokol_app.h**, update TRUSSC_MODIFICATIONS.md, retire the + fork patches. + +Keyboard keycode tables, clipboard, drag&drop, fullscreen, mouse +lock/cursor images are lifted from sokol_app per platform as each driver +ports (zlib allows it; keep attribution). The sapp-inventory +(docs/dev/sapp-inventory.md) is the authoritative checklist of what each +phase must provide. + +## Risks + +1. Long-tail input correctness per platform (IME interplay, dead keys, + clipboard formats, dnd) — mitigated by lifting sokol_app's tables and + by the inventory's "unused features" list (don't port what nothing + calls). +2. Android EGL lifecycle — the one genuinely hairy port; scheduled last. +3. Mobile/web CI blindness — mac/win/linux/web are in CI, iOS/Android are + not; regressions there surface late. Device checklist required. +4. Hot-reload: C API is DLL-friendly; per-window state lives behind the + header's own internal singleton — needs the same host/guest awareness + audit as WindowContext had. From 6d73d9a2fc96e3b90fdd20a0e2b5afa8740858aa Mon Sep 17 00:00:00 2001 From: tettou771 Date: Tue, 7 Jul 2026 12:16:33 +0900 Subject: [PATCH 15/87] =?UTF-8?q?feat:=20sokol=5Fapp=5Ftc.h=20=E2=80=94=20?= =?UTF-8?q?secondary=20windows=20behind=20a=20standalone=20C=20API=20(P0a)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New core/include/sokol/sokol_app_tc.h: a sokol-style single-header C API for additional native windows (fork lineage of sokol_app.h, zlib, API shape from floooh's multi-window sketch PR #437: sapp_window handle, sapp_window_desc, window-parameterized queries). The frame model is the new part: per-window vsync ticks (one CADisplayLink per window on the main run loop) with an occlusion gate before drawable acquisition, which makes the CAMetalLayer nextDrawable stall structurally impossible — an occluded window simply stops ticking (SUSPENDED/RESUMED at transitions). The macOS implementation absorbs everything native from tcWindowMac.mm and adds what the old glue lacked: the full sokol_app keycode table (arrows/ESC/F-keys now reach secondary windows, key == SAPP_KEYCODE_* identical to the main window), CHAR events, scroll wheel, and modifier keys via flagsChanged. Implemented for macOS in sokol_impl.mm (SOKOL_APP_TC_IMPL); other platforms get explicit stubs until their ports (Windows: DXGI flip + waitable ticks, Linux: X11 + shared GL). tcWindowMac.mm shrinks to a TrussC adapter: tick -> WindowContext switch + per-window dt + tree update/draw; sapp_event -> CoreEvents/App hooks/ tree dispatch (drag vs move split now matches the main window; scroll events delivered for the first time). Verified: multiWindowExample runs (main + second window, shared FBO), SAPP_KEYCODE_LEFT=263 received in the second window, occluded-at-launch window ticks zero times for 24s then resumes on visibility; 107/107 examples build against the worktree core; core tests pass. --- core/include/sokol/sokol_app_tc.h | 818 ++++++++++++++++++++++++++++++ core/platform/mac/sokol_impl.mm | 2 + core/platform/mac/tcWindowMac.mm | 529 ++++++++----------- 3 files changed, 1035 insertions(+), 314 deletions(-) create mode 100644 core/include/sokol/sokol_app_tc.h diff --git a/core/include/sokol/sokol_app_tc.h b/core/include/sokol/sokol_app_tc.h new file mode 100644 index 00000000..97c9d7e1 --- /dev/null +++ b/core/include/sokol/sokol_app_tc.h @@ -0,0 +1,818 @@ +#if defined(SOKOL_APP_TC_IMPL) && !defined(SOKOL_APP_TC_IMPL_INCLUDED) +#define SOKOL_APP_TC_IMPL_INCLUDED (1) +#endif +#ifndef SOKOL_APP_TC_INCLUDED +/* + sokol_app_tc.h -- multi-window extension for sokol_app.h + + Project URL: https://github.com/TrussC-org/TrussC + Fork lineage: sokol_app.h by Andre Weissflog (https://github.com/floooh/sokol) + License: zlib/libpng (same as sokol_app.h, see end of file) + + Do this: + #define SOKOL_APP_TC_IMPL + before you include this file in *one* C++/Objective-C++ source file to + create the implementation. Include AFTER sokol_app.h (this header reuses + sapp_event / sapp_keycode / SAPP_* constants so that events from any + window flow through the exact same handling code as the main window's). + + WHAT IT IS + ========== + Additional native windows next to the single window owned by sokol_app.h. + The API shape follows floooh's multi-window sketch (sokol PR #437 / + issue #229): a small `sapp_window` handle, a `sapp_window_desc`, and + window-parameterized query functions. What is NEW compared to #437 is the + frame model, and it is the load-bearing idea: + + - There is no shared frame loop that iterates windows. Each window has + its OWN vsync source (macOS: one CADisplayLink per window, delivered + on the main run loop) which invokes the window's `tick_cb`. + - A fully occluded window's display link suspends, and an explicit + occlusion gate guards drawable acquisition. The `[CAMetalLayer + nextDrawable]` ~1s stall that killed PR #437 cannot occur, because + the acquiring code path is only ever entered for a visible window. + An occluded window simply stops ticking (fps 0) and resumes on + un-occlusion; SAPP_EVENTTYPE_SUSPENDED / _RESUMED are sent at the + transitions. + - Windows on displays with different refresh rates each tick at their + own display's rate. Tick coalescing is depth-1 per window by + construction (the run loop is the queue and a display link never + stacks callbacks). + + Rendering: all windows share the one sokol_gfx context (post-2024 + sokol_gfx takes the swapchain per-pass). During `tick_cb`, acquire this + window's swapchain handles via sapp_window_metal_current_drawable() / + ..._depth_stencil_texture() / ..._msaa_color_texture() and start a pass + with them. The drawable is acquired lazily right before tick_cb and is + valid only for that callback. + + Events: delivered as plain sapp_event through the desc's `event_cb`, + with the owning window passed alongside (sapp_event itself has no window + field while we coexist with upstream sokol_app.h). Keyboard uses the + same keycode translation table as sokol_app.h, so SAPP_KEYCODE_* values + are identical across the main window and secondary windows. + + Coordinates follow sokol_app.h conventions: mouse positions arrive in + framebuffer pixels (divide by sapp_window_dpi_scale() for logical + points). dpi_scale is derived from ONE source (the ratio actually used + to size the layer's drawable), never assumed to be 2. + + Everything runs on the main thread. All functions must be called from + the main thread. + + PLATFORM SUPPORT + ================ + macOS: implemented (NSWindow + CAMetalLayer + CADisplayLink, macOS 14+). + Others: stubs that return an invalid handle (explicit platform gap); + Windows (HWND + DXGI flip swapchain + waitable-object ticks) and Linux + (X11 + shared GL) are the next ports. +*/ +#define SOKOL_APP_TC_INCLUDED (1) +#include +#include + +#if !defined(SOKOL_APP_INCLUDED) +#error "Please include sokol_app.h before sokol_app_tc.h" +#endif + +#if defined(__cplusplus) +extern "C" { +#endif + +/* an opaque window handle (0 is the invalid handle; the sokol_app.h main + window is NOT addressable through this API while both coexist) */ +typedef struct sapp_window { uint32_t id; } sapp_window; + +typedef struct sapp_window_desc { + int x, y; /* top-left position in screen points (-1 = default) */ + int width, height; /* content size in logical points (0 = 640x480) */ + const char* title; + bool borderless; /* no title bar / decorations */ + bool no_high_dpi; /* true: framebuffer = logical size (dpi_scale 1) */ + int sample_count; /* MSAA sample count (0 = 1) */ + const void* mtl_device; /* macOS: the id sokol_gfx renders with */ + + /* callbacks -- all invoked on the main thread */ + void (*tick_cb)(sapp_window win, void* user); /* this window's vsync; only while visible */ + void (*event_cb)(const sapp_event* ev, sapp_window win, void* user); + void (*close_cb)(sapp_window win, void* user); /* user closed the window; call sapp_destroy_window() to accept */ + void* user_data; +} sapp_window_desc; + +SOKOL_APP_API_DECL sapp_window sapp_create_window(const sapp_window_desc* desc); +SOKOL_APP_API_DECL void sapp_destroy_window(sapp_window win); +SOKOL_APP_API_DECL bool sapp_window_valid(sapp_window win); + +/* geometry (valid whenever the window is alive) */ +SOKOL_APP_API_DECL int sapp_window_width(sapp_window win); /* logical points */ +SOKOL_APP_API_DECL int sapp_window_height(sapp_window win); +SOKOL_APP_API_DECL int sapp_window_framebuffer_width(sapp_window win); /* pixels */ +SOKOL_APP_API_DECL int sapp_window_framebuffer_height(sapp_window win); +SOKOL_APP_API_DECL float sapp_window_dpi_scale(sapp_window win); /* fb / logical, ONE source */ +SOKOL_APP_API_DECL int sapp_window_sample_count(sapp_window win); +SOKOL_APP_API_DECL bool sapp_window_occluded(sapp_window win); +SOKOL_APP_API_DECL void sapp_window_set_title(sapp_window win, const char* title); + +/* measured interval between this window's ticks in seconds (its display's + refresh period; a best-effort estimate before the second tick) */ +SOKOL_APP_API_DECL double sapp_window_frame_duration(sapp_window win); + +/* Metal swapchain handles -- valid ONLY inside this window's tick_cb */ +SOKOL_APP_API_DECL const void* sapp_window_metal_current_drawable(sapp_window win); +SOKOL_APP_API_DECL const void* sapp_window_metal_depth_stencil_texture(sapp_window win); +SOKOL_APP_API_DECL const void* sapp_window_metal_msaa_color_texture(sapp_window win); + +/* native escape hatch (macOS: NSWindow*) */ +SOKOL_APP_API_DECL const void* sapp_window_macos_get_window(sapp_window win); + +#if defined(__cplusplus) +} /* extern "C" */ +#endif +#endif /* SOKOL_APP_TC_INCLUDED */ + +/*=== IMPLEMENTATION =========================================================*/ +#if defined(SOKOL_APP_TC_IMPL_INCLUDED) && !defined(SOKOL_APP_TC_IMPL_DONE) +#define SOKOL_APP_TC_IMPL_DONE (1) + +#include +#include +#if defined(__APPLE__) +#include +#endif + +#if defined(__APPLE__) && TARGET_OS_OSX +/*== macOS ==================================================================*/ +#if !defined(__OBJC__) +#error "sokol_app_tc.h macOS implementation must be compiled as Objective-C++ (.mm)" +#endif +#import +#import +#import +#import + +@class _sapp_tc_view; +@class _sapp_tc_win_delegate; + +typedef struct _sapp_tc_window_t { + uint32_t win_id; + sapp_window_desc desc; + NSWindow* window; + _sapp_tc_view* view; + _sapp_tc_win_delegate* delegate; + CAMetalLayer* layer; + CADisplayLink* link; + id depth_tex; /* depth-stencil, drawable-sized */ + id msaa_tex; /* MSAA color (sample_count > 1) */ + id frame_drawable; /* valid during one tick only */ + int fb_width, fb_height; /* aux/drawable size (pixels) */ + int win_width, win_height; /* logical points (last seen) */ + float dpi_scale; + double frame_duration; /* measured tick interval */ + double last_tick_time; /* CADisplayLink timestamp */ + bool occluded; + bool in_tick; +} _sapp_tc_window_t; + +#define _SAPP_TC_MAX_WINDOWS (32) + +static struct { + bool keytable_valid; + sapp_keycode keycodes[SAPP_MAX_KEYCODES]; + uint32_t next_id; + _sapp_tc_window_t* windows[_SAPP_TC_MAX_WINDOWS]; +} _sapp_tc; + +/* keycode table lifted verbatim from sokol_app.h's _sapp_macos_init_keytable() + so SAPP_KEYCODE_* values are identical across main and secondary windows */ +static void _sapp_tc_init_keytable(void) { + if (_sapp_tc.keytable_valid) return; + _sapp_tc.keytable_valid = true; + _sapp_tc.keycodes[0x1D] = SAPP_KEYCODE_0; + _sapp_tc.keycodes[0x12] = SAPP_KEYCODE_1; + _sapp_tc.keycodes[0x13] = SAPP_KEYCODE_2; + _sapp_tc.keycodes[0x14] = SAPP_KEYCODE_3; + _sapp_tc.keycodes[0x15] = SAPP_KEYCODE_4; + _sapp_tc.keycodes[0x17] = SAPP_KEYCODE_5; + _sapp_tc.keycodes[0x16] = SAPP_KEYCODE_6; + _sapp_tc.keycodes[0x1A] = SAPP_KEYCODE_7; + _sapp_tc.keycodes[0x1C] = SAPP_KEYCODE_8; + _sapp_tc.keycodes[0x19] = SAPP_KEYCODE_9; + _sapp_tc.keycodes[0x00] = SAPP_KEYCODE_A; + _sapp_tc.keycodes[0x0B] = SAPP_KEYCODE_B; + _sapp_tc.keycodes[0x08] = SAPP_KEYCODE_C; + _sapp_tc.keycodes[0x02] = SAPP_KEYCODE_D; + _sapp_tc.keycodes[0x0E] = SAPP_KEYCODE_E; + _sapp_tc.keycodes[0x03] = SAPP_KEYCODE_F; + _sapp_tc.keycodes[0x05] = SAPP_KEYCODE_G; + _sapp_tc.keycodes[0x04] = SAPP_KEYCODE_H; + _sapp_tc.keycodes[0x22] = SAPP_KEYCODE_I; + _sapp_tc.keycodes[0x26] = SAPP_KEYCODE_J; + _sapp_tc.keycodes[0x28] = SAPP_KEYCODE_K; + _sapp_tc.keycodes[0x25] = SAPP_KEYCODE_L; + _sapp_tc.keycodes[0x2E] = SAPP_KEYCODE_M; + _sapp_tc.keycodes[0x2D] = SAPP_KEYCODE_N; + _sapp_tc.keycodes[0x1F] = SAPP_KEYCODE_O; + _sapp_tc.keycodes[0x23] = SAPP_KEYCODE_P; + _sapp_tc.keycodes[0x0C] = SAPP_KEYCODE_Q; + _sapp_tc.keycodes[0x0F] = SAPP_KEYCODE_R; + _sapp_tc.keycodes[0x01] = SAPP_KEYCODE_S; + _sapp_tc.keycodes[0x11] = SAPP_KEYCODE_T; + _sapp_tc.keycodes[0x20] = SAPP_KEYCODE_U; + _sapp_tc.keycodes[0x09] = SAPP_KEYCODE_V; + _sapp_tc.keycodes[0x0D] = SAPP_KEYCODE_W; + _sapp_tc.keycodes[0x07] = SAPP_KEYCODE_X; + _sapp_tc.keycodes[0x10] = SAPP_KEYCODE_Y; + _sapp_tc.keycodes[0x06] = SAPP_KEYCODE_Z; + _sapp_tc.keycodes[0x27] = SAPP_KEYCODE_APOSTROPHE; + _sapp_tc.keycodes[0x2A] = SAPP_KEYCODE_BACKSLASH; + _sapp_tc.keycodes[0x2B] = SAPP_KEYCODE_COMMA; + _sapp_tc.keycodes[0x18] = SAPP_KEYCODE_EQUAL; + _sapp_tc.keycodes[0x32] = SAPP_KEYCODE_GRAVE_ACCENT; + _sapp_tc.keycodes[0x21] = SAPP_KEYCODE_LEFT_BRACKET; + _sapp_tc.keycodes[0x1B] = SAPP_KEYCODE_MINUS; + _sapp_tc.keycodes[0x2F] = SAPP_KEYCODE_PERIOD; + _sapp_tc.keycodes[0x1E] = SAPP_KEYCODE_RIGHT_BRACKET; + _sapp_tc.keycodes[0x29] = SAPP_KEYCODE_SEMICOLON; + _sapp_tc.keycodes[0x2C] = SAPP_KEYCODE_SLASH; + _sapp_tc.keycodes[0x0A] = SAPP_KEYCODE_WORLD_1; + _sapp_tc.keycodes[0x33] = SAPP_KEYCODE_BACKSPACE; + _sapp_tc.keycodes[0x39] = SAPP_KEYCODE_CAPS_LOCK; + _sapp_tc.keycodes[0x75] = SAPP_KEYCODE_DELETE; + _sapp_tc.keycodes[0x7D] = SAPP_KEYCODE_DOWN; + _sapp_tc.keycodes[0x77] = SAPP_KEYCODE_END; + _sapp_tc.keycodes[0x24] = SAPP_KEYCODE_ENTER; + _sapp_tc.keycodes[0x35] = SAPP_KEYCODE_ESCAPE; + _sapp_tc.keycodes[0x7A] = SAPP_KEYCODE_F1; + _sapp_tc.keycodes[0x78] = SAPP_KEYCODE_F2; + _sapp_tc.keycodes[0x63] = SAPP_KEYCODE_F3; + _sapp_tc.keycodes[0x76] = SAPP_KEYCODE_F4; + _sapp_tc.keycodes[0x60] = SAPP_KEYCODE_F5; + _sapp_tc.keycodes[0x61] = SAPP_KEYCODE_F6; + _sapp_tc.keycodes[0x62] = SAPP_KEYCODE_F7; + _sapp_tc.keycodes[0x64] = SAPP_KEYCODE_F8; + _sapp_tc.keycodes[0x65] = SAPP_KEYCODE_F9; + _sapp_tc.keycodes[0x6D] = SAPP_KEYCODE_F10; + _sapp_tc.keycodes[0x67] = SAPP_KEYCODE_F11; + _sapp_tc.keycodes[0x6F] = SAPP_KEYCODE_F12; + _sapp_tc.keycodes[0x69] = SAPP_KEYCODE_F13; + _sapp_tc.keycodes[0x6B] = SAPP_KEYCODE_F14; + _sapp_tc.keycodes[0x71] = SAPP_KEYCODE_F15; + _sapp_tc.keycodes[0x6A] = SAPP_KEYCODE_F16; + _sapp_tc.keycodes[0x40] = SAPP_KEYCODE_F17; + _sapp_tc.keycodes[0x4F] = SAPP_KEYCODE_F18; + _sapp_tc.keycodes[0x50] = SAPP_KEYCODE_F19; + _sapp_tc.keycodes[0x5A] = SAPP_KEYCODE_F20; + _sapp_tc.keycodes[0x73] = SAPP_KEYCODE_HOME; + _sapp_tc.keycodes[0x72] = SAPP_KEYCODE_INSERT; + _sapp_tc.keycodes[0x7B] = SAPP_KEYCODE_LEFT; + _sapp_tc.keycodes[0x3A] = SAPP_KEYCODE_LEFT_ALT; + _sapp_tc.keycodes[0x3B] = SAPP_KEYCODE_LEFT_CONTROL; + _sapp_tc.keycodes[0x38] = SAPP_KEYCODE_LEFT_SHIFT; + _sapp_tc.keycodes[0x37] = SAPP_KEYCODE_LEFT_SUPER; + _sapp_tc.keycodes[0x6E] = SAPP_KEYCODE_MENU; + _sapp_tc.keycodes[0x47] = SAPP_KEYCODE_NUM_LOCK; + _sapp_tc.keycodes[0x79] = SAPP_KEYCODE_PAGE_DOWN; + _sapp_tc.keycodes[0x74] = SAPP_KEYCODE_PAGE_UP; + _sapp_tc.keycodes[0x7C] = SAPP_KEYCODE_RIGHT; + _sapp_tc.keycodes[0x3D] = SAPP_KEYCODE_RIGHT_ALT; + _sapp_tc.keycodes[0x3E] = SAPP_KEYCODE_RIGHT_CONTROL; + _sapp_tc.keycodes[0x3C] = SAPP_KEYCODE_RIGHT_SHIFT; + _sapp_tc.keycodes[0x36] = SAPP_KEYCODE_RIGHT_SUPER; + _sapp_tc.keycodes[0x31] = SAPP_KEYCODE_SPACE; + _sapp_tc.keycodes[0x30] = SAPP_KEYCODE_TAB; + _sapp_tc.keycodes[0x7E] = SAPP_KEYCODE_UP; + _sapp_tc.keycodes[0x52] = SAPP_KEYCODE_KP_0; + _sapp_tc.keycodes[0x53] = SAPP_KEYCODE_KP_1; + _sapp_tc.keycodes[0x54] = SAPP_KEYCODE_KP_2; + _sapp_tc.keycodes[0x55] = SAPP_KEYCODE_KP_3; + _sapp_tc.keycodes[0x56] = SAPP_KEYCODE_KP_4; + _sapp_tc.keycodes[0x57] = SAPP_KEYCODE_KP_5; + _sapp_tc.keycodes[0x58] = SAPP_KEYCODE_KP_6; + _sapp_tc.keycodes[0x59] = SAPP_KEYCODE_KP_7; + _sapp_tc.keycodes[0x5B] = SAPP_KEYCODE_KP_8; + _sapp_tc.keycodes[0x5C] = SAPP_KEYCODE_KP_9; + _sapp_tc.keycodes[0x45] = SAPP_KEYCODE_KP_ADD; + _sapp_tc.keycodes[0x41] = SAPP_KEYCODE_KP_DECIMAL; + _sapp_tc.keycodes[0x4B] = SAPP_KEYCODE_KP_DIVIDE; + _sapp_tc.keycodes[0x4C] = SAPP_KEYCODE_KP_ENTER; + _sapp_tc.keycodes[0x51] = SAPP_KEYCODE_KP_EQUAL; + _sapp_tc.keycodes[0x43] = SAPP_KEYCODE_KP_MULTIPLY; + _sapp_tc.keycodes[0x4E] = SAPP_KEYCODE_KP_SUBTRACT; +} + +static sapp_keycode _sapp_tc_translate_key(int scancode) { + if ((scancode >= 0) && (scancode < SAPP_MAX_KEYCODES)) { + return _sapp_tc.keycodes[scancode]; + } + return SAPP_KEYCODE_INVALID; +} + +static _sapp_tc_window_t* _sapp_tc_lookup(sapp_window win) { + if (win.id == 0) return 0; + for (int i = 0; i < _SAPP_TC_MAX_WINDOWS; i++) { + _sapp_tc_window_t* w = _sapp_tc.windows[i]; + if (w && w->win_id == win.id) return w; + } + return 0; +} + +static uint32_t _sapp_tc_mods(NSEvent* ev) { + const NSEventModifierFlags f = ev.modifierFlags; + uint32_t m = 0; + if (f & NSEventModifierFlagShift) m |= SAPP_MODIFIER_SHIFT; + if (f & NSEventModifierFlagControl) m |= SAPP_MODIFIER_CTRL; + if (f & NSEventModifierFlagOption) m |= SAPP_MODIFIER_ALT; + if (f & NSEventModifierFlagCommand) m |= SAPP_MODIFIER_SUPER; + return m; +} + +static void _sapp_tc_send(_sapp_tc_window_t* w, sapp_event* ev) { + if (!w->desc.event_cb) return; + ev->window_width = w->win_width; + ev->window_height = w->win_height; + ev->framebuffer_width = w->fb_width; + ev->framebuffer_height = w->fb_height; + sapp_window handle = { w->win_id }; + w->desc.event_cb(ev, handle, w->desc.user_data); +} + +/*-- content view -----------------------------------------------------------*/ +@interface _sapp_tc_view : NSView +@property (assign) _sapp_tc_window_t* w; +@end + +@implementation _sapp_tc_view + +- (BOOL)acceptsFirstResponder { return YES; } +- (BOOL)isFlipped { return YES; } /* top-left origin */ + +- (void)updateTrackingAreas { + for (NSTrackingArea* a in self.trackingAreas) [self removeTrackingArea:a]; + /* ActiveAlways: hover must work while the window is NOT key (a control + panel window tracks the pointer while the main window keeps focus) */ + NSTrackingArea* area = [[NSTrackingArea alloc] initWithRect:self.bounds + options:(NSTrackingMouseMoved | NSTrackingMouseEnteredAndExited | + NSTrackingActiveAlways | NSTrackingEnabledDuringMouseDrag | + NSTrackingInVisibleRect) + owner:self userInfo:nil]; + [self addTrackingArea:area]; + [super updateTrackingAreas]; +} + +- (void)mouseEvt:(NSEvent*)ev type:(sapp_event_type)type button:(sapp_mousebutton)btn { + _sapp_tc_window_t* w = self.w; + if (!w) return; + NSPoint p = [self convertPoint:ev.locationInWindow fromView:nil]; + sapp_event e; + memset(&e, 0, sizeof(e)); + e.type = type; + e.mouse_button = btn; + /* framebuffer pixels, same convention as sokol_app.h with high_dpi */ + e.mouse_x = (float)p.x * w->dpi_scale; + e.mouse_y = (float)p.y * w->dpi_scale; + e.mouse_dx = (float)ev.deltaX * w->dpi_scale; + e.mouse_dy = (float)ev.deltaY * w->dpi_scale; + e.modifiers = _sapp_tc_mods(ev); + _sapp_tc_send(w, &e); +} + +- (void)mouseDown:(NSEvent*)ev { [self mouseEvt:ev type:SAPP_EVENTTYPE_MOUSE_DOWN button:SAPP_MOUSEBUTTON_LEFT]; } +- (void)mouseUp:(NSEvent*)ev { [self mouseEvt:ev type:SAPP_EVENTTYPE_MOUSE_UP button:SAPP_MOUSEBUTTON_LEFT]; } +- (void)rightMouseDown:(NSEvent*)ev { [self mouseEvt:ev type:SAPP_EVENTTYPE_MOUSE_DOWN button:SAPP_MOUSEBUTTON_RIGHT]; } +- (void)rightMouseUp:(NSEvent*)ev { [self mouseEvt:ev type:SAPP_EVENTTYPE_MOUSE_UP button:SAPP_MOUSEBUTTON_RIGHT]; } +- (void)otherMouseDown:(NSEvent*)ev { [self mouseEvt:ev type:SAPP_EVENTTYPE_MOUSE_DOWN button:SAPP_MOUSEBUTTON_MIDDLE]; } +- (void)otherMouseUp:(NSEvent*)ev { [self mouseEvt:ev type:SAPP_EVENTTYPE_MOUSE_UP button:SAPP_MOUSEBUTTON_MIDDLE]; } +- (void)mouseMoved:(NSEvent*)ev { [self mouseEvt:ev type:SAPP_EVENTTYPE_MOUSE_MOVE button:SAPP_MOUSEBUTTON_INVALID]; } +- (void)mouseDragged:(NSEvent*)ev { [self mouseEvt:ev type:SAPP_EVENTTYPE_MOUSE_MOVE button:SAPP_MOUSEBUTTON_INVALID]; } +- (void)rightMouseDragged:(NSEvent*)ev { [self mouseEvt:ev type:SAPP_EVENTTYPE_MOUSE_MOVE button:SAPP_MOUSEBUTTON_INVALID]; } +- (void)otherMouseDragged:(NSEvent*)ev { [self mouseEvt:ev type:SAPP_EVENTTYPE_MOUSE_MOVE button:SAPP_MOUSEBUTTON_INVALID]; } +- (void)mouseEntered:(NSEvent*)ev { [self mouseEvt:ev type:SAPP_EVENTTYPE_MOUSE_ENTER button:SAPP_MOUSEBUTTON_INVALID]; } +- (void)mouseExited:(NSEvent*)ev { [self mouseEvt:ev type:SAPP_EVENTTYPE_MOUSE_LEAVE button:SAPP_MOUSEBUTTON_INVALID]; } + +- (void)scrollWheel:(NSEvent*)ev { + _sapp_tc_window_t* w = self.w; + if (!w) return; + float dx = (float)ev.scrollingDeltaX; + float dy = (float)ev.scrollingDeltaY; + if (ev.hasPreciseScrollingDeltas) { dx *= 0.1f; dy *= 0.1f; } + if ((fabsf(dx) > 0.0f) || (fabsf(dy) > 0.0f)) { + NSPoint p = [self convertPoint:ev.locationInWindow fromView:nil]; + sapp_event e; + memset(&e, 0, sizeof(e)); + e.type = SAPP_EVENTTYPE_MOUSE_SCROLL; + e.mouse_x = (float)p.x * w->dpi_scale; + e.mouse_y = (float)p.y * w->dpi_scale; + e.scroll_x = dx; + e.scroll_y = dy; + e.modifiers = _sapp_tc_mods(ev); + _sapp_tc_send(w, &e); + } +} + +- (void)keyEvt:(NSEvent*)ev pressed:(bool)down { + _sapp_tc_window_t* w = self.w; + if (!w) return; + sapp_event e; + memset(&e, 0, sizeof(e)); + e.type = down ? SAPP_EVENTTYPE_KEY_DOWN : SAPP_EVENTTYPE_KEY_UP; + e.key_code = _sapp_tc_translate_key(ev.keyCode); + e.key_repeat = ev.isARepeat; + e.modifiers = _sapp_tc_mods(ev); + _sapp_tc_send(w, &e); + + /* CHAR events on key down, same filtering as sokol_app.h (skip the + function-key private range and control characters) */ + if (down) { + NSString* chars = ev.characters; + const NSUInteger len = chars.length; + for (NSUInteger i = 0; i < len; i++) { + uint32_t cp = [chars characterAtIndex:i]; + if (CFStringIsSurrogateHighCharacter((UniChar)cp) && (i + 1) < len) { + const UniChar lo = [chars characterAtIndex:i + 1]; + if (CFStringIsSurrogateLowCharacter(lo)) { + cp = (uint32_t)CFStringGetLongCharacterForSurrogatePair((UniChar)cp, lo); + i++; + } + } + if ((cp & 0xFF00) == 0xF700) continue; /* NSF...FunctionKey range */ + if (cp < 32 || cp == 127) continue; + sapp_event ce; + memset(&ce, 0, sizeof(ce)); + ce.type = SAPP_EVENTTYPE_CHAR; + ce.char_code = cp; + ce.key_repeat = ev.isARepeat; + ce.modifiers = _sapp_tc_mods(ev); + _sapp_tc_send(w, &ce); + } + } +} + +- (void)keyDown:(NSEvent*)ev { [self keyEvt:ev pressed:true]; } +- (void)keyUp:(NSEvent*)ev { [self keyEvt:ev pressed:false]; } + +- (void)flagsChanged:(NSEvent*)ev { + _sapp_tc_window_t* w = self.w; + if (!w) return; + const sapp_keycode kc = _sapp_tc_translate_key(ev.keyCode); + if (kc == SAPP_KEYCODE_INVALID) return; + /* device-dependent modifier bits (same values GLFW/sokol_app rely on) */ + const NSEventModifierFlags f = ev.modifierFlags; + bool down = false; + switch (kc) { + case SAPP_KEYCODE_LEFT_SHIFT: down = (f & 0x0002) != 0; break; + case SAPP_KEYCODE_RIGHT_SHIFT: down = (f & 0x0004) != 0; break; + case SAPP_KEYCODE_LEFT_CONTROL: down = (f & 0x0001) != 0; break; + case SAPP_KEYCODE_RIGHT_CONTROL: down = (f & 0x2000) != 0; break; + case SAPP_KEYCODE_LEFT_ALT: down = (f & 0x0020) != 0; break; + case SAPP_KEYCODE_RIGHT_ALT: down = (f & 0x0040) != 0; break; + case SAPP_KEYCODE_LEFT_SUPER: down = (f & 0x0008) != 0; break; + case SAPP_KEYCODE_RIGHT_SUPER: down = (f & 0x0010) != 0; break; + case SAPP_KEYCODE_CAPS_LOCK: down = (f & NSEventModifierFlagCapsLock) != 0; break; + default: return; + } + sapp_event e; + memset(&e, 0, sizeof(e)); + e.type = down ? SAPP_EVENTTYPE_KEY_DOWN : SAPP_EVENTTYPE_KEY_UP; + e.key_code = kc; + e.modifiers = _sapp_tc_mods(ev); + _sapp_tc_send(w, &e); +} + +/*-- per-frame tick (CADisplayLink target) ---------------------------------*/ +- (void)tick:(CADisplayLink*)link { + _sapp_tc_window_t* w = self.w; + if (!w) return; + + /* Occluded => not due. THIS is what makes the nextDrawable stall + structurally impossible: the acquiring code below only runs for a + provably visible window. (The display link also auto-suspends for + occluded views; this gate covers the transition race.) */ + const bool occluded = (w->window.occlusionState & NSWindowOcclusionStateVisible) == 0; + if (occluded != w->occluded) { + w->occluded = occluded; + sapp_event e; + memset(&e, 0, sizeof(e)); + e.type = occluded ? SAPP_EVENTTYPE_SUSPENDED : SAPP_EVENTTYPE_RESUMED; + _sapp_tc_send(w, &e); + } + if (occluded) return; + + /* measured tick interval (the display's refresh period) */ + if (w->last_tick_time > 0.0) { + w->frame_duration = link.timestamp - w->last_tick_time; + } else { + w->frame_duration = link.targetTimestamp - link.timestamp; + } + w->last_tick_time = link.timestamp; + + /* keep the layer sized to the view; ONE dpi ratio source: the scale we + size the drawable with is the scale we report (and the scale mouse + coordinates are multiplied by) -- never assume backingScale == 2 */ + const CGFloat scale = w->desc.no_high_dpi ? 1.0 : w->window.backingScaleFactor; + const NSSize sz = self.bounds.size; + const int fbw = (int)(sz.width * scale); + const int fbh = (int)(sz.height * scale); + if (fbw <= 0 || fbh <= 0) return; + if ((int)w->layer.drawableSize.width != fbw || (int)w->layer.drawableSize.height != fbh) { + w->layer.contentsScale = scale; + w->layer.drawableSize = CGSizeMake(fbw, fbh); + } + const bool resized = (fbw != w->fb_width) || (fbh != w->fb_height) || + ((int)sz.width != w->win_width) || ((int)sz.height != w->win_height); + w->fb_width = fbw; + w->fb_height = fbh; + w->win_width = (int)sz.width; + w->win_height = (int)sz.height; + w->dpi_scale = (float)scale; + if (resized) { + sapp_event e; + memset(&e, 0, sizeof(e)); + e.type = SAPP_EVENTTYPE_RESIZED; + _sapp_tc_send(w, &e); + } + + /* depth-stencil / MSAA textures, drawable-sized */ + if (w->depth_tex == nil || (int)w->depth_tex.width != fbw || (int)w->depth_tex.height != fbh) { + id dev = w->layer.device; + MTLTextureDescriptor* dd = [MTLTextureDescriptor + texture2DDescriptorWithPixelFormat:MTLPixelFormatDepth32Float_Stencil8 + width:fbw height:fbh mipmapped:NO]; + dd.usage = MTLTextureUsageRenderTarget; + dd.storageMode = MTLStorageModePrivate; + dd.sampleCount = w->desc.sample_count; + dd.textureType = w->desc.sample_count > 1 ? MTLTextureType2DMultisample : MTLTextureType2D; + w->depth_tex = [dev newTextureWithDescriptor:dd]; + if (w->desc.sample_count > 1) { + MTLTextureDescriptor* md = [MTLTextureDescriptor + texture2DDescriptorWithPixelFormat:MTLPixelFormatBGRA8Unorm + width:fbw height:fbh mipmapped:NO]; + md.usage = MTLTextureUsageRenderTarget; + md.storageMode = MTLStorageModePrivate; + md.sampleCount = w->desc.sample_count; + md.textureType = MTLTextureType2DMultisample; + w->msaa_tex = [dev newTextureWithDescriptor:md]; + } + } + + w->frame_drawable = [w->layer nextDrawable]; + if (w->frame_drawable == nil) return; + w->in_tick = true; + if (w->desc.tick_cb) { + sapp_window handle = { w->win_id }; + w->desc.tick_cb(handle, w->desc.user_data); + } + w->in_tick = false; + w->frame_drawable = nil; +} + +@end + +/*-- window delegate --------------------------------------------------------*/ +@interface _sapp_tc_win_delegate : NSObject +@property (assign) _sapp_tc_window_t* w; +@end + +@implementation _sapp_tc_win_delegate +- (void)windowWillClose:(NSNotification*)n { + _sapp_tc_window_t* w = self.w; + if (w && w->desc.close_cb) { + sapp_window handle = { w->win_id }; + w->desc.close_cb(handle, w->desc.user_data); + } +} +- (void)windowDidBecomeKey:(NSNotification*)n { + _sapp_tc_window_t* w = self.w; + if (!w) return; + sapp_event e; memset(&e, 0, sizeof(e)); + e.type = SAPP_EVENTTYPE_FOCUSED; + _sapp_tc_send(w, &e); +} +- (void)windowDidResignKey:(NSNotification*)n { + _sapp_tc_window_t* w = self.w; + if (!w) return; + sapp_event e; memset(&e, 0, sizeof(e)); + e.type = SAPP_EVENTTYPE_UNFOCUSED; + _sapp_tc_send(w, &e); +} +@end + +/*-- public API -------------------------------------------------------------*/ +extern "C" { + +sapp_window sapp_create_window(const sapp_window_desc* desc) { + sapp_window invalid = { 0 }; + if (!desc) return invalid; + _sapp_tc_init_keytable(); + + int slot = -1; + for (int i = 0; i < _SAPP_TC_MAX_WINDOWS; i++) { + if (_sapp_tc.windows[i] == 0) { slot = i; break; } + } + if (slot < 0) return invalid; + + _sapp_tc_window_t* w = new _sapp_tc_window_t(); + w->win_id = ++_sapp_tc.next_id; + w->desc = *desc; + if (w->desc.width <= 0) w->desc.width = 640; + if (w->desc.height <= 0) w->desc.height = 480; + if (w->desc.sample_count <= 0) w->desc.sample_count = 1; + w->dpi_scale = 1.0f; + + const int px = (desc->x >= 0) ? desc->x : 120; + const int py = (desc->y >= 0) ? desc->y : 120; + NSRect rect = NSMakeRect(px, py, w->desc.width, w->desc.height); + NSWindowStyleMask style = NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | + NSWindowStyleMaskMiniaturizable | NSWindowStyleMaskResizable; + if (w->desc.borderless) style = NSWindowStyleMaskBorderless; + w->window = [[NSWindow alloc] initWithContentRect:rect styleMask:style + backing:NSBackingStoreBuffered defer:NO]; + if (w->desc.title) w->window.title = [NSString stringWithUTF8String:w->desc.title]; + w->window.releasedWhenClosed = NO; + w->window.acceptsMouseMovedEvents = YES; + + _sapp_tc_view* view = [[_sapp_tc_view alloc] initWithFrame:rect]; + view.w = w; + w->view = view; + w->layer = [CAMetalLayer layer]; + w->layer.device = (__bridge id)w->desc.mtl_device; + w->layer.pixelFormat = MTLPixelFormatBGRA8Unorm; + w->layer.opaque = YES; + view.wantsLayer = YES; + view.layer = w->layer; + w->window.contentView = view; + + _sapp_tc_win_delegate* del = [_sapp_tc_win_delegate new]; + del.w = w; + w->delegate = del; + w->window.delegate = del; + + _sapp_tc.windows[slot] = w; + [w->window makeKeyAndOrderFront:nil]; + + /* per-window display link: fires on the main run loop at THIS window's + display rate; auto-suspends while the view is not visible (macOS 14+) */ + w->link = [view displayLinkWithTarget:view selector:@selector(tick:)]; + [w->link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes]; + + sapp_window handle = { w->win_id }; + return handle; +} + +void sapp_destroy_window(sapp_window win) { + _sapp_tc_window_t* w = _sapp_tc_lookup(win); + if (!w) return; + for (int i = 0; i < _SAPP_TC_MAX_WINDOWS; i++) { + if (_sapp_tc.windows[i] == w) { _sapp_tc.windows[i] = 0; break; } + } + w->desc.close_cb = 0; /* no callback from the orderOut below */ + if (w->link) { [w->link invalidate]; w->link = nil; } + w->view.w = 0; + w->delegate.w = 0; + if (w->window) { + w->window.delegate = nil; + [w->window orderOut:nil]; + w->window = nil; + } + w->view = nil; + w->delegate = nil; + w->layer = nil; + w->depth_tex = nil; + w->msaa_tex = nil; + w->frame_drawable = nil; + delete w; +} + +bool sapp_window_valid(sapp_window win) { + return _sapp_tc_lookup(win) != 0; +} + +int sapp_window_width(sapp_window win) { + _sapp_tc_window_t* w = _sapp_tc_lookup(win); + if (!w || !w->view) return 0; + return (int)w->view.bounds.size.width; +} + +int sapp_window_height(sapp_window win) { + _sapp_tc_window_t* w = _sapp_tc_lookup(win); + if (!w || !w->view) return 0; + return (int)w->view.bounds.size.height; +} + +int sapp_window_framebuffer_width(sapp_window win) { + _sapp_tc_window_t* w = _sapp_tc_lookup(win); + return w ? w->fb_width : 0; +} + +int sapp_window_framebuffer_height(sapp_window win) { + _sapp_tc_window_t* w = _sapp_tc_lookup(win); + return w ? w->fb_height : 0; +} + +float sapp_window_dpi_scale(sapp_window win) { + _sapp_tc_window_t* w = _sapp_tc_lookup(win); + if (!w) return 1.0f; + if (w->dpi_scale > 0.0f && w->fb_width > 0) return w->dpi_scale; + return w->desc.no_high_dpi ? 1.0f : (float)w->window.backingScaleFactor; +} + +int sapp_window_sample_count(sapp_window win) { + _sapp_tc_window_t* w = _sapp_tc_lookup(win); + return w ? w->desc.sample_count : 1; +} + +bool sapp_window_occluded(sapp_window win) { + _sapp_tc_window_t* w = _sapp_tc_lookup(win); + return w ? w->occluded : false; +} + +void sapp_window_set_title(sapp_window win, const char* title) { + _sapp_tc_window_t* w = _sapp_tc_lookup(win); + if (w && w->window && title) { + w->window.title = [NSString stringWithUTF8String:title]; + } +} + +double sapp_window_frame_duration(sapp_window win) { + _sapp_tc_window_t* w = _sapp_tc_lookup(win); + if (!w) return 1.0 / 60.0; + return (w->frame_duration > 0.0) ? w->frame_duration : 1.0 / 60.0; +} + +const void* sapp_window_metal_current_drawable(sapp_window win) { + _sapp_tc_window_t* w = _sapp_tc_lookup(win); + if (!w || !w->in_tick) return 0; + return (__bridge const void*)w->frame_drawable; +} + +const void* sapp_window_metal_depth_stencil_texture(sapp_window win) { + _sapp_tc_window_t* w = _sapp_tc_lookup(win); + if (!w || !w->in_tick) return 0; + return (__bridge const void*)w->depth_tex; +} + +const void* sapp_window_metal_msaa_color_texture(sapp_window win) { + _sapp_tc_window_t* w = _sapp_tc_lookup(win); + if (!w || !w->in_tick || w->desc.sample_count <= 1) return 0; + return (__bridge const void*)w->msaa_tex; +} + +const void* sapp_window_macos_get_window(sapp_window win) { + _sapp_tc_window_t* w = _sapp_tc_lookup(win); + return w ? (__bridge const void*)w->window : 0; +} + +} /* extern "C" */ + +#else /* !macOS */ +/*== stubs (explicit platform gap; Windows/Linux ports replace these) =======*/ +#if defined(__cplusplus) +extern "C" { +#endif +sapp_window sapp_create_window(const sapp_window_desc* desc) { (void)desc; sapp_window w = {0}; return w; } +void sapp_destroy_window(sapp_window win) { (void)win; } +bool sapp_window_valid(sapp_window win) { (void)win; return false; } +int sapp_window_width(sapp_window win) { (void)win; return 0; } +int sapp_window_height(sapp_window win) { (void)win; return 0; } +int sapp_window_framebuffer_width(sapp_window win) { (void)win; return 0; } +int sapp_window_framebuffer_height(sapp_window win) { (void)win; return 0; } +float sapp_window_dpi_scale(sapp_window win) { (void)win; return 1.0f; } +int sapp_window_sample_count(sapp_window win) { (void)win; return 1; } +bool sapp_window_occluded(sapp_window win) { (void)win; return false; } +void sapp_window_set_title(sapp_window win, const char* title) { (void)win; (void)title; } +double sapp_window_frame_duration(sapp_window win) { (void)win; return 1.0 / 60.0; } +const void* sapp_window_metal_current_drawable(sapp_window win) { (void)win; return 0; } +const void* sapp_window_metal_depth_stencil_texture(sapp_window win) { (void)win; return 0; } +const void* sapp_window_metal_msaa_color_texture(sapp_window win) { (void)win; return 0; } +const void* sapp_window_macos_get_window(sapp_window win) { (void)win; return 0; } +#if defined(__cplusplus) +} /* extern "C" */ +#endif +#endif /* platform select */ + +#endif /* SOKOL_APP_TC_IMPL_INCLUDED */ + +/* + zlib/libpng license + + Copyright (c) 2018 Andre Weissflog + Copyright (c) 2026 tettou771 (multi-window extension) + + This software is provided 'as-is', without any express or implied warranty. + In no event will the authors be held liable for any damages arising from the + use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software in a + product, an acknowledgment in the product documentation would be + appreciated but is not required. + + 2. Altered source versions must be plainly marked as such, and must not + be misrepresented as being the original software. + + 3. This notice may not be removed or altered from any source + distribution. +*/ diff --git a/core/platform/mac/sokol_impl.mm b/core/platform/mac/sokol_impl.mm index 80211689..fd199197 100644 --- a/core/platform/mac/sokol_impl.mm +++ b/core/platform/mac/sokol_impl.mm @@ -14,6 +14,8 @@ #include "sokol_log.h" #include "sokol_app.h" +#define SOKOL_APP_TC_IMPL +#include "sokol_app_tc.h" #include "sokol_gfx.h" #include "sokol_glue.h" #include "util/sokol_gl_tc.h" diff --git a/core/platform/mac/tcWindowMac.mm b/core/platform/mac/tcWindowMac.mm index 156d3962..3cd14e9a 100644 --- a/core/platform/mac/tcWindowMac.mm +++ b/core/platform/mac/tcWindowMac.mm @@ -1,273 +1,84 @@ // ============================================================================= -// tcWindowMac.mm - Secondary window native glue (macOS) +// tcWindowMac.mm - TrussC adapter for secondary windows (macOS) // ============================================================================= -// One NSWindow + CAMetalLayer + CADisplayLink per secondary window. The display -// link fires on the main run loop at the window's own display rate, so a -// window on a 60 Hz display ticks at 60 while the main window runs at 120. -// A fully occluded window skips drawable acquisition entirely (the PoC showed -// acquiring while occluded blocks ~1s and stalls the main thread). +// The native windowing layer (NSWindow + CAMetalLayer + per-window +// CADisplayLink, occlusion-gated ticks, sapp_event conversion with the full +// sokol_app keycode table) lives in sokol/sokol_app_tc.h and knows nothing +// about TrussC. This file maps its C API onto TrussC: +// tick_cb -> WindowContext switch + per-window dt + update/draw the tree +// event_cb -> CoreEvents + App hooks + Node-tree dispatch (same keycode +// semantics as the main window: key == SAPP_KEYCODE_*) +// close_cb -> Window::close() // All rendering shares the one sokol_gfx context; each window gets its own // sokol_gl context (same pattern as Fbo). #include "TrussC.h" #if defined(__APPLE__) -#import -#import -#import -#import +#include "sokol_app_tc.h" // declarations only (impl lives in sokol_impl.mm) using namespace trussc; -// --------------------------------------------------------------------------- -// Native per-window state -// --------------------------------------------------------------------------- -@class TCWindowView; -@class TCWindowDelegate; - namespace { -struct NativeWindow { - Window* owner = nullptr; // back-pointer (owner outlives native) - NSWindow* window = nil; - TCWindowView* view = nil; - TCWindowDelegate* delegate = nil; - CAMetalLayer* layer = nil; - CADisplayLink* link = nil; - id depthTex = nil; // depth-stencil, drawable-sized - id msaaTex = nil; // MSAA color (when sampleCount > 1) - int sampleCount = 1; - int texW = 0, texH = 0; // size the aux textures were made for - id frameDrawable = nil; // valid during one tick only - bool loggedOccluded = false; - bool loggedGeometry = false; +struct AdapterState { + sapp_window win{0}; }; -// The swapchain provider handed to WindowContext::acquireSwapchain. Returns -// the drawable acquired at the top of the current tick (never blocks here). +// Swapchain provider handed to WindowContext::acquireSwapchain. The drawable +// was acquired by the native layer at the top of the current tick (it is only +// ever acquired for a visible window; never blocks here). sg_swapchain acquireSecondarySwapchain(void* user) { - auto* nw = static_cast(user); + auto* st = static_cast(user); sg_swapchain sc = {}; - if (!nw || nw->frameDrawable == nil) return sc; - sc.width = nw->texW; - sc.height = nw->texH; - sc.sample_count = nw->sampleCount; + if (!st) return sc; + const void* drawable = sapp_window_metal_current_drawable(st->win); + if (!drawable) return sc; + sc.width = sapp_window_framebuffer_width(st->win); + sc.height = sapp_window_framebuffer_height(st->win); + sc.sample_count = sapp_window_sample_count(st->win); sc.color_format = SG_PIXELFORMAT_BGRA8; sc.depth_format = SG_PIXELFORMAT_DEPTH_STENCIL; - sc.metal.current_drawable = (__bridge const void*)nw->frameDrawable; - sc.metal.depth_stencil_texture = (__bridge const void*)nw->depthTex; - if (nw->sampleCount > 1) { - sc.metal.msaa_color_texture = (__bridge const void*)nw->msaaTex; + sc.metal.current_drawable = drawable; + sc.metal.depth_stencil_texture = sapp_window_metal_depth_stencil_texture(st->win); + if (sc.sample_count > 1) { + sc.metal.msaa_color_texture = sapp_window_metal_msaa_color_texture(st->win); } return sc; } -void ensureAuxTextures(NativeWindow* nw, int w, int h) { - if (nw->texW == w && nw->texH == h && nw->depthTex != nil && - (nw->sampleCount <= 1 || nw->msaaTex != nil)) return; - id dev = nw->layer.device; - MTLTextureDescriptor* dd = [MTLTextureDescriptor - texture2DDescriptorWithPixelFormat:MTLPixelFormatDepth32Float_Stencil8 - width:w height:h mipmapped:NO]; - dd.usage = MTLTextureUsageRenderTarget; - dd.storageMode = MTLStorageModePrivate; - dd.sampleCount = nw->sampleCount; - dd.textureType = nw->sampleCount > 1 ? MTLTextureType2DMultisample : MTLTextureType2D; - nw->depthTex = [dev newTextureWithDescriptor:dd]; - if (nw->sampleCount > 1) { - MTLTextureDescriptor* md = [MTLTextureDescriptor - texture2DDescriptorWithPixelFormat:MTLPixelFormatBGRA8Unorm - width:w height:h mipmapped:NO]; - md.usage = MTLTextureUsageRenderTarget; - md.storageMode = MTLStorageModePrivate; - md.sampleCount = nw->sampleCount; - md.textureType = MTLTextureType2DMultisample; - nw->msaaTex = [dev newTextureWithDescriptor:md]; - } - nw->texW = w; nw->texH = h; -} - -} // namespace - -// --------------------------------------------------------------------------- -// Content view: input forwarding into the window's WindowContext / CoreEvents -// --------------------------------------------------------------------------- -@interface TCWindowView : NSView -@property (assign) NativeWindow* nw; -@end - -@implementation TCWindowView - -- (BOOL)acceptsFirstResponder { return YES; } -- (BOOL)isFlipped { return YES; } // top-left origin, matches TrussC coords - -- (void)updateTrackingAreas { - for (NSTrackingArea* a in self.trackingAreas) [self removeTrackingArea:a]; - // ActiveAlways: hover must work while the window is NOT key (the main - // window keeps focus; a control-panel window still tracks the pointer). - NSTrackingArea* area = [[NSTrackingArea alloc] initWithRect:self.bounds - options:(NSTrackingMouseMoved | NSTrackingMouseEnteredAndExited | - NSTrackingActiveAlways | NSTrackingEnabledDuringMouseDrag | - NSTrackingInVisibleRect) - owner:self userInfo:nil]; - [self addTrackingArea:area]; - [super updateTrackingAreas]; -} - -- (Vec2)localPos:(NSEvent*)ev { - NSPoint p = [self convertPoint:ev.locationInWindow fromView:nil]; - return Vec2((float)p.x, (float)p.y); // isFlipped => already top-left -} - -- (void)forwardMods:(NSEvent*)ev shift:(bool*)s ctrl:(bool*)c alt:(bool*)a super:(bool*)sp { - NSEventModifierFlags f = ev.modifierFlags; - *s = (f & NSEventModifierFlagShift) != 0; - *c = (f & NSEventModifierFlagControl) != 0; - *a = (f & NSEventModifierFlagOption) != 0; - *sp = (f & NSEventModifierFlagCommand) != 0; -} - -- (void)mouse:(NSEvent*)ev button:(int)btn pressed:(bool)down { - if (!self.nw || !self.nw->owner) return; - Window& win = *self.nw->owner; - auto& ctx = win.context(); - Vec2 p = [self localPos:ev]; - auto* prev = internal::currentWindowCtx; - internal::currentWindowCtx = &ctx; - ctx.mouseX = p.x; ctx.mouseY = p.y; - ctx.mousePressed = down; - ctx.mouseButton = down ? btn : -1; - MouseEventArgs e; - e.x = p.x; e.y = p.y; e.pos = p; e.globalPos = p; e.button = btn; - [self forwardMods:ev shift:&e.shift ctrl:&e.ctrl alt:&e.alt super:&e.super]; - if (down) { - win.events().mousePressed.notify(e); - if (win.app_) win.app_->mousePressed(e); - win.dispatchMousePressToTree(e); - } else { - win.events().mouseReleased.notify(e); - if (win.app_) win.app_->mouseReleased(e); - win.dispatchMouseReleaseToTree(e); - } - internal::currentWindowCtx = prev; -} - -- (void)mouseDown:(NSEvent*)ev { [self mouse:ev button:0 pressed:true]; } -- (void)mouseUp:(NSEvent*)ev { [self mouse:ev button:0 pressed:false]; } -- (void)rightMouseDown:(NSEvent*)ev { [self mouse:ev button:1 pressed:true]; } -- (void)rightMouseUp:(NSEvent*)ev { [self mouse:ev button:1 pressed:false]; } - -- (void)moveWith:(NSEvent*)ev { - if (!self.nw || !self.nw->owner) return; - Window& win = *self.nw->owner; - auto& ctx = win.context(); - Vec2 p = [self localPos:ev]; - ctx.pmouseX = ctx.mouseX; ctx.pmouseY = ctx.mouseY; - ctx.mouseX = p.x; ctx.mouseY = p.y; - MouseMoveEventArgs e; - e.x = p.x; e.y = p.y; - e.deltaX = (float)ev.deltaX; e.deltaY = (float)ev.deltaY; - [self forwardMods:ev shift:&e.shift ctrl:&e.ctrl alt:&e.alt super:&e.super]; - win.events().mouseMoved.notify(e); - if (self.nw->owner->app_) self.nw->owner->app_->mouseMoved(e); -} -- (void)mouseMoved:(NSEvent*)ev { [self moveWith:ev]; } -- (void)mouseDragged:(NSEvent*)ev { [self moveWith:ev]; } -- (void)mouseEntered:(NSEvent*)ev { [self moveWith:ev]; } -- (void)mouseExited:(NSEvent*)ev { - // Park the cursor offscreen so the next tick's updateHoverState clears - // this window's hover (hoveredNode lives in ITS WindowContext). - if (!self.nw || !self.nw->owner) return; - auto& ctx = self.nw->owner->context(); - ctx.pmouseX = ctx.mouseX; ctx.pmouseY = ctx.mouseY; - ctx.mouseX = -1.0f; ctx.mouseY = -1.0f; -} - -- (void)keyEvt:(NSEvent*)ev pressed:(bool)down { - if (!self.nw || !self.nw->owner) return; - Window& win = *self.nw->owner; - auto& ctx = win.context(); - // NSEvent keyCode is a hardware code; map letters/digits via characters - // for the common case (full keycode table is Phase 2 polish). - int key = 0; - NSString* chars = ev.charactersIgnoringModifiers; - if (chars.length > 0) key = toupper([chars characterAtIndex:0]); - if (down) ctx.keysPressed.insert(key); else ctx.keysPressed.erase(key); - KeyEventArgs e; - e.key = key; e.isRepeat = ev.isARepeat; - [self forwardMods:ev shift:&e.shift ctrl:&e.ctrl alt:&e.alt super:&e.super]; - if (down) { - win.events().keyPressed.notify(e); - if (win.app_) win.app_->keyPressed(e); - } else { - win.events().keyReleased.notify(e); - if (win.app_) win.app_->keyReleased(e); - } -} -- (void)keyDown:(NSEvent*)ev { [self keyEvt:ev pressed:true]; } -- (void)keyUp:(NSEvent*)ev { [self keyEvt:ev pressed:false]; } - -// -- per-frame tick (CADisplayLink target) -- -- (void)tick:(CADisplayLink*)link { - NativeWindow* nw = self.nw; - if (!nw || !nw->owner) return; - Window& win = *nw->owner; - auto& ctx = win.context(); - - // Occluded => not due. This is what prevents the ~1s nextDrawable stall. - if ((nw->window.occlusionState & NSWindowOcclusionStateVisible) == 0) { - if (!nw->loggedOccluded) { - nw->loggedOccluded = true; - logNotice("Window") << "second window occluded - skipping frames (no stall)"; - } - return; - } - nw->loggedOccluded = false; +// --- tick: drive this window's update/draw with its context active --------- +void windowTick(sapp_window swin, void* user) { + Window* win = static_cast(user); + if (!win) return; + auto& ctx = win->context(); - // Keep layer size in sync with the view - CGFloat scale = nw->window.backingScaleFactor; - NSSize sz = self.bounds.size; - int fbw = (int)(sz.width * scale), fbh = (int)(sz.height * scale); + const int fbw = sapp_window_framebuffer_width(swin); + const int fbh = sapp_window_framebuffer_height(swin); if (fbw <= 0 || fbh <= 0) return; - if ((int)nw->layer.drawableSize.width != fbw || (int)nw->layer.drawableSize.height != fbh) { - nw->layer.contentsScale = scale; - nw->layer.drawableSize = CGSizeMake(fbw, fbh); - } - ctx.fbWidth = fbw; ctx.fbHeight = fbh; ctx.dpiScale = (float)scale; + ctx.fbWidth = fbw; + ctx.fbHeight = fbh; + ctx.dpiScale = sapp_window_dpi_scale(swin); // Keep a RectNode root in sync with the window's logical size (same // contract as the main App on SAPP_EVENTTYPE_RESIZED). - win.syncRootSize((float)sz.width, (float)sz.height); - if (!nw->loggedGeometry) { - nw->loggedGeometry = true; - logNotice("Window") << "geometry: bounds=" << sz.width << "x" << sz.height - << " backingScale=" << (float)nw->window.backingScaleFactor - << " contentsScale=" << (float)nw->layer.contentsScale - << " drawable=" << fbw << "x" << fbh - << " screen=" << nw->window.screen.frame.size.width << "x" << nw->window.screen.frame.size.height; - } - - nw->frameDrawable = [nw->layer nextDrawable]; - if (nw->frameDrawable == nil) return; - ensureAuxTextures(nw, fbw, fbh); + win->syncRootSize((float)sapp_window_width(swin), (float)sapp_window_height(swin)); - // Per-window delta time: wall-clock between THIS window's processed ticks, - // so getDeltaTime() inside this window's update/draw is correct even when - // its display runs at a different refresh rate than the main window's. - // Same semantics as the main loop: a long gap (occlusion) yields one large - // delta, exactly like an event-driven main window. + // Per-window delta time: wall-clock between THIS window's processed + // ticks, so getDeltaTime() inside this window's update/draw is correct + // even when its display runs at a different refresh rate than the main + // window's. A long gap (occlusion) yields one large delta, exactly like + // an event-driven main window. { auto callNow = std::chrono::high_resolution_clock::now(); if (!ctx.lastUpdateCallTimeInitialized) { ctx.lastUpdateCallTimeInitialized = true; - // First tick: estimate from the display link's frame interval. - ctx.updateDeltaTime = link.targetTimestamp - link.timestamp; + ctx.updateDeltaTime = sapp_window_frame_duration(swin); } else { ctx.updateDeltaTime = std::chrono::duration(callNow - ctx.lastUpdateCallTime).count(); } ctx.lastUpdateCallTime = callNow; } - // --- render this window's tree with its context active --- auto* prev = internal::currentWindowCtx; internal::currentWindowCtx = &ctx; @@ -279,7 +90,7 @@ - (void)tick:(CADisplayLink*)link { cdesc.max_commands = 16384; cdesc.color_format = SG_PIXELFORMAT_BGRA8; cdesc.depth_format = SG_PIXELFORMAT_DEPTH_STENCIL; - cdesc.sample_count = nw->sampleCount; + cdesc.sample_count = sapp_window_sample_count(swin); ctx.swapchainTarget.context = sgl_make_context(&cdesc); } sgl_set_context(ctx.swapchainTarget.context); @@ -287,37 +98,152 @@ - (void)tick:(CADisplayLink*)link { beginFrame(); - win.events().update.notify(); - win.tickTree(); + win->events().update.notify(); + win->tickTree(); - clear(win.clearColor_.r, win.clearColor_.g, win.clearColor_.b, win.clearColor_.a); - win.events().draw.notify(); - win.drawTreeNow(); + const Color& cc = win->clearColor_; + clear(cc.r, cc.g, cc.b, cc.a); + win->events().draw.notify(); + win->drawTreeNow(); present(); - win.events().afterFrame.notify(); + win->events().afterFrame.notify(); sgl_set_context(sgl_default_context()); internal::currentWindowCtx = prev; - nw->frameDrawable = nil; } -@end +// --- events: map sapp_event onto CoreEvents / App hooks / tree dispatch ---- +void windowEvent(const sapp_event* ev, sapp_window swin, void* user) { + Window* win = static_cast(user); + if (!win) return; + auto& ctx = win->context(); + auto* prev = internal::currentWindowCtx; + internal::currentWindowCtx = &ctx; -// --------------------------------------------------------------------------- -// Window delegate: user closes the window -// --------------------------------------------------------------------------- -@interface TCWindowDelegate : NSObject -@property (assign) NativeWindow* nw; -@end - -@implementation TCWindowDelegate -- (void)windowWillClose:(NSNotification*)n { - if (self.nw && self.nw->owner) { - self.nw->owner->close(); // tears down link + native state + // Raw event pass-through (same hook addons use on the main window) + win->events().rawEvent.notify(*ev); + + // sapp coords arrive in framebuffer pixels; TrussC windows use logical + // points (secondary windows have no pixelPerfect mode for now) + const float dpi = sapp_window_dpi_scale(swin); + const float scale = (dpi > 0.0f) ? (1.0f / dpi) : 1.0f; + const bool hasShift = (ev->modifiers & SAPP_MODIFIER_SHIFT) != 0; + const bool hasCtrl = (ev->modifiers & SAPP_MODIFIER_CTRL) != 0; + const bool hasAlt = (ev->modifiers & SAPP_MODIFIER_ALT) != 0; + const bool hasSuper = (ev->modifiers & SAPP_MODIFIER_SUPER) != 0; + + switch (ev->type) { + case SAPP_EVENTTYPE_MOUSE_DOWN: { + const float x = ev->mouse_x * scale, y = ev->mouse_y * scale; + ctx.mouseX = x; ctx.mouseY = y; + ctx.mouseButton = (int)ev->mouse_button; + ctx.mousePressed = true; + MouseEventArgs e; + e.pos = e.globalPos = Vec2(x, y); + e.button = (int)ev->mouse_button; + e.shift = hasShift; e.ctrl = hasCtrl; e.alt = hasAlt; e.super = hasSuper; + e.syncLegacy(); + win->events().mousePressed.notify(e); + if (win->getApp()) win->getApp()->mousePressed(e); + win->dispatchMousePressToTree(e); + break; + } + case SAPP_EVENTTYPE_MOUSE_UP: { + const float x = ev->mouse_x * scale, y = ev->mouse_y * scale; + ctx.mouseX = x; ctx.mouseY = y; + ctx.mouseButton = -1; + ctx.mousePressed = false; + MouseEventArgs e; + e.pos = e.globalPos = Vec2(x, y); + e.button = (int)ev->mouse_button; + e.shift = hasShift; e.ctrl = hasCtrl; e.alt = hasAlt; e.super = hasSuper; + e.syncLegacy(); + win->events().mouseReleased.notify(e); + if (win->getApp()) win->getApp()->mouseReleased(e); + win->dispatchMouseReleaseToTree(e); + break; + } + case SAPP_EVENTTYPE_MOUSE_ENTER: + case SAPP_EVENTTYPE_MOUSE_MOVE: { + const float x = ev->mouse_x * scale, y = ev->mouse_y * scale; + ctx.pmouseX = ctx.mouseX; ctx.pmouseY = ctx.mouseY; + ctx.mouseX = x; ctx.mouseY = y; + internal::MouseEventRaw raw; + raw.pos = raw.globalPos = Vec2(x, y); + raw.delta = raw.globalDelta = Vec2(ev->mouse_dx * scale, ev->mouse_dy * scale); + raw.shift = hasShift; raw.ctrl = hasCtrl; raw.alt = hasAlt; raw.super = hasSuper; + if (ctx.mousePressed && ctx.mouseButton >= 0) { + raw.button = ctx.mouseButton; + MouseDragEventArgs e = internal::toDragArgs(raw); + win->events().mouseDragged.notify(e); + if (win->getApp()) win->getApp()->mouseDragged(e); + } else { + MouseMoveEventArgs e = internal::toMoveArgs(raw); + win->events().mouseMoved.notify(e); + if (win->getApp()) win->getApp()->mouseMoved(e); + } + break; + } + case SAPP_EVENTTYPE_MOUSE_LEAVE: { + // Park the cursor offscreen so the next tick's updateHoverState + // clears this window's hover (hoveredNode lives in ITS context). + ctx.pmouseX = ctx.mouseX; ctx.pmouseY = ctx.mouseY; + ctx.mouseX = -1.0f; ctx.mouseY = -1.0f; + break; + } + case SAPP_EVENTTYPE_MOUSE_SCROLL: { + ScrollEventArgs e; + e.pos = e.globalPos = Vec2(ctx.mouseX, ctx.mouseY); + e.scroll = Vec2(ev->scroll_x, ev->scroll_y); + e.shift = hasShift; e.ctrl = hasCtrl; e.alt = hasAlt; e.super = hasSuper; + e.syncLegacy(); + win->events().mouseScrolled.notify(e); + if (win->getApp()) win->getApp()->mouseScrolled(e); + break; + } + case SAPP_EVENTTYPE_KEY_DOWN: { + KeyEventArgs e; + e.key = ev->key_code; // SAPP_KEYCODE_*, identical to the main window + e.isRepeat = ev->key_repeat; + e.shift = hasShift; e.ctrl = hasCtrl; e.alt = hasAlt; e.super = hasSuper; + if (!ev->key_repeat) ctx.keysPressed.insert(e.key); + win->events().keyPressed.notify(e); + if (win->getApp()) win->getApp()->keyPressed(e); + break; + } + case SAPP_EVENTTYPE_KEY_UP: { + KeyEventArgs e; + e.key = ev->key_code; + e.isRepeat = false; + e.shift = hasShift; e.ctrl = hasCtrl; e.alt = hasAlt; e.super = hasSuper; + ctx.keysPressed.erase(e.key); + win->events().keyReleased.notify(e); + if (win->getApp()) win->getApp()->keyReleased(e); + break; + } + case SAPP_EVENTTYPE_SUSPENDED: + logNotice("Window") << "second window occluded - skipping frames (no stall)"; + break; + case SAPP_EVENTTYPE_RESUMED: + logNotice("Window") << "second window visible again - resuming ticks"; + break; + default: + // CHAR / RESIZED / FOCUSED / UNFOCUSED: rawEvent carries them; + // size sync happens per tick. + break; } + + internal::currentWindowCtx = prev; } -@end + +void windowClosed(sapp_window swin, void* user) { + (void)swin; + Window* win = static_cast(user); + if (win) win->close(); // tears down native + app state +} + +} // namespace // --------------------------------------------------------------------------- // Window methods (macOS implementations) @@ -327,26 +253,18 @@ - (void)windowWillClose:(NSNotification*)n { Window::~Window() { close(); } void Window::close() { - auto* nw = static_cast(native_); - if (!nw) return; - native_ = nullptr; // re-entrancy guard (windowWillClose) + auto* st = static_cast(native_); + if (!st) return; + native_ = nullptr; // re-entrancy guard (close_cb) ctx_.acquireSwapchain = nullptr; ctx_.acquireSwapchainUser = nullptr; - if (nw->link) { [nw->link invalidate]; nw->link = nil; } - nw->view.nw = nullptr; - nw->delegate.nw = nullptr; - if (nw->window) { - nw->window.delegate = nil; - [nw->window orderOut:nil]; - nw->window = nil; - } + sapp_destroy_window(st->win); if (ctx_.swapchainTarget.context.id != SG_INVALID_ID) { sgl_destroy_context(ctx_.swapchainTarget.context); ctx_.swapchainTarget.context.id = SG_INVALID_ID; ctx_.swapchainTarget.cache.clear(); } - nw->owner = nullptr; - delete nw; + delete st; if (app_) { app_->exit(); app_->cleanup(); @@ -357,20 +275,18 @@ - (void)windowWillClose:(NSNotification*)n { } void Window::setTitle(const std::string& title) { - auto* nw = static_cast(native_); - if (nw && nw->window) nw->window.title = [NSString stringWithUTF8String:title.c_str()]; + auto* st = static_cast(native_); + if (st) sapp_window_set_title(st->win, title.c_str()); } int Window::getWidth() const { - auto* nw = static_cast(native_); - if (!nw || !nw->view) return 0; - return (int)nw->view.bounds.size.width; + auto* st = static_cast(native_); + return st ? sapp_window_width(st->win) : 0; } int Window::getHeight() const { - auto* nw = static_cast(native_); - if (!nw || !nw->view) return 0; - return (int)nw->view.bounds.size.height; + auto* st = static_cast(native_); + return st ? sapp_window_height(st->win) : 0; } std::shared_ptr createWindow(const WindowSettings& settings) { @@ -379,48 +295,33 @@ - (void)windowWillClose:(NSNotification*)n { return nullptr; } auto win = std::shared_ptr(new Window()); - auto* nw = new NativeWindow(); - nw->owner = win.get(); - nw->sampleCount = settings.sampleCount; - - NSRect rect = NSMakeRect(120, 120, settings.width, settings.height); - NSWindowStyleMask style = NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | - NSWindowStyleMaskMiniaturizable | NSWindowStyleMaskResizable; - if (!settings.decorated) style = NSWindowStyleMaskBorderless; - nw->window = [[NSWindow alloc] initWithContentRect:rect styleMask:style - backing:NSBackingStoreBuffered defer:NO]; - nw->window.title = [NSString stringWithUTF8String:settings.title.c_str()]; - nw->window.releasedWhenClosed = NO; - nw->window.acceptsMouseMovedEvents = YES; // hover without key status - - TCWindowView* view = [[TCWindowView alloc] initWithFrame:rect]; - view.nw = nw; - nw->view = view; - nw->layer = [CAMetalLayer layer]; + auto* st = new AdapterState(); + + sapp_window_desc d = {}; + d.x = -1; + d.y = -1; + d.width = settings.width; + d.height = settings.height; + d.title = settings.title.c_str(); + d.borderless = !settings.decorated; + d.no_high_dpi = !settings.highDpi; + d.sample_count = settings.sampleCount; // Share the device sokol_gfx renders with (sokol_app created it) - nw->layer.device = (__bridge id)sglue_environment().metal.device; - nw->layer.pixelFormat = MTLPixelFormatBGRA8Unorm; - nw->layer.opaque = YES; - view.wantsLayer = YES; - view.layer = nw->layer; - nw->window.contentView = view; - - TCWindowDelegate* del = [TCWindowDelegate new]; - del.nw = nw; - nw->delegate = del; - nw->window.delegate = del; - - win->native_ = nw; - win->ctx_.acquireSwapchain = &acquireSecondarySwapchain; - win->ctx_.acquireSwapchainUser = nw; - - [nw->window makeKeyAndOrderFront:nil]; - - // Per-window display link: fires on the main run loop at THIS window's - // display rate (macOS 14+). - nw->link = [view displayLinkWithTarget:view selector:@selector(tick:)]; - [nw->link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes]; + d.mtl_device = sglue_environment().metal.device; + d.tick_cb = &windowTick; + d.event_cb = &windowEvent; + d.close_cb = &windowClosed; + d.user_data = win.get(); + st->win = sapp_create_window(&d); + if (st->win.id == 0) { + delete st; + logError("Window") << "createWindow(): native window creation failed"; + return nullptr; + } + win->native_ = st; + win->ctx_.acquireSwapchain = &acquireSecondarySwapchain; + win->ctx_.acquireSwapchainUser = st; return win; } From 6686f27581093b867e151144056f10cacdb0495c Mon Sep 17 00:00:00 2001 From: tettou771 Date: Tue, 7 Jul 2026 22:43:39 +0900 Subject: [PATCH 16/87] feat: sokol_app_tc.h implements the sapp_* API on macOS (P0b: main window = window #0) The header now owns [NSApp run] and the full app lifecycle on macOS; sokol_app.h is included declarations-only (its mac implementation is no longer compiled). TrussC.h / tcHotReloadHost.h are untouched -- all ~52 sapp_* call sites resolve to the header's window-#0 shims. - quit: upstream's cancellable performClose dance + NSApp terminate on main-window close (exits even with secondary windows open) - occlusion: main window keeps upstream's model (display link + 60Hz fallback timer); secondary windows keep the tick-gate model - drawable: lazy acquisition in sapp_get_swapchain() with per-tick cache - ported per docs/dev/sapp-mac-impl-spec.md: RGB10A2 (4 sites) + framebufferOnly=false, Cmd-keyup monitor, Cmd+V paste event, cursor table, drag&drop, clipboard, fullscreen tracking - deviation: pre-run sapp_dpi_scale() returns the main screen scale (upstream returned 0 -- fixes a latent pixelPerfect div-by-zero) Verified: 85/85 examples build, 4/4 core test suites pass (68 asserts), render/resize/fullscreen/quit/minimize/hot-reload E2E on macOS. --- core/include/sokol/sokol_app_tc.h | 930 +++++++++++++++++++++++++++++- core/platform/mac/sokol_impl.mm | 7 +- docs/dev/sapp-mac-impl-spec.md | 400 +++++++++++++ docs/dev/sokol_app_tc-design.md | 19 + 4 files changed, 1346 insertions(+), 10 deletions(-) create mode 100644 docs/dev/sapp-mac-impl-spec.md diff --git a/core/include/sokol/sokol_app_tc.h b/core/include/sokol/sokol_app_tc.h index 97c9d7e1..26e7615a 100644 --- a/core/include/sokol/sokol_app_tc.h +++ b/core/include/sokol/sokol_app_tc.h @@ -18,11 +18,31 @@ WHAT IT IS ========== - Additional native windows next to the single window owned by sokol_app.h. - The API shape follows floooh's multi-window sketch (sokol PR #437 / - issue #229): a small `sapp_window` handle, a `sapp_window_desc`, and - window-parameterized query functions. What is NEW compared to #437 is the - frame model, and it is the load-bearing idea: + Native multi-window support with per-window vsync ticks. On macOS this + header additionally IMPLEMENTS the public single-window sokol_app.h API + (sapp_run, sapp_width, sapp_dpi_scale, ...): the "main window" is simply + window #0, created from the sapp_desc, and the app's OS run loop is owned + here. sokol_app.h must then be included WITHOUT its implementation + (declarations only) in the implementation TU: + + #include "sokol_app.h" // no SOKOL_IMPL / SOKOL_APP_IMPL here + #define SOKOL_IMPL + #define SOKOL_APP_TC_IMPL + #include "sokol_app_tc.h" // implements the sapp_* API on macOS + #include "sokol_gfx.h" + ... + + The single-window API behaves like sokol_app.h's macOS backend (quit + flow via windowShouldClose, lazy drawable acquisition in + sapp_get_swapchain(), RGB10A2 default color format, clipboard, drag&drop, + cursor control, fullscreen), with one deliberate deviation: calling + sapp_dpi_scale() before sapp_run() returns the main screen's backing + scale factor instead of 0. + + The multi-window API shape follows floooh's multi-window sketch (sokol + PR #437 / issue #229): a small `sapp_window` handle, a `sapp_window_desc`, + and window-parameterized query functions. What is NEW compared to #437 is + the frame model, and it is the load-bearing idea: - There is no shared frame loop that iterates windows. Each window has its OWN vsync source (macOS: one CADisplayLink per window, delivered @@ -135,6 +155,7 @@ SOKOL_APP_API_DECL const void* sapp_window_macos_get_window(sapp_window win); #define SOKOL_APP_TC_IMPL_DONE (1) #include +#include #include #if defined(__APPLE__) #include @@ -150,20 +171,27 @@ SOKOL_APP_API_DECL const void* sapp_window_macos_get_window(sapp_window win); #import #import +#if defined(SOKOL_APP_IMPL_INCLUDED) +#error "sokol_app_tc.h owns the macOS implementation of the sapp_* API; include sokol_app.h WITHOUT an implementation define in this TU" +#endif + @class _sapp_tc_view; @class _sapp_tc_win_delegate; +@class _sapp_tc_app_delegate; typedef struct _sapp_tc_window_t { uint32_t win_id; sapp_window_desc desc; + bool is_main; /* window #0: created by sapp_run, drives the app callbacks */ NSWindow* window; _sapp_tc_view* view; _sapp_tc_win_delegate* delegate; CAMetalLayer* layer; CADisplayLink* link; + MTLPixelFormat color_fmt; /* layer + MSAA color texture format */ id depth_tex; /* depth-stencil, drawable-sized */ id msaa_tex; /* MSAA color (sample_count > 1) */ - id frame_drawable; /* valid during one tick only */ + id frame_drawable; /* valid during one tick only (main: lazily acquired) */ int fb_width, fb_height; /* aux/drawable size (pixels) */ int win_width, win_height; /* logical points (last seen) */ float dpi_scale; @@ -175,13 +203,91 @@ typedef struct _sapp_tc_window_t { #define _SAPP_TC_MAX_WINDOWS (32) +/* EMA-filtered frame timer, port of sokol_app.h's _sapp_timing_* — used only + while the main window ticks from the occluded-fallback NSTimer (the display + link timestamps are the source whenever the link is active) */ +typedef struct { + double last; /* previous sample time, 0 = none yet */ + double ema; + double smooth_dt; + double dt; /* last raw (clamped) delta */ +} _sapp_tc_timing_t; + static struct { bool keytable_valid; sapp_keycode keycodes[SAPP_MAX_KEYCODES]; uint32_t next_id; _sapp_tc_window_t* windows[_SAPP_TC_MAX_WINDOWS]; + + /* ---- app / main-window state (this header owns sapp_run on macOS) ---- */ + struct { + sapp_desc desc; /* sanitized copy; window_title points at the buffer below */ + char window_title[256]; + bool valid; + bool init_called; + bool cleanup_called; + bool quit_requested; + bool quit_ordered; + bool fullscreen; + bool event_consumed; + bool skip_present; /* stored for API parity; ignored on Metal (as upstream) */ + uint64_t frame_count; + _sapp_tc_window_t* main; + _sapp_tc_app_delegate* dlg; + id keyup_monitor; /* Cmd+key-up workaround (Cocoa swallows those) */ + id device; + NSTimer* fallback_timer; /* ~60Hz tick source while the main window is occluded */ + _sapp_tc_timing_t timing; + /* mouse cursor */ + bool mouse_shown; + sapp_mouse_cursor current_cursor; + NSCursor* standard_cursors[_SAPP_MOUSECURSOR_NUM]; + NSCursor* custom_cursors[_SAPP_MOUSECURSOR_NUM]; + bool custom_cursor_bound[_SAPP_MOUSECURSOR_NUM]; + /* clipboard */ + bool clipboard_enabled; + int clipboard_size; + char* clipboard; + /* drag & drop */ + bool drop_enabled; + int drop_max_files; + int drop_max_path_length; + int drop_num_files; + char* drop_buffer; + } app; } _sapp_tc; +static void _sapp_tc_main_tick(CADisplayLink* link); +static void _sapp_tc_update_main_dimensions(bool allow_event); +static void _sapp_tc_apply_cursor(sapp_mouse_cursor cursor, bool shown); + +static void _sapp_tc_timing_reset(_sapp_tc_timing_t* t) { + t->last = 0.0; + t->ema = t->smooth_dt = t->dt = 1.0 / 60.0; +} + +static double _sapp_tc_timing_clamp(double dt) { + if (dt < 0.000001) return 0.000001; + if (dt > 0.1) return 0.1; + return dt; +} + +static void _sapp_tc_timing_update(_sapp_tc_timing_t* t) { + const double now = CACurrentMediaTime(); + if (t->last > 0.0) { + double dt = _sapp_tc_timing_clamp(now - t->last); + t->dt = dt; + /* big jump: reset the filter; otherwise EMA-smooth */ + if (fabs(dt - t->smooth_dt) > 0.004) { + t->ema = t->smooth_dt = dt; + } else { + t->ema += 0.025 * (dt - t->ema); + t->smooth_dt = _sapp_tc_timing_clamp(t->ema); + } + } + t->last = now; +} + /* keycode table lifted verbatim from sokol_app.h's _sapp_macos_init_keytable() so SAPP_KEYCODE_* values are identical across main and secondary windows */ static void _sapp_tc_init_keytable(void) { @@ -328,6 +434,7 @@ static uint32_t _sapp_tc_mods(NSEvent* ev) { static void _sapp_tc_send(_sapp_tc_window_t* w, sapp_event* ev) { if (!w->desc.event_cb) return; + ev->frame_count = _sapp_tc.app.frame_count; ev->window_width = w->win_width; ev->window_height = w->win_height; ev->framebuffer_width = w->fb_width; @@ -336,6 +443,78 @@ static void _sapp_tc_send(_sapp_tc_window_t* w, sapp_event* ev) { w->desc.event_cb(ev, handle, w->desc.user_data); } +/*-- main window: event routing to the sapp_desc callbacks ------------------*/ +static bool _sapp_tc_app_events_enabled(void) { + /* same gate as sokol_app.h: nothing fires before the first tick's init_cb */ + return _sapp_tc.app.init_called && + (_sapp_tc.app.desc.event_cb || _sapp_tc.app.desc.event_userdata_cb); +} + +static void _sapp_tc_main_event_tramp(const sapp_event* ev, sapp_window win, void* user) { + (void)win; (void)user; + if (!_sapp_tc_app_events_enabled()) return; + _sapp_tc.app.event_consumed = false; + if (_sapp_tc.app.desc.event_cb) { + _sapp_tc.app.desc.event_cb(ev); + } else { + _sapp_tc.app.desc.event_userdata_cb(ev, _sapp_tc.app.desc.user_data); + } +} + +/*-- mouse cursor (app-global, same model as sokol_app.h) -------------------*/ +/* private-but-stable AppKit cursor selectors (same set sokol_app/GLFW use) */ +@interface NSCursor(_sapp_tc_private_cursors) ++ (NSCursor*)_windowResizeEastWestCursor; ++ (NSCursor*)_windowResizeNorthSouthCursor; ++ (NSCursor*)_windowResizeNorthWestSouthEastCursor; ++ (NSCursor*)_windowResizeNorthEastSouthWestCursor; +@end + +static NSCursor* _sapp_tc_priv_cursor(SEL sel, NSCursor* fallback) { + if ([NSCursor respondsToSelector:sel]) { + #pragma clang diagnostic push + #pragma clang diagnostic ignored "-Warc-performSelector-leaks" + NSCursor* cur = [NSCursor performSelector:sel]; + #pragma clang diagnostic pop + if (cur) return cur; + } + return fallback; +} + +static void _sapp_tc_init_cursors(void) { + _sapp_tc.app.standard_cursors[SAPP_MOUSECURSOR_DEFAULT] = nil; /* uses arrowCursor */ + _sapp_tc.app.standard_cursors[SAPP_MOUSECURSOR_ARROW] = [NSCursor arrowCursor]; + _sapp_tc.app.standard_cursors[SAPP_MOUSECURSOR_IBEAM] = [NSCursor IBeamCursor]; + _sapp_tc.app.standard_cursors[SAPP_MOUSECURSOR_CROSSHAIR] = [NSCursor crosshairCursor]; + _sapp_tc.app.standard_cursors[SAPP_MOUSECURSOR_POINTING_HAND] = [NSCursor pointingHandCursor]; + _sapp_tc.app.standard_cursors[SAPP_MOUSECURSOR_RESIZE_EW] = + _sapp_tc_priv_cursor(@selector(_windowResizeEastWestCursor), [NSCursor resizeLeftRightCursor]); + _sapp_tc.app.standard_cursors[SAPP_MOUSECURSOR_RESIZE_NS] = + _sapp_tc_priv_cursor(@selector(_windowResizeNorthSouthCursor), [NSCursor resizeUpDownCursor]); + _sapp_tc.app.standard_cursors[SAPP_MOUSECURSOR_RESIZE_NWSE] = + _sapp_tc_priv_cursor(@selector(_windowResizeNorthWestSouthEastCursor), [NSCursor closedHandCursor]); + _sapp_tc.app.standard_cursors[SAPP_MOUSECURSOR_RESIZE_NESW] = + _sapp_tc_priv_cursor(@selector(_windowResizeNorthEastSouthWestCursor), [NSCursor closedHandCursor]); + _sapp_tc.app.standard_cursors[SAPP_MOUSECURSOR_RESIZE_ALL] = [NSCursor closedHandCursor]; + _sapp_tc.app.standard_cursors[SAPP_MOUSECURSOR_NOT_ALLOWED] = [NSCursor operationNotAllowedCursor]; +} + +static void _sapp_tc_apply_cursor(sapp_mouse_cursor cursor, bool shown) { + /* [NSCursor hide/unhide] stack, so only act on an actual change */ + if (shown != _sapp_tc.app.mouse_shown) { + if (shown) [NSCursor unhide]; else [NSCursor hide]; + } + NSCursor* ns = [NSCursor arrowCursor]; + if (_sapp_tc.app.custom_cursor_bound[cursor] && _sapp_tc.app.custom_cursors[cursor]) { + ns = _sapp_tc.app.custom_cursors[cursor]; + } else if (_sapp_tc.app.standard_cursors[cursor]) { + ns = _sapp_tc.app.standard_cursors[cursor]; + } + [ns set]; + _sapp_tc.app.current_cursor = cursor; + _sapp_tc.app.mouse_shown = shown; +} + /*-- content view -----------------------------------------------------------*/ @interface _sapp_tc_view : NSView @property (assign) _sapp_tc_window_t* w; @@ -353,12 +532,18 @@ static void _sapp_tc_send(_sapp_tc_window_t* w, sapp_event* ev) { NSTrackingArea* area = [[NSTrackingArea alloc] initWithRect:self.bounds options:(NSTrackingMouseMoved | NSTrackingMouseEnteredAndExited | NSTrackingActiveAlways | NSTrackingEnabledDuringMouseDrag | - NSTrackingInVisibleRect) + NSTrackingCursorUpdate | NSTrackingInVisibleRect) owner:self userInfo:nil]; [self addTrackingArea:area]; [super updateTrackingAreas]; } +/* keeps a custom/hidden cursor sticky when the pointer (re)enters the view */ +- (void)cursorUpdate:(NSEvent*)ev { + (void)ev; + _sapp_tc_apply_cursor(_sapp_tc.app.current_cursor, _sapp_tc.app.mouse_shown); +} + - (void)mouseEvt:(NSEvent*)ev type:(sapp_event_type)type button:(sapp_mousebutton)btn { _sapp_tc_window_t* w = self.w; if (!w) return; @@ -445,6 +630,59 @@ static void _sapp_tc_send(_sapp_tc_window_t* w, sapp_event* ev) { _sapp_tc_send(w, &ce); } } + + /* Cmd+V paste notification, same trigger as sokol_app.h (exact-match on + the Super modifier); the app reads the clipboard in its handler */ + if (down && _sapp_tc.app.clipboard_enabled && + (_sapp_tc_mods(ev) == SAPP_MODIFIER_SUPER) && + (_sapp_tc_translate_key(ev.keyCode) == SAPP_KEYCODE_V)) { + sapp_event pe; + memset(&pe, 0, sizeof(pe)); + pe.type = SAPP_EVENTTYPE_CLIPBOARD_PASTED; + pe.modifiers = SAPP_MODIFIER_SUPER; + _sapp_tc_send(w, &pe); + } +} + +/*-- drag & drop (NSDraggingDestination; registered on the main view) -------*/ +- (NSDragOperation)draggingEntered:(id)sender { + (void)sender; + return NSDragOperationCopy; +} + +- (NSDragOperation)draggingUpdated:(id)sender { + (void)sender; + return NSDragOperationCopy; +} + +- (BOOL)performDragOperation:(id)sender { + _sapp_tc_window_t* w = self.w; + if (!w || !_sapp_tc.app.drop_enabled || !_sapp_tc.app.drop_buffer) return NO; + NSPasteboard* pboard = sender.draggingPasteboard; + if (![pboard.types containsObject:NSPasteboardTypeFileURL]) return NO; + const int max_files = _sapp_tc.app.drop_max_files; + const int max_path = _sapp_tc.app.drop_max_path_length; + memset(_sapp_tc.app.drop_buffer, 0, (size_t)max_files * (size_t)max_path); + _sapp_tc.app.drop_num_files = 0; + int num = (int)pboard.pasteboardItems.count; + if (num > max_files) num = max_files; + for (int i = 0; i < num; i++) { + NSString* str = [pboard.pasteboardItems[(NSUInteger)i] stringForType:NSPasteboardTypeFileURL]; + if (!str) return NO; + NSURL* url = [NSURL URLWithString:str]; + const char* path = url.standardizedURL.path.UTF8String; + if (!path || (int)strlen(path) >= max_path) return NO; /* path too long: drop everything */ + strcpy(_sapp_tc.app.drop_buffer + (size_t)i * (size_t)max_path, path); + } + _sapp_tc.app.drop_num_files = num; + sapp_event e; + memset(&e, 0, sizeof(e)); + e.type = SAPP_EVENTTYPE_FILES_DROPPED; + NSPoint p = [self convertPoint:sender.draggingLocation fromView:nil]; + e.mouse_x = (float)p.x * w->dpi_scale; + e.mouse_y = (float)p.y * w->dpi_scale; + _sapp_tc_send(w, &e); + return YES; } - (void)keyDown:(NSEvent*)ev { [self keyEvt:ev pressed:true]; } @@ -483,6 +721,14 @@ static void _sapp_tc_send(_sapp_tc_window_t* w, sapp_event* ev) { _sapp_tc_window_t* w = self.w; if (!w) return; + /* window #0 runs the app frame loop (sokol_app.h parity: no occlusion + gate — while occluded the link auto-suspends and a ~60Hz fallback + timer keeps the frame callback alive, exactly like upstream) */ + if (w->is_main) { + _sapp_tc_main_tick(link); + return; + } + /* Occluded => not due. THIS is what makes the nextDrawable stall structurally impossible: the acquiring code below only runs for a provably visible window. (The display link also auto-suspends for @@ -572,14 +818,92 @@ static void _sapp_tc_send(_sapp_tc_window_t* w, sapp_event* ev) { @property (assign) _sapp_tc_window_t* w; @end +static void _sapp_tc_start_fallback_timer(void); +static void _sapp_tc_stop_fallback_timer(void); + @implementation _sapp_tc_win_delegate +- (BOOL)windowShouldClose:(id)sender { + (void)sender; + _sapp_tc_window_t* w = self.w; + if (!w || !w->is_main) return YES; + /* main window: sokol_app.h's cancellable quit dance. sapp_quit() pre-sets + quit_ordered (unvetoable); otherwise QUIT_REQUESTED is delivered and a + handler may call sapp_cancel_quit() to keep the window open. */ + if (!_sapp_tc.app.quit_ordered) { + _sapp_tc.app.quit_requested = true; + sapp_event e; memset(&e, 0, sizeof(e)); + e.type = SAPP_EVENTTYPE_QUIT_REQUESTED; + _sapp_tc_send(w, &e); + if (_sapp_tc.app.quit_requested) { + _sapp_tc.app.quit_ordered = true; + } + } + return _sapp_tc.app.quit_ordered ? YES : NO; +} - (void)windowWillClose:(NSNotification*)n { + (void)n; _sapp_tc_window_t* w = self.w; - if (w && w->desc.close_cb) { + if (!w) return; + if (w->is_main) { + /* main window gone = app gone, even if secondary windows are still + open. terminate: runs applicationWillTerminate (cleanup_cb) and + never returns to the run loop. */ + [NSApp terminate:nil]; + return; + } + if (w->desc.close_cb) { sapp_window handle = { w->win_id }; w->desc.close_cb(handle, w->desc.user_data); } } +- (void)windowDidResize:(NSNotification*)n { + (void)n; + _sapp_tc_window_t* w = self.w; + if (w && w->is_main) _sapp_tc_update_main_dimensions(true); +} +- (void)windowDidChangeScreen:(NSNotification*)n { + (void)n; + _sapp_tc_window_t* w = self.w; + if (w && w->is_main) _sapp_tc_update_main_dimensions(true); +} +- (void)windowDidChangeOcclusionState:(NSNotification*)n { + (void)n; + _sapp_tc_window_t* w = self.w; + if (!w || !w->is_main) return; + if (w->window.occlusionState & NSWindowOcclusionStateVisible) { + _sapp_tc_stop_fallback_timer(); /* the display link auto-resumes */ + } else { + _sapp_tc_start_fallback_timer(); /* keep frame_cb alive while hidden */ + } +} +- (void)windowDidMiniaturize:(NSNotification*)n { + (void)n; + _sapp_tc_window_t* w = self.w; + if (!w || !w->is_main) return; + _sapp_tc_start_fallback_timer(); + sapp_event e; memset(&e, 0, sizeof(e)); + e.type = SAPP_EVENTTYPE_ICONIFIED; + _sapp_tc_send(w, &e); +} +- (void)windowDidDeminiaturize:(NSNotification*)n { + (void)n; + _sapp_tc_window_t* w = self.w; + if (!w || !w->is_main) return; + _sapp_tc_stop_fallback_timer(); + sapp_event e; memset(&e, 0, sizeof(e)); + e.type = SAPP_EVENTTYPE_RESTORED; + _sapp_tc_send(w, &e); +} +- (void)windowDidEnterFullScreen:(NSNotification*)n { + (void)n; + _sapp_tc_window_t* w = self.w; + if (w && w->is_main) _sapp_tc.app.fullscreen = true; +} +- (void)windowDidExitFullScreen:(NSNotification*)n { + (void)n; + _sapp_tc_window_t* w = self.w; + if (w && w->is_main) _sapp_tc.app.fullscreen = false; +} - (void)windowDidBecomeKey:(NSNotification*)n { _sapp_tc_window_t* w = self.w; if (!w) return; @@ -596,6 +920,274 @@ static void _sapp_tc_send(_sapp_tc_window_t* w, sapp_event* ev) { } @end +/*-- main window / app lifecycle (this header owns sapp_run on macOS) -------*/ + +/* drawable-sized depth-stencil + MSAA color textures for one window */ +static void _sapp_tc_ensure_swapchain_textures(_sapp_tc_window_t* w) { + if (w->fb_width <= 0 || w->fb_height <= 0) return; + if (w->depth_tex != nil && + (int)w->depth_tex.width == w->fb_width && + (int)w->depth_tex.height == w->fb_height) return; + id dev = w->layer.device; + MTLTextureDescriptor* dd = [MTLTextureDescriptor + texture2DDescriptorWithPixelFormat:MTLPixelFormatDepth32Float_Stencil8 + width:(NSUInteger)w->fb_width height:(NSUInteger)w->fb_height mipmapped:NO]; + dd.usage = MTLTextureUsageRenderTarget; + dd.storageMode = MTLStorageModePrivate; + dd.sampleCount = (NSUInteger)w->desc.sample_count; + dd.textureType = w->desc.sample_count > 1 ? MTLTextureType2DMultisample : MTLTextureType2D; + w->depth_tex = [dev newTextureWithDescriptor:dd]; + if (w->desc.sample_count > 1) { + MTLTextureDescriptor* md = [MTLTextureDescriptor + texture2DDescriptorWithPixelFormat:w->color_fmt + width:(NSUInteger)w->fb_width height:(NSUInteger)w->fb_height mipmapped:NO]; + md.usage = MTLTextureUsageRenderTarget; + md.storageMode = MTLStorageModePrivate; + md.sampleCount = (NSUInteger)w->desc.sample_count; + md.textureType = MTLTextureType2DMultisample; + w->msaa_tex = [dev newTextureWithDescriptor:md]; + } +} + +/* re-sample scale + sizes, keep the layer drawable in sync, emit RESIZED. + Called from the tick, windowDidResize / windowDidChangeScreen, and once + silently right after window creation (allow_event=false, sokol parity: + the startup update never fires RESIZED). */ +static void _sapp_tc_update_main_dimensions(bool allow_event) { + _sapp_tc_window_t* w = _sapp_tc.app.main; + if (!w || !w->view) return; + CGFloat scale = 1.0; + if (_sapp_tc.app.desc.high_dpi) { + NSScreen* screen = w->window.screen ? w->window.screen : [NSScreen mainScreen]; + scale = screen.backingScaleFactor; + if (scale <= 0.0) scale = 1.0; + } + w->dpi_scale = (float)scale; + const NSSize sz = w->view.bounds.size; + int fbw = (int)((CGFloat)sz.width * scale); + int fbh = (int)((CGFloat)sz.height * scale); + if (fbw <= 0 || fbh <= 0) return; + /* re-apply contentsScale every time: live-resize installs a non-scaling + layer placement that would otherwise stick (sokol_app.h does the same) */ + w->layer.contentsScale = scale; + if ((int)w->layer.drawableSize.width != fbw || (int)w->layer.drawableSize.height != fbh) { + w->layer.drawableSize = CGSizeMake((CGFloat)fbw, (CGFloat)fbh); + } + const bool changed = (fbw != w->fb_width) || (fbh != w->fb_height) || + ((int)sz.width != w->win_width) || ((int)sz.height != w->win_height); + w->fb_width = fbw; + w->fb_height = fbh; + w->win_width = (int)sz.width; + w->win_height = (int)sz.height; + _sapp_tc_ensure_swapchain_textures(w); + if (changed && allow_event) { + sapp_event e; + memset(&e, 0, sizeof(e)); + e.type = SAPP_EVENTTYPE_RESIZED; + _sapp_tc_send(w, &e); + } +} + +/* one main-window frame: timing -> dimensions -> init_cb (first tick only) + -> frame_cb -> quit check. The drawable is NOT acquired here; it is + acquired lazily by sapp_get_swapchain() (upstream parity) and released + when the tick ends. `link` is nil when driven by the fallback timer. */ +static void _sapp_tc_main_tick(CADisplayLink* link) { + _sapp_tc_window_t* w = _sapp_tc.app.main; + if (!w || w->in_tick) return; + _sapp_tc_timing_update(&_sapp_tc.app.timing); /* feeds the fallback estimate */ + if (link) { + if (w->last_tick_time > 0.0) { + const double dt = link.timestamp - w->last_tick_time; + if ((dt > 0.000001) && (dt < 0.1)) w->frame_duration = dt; + } + w->last_tick_time = link.timestamp; + } + _sapp_tc_update_main_dimensions(true); + @autoreleasepool { + w->in_tick = true; + if (!_sapp_tc.app.init_called) { + _sapp_tc.app.init_called = true; + if (_sapp_tc.app.desc.init_cb) { + _sapp_tc.app.desc.init_cb(); + } else if (_sapp_tc.app.desc.init_userdata_cb) { + _sapp_tc.app.desc.init_userdata_cb(_sapp_tc.app.desc.user_data); + } + } + if (_sapp_tc.app.desc.frame_cb) { + _sapp_tc.app.desc.frame_cb(); + } else if (_sapp_tc.app.desc.frame_userdata_cb) { + _sapp_tc.app.desc.frame_userdata_cb(_sapp_tc.app.desc.user_data); + } + _sapp_tc.app.frame_count++; + w->in_tick = false; + w->frame_drawable = nil; /* presented by sg_commit; release our ref */ + } + if (_sapp_tc.app.quit_requested || _sapp_tc.app.quit_ordered) { + [w->window performClose:nil]; + } +} + +static void _sapp_tc_start_fallback_timer(void) { + if (_sapp_tc.app.fallback_timer != nil || _sapp_tc.app.dlg == nil) return; + NSTimer* t = [NSTimer timerWithTimeInterval:(1.0 / 60.0) + target:_sapp_tc.app.dlg + selector:@selector(fallbackTick:) + userInfo:nil + repeats:YES]; + [[NSRunLoop mainRunLoop] addTimer:t forMode:NSRunLoopCommonModes]; + _sapp_tc.app.fallback_timer = t; +} + +static void _sapp_tc_stop_fallback_timer(void) { + if (_sapp_tc.app.fallback_timer == nil) return; + [_sapp_tc.app.fallback_timer invalidate]; + _sapp_tc.app.fallback_timer = nil; +} + +static void _sapp_tc_create_main_window(void) { + const sapp_desc* d = &_sapp_tc.app.desc; + int slot = -1; + for (int i = 0; i < _SAPP_TC_MAX_WINDOWS; i++) { + if (_sapp_tc.windows[i] == 0) { slot = i; break; } + } + if (slot < 0) return; + + _sapp_tc_window_t* w = new _sapp_tc_window_t(); + w->win_id = ++_sapp_tc.next_id; + w->is_main = true; + w->color_fmt = MTLPixelFormatRGB10A2Unorm; /* TrussC 10-bit output */ + w->dpi_scale = 1.0f; + + /* content size in points; 0 = 4/5 of the main screen (sokol parity) */ + int width = d->width; + int height = d->height; + const NSRect screen_rect = NSScreen.mainScreen.frame; + if (width <= 0) width = (int)(screen_rect.size.width * 0.8); + if (height <= 0) height = (int)(screen_rect.size.height * 0.8); + + /* per-window desc: the app callbacks are reached via the trampoline */ + w->desc.width = width; + w->desc.height = height; + w->desc.sample_count = d->sample_count; + w->desc.no_high_dpi = !d->high_dpi; + w->desc.event_cb = _sapp_tc_main_event_tramp; + + id dev = MTLCreateSystemDefaultDevice(); + _sapp_tc.app.device = dev; + + const NSRect rect = NSMakeRect(0, 0, width, height); + const NSWindowStyleMask style = NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | + NSWindowStyleMaskMiniaturizable | NSWindowStyleMaskResizable; + w->window = [[NSWindow alloc] initWithContentRect:rect styleMask:style + backing:NSBackingStoreBuffered defer:NO]; + w->window.title = [NSString stringWithUTF8String:_sapp_tc.app.window_title]; + w->window.releasedWhenClosed = NO; /* the object must outlive performClose */ + w->window.acceptsMouseMovedEvents = YES; + w->window.restorable = YES; + + _sapp_tc_view* view = [[_sapp_tc_view alloc] initWithFrame:rect]; + view.w = w; + w->view = view; + w->layer = [CAMetalLayer layer]; + w->layer.device = dev; + w->layer.pixelFormat = w->color_fmt; + w->layer.opaque = YES; + w->layer.magnificationFilter = kCAFilterNearest; + w->layer.framebufferOnly = NO; /* captureWindow() reads the drawable */ + view.wantsLayer = YES; + view.layer = w->layer; + [view registerForDraggedTypes:@[NSPasteboardTypeFileURL]]; + w->window.contentView = view; + [w->window makeFirstResponder:view]; + + _sapp_tc_win_delegate* del = [_sapp_tc_win_delegate new]; + del.w = w; + w->delegate = del; + w->window.delegate = del; + + [w->window center]; + _sapp_tc.windows[slot] = w; + _sapp_tc.app.main = w; + _sapp_tc.app.valid = true; + + if (d->fullscreen) { + _sapp_tc.app.fullscreen = true; + [w->window toggleFullScreen:nil]; + } + [w->window makeKeyAndOrderFront:nil]; + _sapp_tc_update_main_dimensions(false); /* silent startup sizing */ + + /* window #0's vsync source; swap_interval > 1 divides the display rate */ + w->link = [view displayLinkWithTarget:view selector:@selector(tick:)]; + if (d->swap_interval > 1) { + const float maxfps = (float)[NSScreen mainScreen].maximumFramesPerSecond; + const float p = maxfps / (float)d->swap_interval; + w->link.preferredFrameRateRange = CAFrameRateRangeMake(p, p, p); + } + [w->link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes]; +} + +@interface _sapp_tc_app_delegate : NSObject +@end + +@implementation _sapp_tc_app_delegate +- (void)applicationDidFinishLaunching:(NSNotification*)n { + (void)n; + /* activation policy must be set before window creation (sokol #1500) */ + [NSApp setActivationPolicy:NSApplicationActivationPolicyRegular]; + _sapp_tc_init_cursors(); + _sapp_tc_create_main_window(); + [NSEvent setMouseCoalescingEnabled:NO]; + [NSApp activateIgnoringOtherApps:YES]; + /* focus workaround (sokol #982): make sure the window has focus even if + the first tick's init callback blocks for a long time */ + NSEvent* focusevent = [NSEvent otherEventWithType:NSEventTypeAppKitDefined + location:NSZeroPoint + modifierFlags:0 + timestamp:0 + windowNumber:0 + context:nil + subtype:NSEventSubtypeApplicationActivated + data1:0 + data2:0]; + [NSApp postEvent:focusevent atStart:YES]; +} +- (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication*)sender { + (void)sender; + return YES; +} +- (void)applicationWillTerminate:(NSNotification*)n { + (void)n; + /* stop all tick sources first so no frame runs during teardown */ + _sapp_tc_stop_fallback_timer(); + for (int i = 0; i < _SAPP_TC_MAX_WINDOWS; i++) { + _sapp_tc_window_t* w = _sapp_tc.windows[i]; + if (w && w->link) { [w->link invalidate]; w->link = nil; } + } + /* user cleanup runs BEFORE any GPU object release (the app shuts down + sokol_gfx here; we only hold the device/layer references) */ + if (!_sapp_tc.app.cleanup_called) { + _sapp_tc.app.cleanup_called = true; + if (_sapp_tc.app.desc.cleanup_cb) { + _sapp_tc.app.desc.cleanup_cb(); + } else if (_sapp_tc.app.desc.cleanup_userdata_cb) { + _sapp_tc.app.desc.cleanup_userdata_cb(_sapp_tc.app.desc.user_data); + } + } + if (_sapp_tc.app.keyup_monitor) { + [NSEvent removeMonitor:_sapp_tc.app.keyup_monitor]; + _sapp_tc.app.keyup_monitor = nil; + } + if (_sapp_tc.app.clipboard) { free(_sapp_tc.app.clipboard); _sapp_tc.app.clipboard = 0; } + if (_sapp_tc.app.drop_buffer) { free(_sapp_tc.app.drop_buffer); _sapp_tc.app.drop_buffer = 0; } +} +- (void)fallbackTick:(NSTimer*)t { + (void)t; + _sapp_tc_main_tick(nil); +} +@end + /*-- public API -------------------------------------------------------------*/ extern "C" { @@ -761,6 +1353,328 @@ const void* sapp_window_macos_get_window(sapp_window win) { return w ? (__bridge const void*)w->window : 0; } +/*-- sokol_app.h public API (macOS implementation lives here) ---------------*/ + +void sapp_run(const sapp_desc* desc) { + if (!desc) return; + _sapp_tc.app.desc = *desc; + sapp_desc* d = &_sapp_tc.app.desc; + if (d->sample_count <= 0) d->sample_count = 1; + if (d->swap_interval <= 0) d->swap_interval = 1; + if (d->clipboard_size <= 0) d->clipboard_size = 8192; + if (d->max_dropped_files <= 0) d->max_dropped_files = 1; + if (d->max_dropped_file_path_length <= 0) d->max_dropped_file_path_length = 2048; + strncpy(_sapp_tc.app.window_title, d->window_title ? d->window_title : "sokol", + sizeof(_sapp_tc.app.window_title) - 1); + d->window_title = _sapp_tc.app.window_title; + if (d->enable_clipboard) { + _sapp_tc.app.clipboard_enabled = true; + _sapp_tc.app.clipboard_size = d->clipboard_size; + _sapp_tc.app.clipboard = (char*)calloc(1, (size_t)d->clipboard_size); + } + if (d->enable_dragndrop) { + _sapp_tc.app.drop_enabled = true; + _sapp_tc.app.drop_max_files = d->max_dropped_files; + _sapp_tc.app.drop_max_path_length = d->max_dropped_file_path_length; + _sapp_tc.app.drop_buffer = (char*)calloc( + (size_t)d->max_dropped_files, (size_t)d->max_dropped_file_path_length); + } + _sapp_tc.app.mouse_shown = true; + _sapp_tc.app.current_cursor = SAPP_MOUSECURSOR_DEFAULT; + _sapp_tc_timing_reset(&_sapp_tc.app.timing); + _sapp_tc_init_keytable(); + + [NSApplication sharedApplication]; + _sapp_tc.app.dlg = [[_sapp_tc_app_delegate alloc] init]; + NSApp.delegate = _sapp_tc.app.dlg; + /* Cocoa swallows key-up events while Cmd is held; force-forward them + (same workaround as sokol_app.h / GLFW) */ + _sapp_tc.app.keyup_monitor = [NSEvent + addLocalMonitorForEventsMatchingMask:NSEventMaskKeyUp + handler:^NSEvent* (NSEvent* event) { + if ([event modifierFlags] & NSEventModifierFlagCommand) { + [[NSApp keyWindow] sendEvent:event]; + } + return event; + }]; + [NSApp run]; + /* never returns; cleanup runs in applicationWillTerminate */ +} + +bool sapp_isvalid(void) { + return _sapp_tc.app.valid; +} + +int sapp_width(void) { + _sapp_tc_window_t* w = _sapp_tc.app.main; + return (w && w->fb_width > 0) ? w->fb_width : 1; +} + +float sapp_widthf(void) { + return (float)sapp_width(); +} + +int sapp_height(void) { + _sapp_tc_window_t* w = _sapp_tc.app.main; + return (w && w->fb_height > 0) ? w->fb_height : 1; +} + +float sapp_heightf(void) { + return (float)sapp_height(); +} + +int sapp_sample_count(void) { + return _sapp_tc.app.desc.sample_count > 0 ? _sapp_tc.app.desc.sample_count : 1; +} + +bool sapp_high_dpi(void) { + _sapp_tc_window_t* w = _sapp_tc.app.main; + return _sapp_tc.app.desc.high_dpi && w && (w->dpi_scale >= 1.5f); +} + +float sapp_dpi_scale(void) { + _sapp_tc_window_t* w = _sapp_tc.app.main; + if (w) return w->dpi_scale; + /* before sapp_run: report the main screen's scale (deliberate deviation + from sokol_app.h, which returns 0 here) */ + const float s = (float)[NSScreen mainScreen].backingScaleFactor; + return s > 0.0f ? s : 1.0f; +} + +uint64_t sapp_frame_count(void) { + return _sapp_tc.app.frame_count; +} + +double sapp_frame_duration(void) { + _sapp_tc_window_t* w = _sapp_tc.app.main; + if (!w) return 1.0 / 60.0; + if (_sapp_tc.app.fallback_timer != nil) return _sapp_tc.app.timing.smooth_dt; + if (w->frame_duration > 0.0) return w->frame_duration; + const double maxfps = (double)[NSScreen mainScreen].maximumFramesPerSecond; + return (maxfps > 0.0) ? (1.0 / maxfps) : (1.0 / 60.0); +} + +void sapp_request_quit(void) { + _sapp_tc.app.quit_requested = true; +} + +void sapp_cancel_quit(void) { + _sapp_tc.app.quit_requested = false; +} + +void sapp_quit(void) { + _sapp_tc.app.quit_ordered = true; +} + +void sapp_consume_event(void) { + _sapp_tc.app.event_consumed = true; +} + +void sapp_skip_present(void) { + /* API parity: stored but ignored on Metal (upstream behaves the same; + not starting a swapchain pass already skips the present) */ + _sapp_tc.app.skip_present = true; +} + +void sapp_show_keyboard(bool show) { + (void)show; /* on-screen keyboard: mobile only */ +} + +bool sapp_keyboard_shown(void) { + return false; +} + +void sapp_show_mouse(bool show) { + if (_sapp_tc.app.mouse_shown != show) { + _sapp_tc_apply_cursor(_sapp_tc.app.current_cursor, show); + } +} + +bool sapp_mouse_shown(void) { + return _sapp_tc.app.mouse_shown; +} + +void sapp_set_mouse_cursor(sapp_mouse_cursor cursor) { + if ((int)cursor < 0 || (int)cursor >= _SAPP_MOUSECURSOR_NUM) return; + if (cursor != _sapp_tc.app.current_cursor) { + _sapp_tc_apply_cursor(cursor, _sapp_tc.app.mouse_shown); + } +} + +sapp_mouse_cursor sapp_get_mouse_cursor(void) { + return _sapp_tc.app.current_cursor; +} + +sapp_mouse_cursor sapp_bind_mouse_cursor_image(sapp_mouse_cursor cursor, const sapp_image_desc* desc) { + if ((int)cursor < 0 || (int)cursor >= _SAPP_MOUSECURSOR_NUM) return cursor; + if (!desc || !desc->pixels.ptr || desc->width <= 0 || desc->height <= 0) return cursor; + if (desc->pixels.size < (size_t)(desc->width * desc->height * 4)) return cursor; + sapp_unbind_mouse_cursor_image(cursor); + NSBitmapImageRep* rep = [[NSBitmapImageRep alloc] + initWithBitmapDataPlanes:NULL + pixelsWide:desc->width pixelsHigh:desc->height + bitsPerSample:8 samplesPerPixel:4 hasAlpha:YES isPlanar:NO + colorSpaceName:NSCalibratedRGBColorSpace + bitmapFormat:NSBitmapFormatAlphaNonpremultiplied + bytesPerRow:desc->width * 4 bitsPerPixel:32]; + if (rep == nil) return cursor; + memcpy(rep.bitmapData, desc->pixels.ptr, (size_t)(desc->width * desc->height * 4)); + NSImage* img = [[NSImage alloc] initWithSize:NSMakeSize(desc->width, desc->height)]; + [img addRepresentation:rep]; + NSCursor* cur = [[NSCursor alloc] initWithImage:img + hotSpot:NSMakePoint(desc->cursor_hotspot_x, desc->cursor_hotspot_y)]; + _sapp_tc.app.custom_cursors[cursor] = cur; + _sapp_tc.app.custom_cursor_bound[cursor] = true; + if (_sapp_tc.app.current_cursor == cursor) { + _sapp_tc_apply_cursor(cursor, _sapp_tc.app.mouse_shown); + } + return cursor; +} + +void sapp_unbind_mouse_cursor_image(sapp_mouse_cursor cursor) { + if ((int)cursor < 0 || (int)cursor >= _SAPP_MOUSECURSOR_NUM) return; + if (!_sapp_tc.app.custom_cursor_bound[cursor]) return; + _sapp_tc.app.custom_cursor_bound[cursor] = false; + if (_sapp_tc.app.current_cursor == cursor) { + _sapp_tc_apply_cursor(cursor, _sapp_tc.app.mouse_shown); + } + _sapp_tc.app.custom_cursors[cursor] = nil; +} + +void sapp_set_clipboard_string(const char* str) { + if (!_sapp_tc.app.clipboard_enabled || !str) return; + @autoreleasepool { + NSPasteboard* pboard = [NSPasteboard generalPasteboard]; + [pboard declareTypes:@[NSPasteboardTypeString] owner:nil]; + [pboard setString:[NSString stringWithUTF8String:str] forType:NSPasteboardTypeString]; + } + strncpy(_sapp_tc.app.clipboard, str, (size_t)_sapp_tc.app.clipboard_size - 1); + _sapp_tc.app.clipboard[_sapp_tc.app.clipboard_size - 1] = 0; +} + +const char* sapp_get_clipboard_string(void) { + if (!_sapp_tc.app.clipboard_enabled || !_sapp_tc.app.clipboard) return ""; + @autoreleasepool { + _sapp_tc.app.clipboard[0] = 0; + NSPasteboard* pboard = [NSPasteboard generalPasteboard]; + if ([pboard.types containsObject:NSPasteboardTypeString]) { + NSString* str = [pboard stringForType:NSPasteboardTypeString]; + if (str) { + strncpy(_sapp_tc.app.clipboard, str.UTF8String, + (size_t)_sapp_tc.app.clipboard_size - 1); + _sapp_tc.app.clipboard[_sapp_tc.app.clipboard_size - 1] = 0; + } + } + } + return _sapp_tc.app.clipboard; +} + +void sapp_set_window_title(const char* str) { + if (!str) return; + strncpy(_sapp_tc.app.window_title, str, sizeof(_sapp_tc.app.window_title) - 1); + _sapp_tc_window_t* w = _sapp_tc.app.main; + if (w && w->window) { + w->window.title = [NSString stringWithUTF8String:_sapp_tc.app.window_title]; + } +} + +bool sapp_is_fullscreen(void) { + return _sapp_tc.app.fullscreen; +} + +void sapp_toggle_fullscreen(void) { + _sapp_tc_window_t* w = _sapp_tc.app.main; + if (!w || !w->window) return; + /* optimistic flip; the delegate notifications are the authority */ + _sapp_tc.app.fullscreen = !_sapp_tc.app.fullscreen; + [w->window toggleFullScreen:nil]; +} + +int sapp_get_num_dropped_files(void) { + return _sapp_tc.app.drop_enabled ? _sapp_tc.app.drop_num_files : 0; +} + +const char* sapp_get_dropped_file_path(int index) { + if (!_sapp_tc.app.drop_enabled || !_sapp_tc.app.drop_buffer) return ""; + if (index < 0 || index >= _sapp_tc.app.drop_num_files) return ""; + return _sapp_tc.app.drop_buffer + (size_t)index * (size_t)_sapp_tc.app.drop_max_path_length; +} + +sapp_pixel_format sapp_color_format(void) { + return SAPP_PIXELFORMAT_RGB10A2; /* TrussC 10-bit output on Metal */ +} + +sapp_pixel_format sapp_depth_format(void) { + return SAPP_PIXELFORMAT_DEPTH_STENCIL; +} + +const void* sapp_macos_get_window(void) { + _sapp_tc_window_t* w = _sapp_tc.app.main; + return w ? (__bridge const void*)w->window : 0; +} + +const void* sapp_metal_get_device(void) { + return (__bridge const void*)_sapp_tc.app.device; +} + +/* main-window drawable: acquired lazily HERE (upstream parity — sokol_gfx's + begin-pass is the acquisition point via sglue_swapchain()). Inside a tick + the drawable is cached so repeated queries stay safe; outside a tick each + call acquires a fresh drawable, exactly like sokol_app.h. */ +static id _sapp_tc_main_next_drawable(void) { + _sapp_tc_window_t* w = _sapp_tc.app.main; + if (!w || !w->layer) return nil; + if (w->in_tick) { + if (w->frame_drawable == nil) { + w->frame_drawable = [w->layer nextDrawable]; + } + return w->frame_drawable; + } + return [w->layer nextDrawable]; +} + +const void* sapp_metal_get_current_drawable(void) { + return (__bridge const void*)_sapp_tc_main_next_drawable(); +} + +const void* sapp_metal_get_depth_stencil_texture(void) { + _sapp_tc_window_t* w = _sapp_tc.app.main; + return w ? (__bridge const void*)w->depth_tex : 0; +} + +const void* sapp_metal_get_msaa_color_texture(void) { + _sapp_tc_window_t* w = _sapp_tc.app.main; + if (!w || w->desc.sample_count <= 1) return 0; + return (__bridge const void*)w->msaa_tex; +} + +sapp_environment sapp_get_environment(void) { + sapp_environment env; + memset(&env, 0, sizeof(env)); + env.defaults.color_format = sapp_color_format(); + env.defaults.depth_format = sapp_depth_format(); + env.defaults.sample_count = sapp_sample_count(); + env.metal.device = (__bridge const void*)_sapp_tc.app.device; + return env; +} + +sapp_swapchain sapp_get_swapchain(void) { + sapp_swapchain sc; + memset(&sc, 0, sizeof(sc)); + _sapp_tc_window_t* w = _sapp_tc.app.main; + if (!w) return sc; + sc.width = sapp_width(); + sc.height = sapp_height(); + sc.sample_count = sapp_sample_count(); + sc.color_format = sapp_color_format(); + sc.depth_format = sapp_depth_format(); + sc.metal.current_drawable = (__bridge const void*)_sapp_tc_main_next_drawable(); + sc.metal.depth_stencil_texture = (__bridge const void*)w->depth_tex; + sc.metal.msaa_color_texture = (w->desc.sample_count > 1) + ? (__bridge const void*)w->msaa_tex : 0; + return sc; +} + } /* extern "C" */ #else /* !macOS */ diff --git a/core/platform/mac/sokol_impl.mm b/core/platform/mac/sokol_impl.mm index fd199197..21b8a299 100644 --- a/core/platform/mac/sokol_impl.mm +++ b/core/platform/mac/sokol_impl.mm @@ -2,7 +2,6 @@ // sokol バックエンド実装 (macOS / Metal) // ============================================================================= -#define SOKOL_IMPL #define SOKOL_NO_ENTRY // main() を自分で定義するため #if defined(__GNUC__) || defined(__clang__) @@ -12,8 +11,12 @@ # pragma GCC diagnostic ignored "-Wmissing-field-initializers" #endif -#include "sokol_log.h" +// sokol_app.h: declarations only — the macOS implementation of the sapp_* API +// (main window, run loop, events) lives in sokol_app_tc.h below. #include "sokol_app.h" + +#define SOKOL_IMPL +#include "sokol_log.h" #define SOKOL_APP_TC_IMPL #include "sokol_app_tc.h" #include "sokol_gfx.h" diff --git a/docs/dev/sapp-mac-impl-spec.md b/docs/dev/sapp-mac-impl-spec.md new file mode 100644 index 00000000..fa55ea89 --- /dev/null +++ b/docs/dev/sapp-mac-impl-spec.md @@ -0,0 +1,400 @@ +# sokol_app.h macOS (Metal) Backend — Behavioral Specification + +Implementation contract for replacing sokol_app.h's macOS main-window / app-lifecycle +with `sokol_app_tc.h`. All line references are into +`core/include/sokol/sokol_app.h` (14602 lines, this worktree). Focus: `_SAPP_MACOS` ++ `SOKOL_METAL`. GL/WGPU paths noted only where they diverge; iOS/Win/Linux/web ignored. + +The replacement already covers secondary windows (NSWindow+CAMetalLayer, CADisplayLink, +event mapping w/ 111-key table, occlusion gating, depth/MSAA textures). This document +specifies the **main window + app lifecycle** that is still missing. + +--- + +## 0. Global state touched (the shared contract) + +The single global is `static _sapp_t _sapp;` (definition around line 3298 embeds +`_sapp_macos_t macos;`). The macOS struct is at **2790–2823**: + +```c +typedef struct { + uint32_t flags_changed_store; // last NSEventModifierFlags seen by flagsChanged + uint8_t mouse_buttons; // bitmask of held buttons (enter/leave suppression during drag) + NSWindow* window; + NSTrackingArea* tracking_area; + id keyup_monitor; // local event monitor (Cmd+key-up workaround) + _sapp_macos_app_delegate* app_dlg; + _sapp_macos_window_delegate* win_dlg; + _sapp_macos_view* view; + NSCursor* standard_cursors[_SAPP_MOUSECURSOR_NUM]; + NSCursor* custom_cursors[_SAPP_MOUSECURSOR_NUM]; + struct { // SOKOL_METAL only + id device; + CAMetalLayer* layer; + CADisplayLink* display_link; + NSTimer* fallback_timer; + id depth_tex; + id msaa_tex; + struct { CFTimeInterval timestamp; CFTimeInterval frame_duration_sec; } timing; + } mtl; +} _sapp_macos_t; +``` + +Cross-platform `_sapp` fields the mac path reads/writes: `desc`, `valid`, +`first_frame`, `init_called`, `cleanup_called`, `quit_requested`, `quit_ordered`, +`fullscreen`, `window_width/height`, `framebuffer_width/height`, `dpi_scale`, +`sample_count`, `swap_interval`, `frame_count`, `timing`, `mouse.*`, `clipboard.*`, +`drop.*`, `event`, `keycodes[]`, `window_title`, `custom_cursor_bound[]`, +`event_consumed`, `skip_present`, `onscreen_keyboard_shown`. + +`_sapp_init_state()` (**3554**) is called first by `_sapp_macos_run` and initializes +all of the above from `desc` (defaults applied by `_sapp_desc_defaults` at **3524**: +sample_count→1, swap_interval→1, clipboard_size→8192, max_dropped_files→1, +max_dropped_file_path_length→2048, window_title→"sokol"). Notably it sets +`first_frame=true`, `dpi_scale=1.0`, `mouse.shown=true`, `fullscreen=desc.fullscreen`, +and copies width/height verbatim (**may be 0** — the mac backend must resolve 0). + +--- + +## 1. NSApplication bootstrap + +### Entry (`_sapp_macos_run`, 5519–5542; `main`, 5545–5551; `sapp_run`, 13941) + +Order is exact and load-bearing: + +1. `_sapp_init_state(desc)` — zero+init global state. +2. `_sapp_macos_init_keytable()` — fill `_sapp.keycodes[]` (111 entries, 5359–5471). +3. `[NSApplication sharedApplication]` — creates NSApp (does **not** set activation policy yet). +4. `sapp_set_icon(&_sapp.desc.icon)` — set dock icon "as early as possible" (comment at 5524). This calls `_sapp_macos_set_icon` (5816) which writes `NSApp.dockTile.contentView`. **Main-window path does NOT require icon support** — it is a dock-tile decoration only, safe to stub/skip. `sapp_set_icon` (14291) no-ops when `num_images==0` / default not requested. +5. `_sapp.macos.app_dlg = [[_sapp_macos_app_delegate alloc] init]; NSApp.delegate = app_dlg;` +6. Install **Cmd-key-up workaround** (5530–5537): a block-based `addLocalMonitorForEventsMatchingMask:NSEventMaskKeyUp`. When a keyUp arrives while `NSEventModifierFlagCommand` is held, it force-forwards the event to `[NSApp keyWindow]` (Cocoa otherwise swallows key-ups under Cmd). The monitor id is stored in `_sapp.macos.keyup_monitor`. Removed in `_sapp_macos_discard_state` via `[NSEvent removeMonitor:]` (which also releases it). +7. `[NSApp run]` — **never returns**. All cleanup lives in `applicationWillTerminate`. + +`SOKOL_NO_ENTRY`: when defined, the `int main(...)` at 5546 is compiled out; the user +supplies their own entry that calls `sapp_run(desc)` → `_sapp_macos_run`. Otherwise +sokol's `main` calls `sokol_main(argc,argv)` to obtain `desc` then `_sapp_macos_run`. + +### NSApplicationDelegate methods (`@implementation _sapp_macos_app_delegate`, 5872–5945) + +- **`applicationDidFinishLaunching:` (5873–5932)** — the real app setup, runs inside `[NSApp run]`. See §2. +- **`applicationShouldTerminateAfterLastWindowClosed:` (5934–5937)** — returns `YES`. Closing the (sole) window terminates the app. +- **`applicationWillTerminate:` (5939–5944)** — the cleanup site. Runs, in order: `_sapp_call_cleanup()` (user cleanup_cb, once), `_sapp_macos_discard_state()` (release ObjC objects incl. Metal device/layer/textures, stop display link/timer, remove tracking area + keyup monitor), `_sapp_discard_state()` (free clipboard/drop/icon buffers, unbind custom cursors, zero `_sapp`). + +No `applicationShouldTerminate:` is implemented — termination is driven by window close +(see §3). **sokol does NOT create a default menu bar** (no `NSMenu`/`mainMenu` setup +anywhere in the mac path). Activation policy is `NSApplicationActivationPolicyRegular` +set inside `applicationDidFinishLaunching` (5876), explicitly kept **before window +creation** (comment cites floooh/sokol#1500). + +--- + +## 2. Main window creation (`applicationDidFinishLaunching:`, 5873–5932) + +Exact sequence: + +1. `NSApp.activationPolicy = NSApplicationActivationPolicyRegular;` (must precede window creation). +2. `_sapp_macos_init_cursors()` (5502) — populate `standard_cursors[]` (see §6). +3. If `window_width==0 || window_height==0` → `_sapp_macos_init_default_dimensions()` (5608): sets `dpi_scale` (backingScaleFactor if `high_dpi` else 1.0), computes default = **4/5 of `NSScreen.mainScreen.frame`** for each 0 dimension, and sets `framebuffer_width/height = default * dpi_scale`. +4. **Style mask is fixed** (5881–5885): `Titled | Closable | Miniaturizable | Resizable`. There is **no borderless / no fullscreen-derived style** — `desc.fullscreen` does not change the style mask; fullscreen is entered via `toggleFullScreen:` after creation (step 11). +5. `window_rect = NSMakeRect(0,0,window_width,window_height)` (content rect, points). +6. `_sapp.macos.window = [[_sapp_macos_window alloc] initWithContentRect:window_rect styleMask:style backing:NSBackingStoreBuffered defer:NO]`. The custom `_sapp_macos_window initWithContentRect:` (6044) also calls `registerForDraggedTypes:@[NSPasteboardTypeFileURL]` (drag&drop is always registered on the main window regardless of `enable_dragndrop`; the *handler* gates on `_sapp.drop.enabled` indirectly, see §7). +7. `window.releasedWhenClosed = NO` — **required** so the window object survives `performClose`/close until `applicationWillTerminate` cleanup (comment at 5892). +8. `window.title = NSString(window_title)`, `window.acceptsMouseMovedEvents = YES`, `window.restorable = YES`. +9. `win_dlg = [[_sapp_macos_window_delegate alloc] init]; window.delegate = win_dlg;` +10. Renderer init: **`_sapp_macos_mtl_init()` (5211)** for Metal (creates device, CAMetalLayer, view, starts display link, inits timing — see §10). Then `window.contentView = _sapp.macos.view; [window makeFirstResponder:view]; [window center];` +11. `_sapp.valid = true;` (set **before** fullscreen toggle — comment 5911 notes GL renders a frame during toggle). If `_sapp.fullscreen`: `[window toggleFullScreen:self]`. +12. `[NSApp activateIgnoringOtherApps:YES];` then `[window makeKeyAndOrderFront:nil];` +13. `_sapp_macos_update_dimensions()` (5632) — sample real bounds, resize swapchain, but **suppresses** the RESIZED event because `first_frame` is still true (see §11). +14. `[NSEvent setMouseCoalescingEnabled:NO]` — full-resolution mouse-move events. +15. **Focus workaround** (5919–5931, cites sokol#982): posts a synthetic `NSEventTypeAppKitDefined` / `NSEventSubtypeApplicationActivated` event to `NSApp` at-start, so the window is focused even if the init callback blocks for a long time. + +### High-DPI / dpi_scale sourcing + +- `desc.high_dpi` gates everything. `dpi_scale` is sampled from `backingScaleFactor` of the *screen* (`NSScreen.mainScreen.backingScaleFactor` at startup in `_sapp_macos_init_default_dimensions`; `[_sapp.macos.window screen].backingScaleFactor` on every `_sapp_macos_update_dimensions`, 5634). If `!high_dpi`, `dpi_scale = 1.0` always. +- `framebuffer = round(view.bounds.size * dpi_scale)`; `window_width/height = round(view.bounds.size)` in points. +- `view.layer.contentsScale = dpi_scale` is re-applied every update (5639) because `windowWillStartLiveResize` sets a non-scaling `layerContentsPlacement`. +- `sapp_high_dpi()` (14056) returns `desc.high_dpi && dpi_scale >= 1.5` (NOT just `high_dpi`). + +Initial size logic: `desc.width/height==0` → 4/5 of main screen. Centering via +`[window center]`. Title from `window_title`. `collectionBehavior` is **not** set +(default) — standard AppKit fullscreen behavior applies via `toggleFullScreen:`. + +--- + +## 3. Quit flow semantics (critical) + +State flags: `quit_requested`, `quit_ordered` (both bool in `_sapp`). + +### Public API +- `sapp_request_quit()` (14225): `quit_requested = true;` (nothing else). +- `sapp_cancel_quit()` (14229): `quit_requested = false;`. +- `sapp_quit()` (14233): `quit_ordered = true;` (hard, unconditional — no user veto). + +### How a quit reaches the OS + +There is **no NSApp terminate call** in the mac path. Termination is driven entirely +through **window close**: + +1. **`_sapp_macos_frame()` tail (5867–5869):** after every frame, `if (quit_requested || quit_ordered) [_sapp.macos.window performClose:nil];`. So a `sapp_request_quit()` or `sapp_quit()` made from user code takes effect on the *next frame boundary* by asking the window to close. +2. **`windowShouldClose:` (5948–5966)** — the decision point, invoked by `performClose:`, the red close button, or Cmd+Q routed to the window: + ``` + if (!quit_ordered) { // sapp_quit() not already called + quit_requested = true; + fire SAPP_EVENTTYPE_QUIT_REQUESTED; // user can call sapp_cancel_quit() here + if (quit_requested) quit_ordered = true; // user did NOT cancel → commit + } + return quit_ordered ? YES : NO; // YES closes the window + ``` + - **Red close button / Cmd+Q path:** `quit_ordered` starts false → QUIT_REQUESTED is delivered synchronously; if the event handler calls `sapp_cancel_quit()`, `quit_requested` becomes false, `quit_ordered` stays false, returns **NO** (window stays open). Otherwise returns **YES**. + - **`sapp_quit()` path:** `quit_ordered` already true → skips the whole block, no QUIT_REQUESTED fired, returns **YES** immediately (unvetoable). +3. Window actually closes → (sole window) `applicationShouldTerminateAfterLastWindowClosed:`=YES → app terminates → **`applicationWillTerminate:`** runs cleanup: `_sapp_call_cleanup()` (frame/user cleanup_cb) **then** `_sapp_macos_discard_state()` (which releases the Metal device etc.). **Ordering guarantee:** user `cleanup_cb` runs *before* any GPU/Metal teardown. (TrussC does its own `sg_shutdown` inside cleanup_cb; sokol only releases the *view/layer/device* afterward — sokol_gfx shutdown is the app's responsibility.) + +QUIT_REQUESTED event population: plain `_sapp_init_event(SAPP_EVENTTYPE_QUIT_REQUESTED)` +via `_sapp_macos_app_event` (5600), only if `_sapp_events_enabled()` (event_cb set AND +`init_called`). + +--- + +## 4. Frame timing + +Two layers: the platform-agnostic filtered timer (`_sapp.timing`, a `_sapp_timing_t`) +and a Metal-specific CADisplayLink-timestamp timer (`_sapp.macos.mtl.timing`). + +### Platform-agnostic filter (`_sapp_timing_*`, 2643–2711) +Config (`_sapp_timing_init`, 2655): `dt_min=1µs`, `dt_max=100ms`, `dt_threshold=4ms`, +`alpha=0.025`, initial `dt=ema=smooth_dt=1/60`. + +`_sapp_timing_update(t, external_now)` (2694): `now = external_now==0 ? timestamp_now() +: external_now`. On the **first** call `t->last==0` so no delta is produced (just stores +`last=now`). Subsequent calls compute `dt = now - last` → `_sapp_timing_delta`. + +`_sapp_timing_delta` (2677): clamp raw `dt` to [dt_min,dt_max] → store as unfiltered +`t->dt`. Then `error = |dt - smooth_dt|`; if `error > dt_threshold` **reset** the filter +(`ema = smooth_dt = dt`), else EMA: `ema += alpha*(dt - ema); smooth_dt = clamp(ema)`. + +`_sapp_timing_get` (2709) returns `smooth_dt`. `sapp_frame_duration_unfiltered()` +(13998) returns raw `t->dt`. + +### Metal CADisplayLink timing (`_sapp_macos_mtl_timing_*`, 5126–5154) +Because `CADisplayLink.timestamp` is very stable, Metal uses it *instead of* the measured +filter when the display link is active: +- `_timing_init` (5126): `timestamp=0`, `frame_duration_sec = 1.0/_sapp_macos_max_fps()` (`max_fps = [NSScreen.mainScreen maximumFramesPerSecond]`, 5056). +- `_timing_update` (5131), called each frame from `_sapp_macos_frame`: only when display link active. `cur = display_link.timestamp`; **first frame is skipped** (`timing.timestamp>0` guard) — `frame_duration_sec` keeps its refresh-rate seed; otherwise `dt = cur - prev`, run through `_sapp_timing_clamp` (min/max clamp only, NOT the EMA) → `frame_duration_sec`. Store `timestamp=cur`. +- `_timing_frame_duration` (5147): if display link active return `frame_duration_sec`; else fall back to `_sapp_timing_get(&_sapp.timing)` (the EMA) — this is the **occluded / fallback-timer** case. + +`sapp_frame_duration()` (13988) on macOS+Metal → `_sapp_macos_mtl_timing_frame_duration()`. + +### Frame count +`_sapp.frame_count` incremented in `_sapp_frame()` (3655), *after* `_sapp_call_frame()`. +On the very first `_sapp_frame` call `first_frame` is cleared and `init_cb` is called +before the first `frame_cb`. `sapp_frame_count()` (13984) returns the raw counter. +`_sapp_init_event` stamps `event.frame_count = _sapp.frame_count` (3617), so events fired +during frame N carry count N (count is incremented only after the frame_cb returns). +First frame value: 0 (frame_cb runs with count still 0, becomes 1 after). + +Per-frame timing driver (`_sapp_macos_frame`, 5849): `_sapp_timing_update(&_sapp.timing, 0.0)` +runs **every** frame (feeds the fallback path), then `_sapp_macos_mtl_timing_update()`, +then (inside `@autoreleasepool`) `_sapp_frame()`. + +--- + +## 5. Clipboard + +Enabled only if `desc.enable_clipboard`. `_sapp_init_state` allocates +`clipboard.buffer` of `clipboard_size` bytes (default 8192) and sets `clipboard.enabled`, +`clipboard.buf_size` (3576–3580). + +- **`sapp_set_clipboard_string(str)` (14242):** no-op if `!clipboard.enabled`. Calls `_sapp_macos_set_clipboard_string` (5664): inside `@autoreleasepool`, `[[NSPasteboard generalPasteboard] declareTypes:@[NSPasteboardTypeString] owner:nil]` then `setString:@(str) forType:NSPasteboardTypeString`. Then also copies `str` into `_sapp.clipboard.buffer`. +- **`sapp_get_clipboard_string()` (14261):** returns `""` if disabled; else `_sapp_macos_get_clipboard_string` (5672): zeroes `buffer[0]`, checks pasteboard `types` contains `NSPasteboardTypeString`, reads `stringForType:` UTF8 into `buffer` (clamped to `buf_size`). Returns the internal buffer pointer. +- **CLIPBOARD_PASTED on mac:** fired from `keyDown:` (6292–6296): if `clipboard.enabled && mods == SAPP_MODIFIER_SUPER && key_code == SAPP_KEYCODE_V`, a `SAPP_EVENTTYPE_CLIPBOARD_PASTED` event is emitted (after the KEY_DOWN and CHAR events). **Exact-match** on modifiers: `mods == SAPP_MODIFIER_SUPER` (Cmd alone; Cmd+Shift+V would not match). Unlike Win/Linux, mac does *not* pre-populate the clipboard buffer before the event — the app is expected to call `sapp_get_clipboard_string()` in its handler. + +--- + +## 6. Mouse cursor + +### Show/hide +- `sapp_show_mouse(show)` (14131): **does not stack** (comment 14130). Only acts when `mouse.shown != show`, routing through `_sapp_update_cursor(current_cursor, show)` (14116) which calls `_sapp_macos_update_cursor` then stores `mouse.current_cursor`/`mouse.shown`. +- `_sapp_macos_show_mouse(visible)` (5715) — used by lock path only; uses `CGDisplayShowCursor`/`CGDisplayHideCursor(kCGDirectMainDisplay)`. +- `_sapp_macos_update_cursor(cursor, shown)` (5750): if `shown != mouse.shown` it stacks `[NSCursor unhide]`/`[NSCursor hide]` (note: this *does* stack, so it is gated on an actual change). Then selects the NSCursor: custom if `custom_cursor_bound[cursor]`, else `standard_cursors[cursor]`, else `[NSCursor arrowCursor]`; `[ns_cursor set]`. + +### Standard cursor table (`_sapp_macos_init_cursors`, 5502–5517) +`ARROW→arrowCursor`, `IBEAM→IBeamCursor`, `CROSSHAIR→crosshairCursor`, +`POINTING_HAND→pointingHandCursor`, and the resize cursors use **undocumented private +selectors** declared in a category (5495–5500): +`RESIZE_EW→_windowResizeEastWestCursor` (fallback `resizeLeftRightCursor`), +`RESIZE_NS→_windowResizeNorthSouthCursor` (fb `resizeUpDownCursor`), +`RESIZE_NWSE→_windowResizeNorthWestSouthEastCursor` (fb `closedHandCursor`), +`RESIZE_NESW→_windowResizeNorthEastSouthWestCursor` (fb `closedHandCursor`), +`RESIZE_ALL→closedHandCursor`, `NOT_ALLOWED→operationNotAllowedCursor`. +Each uses `[NSCursor respondsToSelector:...] ? private : fallback`. + +### Custom cursors +- `sapp_bind_mouse_cursor_image(cursor, desc)` (14170): asserts hotspot `< dim-1` and `pixels.size == w*h*4`. Unbinds any existing, then `_sapp_macos_make_custom_mouse_cursor` (5774): builds an `NSBitmapImageRep` (RGBA8, `NSBitmapFormatAlphaNonpremultiplied`, `NSCalibratedRGBColorSpace`), `memcpy`s `desc->pixels`, wraps in `NSImage`, creates `[[NSCursor alloc] initWithImage:hotSpot:]` into `custom_cursors[cursor]`. Sets `custom_cursor_bound[cursor]=res`. If the bound cursor is current, re-applies immediately. Returns the passed-in cursor. +- `sapp_unbind_mouse_cursor_image` (14203): if bound, clears the flag, re-applies default if current (before destroy), then `_sapp_macos_destroy_custom_mouse_cursor` releases the NSCursor. There are `_SAPP_MOUSECURSOR_NUM` slots (the "CUSTOM_0..15" model is per-enum-slot; on mac every standard cursor enum can be overridden by a bound custom image). + +### Cursor re-application (tracking area interplay) +`updateTrackingArea` (6134) builds an `NSTrackingArea` with options +`MouseEnteredAndExited | ActiveInKeyWindow | EnabledDuringMouseDrag | CursorUpdate | +InVisibleRect | AssumeInside`. Because `CursorUpdate` is set, AppKit calls +**`cursorUpdate:` (6351)** which re-applies `_sapp_macos_update_cursor(current_cursor, +shown)` whenever the pointer (re)enters — this is what keeps a custom/hidden cursor +sticky across window regions. `mouseEntered:` (6157) also re-syncs mouse position. + +--- + +## 7. Drag & drop + +- Registration: **always** on the main window (`registerForDraggedTypes:@[NSPasteboardTypeFileURL]` in `_sapp_macos_window initWithContentRect:` at 6050, guarded by `__MAC_OS_X_VERSION_MAX_ALLOWED >= 101300`). Not gated on `enable_dragndrop`; the buffer/handler is what gates. +- `desc.enable_dragndrop` → `_sapp_init_state` allocates `drop.buffer` of `max_files * max_path_length` bytes and sets `drop.enabled/max_files/max_path_length/buf_size` (3581–3587). Defaults: 1 file, 2048 bytes/path. +- NSDraggingDestination methods on `_sapp_macos_window` (6056–6094): `draggingEntered:`→`NSDragOperationCopy`, `draggingUpdated:`→`NSDragOperationCopy`, `performDragOperation:` does the extraction. +- `performDragOperation:` (6064): if pasteboard has `NSPasteboardTypeFileURL`: `_sapp_clear_drop_buffer()`; `num_files = min(pasteboardItems.count, drop.max_files)`; for each item build `[NSURL fileURLWithPath:[item stringForType:NSPasteboardTypeFileURL]]` and `_sapp_strcpy(fileUrl.standardizedURL.path.UTF8String, _sapp_dropped_file_path_ptr(i), max_path_length)`. If any path overflows → `_SAPP_ERROR(DROPPED_FILE_PATH_TOO_LONG)`, abort, clear buffer, `num_files=0`. On success, if `_sapp_events_enabled()`: update mouse pos from `draggingLocation`, fire `SAPP_EVENTTYPE_FILES_DROPPED` with `modifiers = _sapp_macos_mods(nil)`. Returns YES if handled. +- Retrieval: `sapp_get_num_dropped_files()` (14319) returns `drop.num_files` (0 if disabled); `sapp_get_dropped_file_path(i)` (14326) returns `_sapp_dropped_file_path_ptr(i)`. +- Path extraction uses **NSURL / NSPasteboardTypeFileURL**, not the legacy NSFilenamesPboardType. + +--- + +## 8. Fullscreen + +- `sapp_is_fullscreen()` (14100): returns `_sapp.fullscreen` (a tracked bool, NOT a styleMask query). +- `sapp_toggle_fullscreen()` (14104) → `_sapp_macos_toggle_fullscreen` (5655): flips `_sapp.fullscreen` **and** calls `[window toggleFullScreen:nil]`. Note the flag is toggled optimistically here **and** authoritatively corrected by the notifications below — a double source of truth. +- `windowDidEnterFullScreen:` (6032) sets `fullscreen=true`; `windowDidExitFullScreen:` (6037) sets `fullscreen=false`. These are the reliable authority (native OS fullscreen, mission-control gestures, etc.). +- Startup `desc.fullscreen=true`: after window creation and `valid=true`, `applicationDidFinishLaunching` calls `[window toggleFullScreen:self]` (5910–5913). The window is created windowed first, then toggled. + +--- + +## 9. sapp_get_environment / sapp_get_swapchain (Metal) + +### `sapp_get_environment()` (14387) — asserts `_sapp.valid` +- `defaults.color_format = sapp_color_format()` → **`SAPP_PIXELFORMAT_RGB10A2`** on Metal (TrussC 10-bit patch, 14040–14042; upstream would be BGRA8). +- `defaults.depth_format = sapp_depth_format()` → always `SAPP_PIXELFORMAT_DEPTH_STENCIL` (14048). +- `defaults.sample_count = sapp_sample_count()` → `_sapp.sample_count`. +- `metal.device = (__bridge const void*) _sapp.macos.mtl.device` (14395). + +### `sapp_get_swapchain()` (14417) — asserts `_sapp.valid` +Metal branch (14421–14424): +- `metal.current_drawable = (__bridge) _sapp_macos_mtl_swapchain_next()` — **`[layer nextDrawable]` is called HERE, lazily, at swapchain-query time** (5116; asserts non-nil). It is NOT acquired at frame start. TrussC calls `sglue_swapchain()` inside `sg_begin_pass`, so the drawable is acquired at begin-pass time each frame. Calling `sapp_get_swapchain()` twice per frame would acquire two drawables — must be called exactly once per frame. +- `metal.depth_stencil_texture = _sapp.macos.mtl.depth_tex` +- `metal.msaa_color_texture = _sapp.macos.mtl.msaa_tex` (nil when sample_count==1). +- Common tail: `width = sapp_width()` (framebuffer_width, min 1), `height = sapp_height()`, `color_format = RGB10A2`, `depth_format = DEPTH_STENCIL`, `sample_count`. + +### glue contract (`sokol_glue.h`) +`sglue_environment()` (159) reads only: `env.defaults.color_format/depth_format/sample_count` +and `env.metal.device` (maps sapp_pixel_format→sg_pixel_format via `_sglue_to_sgpixelformat` +which handles `RGB10A2→SG_PIXELFORMAT_RGB10A2`, 149). `sglue_swapchain()` (178) reads: +`sc.width/height/sample_count/color_format/depth_format` and +`sc.metal.current_drawable / depth_stencil_texture / msaa_color_texture`. **These are the +only sapp fields the shim must fill for the Metal render path.** All d3d11/wgpu/vulkan/gl +fields are memset-0 and ignored on mac. + +--- + +## 10. Metal view/layer details (main window) + +### Layer/view creation (`_sapp_macos_mtl_init`, 5211–5227) +- `device = MTLCreateSystemDefaultDevice()`. +- `layer = [CAMetalLayer layer]`; `layer.device = device`; `layer.magnificationFilter = kCAFilterNearest`; `layer.opaque = true`; **`layer.pixelFormat = MTLPixelFormatRGB10A2Unorm`** (TrussC 10-bit patch, 5217); **`layer.framebufferOnly = false`** (TrussC patch enabling `captureWindow()` GPU reads, issue #56, 5218). `maximumDrawableCount` left at default 3 (commented note about 2, 5219). `colorspace` not set (FIXME). +- `view = [[_sapp_macos_view alloc] init]; [view updateTrackingAreas]; view.wantsLayer = YES; view.layer = layer;` (a **CAMetalLayer-backed NSView**, not MTKView — TRUSSC_MODIFICATIONS note confirms upstream migrated off MTKView to CAMetalLayer). +- `_sapp_macos_mtl_start_display_link()` (5156), `_sapp_macos_mtl_timing_init()`. + +### Display link + swap interval +`_sapp_macos_mtl_start_display_link` (5156): if link exists, unpause and return. Else +`[view displayLinkWithTarget:view selector:@selector(displayLinkFired:)]`; sets +`preferredFrameRateRange = {p,p,p}` where `p = max_fps / swap_interval`; adds to current +run loop in `NSRunLoopCommonModes`. `displaySyncEnabled`/`maximumDrawableCount` are not +explicitly set; vsync is implied by the CAMetalLayer + display-link cadence and +`swap_interval` scales the preferred fps. `_sapp_macos_mtl_display_link_active()` = +link non-nil and not paused. + +### Occlusion / fallback timer (already handled for secondary windows, documented for parity) +`_transition_to_occluded` (5197): if active → stop display link (pause) + start a repeating +`NSTimer` at `_SAPP_MACOS_MTL_OBSCURED_FRAME_DURATION_IN_SECONDS = 0.0166667` (~60Hz) +firing `fallbackTimerFired:` (5119). `_transition_to_visible` (5204): stop fallback timer, +(re)start display link. Triggered by `windowDidChangeOcclusionState:` (6012, +`occlusionState & NSWindowOcclusionStateVisible`), `windowDidMiniaturize:`/`Deminiaturize:`. + +### Swapchain textures (`_sapp_macos_mtl_create_texture`, 5061; `_swapchain_create`, 5089) +- Depth: `MTLPixelFormatDepth32Float_Stencil8`, sample_count, panic on nil. +- MSAA color (only if `sample_count > 1`): **`MTLPixelFormatRGB10A2Unorm`** (TrussC patch, 5095). +- Descriptor: `textureType = MTLTextureType2DMultisample` if sample_count>1 else `2D`; `usage = MTLTextureUsageRenderTarget`; `resourceOptions = MTLResourceStorageModePrivate`; mip=1, arrayLength=1, depth=1. +- `_swapchain_resize` (5111) = destroy + create. Called from `_sapp_macos_mtl_update_framebuffer_dimensions` (5237) only when the drawable size actually changed; also sets `layer.drawableSize = {framebuffer_width, framebuffer_height}`. + +### RGB10A2 patch locations (must replicate all four) +1. `layer.pixelFormat = MTLPixelFormatRGB10A2Unorm` (5217). +2. MSAA texture format `MTLPixelFormatRGB10A2Unorm` (5095). +3. `sapp_color_format()` returns `SAPP_PIXELFORMAT_RGB10A2` on Metal/D3D11 (14042). +4. glue `_sglue_to_sgpixelformat` maps `RGB10A2→SG_PIXELFORMAT_RGB10A2` (149). +Plus `layer.framebufferOnly = false` (5218) for captureWindow reads. + +--- + +## 11. Window/app events delivered on mac (beyond raw input) + +All go through `_sapp_macos_app_event(type)` (5600) → gated by `_sapp_events_enabled()`. +`_sapp_init_event` (3614) populates every event with: `frame_count`, +`mouse_button=INVALID`, `window_width/height` (points), `framebuffer_width/height` +(pixels), `mouse_x/y/dx/dy`. Per-type specifics: + +- **RESIZED** — fired from `_sapp_macos_update_dimensions` (5632→5651) **only if** dimensions changed **and** `!first_frame` (so the startup update at step 13 is silent). Triggered by `windowDidResize:` (5986) and `windowDidChangeScreen:` (5991). Event carries the new `window_width/height` (points) and `framebuffer_width/height` (pixels) — both already updated before the event fires. +- **ICONIFIED** — `windowDidMiniaturize:` (5996); also transitions Metal to occluded first. +- **RESTORED** — `windowDidDeminiaturize:` (6004); transitions Metal to visible first. +- **FOCUSED** — `windowDidBecomeKey:` (6022). +- **UNFOCUSED** — `windowDidResignKey:` (6027). +- **QUIT_REQUESTED** — `windowShouldClose:` (see §3). +- **SUSPENDED / RESUMED** — **not used on macOS** (event table lines 156–157 mark them iOS/Android/web only; the mac window delegate never fires them). `windowDidChangeOcclusionState:` only drives Metal render gating, it does NOT emit an app event. +- **DISPLAY_CHANGED** — no such event type on mac; `windowDidChangeScreen:` re-runs `update_dimensions` (may emit RESIZED if dpi/size changed) but there is no dedicated display-changed event. +- FILES_DROPPED / CLIPBOARD_PASTED — see §7 / §5 (fired from window/view, not the delegate). + +`_sapp_events_enabled()` (3629) requires `(event_cb || event_userdata_cb) && init_called` +— so **no events fire before the first frame's init_cb** runs. This is why the RESIZED +`!first_frame` guard matters (events would be dropped anyway pre-init, but the guard also +covers the case where init already ran). + +--- + +## 12. Misc + +- **`sapp_set_window_title(str)` (14279):** copies into `_sapp.window_title`, then `_sapp_macos_update_window_title` (5689): `[window setTitle:NSString(window_title)]`. +- **`sapp_high_dpi()` (14056):** `desc.high_dpi && dpi_scale >= 1.5` (not a plain flag). +- **`sapp_isvalid()` (13972):** returns `_sapp.valid` (set true at 5909, inside `applicationDidFinishLaunching` after renderer init, before fullscreen toggle). +- **`sapp_show_keyboard(show)` (14086):** **mac no-op** (only iOS/Android implemented). `sapp_keyboard_shown()` returns `_sapp.onscreen_keyboard_shown` (always false on mac). +- **`sapp_skip_present()` (14597):** sets `_sapp.skip_present=true`, but on the **Metal path it is IGNORED** — only the Vulkan (`_sapp_vk_frame`, 5023) and D3D11/WGPU frame functions honor it. `_sapp_macos_frame` (5849) has no skip_present check; the Metal drawable is presented by sokol_gfx's commit. So on mac+Metal, `sapp_skip_present()` has no effect (event-driven present suppression is a D3D11/Vulkan feature). +- **Dock icon / `sapp_set_icon`:** dock-tile only (`_sapp_macos_set_icon`, 5816 writes `NSApp.dockTile.contentView`). **Main-window rendering path does not require it** — safe to omit in the shim. +- **AppKit-notification-driven event_cb sites** (outside the NSView): `windowShouldClose:`, `windowDidResize:`, `windowDidChangeScreen:`, `windowDidMiniaturize:`, `windowDidDeminiaturize:`, `windowDidBecomeKey:`, `windowDidResignKey:` (all in the window delegate), and `performDragOperation:` (in the NSWindow subclass). The shim must route these to `event_cb` just like the view routes input. +- **GL/WGPU divergences** (not needed for Metal shim, noted for completeness): GL uses `NSOpenGLView` + a 1ms `NSTimer`→`setNeedsDisplay`→`drawRect:`→`_sapp_macos_frame`, and `prepareOpenGL` sets swap interval 1; WGPU mirrors the Metal CADisplayLink setup. Both share the same window/delegate/event code. + +--- + +## Appendix: function → line index (Metal-relevant) + +| function | line | +|---|---| +| `_sapp_macos_max_fps` | 5056 | +| `_sapp_macos_mtl_create_texture` | 5061 | +| `_sapp_macos_mtl_swapchain_create/destroy/resize/next` | 5089/5102/5111/5116 | +| `_sapp_macos_mtl_timing_init/update/frame_duration` | 5126/5131/5147 | +| `_sapp_macos_mtl_start/stop_display_link` | 5156/5173 | +| `_sapp_macos_mtl_start/stop_fallback_timer` | 5179/5190 | +| `_sapp_macos_mtl_transition_to_occluded/visible` | 5197/5204 | +| `_sapp_macos_mtl_init/discard_state` | 5211/5229 | +| `_sapp_macos_mtl_update_framebuffer_dimensions` | 5237 | +| `_sapp_macos_init_keytable` | 5359 | +| `_sapp_macos_discard_state` | 5473 | +| `_sapp_macos_init_cursors` | 5502 | +| `_sapp_macos_run` / `main` | 5519 / 5546 | +| `_sapp_macos_mods` | 5553 | +| `_sapp_macos_mouse/key/app_event` | 5581/5590/5600 | +| `_sapp_macos_init_default_dimensions` | 5608 | +| `_sapp_macos_update_dimensions` | 5632 | +| `_sapp_macos_toggle_fullscreen` | 5655 | +| `_sapp_macos_set/get_clipboard_string` | 5664/5672 | +| `_sapp_macos_update_window_title` | 5689 | +| `_sapp_macos_mouse_update_from_nspoint/nsevent` | 5693/5711 | +| `_sapp_macos_show/lock_mouse` | 5715/5724 | +| `_sapp_macos_update_cursor` | 5750 | +| `_sapp_macos_make/destroy_custom_mouse_cursor` | 5774/5810 | +| `_sapp_macos_set_icon` | 5816 | +| `_sapp_macos_frame` | 5849 | +| `app_delegate` (`applicationDidFinishLaunching:` etc.) | 5872 | +| `window_delegate` | 5947 | +| `_sapp_macos_window` (drag&drop) | 6043 | +| `_sapp_macos_view` (input, timers) | 6097 | diff --git a/docs/dev/sokol_app_tc-design.md b/docs/dev/sokol_app_tc-design.md index c8b4146b..2f77144d 100644 --- a/docs/dev/sokol_app_tc-design.md +++ b/docs/dev/sokol_app_tc-design.md @@ -128,6 +128,25 @@ can feed events manually, so imgui does not chain us to sokol_app. CoreEvents). TrussC.h main loop consumes window ticks. Per-window dt. Gate: all examples + multiWindowExample + hot-reload run; occlusion gate holds; mixed-Hz holds. + - **P0a DONE (commit 6d73d9a2):** secondary windows live in the header + behind the C API; tcWindowMac.mm is the thin adapter. + - **P0b DONE:** the header implements sokol_app.h's public API on macOS + (`sapp_run` owns `[NSApp run]`; the main window is window #0 on the + same view/delegate/tick machinery). sokol_app.h is included + declarations-only on macOS — its mac implementation is no longer + compiled. Key decisions: TrussC.h / tcHotReloadHost.h stayed UNTOUCHED + (the ~52 sapp_* call sites now resolve to the header's shims); the main + window keeps upstream's occlusion model (display link + ~60Hz fallback + NSTimer, frame_cb never stops) while secondary windows keep the + tick-gate model; drawable acquisition is lazy in sapp_get_swapchain() + with per-tick caching (repeated calls per frame are now safe, unlike + upstream); quit = upstream's cancellable performClose dance, plus + `[NSApp terminate:]` on main-window close so the app exits even with + secondary windows open; RGB10A2 + framebufferOnly=false + Cmd-keyup + monitor + Cmd+V paste event + cursor table + drag&drop ported per + docs/dev/sapp-mac-impl-spec.md (the implementation contract). + Deliberate deviation: sapp_dpi_scale() before sapp_run() returns the + main screen scale (upstream: 0). - **P1 — Windows driver.** Cannibalize feat/multi-window-win (DXGI flip swapchain, WndProc, DPI v2 — audited good) + sokol_app's win32 keyboard tables; DXGI waitable ticks replace WM_TIMER (kills the 64 Hz cap); main From d09dafc007de47cdb9fe254c9ae66b2aa5e38885 Mon Sep 17 00:00:00 2001 From: tettou771 Date: Thu, 9 Jul 2026 17:32:20 +0900 Subject: [PATCH 17/87] doc: win32/D3D11 implementation contract + multi-window-win branch audit (P1 kickoff) --- docs/dev/multi-window-win-audit.md | 287 ++++++++++++ docs/dev/sapp-win32-impl-spec.md | 694 +++++++++++++++++++++++++++++ 2 files changed, 981 insertions(+) create mode 100644 docs/dev/multi-window-win-audit.md create mode 100644 docs/dev/sapp-win32-impl-spec.md diff --git a/docs/dev/multi-window-win-audit.md b/docs/dev/multi-window-win-audit.md new file mode 100644 index 00000000..149d6d0a --- /dev/null +++ b/docs/dev/multi-window-win-audit.md @@ -0,0 +1,287 @@ +# Multi-window Windows driver — cannibalization audit + +Audit of `origin/feat/multi-window-win` as **reference source** for a new Windows +secondary-window driver to be written into `core/include/sokol/sokol_app_tc.h`. +Not a merge — pieces are lifted/adapted/replaced individually. + +All file:line references below are into the branch file +`core/platform/win/tcWindowWin.cpp` (708 lines) unless noted. Read via +`git show origin/feat/multi-window-win:`. + +## Scope of the branch + +`git diff $(git merge-base HEAD origin/feat/multi-window-win)..origin/feat/multi-window-win --stat`: + +``` + core/include/tc/app/tcWindow.h | 25 +- (comments + TC_PLATFORMS("macos,windows") + stub #if guard) + core/platform/win/tcWindowWin.cpp | 708 ++++ (the whole driver — new file) + docs/reference/api-reference.toml | 12 +- (doc strings macOS -> macOS/Windows) +``` + +Key facts up front: + +- **Does NOT touch `sokol_app.h`, `sokol_impl.cpp`, or any sokol win TU.** It is a + standalone C++ file that plugs into the existing TrussC `Window`/`WindowContext` + abstraction and borrows sokol_app's already-created D3D11 device via + `sg_d3d11_device()` / `sg_d3d11_device_context()`. The new design integrates + into `sokol_app_tc.h` itself (a C header), so all of this C++/TrussC glue must be + re-expressed against the new per-window frame model. +- **No per-window dt story.** `tickWindow()` calls the *global* `beginFrame()` + (line 286) and global `present()` (295). There is no per-window EMA frame timer. + The new `sokol_app_tc.h` already has a per-window EMA timer (header line ~206), + so dt must come from there, not from this branch. +- **Only two "not-done" markers** (`git grep -i todo/fixme/hack` is clean): + - line 273: "Same lifecycle caveats as Fbo contexts on sgl resize" (advisory). + - **line 436: keycode table is a known gap** — see Event handling below. + +--- + +## 1. Window creation — `createWindow()` (637-704), `ensureWindowClass()` (552-564) + +- **Window class** (555-562): `WNDCLASSEXW`, class name `L"TrussCSecondaryWindow"` + (45), style `CS_HREDRAW | CS_VREDRAW | CS_OWNDC`, `IDC_ARROW` cursor, registered + once via a `static bool` guard (553). Straightforward. +- **Styles** (654): `WS_OVERLAPPEDWINDOW` if `settings.decorated` else `WS_POPUP`. +- **DPI handling** (669-677): creates at the requested logical size interpreted as + 96 DPI (`CW_USEDEFAULT` position), then reads the real per-monitor DPI with + `GetDpiForWindow`, computes `scale`, and resizes the CLIENT area to + `logical * scale` using `AdjustWindowRectExForDpi` + `SetWindowPos`. Assumes + sokol_app has already set per-monitor-v2 awareness (comment 656-657). Clean and + correct; the two-step create-then-resize is the standard per-monitor-v2 dance. +- `HWND` create at 658-661 stashes `nw` via `lpCreateParams`; retrieved in + `WM_NCCREATE` (460-463) into `GWLP_USERDATA`. + +**Rating: LIFT (logic) / ADAPT (packaging).** The Win32 sequence is correct and +directly reusable, but it lives inside a C++ `Window`/`shared_ptr` factory. In the +C header it becomes a `_sapp_tc` window-slot init function. Class registration, +style selection, and the DPI create-then-resize block port almost verbatim. + +--- + +## 2. Swapchain — `createSwapchain()` (181-219), `createRenderTargets()` (108-156), `resizeSwapchain()` (166-179), `acquireSecondarySwapchain()` (74-91) + +- **Factory acquisition** (186-192): `device->QueryInterface(IDXGIDevice)` → + `GetAdapter` → `adapter->GetParent(IDXGIFactory2)`. Reaches the *same* factory + that made sokol_app's device — correct approach, avoids a second `CreateDXGIFactory`. +- **Swapchain desc** (194-204): `DXGI_SWAP_CHAIN_DESC1`, format + `DXGI_FORMAT_R10G10B10A2_UNORM` (10-bit, matches the patched sokol_app main + window, const at 41), `SampleDesc.Count = 1` (flip model is always single-sample), + `BufferUsage = RENDER_TARGET_OUTPUT`, **`BufferCount = 2`**, + **`SwapEffect = DXGI_SWAP_EFFECT_FLIP_DISCARD`**, `Scaling = STRETCH`, + `AlphaMode = IGNORE`. `CreateSwapChainForHwnd` (206). +- **Alt-Enter suppression** (207): `factory->MakeWindowAssociation(hwnd, DXGI_MWA_NO_ALT_ENTER)` + right after create — the correct flip-model gotcha handling. +- **MSAA approach** (133-143): flip-model swapchains can't be multisampled, so when + `sampleCount > 1` it creates a *separate* MSAA color texture + (`D3D11_STANDARD_MULTISAMPLE_PATTERN`) and hands sokol_gfx both a `render_view` + (MSAA) and `resolve_view` (backbuffer) via `sg_swapchain.d3d11` (83-88). Correct + and matches how sokol resolves. +- **Depth-stencil** (146-152): `D24_UNORM_S8_UINT`, drawable-sized, sample count + matched to color. Recreated on resize. +- **Backbuffer RTV** (114-121): `GetBuffer(0)` → `CreateRenderTargetView`. Comment + notes flip model keeps buffer 0 as the current back buffer every frame so the + view is reusable (persistent, not per-frame) — this is why + `acquireSecondarySwapchain` (74-91) is a trivial "hand back current views + size" + with no per-frame drawable acquisition (contrast Metal). Good insight to preserve. +- **Resize** (166-179): releases all views, then the critical flip-model gotcha + (170-175): **before `ResizeBuffers` you must release EVERY reference to the back + buffers, including the RTV still bound on sokol's shared immediate context** — + so it does `dc->OMSetRenderTargets(0, nullptr, nullptr); dc->Flush();` first. + Then `ResizeBuffers(0, w, h, kSwapFormat, 0)` and recreate targets. This + ordering is the single most valuable encoded gotcha in the file. + +**Rating:** +- Factory acquisition, desc, flip-discard/BufferCount=2, MakeWindowAssociation, + MSAA-side-texture + resolve wiring, depth-stencil, ResizeBuffers unbind/flush + ordering: **LIFT** — all correct D3D11, portable to C almost as-is. +- `BufferCount = 2` and swapchain-flags: **ADAPT.** The new design uses **DXGI + frame-latency waitable objects**, which requires + `DXGI_SWAP_CHAIN_FLAG_FRAME_LATENCY_WAITABLE_OBJECT` on create, usually + `BufferCount = 2` or 3 with `SetMaximumFrameLatency(1)`, and passing that same + flag to every `ResizeBuffers` call (a common bug source: flag dropped on resize + → waitable handle invalidated). This branch passes `0` flags — must change. +- `acquireSecondarySwapchain` hook: **REPLACE** — it targets TrussC's + `WindowContext::acquireSwapchain` callback (C++), which does not exist in the + C-header world. The new driver returns/fills `sg_swapchain` directly at frame time. + +--- + +## 3. Tick model — `tickWindow()` (224-312), `SetTimer` (696-701), `monitorRefreshHz()` (94-105) + +- **Driver: `WM_TIMER`.** `SetTimer(hwnd, kTickTimerId, interval, nullptr)` at + 699-701 with `interval = 1000 / monitorRefreshHz(hwnd)` (integer ms). Fires + `WM_TIMER` (469-470) → `tickWindow`. Delivered on the main thread by sokol_app's + own message pump; the comment (696-698) leans on Windows auto-coalescing timers + to depth 1. +- **THIS IS THE PART BEING REPLACED.** `WM_TIMER` resolution is ~15.6 ms + (`USER_TIMER_MINIMUM` 10 ms, and the timer floor is the system tick), so it + effectively caps at ~64 Hz and jitters badly — it cannot hit 120/144 Hz displays + and is not vsync-locked. Integer `1000/hz` (699) also quantizes (e.g. 144 Hz → + 6 ms → 166 Hz nominal, then clamped by the timer floor). The whole point of the + redesign is to drive each window off a **DXGI frame-latency waitable object** + (`GetFrameLatencyWaitableObject` → `WaitForSingleObjectEx`) so the tick is + vsync-paced and jitter-free. +- **Integration with the main loop** (295, 303-304): renders the tree, then + `Present(0, 0)` with **SyncInterval 0** so the present never blocks the main + thread on this window's vblank (comment 300-303: a blocking wait would halve the + main window's rate). This "don't block on secondary present" principle is exactly + what the waitable-object model formalizes. +- **`monitorRefreshHz`** (94-105): `MonitorFromWindow` → `GetMonitorInfoW` → + `EnumDisplaySettingsW(ENUM_CURRENT_SETTINGS).dmDisplayFrequency`, fallback 60. + Still useful as a *fallback* dt estimate, but not as the pacing source. + +**Rating: REPLACE (the pacing) / keep the shape.** The per-window-tick-on-main-thread +*structure* (each window renders synchronously, present non-blocking) is exactly +what the new design keeps and what `sokol_app_tc.h` already assumes. But the +`SetTimer`/`WM_TIMER` mechanism is thrown out and replaced by the frame-latency +waitable object polled from the main loop. `monitorRefreshHz` survives only as a +fallback constant. + +--- + +## 4. Event handling — `wndProc()` (458-550) + input helpers (317-453) + +Messages handled: + +- Mouse buttons L/R/M down+up (473-478) → `mouseButtonEvent` (330-365): tracks a + `buttonMask`, `SetCapture`/`ReleaseCapture` on first-press/last-release (346, 355) + so drags survive leaving the client area. Maps to TrussC `MouseEventArgs` + + fires `events().mousePressed`, `app_->mousePressed`, and + `dispatchMousePressToTree`. +- `WM_MOUSEMOVE` (480-482) → `mouseMoveEvent` (367-409): arms `TrackMouseEvent` + (TME_LEAVE) once (372-377), computes delta, emits drag vs move depending on + `buttonMask`. +- `WM_MOUSELEAVE` (484-493): disarms tracking, parks cursor at (-1,-1) so the next + tick's `updateHoverState` clears hover. +- `WM_MOUSEWHEEL` (495-498) → `mouseScrollEvent` (411-428): converts screen→client, + `wheel = WHEEL_DELTA_WPARAM / WHEEL_DELTA`. +- `WM_KEYDOWN/SYSKEYDOWN/KEYUP/SYSKEYUP` (500-509) → `keyEvent` (430-453). SYS + variants `break` to `DefWindowProc` so Alt/F10 system handling survives (503, 508). + Repeat flag from `lParam bit 30` (502). +- `WM_SIZE` (511-526): non-minimized → `resizeSwapchain` + re-read DPI + + `syncRootSize` (fires `windowResized`). +- `WM_DPICHANGED` (528-537): adopts the suggested rect (per-monitor v2); next tick + picks up the new scale. +- `WM_CLOSE` (539-544): calls `owner->close()` (which deletes `nw` — comment warns + touch nothing after). +- `WM_DESTROY` (546-547): no-op. + +**Modifier keys** (`fillMods`, 317-322): `GetKeyState` for Shift/Ctrl/Alt/LWIN|RWIN. +**DPI→logical conversion** (`logicalPos`, 325-328): `px / (dpi/96)`. + +- **KEYCODE GAP (line 430-437, explicit).** `keyEvent` does `int key = vk;` — it + passes the raw **Win32 virtual-key code straight through as the TrussC/sokol + keycode**. The comment admits this only works because letters/digits happen to + share ASCII == VK == SAPP_KEYCODE; **the full `SAPP_KEYCODE` translation table is + deferred as "Phase 2 polish"**. So arrows, F-keys, punctuation, keypad, etc. are + wrong. This is the direct analogue of the gap macOS had. The new driver should + port sokol_app.h's existing `_sapp_win32_key()` VK→`SAPP_KEYCODE` table rather + than copy this shortcut. No text/char (`WM_CHAR`) handling either. + +**Rating:** +- Mouse (buttons/capture/move/drag/leave/wheel), DPI conversion, SYS-key + pass-through, WM_SIZE/WM_DPICHANGED handling: **ADAPT** — correct behavior, but + every handler writes into TrussC `WindowContext`/`CoreEvents`/Node-tree dispatch + (`internal::currentWindowCtx`, `win.events()...`, `win.dispatchMousePressToTree`). + In the C header these become `sapp_event` fills dispatched through the event + callback. The *message set* and the capture/track/leave logic transfer; the + *payload target* is rewritten. +- Keycode mapping: **REPLACE** — use sokol_app.h's VK→SAPP_KEYCODE table; add + `WM_CHAR` for text. + +--- + +## 5. Occlusion / minimize — `tickWindow()` (229-242, 304-311) + +- **Minimize** (231): `if (IsIconic(hwnd)) return;` — timer keeps firing, resumes + on restore. Fully idle while minimized. +- **Occlusion** (235-242, 304-311): after `Present(0,0)`, if it returns + `DXGI_STATUS_OCCLUDED` (305) sets `nw->occluded`; subsequent ticks cheaply poll + with `Present(0, DXGI_PRESENT_TEST)` (236) and skip all rendering until it + returns `S_OK`. Logs once each way. This is the D3D11 analogue of the mac + occlusion-skip and is genuinely good — a fully covered window costs one + present-test per tick, never stalls others. + +**Rating: LIFT (logic).** `IsIconic` skip and the `DXGI_PRESENT_TEST` occlusion +poll are the correct idioms and port directly. Note under the waitable-object +model you must still not *block* on the waitable for an occluded/minimized window +(or you'd stall) — keep the "test cheaply, skip render" path. + +--- + +## 6. How it plugs into TrussC — the C++ seam that must be re-expressed + +- Renders through TrussC's `Window` / `internal::WindowContext`: + `internal::currentWindowCtx` swap (269-270, 299), a per-window `sgl_context` + created lazily on first tick (274-282) with matching color/depth/sample formats, + `sgl_set_context` + `sgl_defaults` (283-284), global `beginFrame()` (286), + `win.events().update/draw/afterFrame` + `win.tickTree()` + `win.drawTreeNow()` + (288-293), then global `present()` (295) which does `sg_end_pass`+`sg_commit`. +- Swapchain handed to sokol via TrussC's `WindowContext::acquireSwapchain` + function-pointer hook (688-689) → `acquireSecondarySwapchain` (74-91). +- Teardown (`close()`, 583-614): `KillTimer`, clear `GWLP_USERDATA`, + `DestroyWindow`, release views + swapchain, `sgl_destroy_context`, then App + `exit()/cleanup()` and unregister from `attachedApps`. + +**Rating: REPLACE / ADAPT.** All of this is TrussC-C++-abstraction plumbing that +does not exist in `sokol_app_tc.h`. The new driver owns the window slot in the C +header and calls back out. What transfers conceptually: the per-window sgl context +lifecycle, format agreement across swapchain/sgl/MSAA/depth, and the teardown +ordering (kill tick source → clear userdata → destroy window → release GPU → free +slot). The `beginFrame()`/`present()` being *global* (single frame counter, single +pass bracket) is a limitation to fix in the new model — each window needs its own +frame timing (the header's EMA timer) and its own pass. + +--- + +## 7. D3D11 gotchas encoded in the file (the real value) + +1. **Flip-model `ResizeBuffers` ordering** (170-175): unbind RTV from the shared + immediate context (`OMSetRenderTargets(0,nullptr,nullptr)`) + `Flush()` before + `ResizeBuffers`, or it fails and the next present crashes. **Highest-value comment.** +2. **Flip-model + MSAA** (133-143, 83-88): swapchain is single-sample; MSAA is a + side texture that sokol resolves into the backbuffer via `resolve_view`. +3. **Alt-Enter suppression** (207): `MakeWindowAssociation(DXGI_MWA_NO_ALT_ENTER)`. +4. **Persistent backbuffer RTV** (112-113): flip model keeps buffer 0 current, so + the RTV is reusable frame-to-frame (no per-frame acquire). +5. **Non-blocking secondary present** (300-304): `Present(0,0)` SyncInterval 0 so a + secondary never gates the main window's frame rate. +6. **Shared device/context**: uses `sg_d3d11_device()` / + `sg_d3d11_device_context()` — one device, one immediate context, single-threaded + (174 comment). Any per-window work runs on that shared context; ordering matters. + +All six are LIFT-grade knowledge to carry into the new driver. + +--- + +## Rating summary + +| Piece | Rating | Note | +|---|---|---| +| Window class + style + per-monitor-v2 create/resize | LIFT logic / ADAPT packaging | port into C window-slot init | +| DXGI factory acquisition from sokol device | LIFT | verbatim | +| Swapchain desc (flip-discard, 10-bit, MakeWindowAssociation) | LIFT + **ADAPT** | must add FRAME_LATENCY_WAITABLE flag + carry flag through ResizeBuffers | +| MSAA side-texture + resolve wiring | LIFT | correct | +| Depth-stencil creation | LIFT | correct | +| ResizeBuffers unbind/flush ordering | **LIFT (keep verbatim)** | top gotcha | +| `acquireSecondarySwapchain` hook | REPLACE | fill `sg_swapchain` directly, no C++ callback | +| **WM_TIMER / SetTimer pacing** | **REPLACE** | → DXGI frame-latency waitable object | +| `monitorRefreshHz` | keep as fallback | not the pacing source | +| Mouse handlers (capture/track/leave/wheel/drag) | ADAPT | retarget payload to `sapp_event` | +| **Keycode `int key = vk`** | **REPLACE** | use sokol_app VK→SAPP_KEYCODE table; add WM_CHAR | +| Occlusion (PRESENT_TEST) + IsIconic skip | LIFT logic | keep non-blocking skip under waitable model | +| WindowContext/sgl/beginFrame/present plumbing | REPLACE/ADAPT | re-express against C header + per-window timer | +| Teardown ordering | ADAPT | same ordering, C-header resources | + +--- + +## Cross-checks + +- **Touches `sokol_app.h` / sokol win TU?** No. Standalone file, borrows the device + via `sg_d3d11_device()`. The new work lands *inside* `sokol_app_tc.h`. +- **Per-window dt story?** None. Global `beginFrame()`/`present()`, single frame + counter. The new `sokol_app_tc.h` already carries a per-window EMA frame timer + (header ~line 206) — that becomes the dt source; this branch offers nothing here. +- **Known-broken / deferred:** the keycode table (line 436, "Phase 2 polish") — the + only functional gap. No text input (`WM_CHAR`). No TODO/FIXME/hack strings + otherwise; the code is otherwise complete and self-describing. diff --git a/docs/dev/sapp-win32-impl-spec.md b/docs/dev/sapp-win32-impl-spec.md new file mode 100644 index 00000000..c4a6d24c --- /dev/null +++ b/docs/dev/sapp-win32-impl-spec.md @@ -0,0 +1,694 @@ +# sokol_app.h Win32 (D3D11) Backend — Behavioral Specification + +Implementation contract for replacing sokol_app.h's Win32 main-window / app-lifecycle +with `sokol_app_tc.h`. All line references are into +`core/include/sokol/sokol_app.h` (14602 lines, this worktree). Focus: `_SAPP_WIN32` ++ `SOKOL_D3D11`. TrussC builds Windows with D3D11 only; GL/WGL, WGPU, Vulkan paths on +Win32 are noted only where a shared code path forks on them. iOS/macOS/Linux/web ignored. + +This is the sibling of `sapp-mac-impl-spec.md`. Where the mac backend is event-driven +(`[NSApp run]` + CADisplayLink), the Win32 backend is an **explicit owned message+render +loop** (`_sapp_win32_run`) — this is the single biggest structural difference and shapes +everything below. + +**Per-window vs process-global (read this first — the replacement makes windows plural):** +Almost everything the Win32 backend touches is currently **single-window global state** +baked into `static _sapp_t _sapp`. The truly *process-global* pieces (must stay global / +be registered once regardless of window count) are: +- **Window class** `L"SOKOLAPP"` — `RegisterClassW` / `UnregisterClassW` (§2). One class, + many windows; register once, unregister when the last window dies. +- **DPI awareness** — a *process* setting (`SetProcessDpiAwarenessContext`, §1). Set once + at startup, never per-window. +- **Console** attach/alloc + codepage (§1) — process-global. +- **Cursor clip / ShowCursor counter / raw-input registration** (mouse lock, §6) — global + OS state; only one window can own the locked cursor. +- **Standard cursors** (`LoadImageW(..., LR_SHARED)`) — shareable HCURSORs, effectively global. +- **Keytable** `_sapp.keycodes[]` (§4) — global, fill once. + +Everything else is **per-window** and must move into a per-window struct when windows go +plural: `hwnd`, `hmonitor`, `dc`, `big_icon`/`small_icon`, `custom_cursors[]`, `surrogate` +(WM_CHAR surrogate accumulator), `stored_window_rect`, `iconified`, `in_create_window`, +per-window `dpi` scales, `mouse.tracked`/`capture_mask`, and the entire D3D11 device+ +swapchain+RTV/DSV set (`_sapp_d3d11_t`). Note the D3D11 **device+context** could be +shared across windows but each window needs its own **swapchain + RTV/DSV/MSAA**. + +--- + +## 0. Global state touched (the shared contract) + +Single global `static _sapp_t _sapp;` (definition ~3298) embeds `_sapp_win32_t win32;` +(3305) and, under D3D11, `_sapp_d3d11_t d3d11;` (3307). + +### `_sapp_d3d11_t` (2889–2903, `SOKOL_D3D11 && _SAPP_WIN32`) +```c +typedef struct { + ID3D11Device* device; + ID3D11DeviceContext* device_context; + ID3D11Texture2D* rt; // swapchain backbuffer texture + ID3D11RenderTargetView* rtv; // view onto backbuffer (== resolve target when MSAA) + ID3D11Texture2D* msaa_rt; // MSAA color texture (only if sample_count>1) + ID3D11RenderTargetView* msaa_rtv; // view onto MSAA color (render target when MSAA) + ID3D11Texture2D* ds; // depth-stencil texture + ID3D11DepthStencilView* dsv; + DXGI_SWAP_CHAIN_DESC swap_chain_desc;// kept around; BufferCount reused on resize + IDXGISwapChain* swap_chain; + IDXGIDevice1* dxgi_device; // held only to set max frame latency / disable Alt-Enter +} _sapp_d3d11_t; +``` + +### `_sapp_win32_dpi_t` (2922–2927) +```c +typedef struct { + bool aware; // did we successfully set a DPI-aware mode + float content_scale; // framebuffer px per window pt (== window_scale if high_dpi else 1.0) + float window_scale; // OS DPI / 96.0 (physical px per logical pt) + float mouse_scale; // multiplier applied to raw WM_MOUSEMOVE coords → sapp coords +} _sapp_win32_dpi_t; +``` + +### `_sapp_win32_t` (2929–2961) +```c +typedef struct { + HWND hwnd; + HMONITOR hmonitor; + HDC dc; // GetDC(hwnd); CS_OWNDC so persistent + HICON big_icon, small_icon; + HCURSOR standard_cursors[_SAPP_MOUSECURSOR_NUM]; // LR_SHARED (global-ish) + HCURSOR custom_cursors[_SAPP_MOUSECURSOR_NUM]; + UINT orig_codepage; // to restore console CP on exit + WCHAR surrogate; // WM_CHAR high-surrogate accumulator + RECT stored_window_rect; // saved windowed pos/size for fullscreen→windowed restore + bool is_win10_or_greater; // gates FLIP_DISCARD + DXGI_PRESENT_DO_NOT_WAIT + bool in_create_window; // wndproc no-ops all messages while true (see §3) + bool iconified; + _sapp_win32_dpi_t dpi; + struct { // mouse-lock / capture / raw-input bookkeeping + struct { LONG pos_x, pos_y; bool pos_valid; } lock; + struct { LONG pos_x, pos_y; bool pos_valid; } raw_input; + bool requested_lock; + bool tracked; // TrackMouseEvent enter/leave state + uint8_t capture_mask; // SetCapture refcount by button bit + } mouse; + struct { size_t size; void* ptr; } raw_input_data; // grow-on-demand WM_INPUT buffer +} _sapp_win32_t; +``` + +Cross-platform `_sapp` fields the win32 path reads/writes are the same set as mac +(`desc`, `valid`, `first_frame`, `quit_requested`, `quit_ordered`, `fullscreen`, +`window_width/height`, `framebuffer_width/height`, `dpi_scale`, `sample_count`, +`swap_interval`, `timing`, `mouse.*`, `clipboard.*`, `drop.*`, `event`, `keycodes[]`, +`skip_present`, `custom_cursor_bound[]`) **plus** `window_title_wide[_SAPP_MAX_TITLE_LENGTH]` +(3326) — a UTF-16 title buffer that is win32-specific but declared cross-platform in the +main struct. + +`_sapp_init_state(desc)` runs first inside `_sapp_win32_run` (same as mac): applies +`_sapp_desc_defaults` (sample_count→1, swap_interval→1, clipboard_size→8192, +max_dropped_files→1, path_length→2048, window_title→"sokol"), sets `first_frame=true`, +`dpi_scale=1.0`, `mouse.shown=true`, `fullscreen=desc.fullscreen`, copies width/height +verbatim (**may be 0** — the win32 backend resolves 0 via `CW_USEDEFAULT`, §2). + +--- + +## 1. Entry / run loop (`_sapp_win32_run`, 10154–10227) + +### Entry points (10282–10304) +- Default entry is **`WinMain`** (10291). It calls `_sapp_win32_command_line_to_utf8_argv` + (`CommandLineToArgvW` + `WideCharToMultiByte` to UTF-8, 10229) then `sokol_main(argc,argv)` + → `_sapp_win32_run(&desc)` → frees the argv. +- `SOKOL_WIN32_FORCE_MAIN` compiles a plain `int main()` instead (10284). +- `SOKOL_NO_ENTRY` compiles out both; user calls `sapp_run(desc)` (13941) which dispatches + to `_sapp_win32_run` (13950). + +### Init order (10155–10176) — load-bearing, exact +1. `_sapp_init_state(desc)`. +2. `_sapp_win32_init_console()` (§ below). +3. `_sapp.win32.is_win10_or_greater = _sapp_win32_is_win10_or_greater()` (10157). Detection + is a hack: `GetProcAddress(kernel32, "GetSystemCpuSetInformation") != NULL` (10145). +4. `_sapp_win32_init_keytable()` — fill `_sapp.keycodes[]` (§4). +5. `_sapp_win32_utf8_to_wide(window_title → window_title_wide)` (10159) — pre-convert title + before window creation. +6. `_sapp_win32_init_dpi()` (§DPI below). +7. `_sapp_win32_init_cursors()` — load all standard cursors (§6). +8. `_sapp_win32_create_window()` (§2) — registers class, creates HWND, gets DC/monitor, + sizes, optionally goes fullscreen, `ShowWindow(SW_SHOW)`, `DragAcceptFiles`. +9. `sapp_set_icon(&desc->icon)` (10163) — sets window icon (§ icon). Unlike mac's dock-tile + decoration, on Win32 this is a **real titlebar/taskbar icon** via `WM_SETICON`, but still + optional for rendering. +10. `_sapp_d3d11_create_device_and_swapchain()` then `_sapp_d3d11_create_default_render_target()` + (§6-D3D11). +11. `_sapp.valid = true;` (10176). + +There is **no `applicationDidFinishLaunching` equivalent** — the run function does all setup +inline, then enters the loop. + +### DPI awareness (`_sapp_win32_init_dpi`, 9901–9984) — process-global +Dynamically loads `user32.dll` (`SetProcessDPIAware`, `SetProcessDpiAwarenessContext`) and +`shcore.dll` (`SetProcessDpiAwareness`, `GetDpiForMonitor`). Cascade (D3D11 path always +takes `init_dpi_awareness=true`; the `!high_dpi → PROCESS_DPI_UNAWARE` special-case at +9932–9940 is **`#if !defined(SOKOL_D3D11)` only**, i.e. GL/Vulkan — irrelevant to TrussC): +1. If `SetProcessDpiAwareness` present: set `dpi.aware=true`, **try + `SetProcessDpiAwarenessContext(PER_MONITOR_AWARE_V2)`** (the magic value + `(DPI_AWARENESS_CONTEXT)-4`); if that fails fall back to `SetProcessDpiAwareness(PROCESS_SYSTEM_DPI_AWARE)`. +2. Else if `SetProcessDPIAware` present (Win7): `dpi.aware=true`, call it. +3. Compute `window_scale`: if `dpi.aware && GetDpiForMonitor` present, query the monitor at + point `{1,1}` (`MDT_EFFECTIVE_DPI`) → `window_scale = dpix / 96.0`; else `window_scale=1.0`. +4. Derive `content_scale`/`mouse_scale` from `desc.high_dpi`: + - `high_dpi`: `content_scale = window_scale`, `mouse_scale = 1.0`. + - `!high_dpi`: `content_scale = 1.0`, `mouse_scale = 1.0 / window_scale` (mouse coords are + divided back down so app sees logical pixels). +5. `_sapp.dpi_scale = content_scale`. + +### WM_DPICHANGED (`_sapp_win32_dpi_changed`, 9486–9515) +Only delivered when PER_MONITOR_AWARE_V2 is active. No-op if `!dpi.aware`. Dynamically loads +`GetDpiForWindow`, recomputes `window_scale = GetDpiForWindow(hwnd)/96.0`, re-derives +content/mouse scale exactly as init, updates `_sapp.dpi_scale`, then **honors the OS-proposed +rect**: `SetWindowPos(hWnd, proposed_win_rect, SWP_NOZORDER|SWP_NOACTIVATE)`. Framebuffer +resize itself is picked up by the loop's `_sapp_win32_update_dimensions()` next tick, not here. + +### Console (`_sapp_win32_init_console` 9871 / `_restore_console` 9895) — process-global +Gated on `desc.win32.console_create` / `console_attach` / `console_utf8`: +- `console_attach` → `AttachConsole(ATTACH_PARENT_PROCESS)`; if that fails and + `console_create` → `AllocConsole()`. On success `freopen_s` redirects stdout+stderr to `"CON"`. +- `console_utf8` → save `orig_codepage = GetConsoleOutputCP()`, `SetConsoleOutputCP(CP_UTF8)`. +- Restore on exit sets the codepage back. + +### The main loop (10178–10208) +```c +bool done = false; +while (!(done || _sapp.quit_ordered)) { + _sapp_timing_update(&_sapp.timing, 0.0); // measure dt via QPC-based timer + MSG msg; + while (PeekMessageW(&msg, NULL, 0, 0, PM_REMOVE)) { // drain ALL pending messages + if (WM_QUIT == msg.message) { done = true; continue; } + TranslateMessage(&msg); + DispatchMessageW(&msg); + } + _sapp_win32_frame(false); // render + present ONE frame + if (_sapp_win32_update_dimensions()) { // resize detected post-drain + _sapp_d3d11_resize_default_render_target(); // ResizeBuffers + recreate RTV/DSV + _sapp_win32_app_event(SAPP_EVENTTYPE_RESIZED); + } + if (_sapp.quit_requested) { // user asked to quit this frame + PostMessage(_sapp.win32.hwnd, WM_CLOSE, 0, 0); // route through the WM_CLOSE dance + } + _sapp_win32_update_mouse_lock(); // reconcile requested vs actual lock +} +``` +Key structural facts, contrasted with the assumptions in the task: +- **PeekMessage-drain-then-render**, NOT `GetMessage` (blocking). It is a busy render loop: + drain the whole queue with `PM_REMOVE`, then render exactly one frame, then check resize, + quit, and mouse-lock. `frame_cb` runs inside `_sapp_win32_frame(false)` **once per outer + loop iteration**, not per message. +- **Resize is deliberately handled in the loop, NOT in `WM_SIZE`.** `WM_SIZE` only tracks + iconify/restore (§3). The comment (10192, and the commented-out block in `WM_TIMER` + 9761–9772) explains: calling `ResizeBuffers` every `WM_SIZE` "explodes memory usage," so + resize is coalesced to once per loop iteration via `_sapp_win32_update_dimensions()`. +- **Loop exit** happens two ways, both converging on `WM_QUIT`: + - `_sapp.quit_ordered` becomes true (loop condition) — set by the WM_CLOSE dance. + - `WM_QUIT` dequeued → `done=true`. `WM_QUIT` is produced by `PostQuitMessage(0)` which is + called from `WM_CLOSE` once quit is committed (§3). + So: user close / `sapp_quit()` → `WM_CLOSE` → (maybe QUIT_REQUESTED) → `PostQuitMessage` → + `WM_QUIT` dequeued → loop ends. + +### Cleanup order (10209–10226) — mirrors mac's guarantee +1. `_sapp_call_cleanup()` (user `cleanup_cb`, once) — **before** any GPU teardown. +2. `_sapp_d3d11_destroy_default_render_target()` then `_sapp_d3d11_destroy_device_and_swapchain()`. +3. `_sapp_win32_destroy_window()` (`DestroyWindow` + `UnregisterClassW`). +4. `_sapp_win32_destroy_icons()`, `_sapp_win32_restore_console()`, + `_sapp_win32_free_raw_input_data()`, `_sapp_discard_state()`. + +### Frame pacing (`_sapp_win32_frame`, 9548–9568) +Renders (`_sapp_frame()`), then on D3D11 `_sapp_d3d11_present(do_not_wait)` where +`do_not_wait = from_winproc` (true only from the WM_TIMER modal path, §3). When NOT called +from winproc and the window is iconic (`IsIconic`), it `Sleep(16 * swap_interval)` to avoid a +spin loop while minimized. There is **no vsync-blocking present abstraction beyond DXGI +`Present(sync_interval)`** — the loop otherwise runs as fast as Present allows. + +### Frame timing source +`_sapp.timing` is the platform-agnostic filtered timer (identical config to mac: +dt_min=1µs, dt_max=100ms, threshold=4ms, alpha=0.025, seed 1/60). It is fed by +`_sapp_timing_update(&_sapp.timing, 0.0)` at the top of every loop iteration (10180) **and** +inside `WM_TIMER` (9759). `external_now==0` means it samples `timestamp_now()` internally +(QPC-based `_sapp_timing`), i.e. **there is no CADisplayLink equivalent** — Win32 always uses +the measured EMA filter. `sapp_frame_duration()` → `_sapp_timing_get`, +`sapp_frame_duration_unfiltered()` → raw `t->dt`. + +--- + +## 2. Window creation (`_sapp_win32_create_window`, 9797–9853) + +### Window class (RegisterClassW, 9798–9805) — process-global, once +```c +WNDCLASSW wndclassw = {0}; +wndclassw.style = CS_HREDRAW | CS_VREDRAW | CS_OWNDC; // CS_OWNDC → persistent HDC +wndclassw.lpfnWndProc = _sapp_win32_wndproc; +wndclassw.hInstance = GetModuleHandleW(NULL); +wndclassw.hCursor = LoadCursor(NULL, IDC_ARROW); +wndclassw.hIcon = LoadIcon(NULL, IDI_WINLOGO); // default; real icon set later +wndclassw.lpszClassName = L"SOKOLAPP"; +RegisterClassW(&wndclassw); +``` + +### Style + initial size (9811–9835) +- `win_ex_style = WS_EX_APPWINDOW | WS_EX_WINDOWEDGE`. +- `win_style = WS_CLIPSIBLINGS | WS_CLIPCHILDREN | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX | + WS_MAXIMIZEBOX | WS_SIZEBOX` — **always resizable**; there is no non-resizable option and + fullscreen is not baked into the initial style (a windowed window is always created first, + hidden, then toggled to fullscreen — see below). +- Requested client size is DPI-scaled: `rect.right = window_width * dpi.window_scale`, + `rect.bottom = window_height * dpi.window_scale`, then `AdjustWindowRectEx(&rect, win_style, + FALSE, win_ex_style)` converts client→window rect. +- **`desc.width/height == 0` handling:** `use_default_width/height = (0 == window_width/height)`. + If set, `CreateWindowExW` is passed `CW_USEDEFAULT` for that dimension so the OS picks it + (NOT mac's 4/5-of-screen rule). Caveat noted in-source (9831): if width is `CW_USEDEFAULT`, + the height arg is ignored by Win32. + +### CreateWindowExW (9821–9836) +- `in_create_window = true` around the call so the wndproc **no-ops every message during + creation** (9571 guard) — creation-time messages (WM_CREATE etc.) are swallowed. +- Position: `X = CW_USEDEFAULT`, `Y = SW_HIDE` — note (9829) this is a quirk: the window is + created hidden. Class name `L"SOKOLAPP"`, title `window_title_wide`, `hInstance = + GetModuleHandle(NULL)`, no parent, no menu. +- After: `dc = GetDC(hwnd)` (persistent via CS_OWNDC), `hmonitor = MonitorFromWindow(..., + MONITOR_DEFAULTTONULL)`. + +### Show sequence (9846–9852) +1. `_sapp_win32_update_dimensions()` — sample real client rect into window/framebuffer size. +2. If `_sapp.fullscreen`: `_sapp_win32_set_fullscreen(true, SWP_HIDEWINDOW)` then + `_sapp_win32_update_dimensions()` again. +3. `ShowWindow(hwnd, SW_SHOW)`. +4. `DragAcceptFiles(hwnd, 1)` — drag&drop is **always** registered on the window (like mac), + independent of `desc.enable_dragndrop`; the *handler* (`_sapp_win32_files_dropped`) gates + on `drop.enabled`. + +### `_sapp_win32_update_dimensions` (9080–9111) — the size authority +`GetClientRect` → `window_width/height = (client px) / dpi.window_scale` (logical pts). If +both are 0 → **minimized**, return false (pretend no change, matches other backends, +sokol#1465). Else `framebuffer_width/height = round(window_width * dpi.content_scale)`; +returns **true only if the framebuffer size actually changed** (this is what drives +`ResizeBuffers` in the loop). On `GetClientRect` failure clamps everything to 1. + +--- + +## 3. WndProc (`_sapp_win32_wndproc`, 9570–9795) + +All handling is gated by `if (!_sapp.win32.in_create_window)` (9571); unhandled messages fall +through to `DefWindowProcW` (9794). Complete list of handled messages: + +- **WM_CLOSE (9573–9589) — the quit dance** (mirror of mac's `windowShouldClose:`): + ```c + if (!quit_ordered) { // sapp_quit() not already forced + quit_requested = true; + _sapp_win32_app_event(QUIT_REQUESTED); // user may sapp_cancel_quit() here + if (quit_requested) quit_ordered = true; // not cancelled → commit + } + if (quit_ordered) PostQuitMessage(0); + return 0; // never let DefWindowProc DestroyWindow us directly + ``` + Returns 0 (does not forward to DefWindowProc, so the window is NOT auto-destroyed here; + teardown happens after the loop exits). If cancelled, `PostQuitMessage` is skipped and the + app keeps running. +- **WM_SYSCOMMAND (9590–9603):** `switch (wParam & 0xFFF0)`: + - `SC_SCREENSAVE` / `SC_MONITORPOWER` → `return 0` **only if `_sapp.fullscreen`** (suppress + screensaver/monitor-blank in fullscreen; in windowed mode falls through to default). + - `SC_KEYMENU` → `return 0` (swallow the Alt-menu activation so Alt doesn't freeze rendering). +- **WM_ERASEBKGND (9604):** `return 1` (claim erased; prevents flicker, no GDI clear). +- **WM_SIZE (9606–9618):** tracks only iconify state. `iconified = (wParam == SIZE_MINIMIZED)`; + on change updates `win32.iconified` and fires `ICONIFIED` / `RESTORED`. **Does NOT fire + RESIZED and does NOT resize the swapchain** — that is the loop's job (§1). No RESTORED/ + MAXIMIZED distinction beyond minimize. +- **WM_SETFOCUS (9619) → FOCUSED**, **WM_KILLFOCUS (9622) → UNFOCUSED**. +- **WM_SETCURSOR (9625–9630):** if `LOWORD(lParam) == HTCLIENT`, call + `_sapp_win32_update_cursor(current_cursor, shown, skip_area_test=true)` and `return TRUE` + (§6); otherwise fall through (non-client area keeps system cursor). +- **WM_DPICHANGED (9631–9638):** `_sapp_win32_dpi_changed` (§1). +- **Mouse buttons (9639–9668):** each of L/R/M down/up runs `_sapp_win32_mouse_update(lParam)` + then `_sapp_win32_mouse_event(MOUSE_DOWN/UP, button)` then capture/release: + - **down** → `_sapp_win32_capture_mouse(1<GetBuffer(0)`, `rtv = CreateRenderTargetView(rt)` — view onto the + single-sampled backbuffer. +2. Common `D3D11_TEXTURE2D_DESC`: `Width/Height = framebuffer`, `MipLevels=1, ArraySize=1`, + `Usage=DEFAULT`, `BindFlags=RENDER_TARGET`, `SampleDesc.Count = sample_count`, + `Quality = (sample_count>1 ? D3D11_STANDARD_MULTISAMPLE_PATTERN : 0)`. +3. **If `sample_count > 1`:** `tex_desc.Format = DXGI_FORMAT_R10G10B10A2_UNORM` — **TrussC + 10-bit patch #2** (8717) — create `msaa_rt` + `msaa_rtv` (the actual render target; resolved + into `rt`/`rtv` at present by sokol_gfx). +4. Depth-stencil: `Format = DXGI_FORMAT_D24_UNORM_S8_UINT`, `BindFlags = DEPTH_STENCIL`, create + `ds` + `dsv`. (Depth is D24S8, NOT patched to 10-bit — only color is.) + +### Resize (`_sapp_d3d11_resize_default_render_target`, 8742–8748) +Guarded on `swap_chain` non-null: **destroy the render target first** +(`destroy_default_render_target` releases rt/rtv/msaa/ds/dsv), then +`swap_chain->ResizeBuffers(BufferCount, framebuffer_width, framebuffer_height, +DXGI_FORMAT_R10G10B10A2_UNORM, 0)` — **TrussC 10-bit patch #3** (8745) — then recreate the +render target. Ordering matters: all views onto the backbuffer must be released before +`ResizeBuffers` or it fails. Called from the loop after `_sapp_win32_update_dimensions()` +returns true (10195–10197), NOT from WM_SIZE. + +### Present + skip_present (`_sapp_d3d11_present`, 8750–8766) +```c +if (_sapp.skip_present) { _sapp.skip_present = false; return; } // TrussC patch, HONORED on D3D11 +UINT flags = 0; +if (is_win10_or_greater && do_not_wait) flags = DXGI_PRESENT_DO_NOT_WAIT; // modal-loop responsiveness +swap_chain->Present((UINT)_sapp.swap_interval, flags); // sync interval = swap_interval +``` +- **`skip_present` IS honored on Windows/D3D11** (unlike mac/Metal where it's ignored). It is a + one-shot: consumed (reset to false) each time. This is the TrussC event-driven present + suppression that fixes D3D11 flickering (patch #4, and the whole reason skip_present exists). +- `do_not_wait` is only true when presenting from `WM_TIMER` during a resize/move modal loop + (a NVIDIA/Win10 responsiveness workaround, 8758–8762). +- Sync interval comes straight from `_sapp.swap_interval` (1 = vsync on). + +### Win32 D3D11 vtable shims (8477–8586) +C++ vs C dispatch macros (`_sapp_d3d11_Release`, `_sapp_win32_refiid`) and thin inline wrappers +(`_sapp_dxgi_GetBuffer`, `_sapp_d3d11_CreateRenderTargetView/CreateTexture2D/ +CreateDepthStencilView`, `_sapp_dxgi_ResizeBuffers/Present/GetFrameStatistics/ +SetMaximumFrameLatency/GetAdapter/GetParent/MakeWindowAssociation`). TrussC builds as C++ so the +`->Method()` form is used. `_SAPP_SAFE_RELEASE` releases+nulls. + +--- + +## 7. sapp_get_environment / sapp_get_swapchain (D3D11) + +### `sapp_get_environment()` (14387–14415) — asserts `_sapp.valid` +- `defaults.color_format = sapp_color_format()` → **`SAPP_PIXELFORMAT_RGB10A2`** on D3D11 + (14042, `#elif defined(SOKOL_METAL) || defined(SOKOL_D3D11)` — **TrussC 10-bit patch**). +- `defaults.depth_format = SAPP_PIXELFORMAT_DEPTH_STENCIL` (14048), `sample_count`. +- `d3d11.device = _sapp.d3d11.device`, `d3d11.device_context = _sapp.d3d11.device_context` + (14401–14402). (`sapp_d3d11_environment` = {device, device_context}, 1894.) + +### `sapp_get_swapchain()` (14417–14485) — asserts `_sapp.valid` +D3D11 branch (14431–14441), asserts `rtv`: +- **`sample_count > 1`:** `render_view = msaa_rtv`, `resolve_view = rtv` (render into MSAA, + resolve into backbuffer). +- **`sample_count == 1`:** `render_view = rtv`, `resolve_view` left null. +- `depth_stencil_view = dsv`. +- Common tail: `width = sapp_width()`, `height = sapp_height()`, + `color_format = RGB10A2`, `depth_format = DEPTH_STENCIL`, `sample_count`. + +**Crucial contrast with Metal:** the D3D11 views are **created once at swapchain-create / +resize time and reused every frame** — `sapp_get_swapchain()` on D3D11 does NOT acquire or +rotate anything per frame (no `nextDrawable` equivalent). The FLIP_DISCARD swapchain rotates +its backbuffer internally; sokol keeps rendering into the same RTV. So the "must call exactly +once per frame" rule (documented at 1925) is a Metal/WGPU/Vulkan constraint, harmless on D3D11. + +### glue contract (`sokol_glue.h`) +`sglue_environment()` reads `env.defaults.{color,depth}_format/sample_count` plus +`env.d3d11.device` (167) and `env.d3d11.device_context` (168). `sglue_swapchain()` reads +`sc.width/height/sample_count/color_format/depth_format` plus `sc.d3d11.render_view` (190), +`sc.d3d11.resolve_view` (191), `sc.d3d11.depth_stencil_view` (192). **These are the only sapp +fields the shim must fill for the D3D11 render path** — metal/wgpu/vulkan/gl fields are +memset-0. Color format maps RGB10A2 → SG_PIXELFORMAT_RGB10A2 (same helper as mac). + +The declared getters `sapp_d3d11_get_device/device_context/render_view/resolve_view/ +depth_stencil_view` (docs 334–338) are **NOT implemented in this fork** — only +`sapp_d3d11_get_swap_chain` (14522) exists. The glue reads the environment/swapchain structs +directly, so the missing getters are not load-bearing. + +--- + +## 8. Quit flow (Win32) + +Public API is cross-platform and identical to mac: `sapp_request_quit()` sets `quit_requested`; +`sapp_cancel_quit()` clears it; `sapp_quit()` sets `quit_ordered` (unvetoable). The Win32-specific +plumbing: +1. From user code, `quit_requested` is noticed at the **bottom of the loop** (10203): + `PostMessage(hwnd, WM_CLOSE, 0, 0)` — routes even a programmatic quit through the same + `WM_CLOSE` dance so QUIT_REQUESTED still fires (for `sapp_request_quit`) or is skipped (for + `sapp_quit`, since `quit_ordered` is already true). +2. `WM_CLOSE` (§3) either fires QUIT_REQUESTED (user can cancel) or commits, then + `PostQuitMessage(0)`. +3. `WM_QUIT` is dequeued in the loop → `done = true` → loop exits → cleanup (§1) runs + `cleanup_cb` **before** D3D11 teardown (same ordering guarantee as mac). + +Note: `sapp_quit()` also makes the loop condition `!(done || quit_ordered)` false directly, so +even without the WM_CLOSE round-trip the loop would exit; the PostMessage path just ensures the +event semantics are consistent. + +--- + +## 9. Clipboard (process-global OS clipboard, per-window HWND owner) + +Enabled only if `desc.enable_clipboard` (allocates `clipboard.buffer`). Uses `hwnd` as the +clipboard owner. +- **`sapp_set_clipboard_string` → `_sapp_win32_set_clipboard_string` (9986–10029):** + `OpenClipboard(hwnd)`; `GlobalAlloc(GMEM_MOVEABLE, buf_size*sizeof(wchar_t))`; `GlobalLock`; + `_sapp_win32_utf8_to_wide`; `GlobalUnlock`; `EmptyClipboard()`; + `SetClipboardData(CF_UNICODETEXT, object)` (**clipboard takes ownership of the global on + success**); `CloseClipboard`. Full goto-based error cleanup. +- **`sapp_get_clipboard_string` → `_sapp_win32_get_clipboard_string` (10031–10056):** + `OpenClipboard(hwnd)`; `GetClipboardData(CF_UNICODETEXT)`; `GlobalLock`; + `_sapp_win32_wide_to_utf8` into `clipboard.buffer` (`CLIPBOARD_STRING_TOO_BIG` error if it + overflows); returns the internal buffer. On any failure silently returns the current buffer. +- **CLIPBOARD_PASTED trigger:** unlike mac (which detects Cmd+V in `keyDown:`), Win32 detects + it inside `_sapp_win32_key_event` (9455–9463): on `KEY_DOWN` with `modifiers == SAPP_MODIFIER_CTRL` + (exact match) and `key_code == SAPP_KEYCODE_V`, fire `CLIPBOARD_PASTED` (reusing the already- + populated event). There is **no WM_PASTE handling** — it's key-detection based, same idea as mac + but with Ctrl instead of Super, and Win32 does not pre-populate the buffer (app calls + `sapp_get_clipboard_string()` in its handler). + +--- + +## 10. Mouse cursor + +### Standard cursors (`_sapp_win32_init_cursor` 9151 / `_init_cursors` 9182) — global-ish (LR_SHARED) +Per-enum `LoadImageW(NULL, MAKEINTRESOURCEW(OCR_*), IMAGE_CURSOR, 0,0, LR_DEFAULTSIZE|LR_SHARED)`. +OCR ids inlined (OEMRESOURCE may not be defined): ARROW→32512, IBEAM→32513, CROSSHAIR→32515, +POINTING_HAND→32649, RESIZE_EW→32644, RESIZE_NS→32645, RESIZE_NWSE→32642, RESIZE_NESW→32643, +RESIZE_ALL→32646, NOT_ALLOWED→32648. Fallback to `LoadCursorW(IDC_ARROW=32512)` if load fails. + +### Show/hide + apply (`_sapp_win32_update_cursor`, 9203–9224) +Called with `(cursor, shown, skip_area_test)`. Unless `skip_area_test`, it first checks +`_sapp_win32_cursor_in_content_area()` (GetCursorPos + WindowFromPoint==hwnd + PtInRect on the +client rect, 9188) and bails if the pointer isn't over the client area. Then: if `shown`, pick +`custom_cursors[cursor]` (when `custom_cursor_bound[cursor]`) else `standard_cursors[cursor]`; +if `!shown`, `cursor_handle = NULL`. `SetCursor(cursor_handle)` — **NULL hides the cursor**. +Driven from **WM_SETCURSOR** (skip_area_test=true, HTCLIENT only) and from the public +`sapp_show_mouse`/cursor-type setters (skip_area_test=false, 14120). Note: hide is via +`SetCursor(NULL)` per WM_SETCURSOR, NOT the `ShowCursor` counter — the `ShowCursor(FALSE/TRUE)` +counter is used **only** by the mouse-lock path (9281/9330), avoiding the counter-stacking bug. + +### Custom cursors (`_sapp_win32_make_custom_mouse_cursor` 10259 / `_destroy` 10267) +`make` = `_sapp_win32_create_icon_from_image(desc, is_cursor=true)` (shared with icon creation, +10063) → an HCURSOR via `CreateIconIndirect` with the hotspot. `destroy` = `DestroyIcon` +(warns `WIN32_DESTROYICON_FOR_CURSOR_FAILED` on failure but doesn't hard-fail). Public +`sapp_bind/unbind_mouse_cursor_image` route here (14188/14218). + +--- + +## 11. Fullscreen (`_sapp_win32_set_fullscreen`, 9113–9145) + +`sapp_is_fullscreen()` returns `_sapp.fullscreen`. `sapp_toggle_fullscreen()` → +`_sapp_win32_toggle_fullscreen()` → `_sapp_win32_set_fullscreen(!fullscreen, SWP_SHOWWINDOW)`. +The set function does a **style-swap + reposition** (no OS "fullscreen mode"): +- Monitor rect via `MonitorFromWindow(MONITOR_DEFAULTTONEAREST)` + `GetMonitorInfo`. +- **→ fullscreen:** save current `GetWindowRect` into `stored_window_rect`; style becomes + `WS_POPUP | WS_SYSMENU | WS_VISIBLE`; target rect = full monitor rect, adjusted via + `AdjustWindowRectEx`. +- **→ windowed:** style restored to `WS_CLIPSIBLINGS|WS_CLIPCHILDREN|WS_CAPTION|WS_SYSMENU| + WS_MINIMIZEBOX|WS_MAXIMIZEBOX|WS_SIZEBOX`; rect = `stored_window_rect`. +- Apply: `SetWindowLongPtr(GWL_STYLE)` then `SetWindowPos(HWND_TOP, ..., swp_flags | + SWP_FRAMECHANGED)`. +- `_sapp.fullscreen` is set at the top (9126) — single source of truth (no separate + notification correction like mac's `windowDidEnter/ExitFullScreen`). At startup, fullscreen + is entered from `create_window` with `SWP_HIDEWINDOW` (window built windowed+hidden first, + 9848). This is **borderless-fullscreen-window**, not exclusive DXGI fullscreen (which is + explicitly disabled via `DXGI_MWA_NO_ALT_ENTER`). + +--- + +## 12. Misc + +- **`sapp_set_window_title` → `_sapp_win32_update_window_title` (10058–10061):** convert + `window_title` → `window_title_wide` (`_sapp_win32_utf8_to_wide`), `SetWindowTextW`. +- **UTF-8 ↔ UTF-16 helpers:** `_sapp_win32_utf8_to_wide` (8328, `MultiByteToWideChar(CP_UTF8)`, + returns false if it won't fit) and `_sapp_win32_wide_to_utf8` (9067, + `WideCharToMultiByte(CP_UTF8)`). Used for title, clipboard, dropped-file paths, argv. WM_CHAR + surrogate accumulation is separate (§3, `win32.surrogate`). +- **`sapp_win32_get_hwnd` (14531):** returns `_sapp.win32.hwnd` (asserts valid). +- **`sapp_high_dpi` (14056):** `desc.high_dpi && dpi_scale >= 1.5` (same as mac). +- **`sapp_show_keyboard`:** Win32 no-op (`onscreen_keyboard_shown` stays false). +- **Icon (`_sapp_win32_set_icon`, 10115–10138):** best-match big+small images → + `_sapp_win32_create_icon_from_image` (BITMAPV5 top-down BGRA DIB + mono mask → + `CreateIconIndirect`) → `WM_SETICON ICON_BIG/ICON_SMALL`. Destroys prior icons. Optional for + rendering; a real titlebar/taskbar icon (unlike mac's dock-tile-only). Destroyed at exit. +- **`iconified` tracking:** `win32.iconified` set in WM_SIZE (§3); `IsIconic(hwnd)` used in + `_sapp_win32_frame` to throttle while minimized. +- **TrussC patches in the win32/D3D11 path** (all cross-reference TRUSSC_MODIFICATIONS.md): + 1. Swapchain `BufferDesc.Format = R10G10B10A2_UNORM` (8592). + 2. MSAA texture `Format = R10G10B10A2_UNORM` (8717). + 3. `ResizeBuffers(... R10G10B10A2_UNORM ...)` (8745). + 4. `sapp_color_format()` returns `SAPP_PIXELFORMAT_RGB10A2` on D3D11 (14042). + 5. **`skip_present` honored in `_sapp_d3d11_present`** (8752, the big one — event-driven + present suppression to fix D3D11 flicker; one-shot consume). No such patch exists/works on + Metal. + `SAPP_PIXELFORMAT_RGB10A2` enum itself is a TrussC addition (1865). + +--- + +## Contradictions / surprises vs the task's stated assumptions + +See the summary at the top of the final report. + +--- + +## Appendix: function → line index (D3D11-relevant) + +| function | line | +|---|---| +| `_sapp_win32_utf8_to_wide` | 8328 | +| `_sapp_win32_app_event` | 8342 | +| `_sapp_win32_init_keytable` | 8349 | +| d3d11 vtable shims | 8477–8586 | +| `_sapp_d3d11_create_device_and_swapchain` | 8588 | +| `_sapp_d3d11_destroy_device_and_swapchain` | 8681 | +| `_sapp_d3d11_create_default_render_target` | 8688 | +| `_sapp_d3d11_destroy_default_render_target` | 8733 | +| `_sapp_d3d11_resize_default_render_target` | 8742 | +| `_sapp_d3d11_present` | 8750 | +| `_sapp_win32_wide_to_utf8` | 9067 | +| `_sapp_win32_update_dimensions` | 9080 | +| `_sapp_win32_set_fullscreen` / `_toggle_fullscreen` | 9113 / 9147 | +| `_sapp_win32_init_cursor` / `_init_cursors` | 9151 / 9182 | +| `_sapp_win32_cursor_in_content_area` | 9188 | +| `_sapp_win32_update_cursor` | 9203 | +| `_sapp_win32_capture_mouse` / `_release_mouse` | 9226 / 9233 | +| `_sapp_win32_lock_mouse` / `_do_lock` / `_do_unlock` / `_update_mouse_lock` | 9246 / 9276 / 9326 / 9353 | +| `_sapp_win32_mods` | 9387 | +| `_sapp_win32_mouse_update` | 9414 | +| `_sapp_win32_mouse_event` / `_scroll_event` / `_key_event` / `_char_event` | 9429 / 9438 / 9448 / 9467 | +| `_sapp_win32_dpi_changed` | 9486 | +| `_sapp_win32_files_dropped` | 9517 | +| `_sapp_win32_frame` | 9548 | +| `_sapp_win32_wndproc` | 9570 | +| `_sapp_win32_create_window` / `_destroy_window` | 9797 / 9855 | +| `_sapp_win32_init_console` / `_restore_console` | 9871 / 9895 | +| `_sapp_win32_init_dpi` | 9901 | +| `_sapp_win32_set/get_clipboard_string` | 9986 / 10031 | +| `_sapp_win32_update_window_title` | 10058 | +| `_sapp_win32_create_icon_from_image` / `_set_icon` | 10063 / 10115 | +| `_sapp_win32_is_win10_or_greater` | 10145 | +| `_sapp_win32_run` | 10154 | +| `_sapp_win32_command_line_to_utf8_argv` | 10229 | +| `_sapp_win32_make/destroy_custom_mouse_cursor` | 10259 / 10267 | +| `main` / `WinMain` | 10284 / 10291 | +| `sapp_color_format` | 14018 | +| `sapp_get_environment` (d3d11 fields) | 14400 | +| `sapp_get_swapchain` (d3d11 fields) | 14431 | +| `sapp_d3d11_get_swap_chain` | 14522 | +| `sapp_win32_get_hwnd` | 14531 | From 30da93d16c4139d190217080863ef7b3c4f62120 Mon Sep 17 00:00:00 2001 From: tettou771 Date: Thu, 9 Jul 2026 17:32:20 +0900 Subject: [PATCH 18/87] feat: sokol_app_tc.h implements the sapp_* API on Windows (P1: win32/D3D11 driver) - win32/D3D11 section in sokol_app_tc.h: main window = window #0 on the same wndproc/tick machinery as secondary windows; sapp_* shims mirror the mac P0b strategy (sokol_app.h included declarations-only on win, TrussC.h untouched) - per-window DXGI frame latency waitable ticks replace the upstream PeekMessage busy-render-loop (and the old WM_TIMER ~64Hz cap): the message loop blocks in MsgWaitForMultipleObjectsEx, each window ticks at its own display's rate; skipped presents hold a waitable credit and are timer-re-paced (no busy-spin, no waitable starvation) - flip-discard swapchains (main RGB10A2, secondary BGRA8), waitable flag carried through every ResizeBuffers, RTV-unbind+Flush before resize, MSAA via separate render target + resolve view - scancode-indexed keytable lifted from sokol_app.h, WM_CHAR with surrogate pairs, Ctrl+V CLIPBOARD_PASTED, refcounted mouse capture, borderless fullscreen, per-monitor-v2 DPI, clipboard/dnd/cursors, skip_present honored on Present (TrussC flicker patch) - WM_ENTERSIZEMOVE arms a WM_TIMER that keeps all windows ticking through modal drags; resize coalesced per tick (never in WM_SIZE) - NEW core/platform/win/tcWindowWin.cpp: TrussC adapter mirroring tcWindowMac.mm; tcWindow.h platform gates widened to macos,windows --- core/include/sokol/sokol_app_tc.h | 2019 ++++++++++++++++++++++++++++- core/include/tc/app/tcWindow.h | 23 +- core/platform/win/sokol_impl.cpp | 13 +- core/platform/win/tcWindowWin.cpp | 327 +++++ docs/dev/sokol_app_tc-design.md | 23 + 5 files changed, 2375 insertions(+), 30 deletions(-) create mode 100644 core/platform/win/tcWindowWin.cpp diff --git a/core/include/sokol/sokol_app_tc.h b/core/include/sokol/sokol_app_tc.h index 26e7615a..9978abf7 100644 --- a/core/include/sokol/sokol_app_tc.h +++ b/core/include/sokol/sokol_app_tc.h @@ -18,26 +18,39 @@ WHAT IT IS ========== - Native multi-window support with per-window vsync ticks. On macOS this - header additionally IMPLEMENTS the public single-window sokol_app.h API - (sapp_run, sapp_width, sapp_dpi_scale, ...): the "main window" is simply - window #0, created from the sapp_desc, and the app's OS run loop is owned - here. sokol_app.h must then be included WITHOUT its implementation - (declarations only) in the implementation TU: + Native multi-window support with per-window vsync ticks. On macOS and + Windows this header additionally IMPLEMENTS the public single-window + sokol_app.h API (sapp_run, sapp_width, sapp_dpi_scale, ...): the "main + window" is simply window #0, created from the sapp_desc, and the app's + OS run loop is owned here. sokol_app.h must then be included WITHOUT its + implementation (declarations only) in the implementation TU: #include "sokol_app.h" // no SOKOL_IMPL / SOKOL_APP_IMPL here #define SOKOL_IMPL #define SOKOL_APP_TC_IMPL - #include "sokol_app_tc.h" // implements the sapp_* API on macOS + #include "sokol_app_tc.h" // implements the sapp_* API #include "sokol_gfx.h" ... - The single-window API behaves like sokol_app.h's macOS backend (quit - flow via windowShouldClose, lazy drawable acquisition in - sapp_get_swapchain(), RGB10A2 default color format, clipboard, drag&drop, - cursor control, fullscreen), with one deliberate deviation: calling - sapp_dpi_scale() before sapp_run() returns the main screen's backing - scale factor instead of 0. + The single-window API behaves like sokol_app.h's native backends (the + cancellable quit dance, RGB10A2 default color format, clipboard, + drag&drop, cursor control, fullscreen; macOS: lazy drawable acquisition + in sapp_get_swapchain(); Windows: skip_present honored on Present, + per-monitor-v2 DPI), with one deliberate deviation: calling + sapp_dpi_scale() before sapp_run() returns the OS display scale + instead of 0. + + What replaces sokol_app.h's Windows render loop is the same per-window + tick idea: instead of the upstream PeekMessage busy-render-loop (and + instead of a ~64Hz WM_TIMER), every window's DXGI swapchain is created + with DXGI_SWAP_CHAIN_FLAG_FRAME_LATENCY_WAITABLE_OBJECT and the message + loop blocks in MsgWaitForMultipleObjectsEx on the per-window frame + latency waitable objects: each window ticks when ITS swapchain can + accept a frame (true vsync pacing per display), and the loop sleeps + otherwise. A tick that ends without a Present (skip_present, occluded, + minimized) holds its waitable "credit" and is re-paced by a timer until + the next real Present, so neither busy-spinning nor waitable starvation + can occur. The multi-window API shape follows floooh's multi-window sketch (sokol PR #437 / issue #229): a small `sapp_window` handle, a `sapp_window_desc`, @@ -83,9 +96,10 @@ PLATFORM SUPPORT ================ macOS: implemented (NSWindow + CAMetalLayer + CADisplayLink, macOS 14+). + Windows: implemented (HWND + D3D11 + DXGI flip swapchain, one frame + latency waitable object per window; Windows 10+). Others: stubs that return an invalid handle (explicit platform gap); - Windows (HWND + DXGI flip swapchain + waitable-object ticks) and Linux - (X11 + shared GL) are the next ports. + Linux (X11 + shared GL) is the next port. */ #define SOKOL_APP_TC_INCLUDED (1) #include @@ -110,7 +124,8 @@ typedef struct sapp_window_desc { bool borderless; /* no title bar / decorations */ bool no_high_dpi; /* true: framebuffer = logical size (dpi_scale 1) */ int sample_count; /* MSAA sample count (0 = 1) */ - const void* mtl_device; /* macOS: the id sokol_gfx renders with */ + const void* mtl_device; /* macOS: the id sokol_gfx renders with + (Windows: unused; the shared D3D11 device is owned here) */ /* callbacks -- all invoked on the main thread */ void (*tick_cb)(sapp_window win, void* user); /* this window's vsync; only while visible */ @@ -142,8 +157,17 @@ SOKOL_APP_API_DECL const void* sapp_window_metal_current_drawable(sapp_window wi SOKOL_APP_API_DECL const void* sapp_window_metal_depth_stencil_texture(sapp_window win); SOKOL_APP_API_DECL const void* sapp_window_metal_msaa_color_texture(sapp_window win); -/* native escape hatch (macOS: NSWindow*) */ +/* D3D11 swapchain handles -- valid ONLY inside this window's tick_cb + (render_view is the MSAA target and resolve_view the backbuffer when + sample_count > 1; otherwise render_view is the backbuffer and + resolve_view is 0 -- same contract as sapp_swapchain.d3d11) */ +SOKOL_APP_API_DECL const void* sapp_window_d3d11_render_view(sapp_window win); +SOKOL_APP_API_DECL const void* sapp_window_d3d11_resolve_view(sapp_window win); +SOKOL_APP_API_DECL const void* sapp_window_d3d11_depth_stencil_view(sapp_window win); + +/* native escape hatches (macOS: NSWindow*; Windows: HWND) */ SOKOL_APP_API_DECL const void* sapp_window_macos_get_window(sapp_window win); +SOKOL_APP_API_DECL const void* sapp_window_win32_get_hwnd(sapp_window win); #if defined(__cplusplus) } /* extern "C" */ @@ -1353,6 +1377,12 @@ const void* sapp_window_macos_get_window(sapp_window win) { return w ? (__bridge const void*)w->window : 0; } +/* D3D11 / Win32 handles do not exist on macOS */ +const void* sapp_window_d3d11_render_view(sapp_window win) { (void)win; return 0; } +const void* sapp_window_d3d11_resolve_view(sapp_window win) { (void)win; return 0; } +const void* sapp_window_d3d11_depth_stencil_view(sapp_window win) { (void)win; return 0; } +const void* sapp_window_win32_get_hwnd(sapp_window win) { (void)win; return 0; } + /*-- sokol_app.h public API (macOS implementation lives here) ---------------*/ void sapp_run(const sapp_desc* desc) { @@ -1677,8 +1707,1955 @@ sapp_swapchain sapp_get_swapchain(void) { } /* extern "C" */ -#else /* !macOS */ -/*== stubs (explicit platform gap; Windows/Linux ports replace these) =======*/ +#elif defined(_WIN32) +/*== Windows (D3D11) ======================================================== + Implements the public sokol_app.h API on Windows plus the multi-window + API. Structure mirrors sokol_app.h's win32/D3D11 backend (quit dance in + WM_CLOSE, resize coalesced outside WM_SIZE, scancode-indexed keytable, + borderless fullscreen, refcounted mouse capture, TrussC RGB10A2 + + skip_present patches) but replaces the PeekMessage busy-render-loop: + every swapchain is created with FRAME_LATENCY_WAITABLE_OBJECT and the + loop blocks in MsgWaitForMultipleObjectsEx on the per-window waitables, + so each window ticks at its own display's rate and the process sleeps + when nothing is due. */ +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#include /* GET_X_LPARAM / GET_Y_LPARAM */ +#include /* DragAcceptFiles / DragQueryFileW */ +#include +#include /* IDXGIFactory2 / IDXGISwapChain2 (frame latency waitable) */ + +#if defined(_MSC_VER) +#pragma comment (lib, "kernel32") +#pragma comment (lib, "user32") +#pragma comment (lib, "shell32") +#pragma comment (lib, "gdi32") +#pragma comment (lib, "dxgi") +#pragma comment (lib, "d3d11") +#endif + +#if defined(SOKOL_APP_IMPL_INCLUDED) +#error "sokol_app_tc.h owns the Windows implementation of the sapp_* API; include sokol_app.h WITHOUT an implementation define in this TU" +#endif + +#include /* freopen_s (console redirection) */ + +#ifndef WM_MOUSEHWHEEL +#define WM_MOUSEHWHEEL (0x020E) +#endif +#ifndef WM_DPICHANGED +#define WM_DPICHANGED (0x02E0) +#endif + +#define _SAPP_TC_MAX_WINDOWS (32) +#define _SAPP_TC_MODAL_TIMER (1) +#define _SAPP_TC_RELEASE(obj) do { if (obj) { (obj)->Release(); (obj) = 0; } } while (0) + +/* EMA-filtered frame timer, port of sokol_app.h's _sapp_timing_* (QPC-based; + Windows has no display-link timestamps, so this is the only dt source) */ +typedef struct { + double last; /* previous sample time, 0 = none yet */ + double ema; + double smooth_dt; + double dt; /* last raw (clamped) delta */ +} _sapp_tc_timing_t; + +typedef struct _sapp_tc_window_t { + uint32_t win_id; + sapp_window_desc desc; + bool is_main; /* window #0: created by sapp_run, drives the app callbacks */ + bool ready; /* creation finished; wndproc may deliver events */ + HWND hwnd; + HMONITOR hmonitor; + DXGI_FORMAT color_fmt; + UINT swapchain_flags; /* MUST be passed unchanged to every ResizeBuffers */ + IDXGISwapChain1* swap_chain; + HANDLE frame_wait; /* frame latency waitable (NULL: timer-paced fallback) */ + bool credit_held; /* a waitable credit was consumed without a Present yet */ + ID3D11Texture2D* rt; /* swapchain backbuffer */ + ID3D11RenderTargetView* rtv; + ID3D11Texture2D* msaa_rt; /* MSAA color (sample_count > 1; flip model can't MSAA the backbuffer) */ + ID3D11RenderTargetView* msaa_rtv; + ID3D11Texture2D* ds; /* depth-stencil */ + ID3D11DepthStencilView* dsv; + int fb_width, fb_height; /* framebuffer pixels */ + int win_width, win_height; /* logical points */ + float window_scale; /* OS DPI / 96 (physical px per point) */ + float dpi_scale; /* framebuffer px per point (content scale) */ + float mouse_scale; /* client px -> event coordinate (framebuffer px) */ + double refresh_period; /* this window's display refresh period (pacing fallback) */ + double earliest_next; /* QPC seconds; window is not due before this */ + double frame_duration; + _sapp_tc_timing_t timing; + bool occluded; + bool iconified; + bool in_tick; + bool mouse_tracked; /* TrackMouseEvent enter/leave state */ + bool mouse_pos_valid; + float mouse_x, mouse_y; /* last position in event coordinates */ + float mouse_dx, mouse_dy; + uint8_t capture_mask; /* SetCapture refcount by button bit */ + WCHAR surrogate; /* WM_CHAR high-surrogate accumulator */ + RECT stored_window_rect; /* windowed rect for fullscreen restore */ +} _sapp_tc_window_t; + +static struct { + bool keytable_valid; + sapp_keycode keycodes[SAPP_MAX_KEYCODES]; + uint32_t next_id; + _sapp_tc_window_t* windows[_SAPP_TC_MAX_WINDOWS]; + bool wndclass_registered; + double qpc_period; /* seconds per QueryPerformanceCounter tick */ + /* dynamically loaded DPI functions (user32.dll, Win10 1607+) */ + BOOL (WINAPI* SetProcessDpiAwarenessContext)(void*); + UINT (WINAPI* GetDpiForWindow)(HWND); + BOOL (WINAPI* AdjustWindowRectExForDpi)(LPRECT, DWORD, BOOL, DWORD, UINT); + + /* ---- app / main-window state (this header owns sapp_run on Windows) -- */ + struct { + sapp_desc desc; /* sanitized copy; window_title points at the buffer below */ + char window_title[256]; + WCHAR window_title_wide[256]; + bool valid; + bool init_called; + bool cleanup_called; + bool quit_requested; + bool quit_ordered; + bool fullscreen; + bool event_consumed; + bool skip_present; /* one-shot; honored on D3D11 (TrussC flicker patch) */ + bool dpi_aware; + uint64_t frame_count; + _sapp_tc_window_t* main; + ID3D11Device* device; + ID3D11DeviceContext* device_context; + /* mouse cursor */ + bool mouse_shown; + sapp_mouse_cursor current_cursor; + HCURSOR standard_cursors[_SAPP_MOUSECURSOR_NUM]; + HCURSOR custom_cursors[_SAPP_MOUSECURSOR_NUM]; + bool custom_cursor_bound[_SAPP_MOUSECURSOR_NUM]; + /* console */ + bool console_utf8_set; + UINT orig_codepage; + /* clipboard */ + bool clipboard_enabled; + int clipboard_size; + char* clipboard; + /* drag & drop */ + bool drop_enabled; + int drop_max_files; + int drop_max_path_length; + int drop_num_files; + char* drop_buffer; + } app; +} _sapp_tc; + +static void _sapp_tc_win32_tick(_sapp_tc_window_t* w, bool from_modal); +static bool _sapp_tc_win32_update_dimensions(_sapp_tc_window_t* w); +static void _sapp_tc_d3d11_resize(_sapp_tc_window_t* w); +static void _sapp_tc_win32_apply_cursor(sapp_mouse_cursor cursor, bool shown, bool skip_area_test); + +/*-- timing -----------------------------------------------------------------*/ +static double _sapp_tc_now(void) { + LARGE_INTEGER qpc; + QueryPerformanceCounter(&qpc); + return (double)qpc.QuadPart * _sapp_tc.qpc_period; +} + +static void _sapp_tc_timing_reset(_sapp_tc_timing_t* t) { + t->last = 0.0; + t->ema = t->smooth_dt = t->dt = 1.0 / 60.0; +} + +static double _sapp_tc_timing_clamp(double dt) { + if (dt < 0.000001) return 0.000001; + if (dt > 0.1) return 0.1; + return dt; +} + +static void _sapp_tc_timing_update(_sapp_tc_timing_t* t) { + const double now = _sapp_tc_now(); + if (t->last > 0.0) { + double dt = _sapp_tc_timing_clamp(now - t->last); + t->dt = dt; + /* big jump: reset the filter; otherwise EMA-smooth */ + if (fabs(dt - t->smooth_dt) > 0.004) { + t->ema = t->smooth_dt = dt; + } else { + t->ema += 0.025 * (dt - t->ema); + t->smooth_dt = _sapp_tc_timing_clamp(t->ema); + } + } + t->last = now; +} + +/* keycode table lifted verbatim from sokol_app.h's _sapp_win32_init_keytable(); + indexed by the 9-bit Win32 scancode (HIWORD(lParam) & 0x1FF -- the 0x100 + extended bit distinguishes e.g. left/right Ctrl and keypad Enter) */ +static void _sapp_tc_init_keytable(void) { + if (_sapp_tc.keytable_valid) return; + _sapp_tc.keytable_valid = true; + _sapp_tc.keycodes[0x00B] = SAPP_KEYCODE_0; + _sapp_tc.keycodes[0x002] = SAPP_KEYCODE_1; + _sapp_tc.keycodes[0x003] = SAPP_KEYCODE_2; + _sapp_tc.keycodes[0x004] = SAPP_KEYCODE_3; + _sapp_tc.keycodes[0x005] = SAPP_KEYCODE_4; + _sapp_tc.keycodes[0x006] = SAPP_KEYCODE_5; + _sapp_tc.keycodes[0x007] = SAPP_KEYCODE_6; + _sapp_tc.keycodes[0x008] = SAPP_KEYCODE_7; + _sapp_tc.keycodes[0x009] = SAPP_KEYCODE_8; + _sapp_tc.keycodes[0x00A] = SAPP_KEYCODE_9; + _sapp_tc.keycodes[0x01E] = SAPP_KEYCODE_A; + _sapp_tc.keycodes[0x030] = SAPP_KEYCODE_B; + _sapp_tc.keycodes[0x02E] = SAPP_KEYCODE_C; + _sapp_tc.keycodes[0x020] = SAPP_KEYCODE_D; + _sapp_tc.keycodes[0x012] = SAPP_KEYCODE_E; + _sapp_tc.keycodes[0x021] = SAPP_KEYCODE_F; + _sapp_tc.keycodes[0x022] = SAPP_KEYCODE_G; + _sapp_tc.keycodes[0x023] = SAPP_KEYCODE_H; + _sapp_tc.keycodes[0x017] = SAPP_KEYCODE_I; + _sapp_tc.keycodes[0x024] = SAPP_KEYCODE_J; + _sapp_tc.keycodes[0x025] = SAPP_KEYCODE_K; + _sapp_tc.keycodes[0x026] = SAPP_KEYCODE_L; + _sapp_tc.keycodes[0x032] = SAPP_KEYCODE_M; + _sapp_tc.keycodes[0x031] = SAPP_KEYCODE_N; + _sapp_tc.keycodes[0x018] = SAPP_KEYCODE_O; + _sapp_tc.keycodes[0x019] = SAPP_KEYCODE_P; + _sapp_tc.keycodes[0x010] = SAPP_KEYCODE_Q; + _sapp_tc.keycodes[0x013] = SAPP_KEYCODE_R; + _sapp_tc.keycodes[0x01F] = SAPP_KEYCODE_S; + _sapp_tc.keycodes[0x014] = SAPP_KEYCODE_T; + _sapp_tc.keycodes[0x016] = SAPP_KEYCODE_U; + _sapp_tc.keycodes[0x02F] = SAPP_KEYCODE_V; + _sapp_tc.keycodes[0x011] = SAPP_KEYCODE_W; + _sapp_tc.keycodes[0x02D] = SAPP_KEYCODE_X; + _sapp_tc.keycodes[0x015] = SAPP_KEYCODE_Y; + _sapp_tc.keycodes[0x02C] = SAPP_KEYCODE_Z; + _sapp_tc.keycodes[0x028] = SAPP_KEYCODE_APOSTROPHE; + _sapp_tc.keycodes[0x02B] = SAPP_KEYCODE_BACKSLASH; + _sapp_tc.keycodes[0x033] = SAPP_KEYCODE_COMMA; + _sapp_tc.keycodes[0x00D] = SAPP_KEYCODE_EQUAL; + _sapp_tc.keycodes[0x029] = SAPP_KEYCODE_GRAVE_ACCENT; + _sapp_tc.keycodes[0x01A] = SAPP_KEYCODE_LEFT_BRACKET; + _sapp_tc.keycodes[0x00C] = SAPP_KEYCODE_MINUS; + _sapp_tc.keycodes[0x034] = SAPP_KEYCODE_PERIOD; + _sapp_tc.keycodes[0x01B] = SAPP_KEYCODE_RIGHT_BRACKET; + _sapp_tc.keycodes[0x027] = SAPP_KEYCODE_SEMICOLON; + _sapp_tc.keycodes[0x035] = SAPP_KEYCODE_SLASH; + _sapp_tc.keycodes[0x056] = SAPP_KEYCODE_WORLD_2; + _sapp_tc.keycodes[0x00E] = SAPP_KEYCODE_BACKSPACE; + _sapp_tc.keycodes[0x153] = SAPP_KEYCODE_DELETE; + _sapp_tc.keycodes[0x14F] = SAPP_KEYCODE_END; + _sapp_tc.keycodes[0x01C] = SAPP_KEYCODE_ENTER; + _sapp_tc.keycodes[0x001] = SAPP_KEYCODE_ESCAPE; + _sapp_tc.keycodes[0x147] = SAPP_KEYCODE_HOME; + _sapp_tc.keycodes[0x152] = SAPP_KEYCODE_INSERT; + _sapp_tc.keycodes[0x15D] = SAPP_KEYCODE_MENU; + _sapp_tc.keycodes[0x151] = SAPP_KEYCODE_PAGE_DOWN; + _sapp_tc.keycodes[0x149] = SAPP_KEYCODE_PAGE_UP; + _sapp_tc.keycodes[0x045] = SAPP_KEYCODE_PAUSE; + _sapp_tc.keycodes[0x146] = SAPP_KEYCODE_PAUSE; + _sapp_tc.keycodes[0x039] = SAPP_KEYCODE_SPACE; + _sapp_tc.keycodes[0x00F] = SAPP_KEYCODE_TAB; + _sapp_tc.keycodes[0x03A] = SAPP_KEYCODE_CAPS_LOCK; + _sapp_tc.keycodes[0x145] = SAPP_KEYCODE_NUM_LOCK; + _sapp_tc.keycodes[0x046] = SAPP_KEYCODE_SCROLL_LOCK; + _sapp_tc.keycodes[0x03B] = SAPP_KEYCODE_F1; + _sapp_tc.keycodes[0x03C] = SAPP_KEYCODE_F2; + _sapp_tc.keycodes[0x03D] = SAPP_KEYCODE_F3; + _sapp_tc.keycodes[0x03E] = SAPP_KEYCODE_F4; + _sapp_tc.keycodes[0x03F] = SAPP_KEYCODE_F5; + _sapp_tc.keycodes[0x040] = SAPP_KEYCODE_F6; + _sapp_tc.keycodes[0x041] = SAPP_KEYCODE_F7; + _sapp_tc.keycodes[0x042] = SAPP_KEYCODE_F8; + _sapp_tc.keycodes[0x043] = SAPP_KEYCODE_F9; + _sapp_tc.keycodes[0x044] = SAPP_KEYCODE_F10; + _sapp_tc.keycodes[0x057] = SAPP_KEYCODE_F11; + _sapp_tc.keycodes[0x058] = SAPP_KEYCODE_F12; + _sapp_tc.keycodes[0x064] = SAPP_KEYCODE_F13; + _sapp_tc.keycodes[0x065] = SAPP_KEYCODE_F14; + _sapp_tc.keycodes[0x066] = SAPP_KEYCODE_F15; + _sapp_tc.keycodes[0x067] = SAPP_KEYCODE_F16; + _sapp_tc.keycodes[0x068] = SAPP_KEYCODE_F17; + _sapp_tc.keycodes[0x069] = SAPP_KEYCODE_F18; + _sapp_tc.keycodes[0x06A] = SAPP_KEYCODE_F19; + _sapp_tc.keycodes[0x06B] = SAPP_KEYCODE_F20; + _sapp_tc.keycodes[0x06C] = SAPP_KEYCODE_F21; + _sapp_tc.keycodes[0x06D] = SAPP_KEYCODE_F22; + _sapp_tc.keycodes[0x06E] = SAPP_KEYCODE_F23; + _sapp_tc.keycodes[0x076] = SAPP_KEYCODE_F24; + _sapp_tc.keycodes[0x038] = SAPP_KEYCODE_LEFT_ALT; + _sapp_tc.keycodes[0x01D] = SAPP_KEYCODE_LEFT_CONTROL; + _sapp_tc.keycodes[0x02A] = SAPP_KEYCODE_LEFT_SHIFT; + _sapp_tc.keycodes[0x15B] = SAPP_KEYCODE_LEFT_SUPER; + _sapp_tc.keycodes[0x137] = SAPP_KEYCODE_PRINT_SCREEN; + _sapp_tc.keycodes[0x138] = SAPP_KEYCODE_RIGHT_ALT; + _sapp_tc.keycodes[0x11D] = SAPP_KEYCODE_RIGHT_CONTROL; + _sapp_tc.keycodes[0x036] = SAPP_KEYCODE_RIGHT_SHIFT; + _sapp_tc.keycodes[0x136] = SAPP_KEYCODE_RIGHT_SHIFT; + _sapp_tc.keycodes[0x15C] = SAPP_KEYCODE_RIGHT_SUPER; + _sapp_tc.keycodes[0x150] = SAPP_KEYCODE_DOWN; + _sapp_tc.keycodes[0x14B] = SAPP_KEYCODE_LEFT; + _sapp_tc.keycodes[0x14D] = SAPP_KEYCODE_RIGHT; + _sapp_tc.keycodes[0x148] = SAPP_KEYCODE_UP; + _sapp_tc.keycodes[0x052] = SAPP_KEYCODE_KP_0; + _sapp_tc.keycodes[0x04F] = SAPP_KEYCODE_KP_1; + _sapp_tc.keycodes[0x050] = SAPP_KEYCODE_KP_2; + _sapp_tc.keycodes[0x051] = SAPP_KEYCODE_KP_3; + _sapp_tc.keycodes[0x04B] = SAPP_KEYCODE_KP_4; + _sapp_tc.keycodes[0x04C] = SAPP_KEYCODE_KP_5; + _sapp_tc.keycodes[0x04D] = SAPP_KEYCODE_KP_6; + _sapp_tc.keycodes[0x047] = SAPP_KEYCODE_KP_7; + _sapp_tc.keycodes[0x048] = SAPP_KEYCODE_KP_8; + _sapp_tc.keycodes[0x049] = SAPP_KEYCODE_KP_9; + _sapp_tc.keycodes[0x04E] = SAPP_KEYCODE_KP_ADD; + _sapp_tc.keycodes[0x053] = SAPP_KEYCODE_KP_DECIMAL; + _sapp_tc.keycodes[0x135] = SAPP_KEYCODE_KP_DIVIDE; + _sapp_tc.keycodes[0x11C] = SAPP_KEYCODE_KP_ENTER; + _sapp_tc.keycodes[0x037] = SAPP_KEYCODE_KP_MULTIPLY; + _sapp_tc.keycodes[0x04A] = SAPP_KEYCODE_KP_SUBTRACT; +} + +static sapp_keycode _sapp_tc_translate_key(int scancode) { + if ((scancode >= 0) && (scancode < SAPP_MAX_KEYCODES)) { + return _sapp_tc.keycodes[scancode]; + } + return SAPP_KEYCODE_INVALID; +} + +static _sapp_tc_window_t* _sapp_tc_lookup(sapp_window win) { + if (win.id == 0) return 0; + for (int i = 0; i < _SAPP_TC_MAX_WINDOWS; i++) { + _sapp_tc_window_t* w = _sapp_tc.windows[i]; + if (w && w->win_id == win.id) return w; + } + return 0; +} + +/*-- UTF-8 <-> UTF-16 -------------------------------------------------------*/ +static bool _sapp_tc_win32_utf8_to_wide(const char* src, wchar_t* dst, int dst_num) { + dst[0] = 0; + if (0 == MultiByteToWideChar(CP_UTF8, 0, src, -1, dst, dst_num)) { + dst[dst_num - 1] = 0; + return false; + } + return true; +} + +static bool _sapp_tc_win32_wide_to_utf8(const wchar_t* src, char* dst, int dst_size) { + dst[0] = 0; + if (0 == WideCharToMultiByte(CP_UTF8, 0, src, -1, dst, dst_size, NULL, NULL)) { + dst[dst_size - 1] = 0; + return false; + } + return true; +} + +/*-- event plumbing ---------------------------------------------------------*/ +static uint32_t _sapp_tc_win32_mods(void) { + uint32_t m = 0; + if (GetKeyState(VK_SHIFT) & 0x8000) m |= SAPP_MODIFIER_SHIFT; + if (GetKeyState(VK_CONTROL) & 0x8000) m |= SAPP_MODIFIER_CTRL; + if (GetKeyState(VK_MENU) & 0x8000) m |= SAPP_MODIFIER_ALT; + if ((GetKeyState(VK_LWIN) | GetKeyState(VK_RWIN)) & 0x8000) m |= SAPP_MODIFIER_SUPER; + const bool swapped = (0 != GetSystemMetrics(SM_SWAPBUTTON)); + if (GetAsyncKeyState(VK_LBUTTON) & 0x8000) m |= swapped ? SAPP_MODIFIER_RMB : SAPP_MODIFIER_LMB; + if (GetAsyncKeyState(VK_RBUTTON) & 0x8000) m |= swapped ? SAPP_MODIFIER_LMB : SAPP_MODIFIER_RMB; + if (GetAsyncKeyState(VK_MBUTTON) & 0x8000) m |= SAPP_MODIFIER_MMB; + return m; +} + +static void _sapp_tc_send(_sapp_tc_window_t* w, sapp_event* ev) { + if (!w->desc.event_cb) return; + ev->frame_count = _sapp_tc.app.frame_count; + ev->window_width = w->win_width; + ev->window_height = w->win_height; + ev->framebuffer_width = w->fb_width; + ev->framebuffer_height = w->fb_height; + sapp_window handle = { w->win_id }; + w->desc.event_cb(ev, handle, w->desc.user_data); +} + +/*-- main window: event routing to the sapp_desc callbacks ------------------*/ +static bool _sapp_tc_app_events_enabled(void) { + /* same gate as sokol_app.h: nothing fires before the first tick's init_cb */ + return _sapp_tc.app.init_called && + (_sapp_tc.app.desc.event_cb || _sapp_tc.app.desc.event_userdata_cb); +} + +static void _sapp_tc_main_event_tramp(const sapp_event* ev, sapp_window win, void* user) { + (void)win; (void)user; + if (!_sapp_tc_app_events_enabled()) return; + _sapp_tc.app.event_consumed = false; + if (_sapp_tc.app.desc.event_cb) { + _sapp_tc.app.desc.event_cb(ev); + } else { + _sapp_tc.app.desc.event_userdata_cb(ev, _sapp_tc.app.desc.user_data); + } +} + +/*-- mouse cursor (app-global, same model as sokol_app.h) -------------------*/ +static void _sapp_tc_win32_init_cursor(sapp_mouse_cursor cursor, DWORD ocr_id) { + HCURSOR c = (HCURSOR)LoadImageW(NULL, MAKEINTRESOURCEW(ocr_id), IMAGE_CURSOR, + 0, 0, LR_DEFAULTSIZE | LR_SHARED); + if (!c) c = LoadCursor(NULL, IDC_ARROW); + _sapp_tc.app.standard_cursors[cursor] = c; +} + +static void _sapp_tc_win32_init_cursors(void) { + /* OCR_* resource ids inlined (OEMRESOURCE may not be defined) */ + _sapp_tc_win32_init_cursor(SAPP_MOUSECURSOR_DEFAULT, 32512); /* OCR_NORMAL */ + _sapp_tc_win32_init_cursor(SAPP_MOUSECURSOR_ARROW, 32512); /* OCR_NORMAL */ + _sapp_tc_win32_init_cursor(SAPP_MOUSECURSOR_IBEAM, 32513); /* OCR_IBEAM */ + _sapp_tc_win32_init_cursor(SAPP_MOUSECURSOR_CROSSHAIR, 32515); /* OCR_CROSS */ + _sapp_tc_win32_init_cursor(SAPP_MOUSECURSOR_POINTING_HAND, 32649); /* OCR_HAND */ + _sapp_tc_win32_init_cursor(SAPP_MOUSECURSOR_RESIZE_EW, 32644); /* OCR_SIZEWE */ + _sapp_tc_win32_init_cursor(SAPP_MOUSECURSOR_RESIZE_NS, 32645); /* OCR_SIZENS */ + _sapp_tc_win32_init_cursor(SAPP_MOUSECURSOR_RESIZE_NWSE, 32642); /* OCR_SIZENWSE */ + _sapp_tc_win32_init_cursor(SAPP_MOUSECURSOR_RESIZE_NESW, 32643); /* OCR_SIZENESW */ + _sapp_tc_win32_init_cursor(SAPP_MOUSECURSOR_RESIZE_ALL, 32646); /* OCR_SIZEALL */ + _sapp_tc_win32_init_cursor(SAPP_MOUSECURSOR_NOT_ALLOWED, 32648); /* OCR_NO */ +} + +static bool _sapp_tc_win32_cursor_in_content_area(void) { + POINT pos; + if (!GetCursorPos(&pos)) return false; + HWND hovered = WindowFromPoint(pos); + for (int i = 0; i < _SAPP_TC_MAX_WINDOWS; i++) { + _sapp_tc_window_t* w = _sapp_tc.windows[i]; + if (!w || w->hwnd != hovered) continue; + RECT area; + if (!GetClientRect(w->hwnd, &area)) return false; + ScreenToClient(w->hwnd, &pos); + return PtInRect(&area, pos) == TRUE; + } + return false; +} + +static void _sapp_tc_win32_apply_cursor(sapp_mouse_cursor cursor, bool shown, bool skip_area_test) { + _sapp_tc.app.current_cursor = cursor; + _sapp_tc.app.mouse_shown = shown; + /* WM_SETCURSOR passes skip_area_test=true (it fires for HTCLIENT only); + the public setters check that the pointer is actually over one of our + client areas before touching the global cursor */ + if (!skip_area_test && !_sapp_tc_win32_cursor_in_content_area()) return; + HCURSOR c = NULL; /* SetCursor(NULL) hides (avoids the ShowCursor counter) */ + if (shown) { + if (_sapp_tc.app.custom_cursor_bound[cursor] && _sapp_tc.app.custom_cursors[cursor]) { + c = _sapp_tc.app.custom_cursors[cursor]; + } else { + c = _sapp_tc.app.standard_cursors[cursor]; + } + if (!c) c = LoadCursor(NULL, IDC_ARROW); + } + SetCursor(c); +} + +static HCURSOR _sapp_tc_win32_create_cursor(const sapp_image_desc* desc) { + BITMAPV5HEADER bi; + memset(&bi, 0, sizeof(bi)); + bi.bV5Size = sizeof(bi); + bi.bV5Width = (LONG)desc->width; + bi.bV5Height = -(LONG)desc->height; /* top-down */ + bi.bV5Planes = 1; + bi.bV5BitCount = 32; + bi.bV5Compression = BI_BITFIELDS; + bi.bV5RedMask = 0x00FF0000; + bi.bV5GreenMask = 0x0000FF00; + bi.bV5BlueMask = 0x000000FF; + bi.bV5AlphaMask = 0xFF000000; + uint8_t* target = 0; + HDC dc = GetDC(NULL); + HBITMAP color = CreateDIBSection(dc, (BITMAPINFO*)&bi, DIB_RGB_COLORS, + (void**)&target, NULL, 0); + ReleaseDC(NULL, dc); + if (!color) return NULL; + HBITMAP mask = CreateBitmap(desc->width, desc->height, 1, 1, NULL); + if (!mask) { DeleteObject(color); return NULL; } + /* RGBA -> BGRA */ + const uint8_t* src = (const uint8_t*)desc->pixels.ptr; + const int num_pixels = desc->width * desc->height; + for (int i = 0; i < num_pixels; i++) { + target[i * 4 + 0] = src[i * 4 + 2]; + target[i * 4 + 1] = src[i * 4 + 1]; + target[i * 4 + 2] = src[i * 4 + 0]; + target[i * 4 + 3] = src[i * 4 + 3]; + } + ICONINFO ii; + memset(&ii, 0, sizeof(ii)); + ii.fIcon = FALSE; /* cursor, not icon (hotspot applies) */ + ii.xHotspot = (DWORD)desc->cursor_hotspot_x; + ii.yHotspot = (DWORD)desc->cursor_hotspot_y; + ii.hbmMask = mask; + ii.hbmColor = color; + HCURSOR c = (HCURSOR)CreateIconIndirect(&ii); + DeleteObject(color); + DeleteObject(mask); + return c; +} + +/*-- DPI / display ----------------------------------------------------------*/ +static void _sapp_tc_win32_init_dpi(void) { + HMODULE user32 = GetModuleHandleW(L"user32.dll"); + if (user32) { + _sapp_tc.SetProcessDpiAwarenessContext = + (BOOL(WINAPI*)(void*))(void*)GetProcAddress(user32, "SetProcessDpiAwarenessContext"); + _sapp_tc.GetDpiForWindow = + (UINT(WINAPI*)(HWND))(void*)GetProcAddress(user32, "GetDpiForWindow"); + _sapp_tc.AdjustWindowRectExForDpi = + (BOOL(WINAPI*)(LPRECT, DWORD, BOOL, DWORD, UINT))(void*)GetProcAddress(user32, "AdjustWindowRectExForDpi"); + } + /* D3D11 backend is always DPI-aware; PER_MONITOR_AWARE_V2 when available + (Win10 1703+; the -4 magic constant avoids requiring a newer SDK) */ + if (_sapp_tc.SetProcessDpiAwarenessContext) { + _sapp_tc.SetProcessDpiAwarenessContext((void*)(intptr_t)-4); + _sapp_tc.app.dpi_aware = true; + } else { + _sapp_tc.app.dpi_aware = (TRUE == SetProcessDPIAware()); + } +} + +/* one dpi ratio source per window: the scale used to size the framebuffer is + the scale reported by sapp_window_dpi_scale() and applied to mouse coords */ +static void _sapp_tc_win32_update_scale(_sapp_tc_window_t* w) { + UINT dpi = 96; + if (_sapp_tc.app.dpi_aware && _sapp_tc.GetDpiForWindow && w->hwnd) { + dpi = _sapp_tc.GetDpiForWindow(w->hwnd); + if (dpi == 0) dpi = 96; + } + w->window_scale = (float)dpi / 96.0f; + const bool high_dpi = !w->desc.no_high_dpi; + w->dpi_scale = high_dpi ? w->window_scale : 1.0f; + w->mouse_scale = high_dpi ? 1.0f : 1.0f / w->window_scale; +} + +static void _sapp_tc_win32_update_refresh(_sapp_tc_window_t* w) { + /* this window's display refresh period: the timer-pacing fallback for + ticks that end without a Present (not the primary pacing source -- + that is the swapchain's frame latency waitable) */ + w->refresh_period = 1.0 / 60.0; + if (!w->hmonitor) return; + MONITORINFOEXW mi; + memset(&mi, 0, sizeof(mi)); + mi.cbSize = sizeof(mi); + if (GetMonitorInfoW(w->hmonitor, (MONITORINFO*)&mi)) { + DEVMODEW dm; + memset(&dm, 0, sizeof(dm)); + dm.dmSize = sizeof(dm); + if (EnumDisplaySettingsW(mi.szDevice, ENUM_CURRENT_SETTINGS, &dm) && + (dm.dmDisplayFrequency > 1)) { + w->refresh_period = 1.0 / (double)dm.dmDisplayFrequency; + } + } +} + +/*-- console (process-global) -----------------------------------------------*/ +static void _sapp_tc_win32_init_console(void) { + const sapp_desc* d = &_sapp_tc.app.desc; + if (d->win32.console_create || d->win32.console_attach) { + BOOL con_valid = FALSE; + if (d->win32.console_attach) { + con_valid = AttachConsole(ATTACH_PARENT_PROCESS); + } + if (!con_valid && d->win32.console_create) { + con_valid = AllocConsole(); + } + if (con_valid) { + FILE* res_o = NULL; + FILE* res_e = NULL; + freopen_s(&res_o, "CON", "w", stdout); + freopen_s(&res_e, "CON", "w", stderr); + } + } + if (d->win32.console_utf8) { + _sapp_tc.app.orig_codepage = GetConsoleOutputCP(); + if (SetConsoleOutputCP(CP_UTF8)) { + _sapp_tc.app.console_utf8_set = true; + } + } +} + +static void _sapp_tc_win32_restore_console(void) { + if (_sapp_tc.app.console_utf8_set) { + SetConsoleOutputCP(_sapp_tc.app.orig_codepage); + } +} + +/*-- D3D11 / DXGI -----------------------------------------------------------*/ +static HRESULT _sapp_tc_d3d11_try_create_device(UINT flags) { + D3D_FEATURE_LEVEL levels[] = { D3D_FEATURE_LEVEL_11_1, D3D_FEATURE_LEVEL_11_0 }; + D3D_FEATURE_LEVEL out_level; + HRESULT hr = D3D11CreateDevice(NULL, D3D_DRIVER_TYPE_HARDWARE, NULL, flags, + levels, 2, D3D11_SDK_VERSION, + &_sapp_tc.app.device, &out_level, &_sapp_tc.app.device_context); + if (FAILED(hr)) { + /* 11.1-unaware runtimes report E_INVALIDARG for the 11_1 entry */ + hr = D3D11CreateDevice(NULL, D3D_DRIVER_TYPE_HARDWARE, NULL, flags, + &levels[1], 1, D3D11_SDK_VERSION, + &_sapp_tc.app.device, &out_level, &_sapp_tc.app.device_context); + } + return hr; +} + +static bool _sapp_tc_d3d11_create_device(void) { + UINT flags = D3D11_CREATE_DEVICE_SINGLETHREADED | D3D11_CREATE_DEVICE_BGRA_SUPPORT; +#if defined(SOKOL_DEBUG) + flags |= D3D11_CREATE_DEVICE_DEBUG; +#endif + HRESULT hr = _sapp_tc_d3d11_try_create_device(flags); +#if defined(SOKOL_DEBUG) + if (FAILED(hr)) { + /* debug layer not installed: retry without it (upstream behavior) */ + hr = _sapp_tc_d3d11_try_create_device(flags & ~(UINT)D3D11_CREATE_DEVICE_DEBUG); + } +#endif + return SUCCEEDED(hr); +} + +static void _sapp_tc_d3d11_destroy_render_targets(_sapp_tc_window_t* w) { + _SAPP_TC_RELEASE(w->msaa_rtv); + _SAPP_TC_RELEASE(w->msaa_rt); + _SAPP_TC_RELEASE(w->dsv); + _SAPP_TC_RELEASE(w->ds); + _SAPP_TC_RELEASE(w->rtv); + _SAPP_TC_RELEASE(w->rt); +} + +static bool _sapp_tc_d3d11_create_render_targets(_sapp_tc_window_t* w) { + ID3D11Device* dev = _sapp_tc.app.device; + if (!dev || !w->swap_chain) return false; + if (FAILED(w->swap_chain->GetBuffer(0, __uuidof(ID3D11Texture2D), (void**)&w->rt))) return false; + if (FAILED(dev->CreateRenderTargetView((ID3D11Resource*)w->rt, NULL, &w->rtv))) return false; + D3D11_TEXTURE2D_DESC td; + memset(&td, 0, sizeof(td)); + td.Width = (UINT)(w->fb_width > 0 ? w->fb_width : 1); + td.Height = (UINT)(w->fb_height > 0 ? w->fb_height : 1); + td.MipLevels = 1; + td.ArraySize = 1; + td.Usage = D3D11_USAGE_DEFAULT; + td.SampleDesc.Count = (UINT)w->desc.sample_count; + td.SampleDesc.Quality = (w->desc.sample_count > 1) ? (UINT)D3D11_STANDARD_MULTISAMPLE_PATTERN : 0; + if (w->desc.sample_count > 1) { + /* flip-model swapchains are always single-sampled; render into a + separate MSAA texture, sokol_gfx resolves into the backbuffer */ + td.Format = w->color_fmt; + td.BindFlags = D3D11_BIND_RENDER_TARGET; + if (FAILED(dev->CreateTexture2D(&td, NULL, &w->msaa_rt))) return false; + if (FAILED(dev->CreateRenderTargetView((ID3D11Resource*)w->msaa_rt, NULL, &w->msaa_rtv))) return false; + } + td.Format = DXGI_FORMAT_D24_UNORM_S8_UINT; + td.BindFlags = D3D11_BIND_DEPTH_STENCIL; + if (FAILED(dev->CreateTexture2D(&td, NULL, &w->ds))) return false; + if (FAILED(dev->CreateDepthStencilView((ID3D11Resource*)w->ds, NULL, &w->dsv))) return false; + return true; +} + +static void _sapp_tc_d3d11_resize(_sapp_tc_window_t* w) { + if (!w->swap_chain) return; + _sapp_tc_d3d11_destroy_render_targets(w); + /* flip model: EVERY backbuffer reference must be gone before + ResizeBuffers, including an RTV still bound on the shared immediate + context -- otherwise ResizeBuffers fails and the next Present crashes */ + if (_sapp_tc.app.device_context) { + _sapp_tc.app.device_context->OMSetRenderTargets(0, NULL, NULL); + _sapp_tc.app.device_context->Flush(); + } + /* the creation flags (frame latency waitable!) must be passed unchanged + on every resize, or the waitable handle is silently invalidated */ + w->swap_chain->ResizeBuffers(2, (UINT)w->fb_width, (UINT)w->fb_height, + w->color_fmt, w->swapchain_flags); + _sapp_tc_d3d11_create_render_targets(w); +} + +static bool _sapp_tc_d3d11_create_swapchain(_sapp_tc_window_t* w) { + IDXGIDevice1* dxgi_device = 0; + IDXGIAdapter* adapter = 0; + IDXGIFactory2* factory = 0; + if (FAILED(_sapp_tc.app.device->QueryInterface(__uuidof(IDXGIDevice1), (void**)&dxgi_device))) { + return false; + } + bool ok = false; + if (SUCCEEDED(dxgi_device->GetAdapter(&adapter)) && + SUCCEEDED(adapter->GetParent(__uuidof(IDXGIFactory2), (void**)&factory))) { + DXGI_SWAP_CHAIN_DESC1 d; + memset(&d, 0, sizeof(d)); + d.Width = (UINT)(w->fb_width > 0 ? w->fb_width : 1); + d.Height = (UINT)(w->fb_height > 0 ? w->fb_height : 1); + d.Format = w->color_fmt; + d.SampleDesc.Count = 1; /* MSAA lives in a separate render target */ + d.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT; + d.BufferCount = 2; + d.SwapEffect = DXGI_SWAP_EFFECT_FLIP_DISCARD; + d.Scaling = DXGI_SCALING_STRETCH; + d.AlphaMode = DXGI_ALPHA_MODE_IGNORE; + d.Flags = DXGI_SWAP_CHAIN_FLAG_FRAME_LATENCY_WAITABLE_OBJECT; + HRESULT hr = factory->CreateSwapChainForHwnd((IUnknown*)_sapp_tc.app.device, + w->hwnd, &d, NULL, NULL, &w->swap_chain); + if (FAILED(hr)) { + /* no waitable support: plain flip swapchain, timer-paced ticks */ + d.Flags = 0; + hr = factory->CreateSwapChainForHwnd((IUnknown*)_sapp_tc.app.device, + w->hwnd, &d, NULL, NULL, &w->swap_chain); + } + if (SUCCEEDED(hr)) { + w->swapchain_flags = d.Flags; + /* fullscreen is driven here (borderless); kill DXGI's Alt-Enter */ + factory->MakeWindowAssociation(w->hwnd, DXGI_MWA_NO_ALT_ENTER | DXGI_MWA_NO_PRINT_SCREEN); + if (d.Flags & DXGI_SWAP_CHAIN_FLAG_FRAME_LATENCY_WAITABLE_OBJECT) { + IDXGISwapChain2* sc2 = 0; + if (SUCCEEDED(w->swap_chain->QueryInterface(__uuidof(IDXGISwapChain2), (void**)&sc2))) { + sc2->SetMaximumFrameLatency(1); + w->frame_wait = sc2->GetFrameLatencyWaitableObject(); + sc2->Release(); + } + } + ok = _sapp_tc_d3d11_create_render_targets(w); + } + } + _SAPP_TC_RELEASE(factory); + _SAPP_TC_RELEASE(adapter); + _SAPP_TC_RELEASE(dxgi_device); + return ok; +} + +/* re-sample client size + per-monitor DPI; true when the framebuffer size + changed (drives ResizeBuffers). A 0x0 client area means minimized: report + "no change" and keep the old sizes (sokol_app.h parity, sokol#1465). */ +static bool _sapp_tc_win32_update_dimensions(_sapp_tc_window_t* w) { + RECT rect; + if (!GetClientRect(w->hwnd, &rect)) return false; + const int cw = (int)(rect.right - rect.left); + const int ch = (int)(rect.bottom - rect.top); + if ((cw == 0) || (ch == 0)) return false; + HMONITOR mon = MonitorFromWindow(w->hwnd, MONITOR_DEFAULTTONEAREST); + if (mon != w->hmonitor) { + w->hmonitor = mon; + _sapp_tc_win32_update_refresh(w); + } + _sapp_tc_win32_update_scale(w); + w->win_width = (int)((float)cw / w->window_scale + 0.5f); + w->win_height = (int)((float)ch / w->window_scale + 0.5f); + const bool high_dpi = !w->desc.no_high_dpi; + int fbw = high_dpi ? cw : w->win_width; + int fbh = high_dpi ? ch : w->win_height; + if (fbw <= 0) fbw = 1; + if (fbh <= 0) fbh = 1; + if ((fbw != w->fb_width) || (fbh != w->fb_height)) { + w->fb_width = fbw; + w->fb_height = fbh; + return true; + } + return false; +} + +/*-- input event helpers ----------------------------------------------------*/ +static void _sapp_tc_win32_mouse_update(_sapp_tc_window_t* w, LPARAM lParam) { + /* event coordinates are framebuffer pixels (sokol_app.h convention): + client px when high-dpi, logical points otherwise */ + const float x = (float)GET_X_LPARAM(lParam) * w->mouse_scale; + const float y = (float)GET_Y_LPARAM(lParam) * w->mouse_scale; + if (w->mouse_pos_valid) { + w->mouse_dx = x - w->mouse_x; + w->mouse_dy = y - w->mouse_y; + } else { + w->mouse_dx = 0.0f; + w->mouse_dy = 0.0f; + w->mouse_pos_valid = true; + } + w->mouse_x = x; + w->mouse_y = y; +} + +static void _sapp_tc_win32_mouse_event(_sapp_tc_window_t* w, sapp_event_type type, sapp_mousebutton btn) { + sapp_event e; + memset(&e, 0, sizeof(e)); + e.type = type; + e.mouse_button = btn; + e.mouse_x = w->mouse_x; + e.mouse_y = w->mouse_y; + e.mouse_dx = w->mouse_dx; + e.mouse_dy = w->mouse_dy; + e.modifiers = _sapp_tc_win32_mods(); + _sapp_tc_send(w, &e); +} + +static void _sapp_tc_win32_scroll_event(_sapp_tc_window_t* w, float x, float y) { + sapp_event e; + memset(&e, 0, sizeof(e)); + e.type = SAPP_EVENTTYPE_MOUSE_SCROLL; + e.mouse_x = w->mouse_x; + e.mouse_y = w->mouse_y; + e.scroll_x = x; + e.scroll_y = y; + e.modifiers = _sapp_tc_win32_mods(); + _sapp_tc_send(w, &e); +} + +static void _sapp_tc_win32_key_event(_sapp_tc_window_t* w, sapp_event_type type, int scancode, bool repeat) { + sapp_event e; + memset(&e, 0, sizeof(e)); + e.type = type; + e.key_code = _sapp_tc_translate_key(scancode); + e.key_repeat = repeat; + e.modifiers = _sapp_tc_win32_mods(); + _sapp_tc_send(w, &e); + /* Ctrl+V paste notification (exact-CTRL match, sokol_app.h parity); the + app reads the clipboard in its handler */ + if ((type == SAPP_EVENTTYPE_KEY_DOWN) && _sapp_tc.app.clipboard_enabled && + (e.modifiers == SAPP_MODIFIER_CTRL) && (e.key_code == SAPP_KEYCODE_V)) { + sapp_event pe; + memset(&pe, 0, sizeof(pe)); + pe.type = SAPP_EVENTTYPE_CLIPBOARD_PASTED; + pe.modifiers = SAPP_MODIFIER_CTRL; + _sapp_tc_send(w, &pe); + } +} + +static void _sapp_tc_win32_char_event(_sapp_tc_window_t* w, uint32_t c, bool repeat) { + /* UTF-16 surrogate pair accumulation (per window) */ + if ((c >= 0xD800) && (c <= 0xDBFF)) { + w->surrogate = (WCHAR)c; + return; + } + if ((c >= 0xDC00) && (c <= 0xDFFF)) { + if (!w->surrogate) return; + c = ((((uint32_t)w->surrogate - 0xD800) << 10) | (c - 0xDC00)) + 0x10000; + w->surrogate = 0; + } + if ((c < 32) || (c == 127)) return; /* control characters */ + sapp_event e; + memset(&e, 0, sizeof(e)); + e.type = SAPP_EVENTTYPE_CHAR; + e.char_code = c; + e.key_repeat = repeat; + e.modifiers = _sapp_tc_win32_mods(); + _sapp_tc_send(w, &e); +} + +static void _sapp_tc_win32_app_event(_sapp_tc_window_t* w, sapp_event_type type) { + sapp_event e; + memset(&e, 0, sizeof(e)); + e.type = type; + _sapp_tc_send(w, &e); +} + +/* refcounted capture across buttons: dragging with two buttons held must not + drop capture when the first one is released */ +static void _sapp_tc_win32_capture_mouse(_sapp_tc_window_t* w, uint8_t btn_mask) { + if (w->capture_mask == 0) { + SetCapture(w->hwnd); + } + w->capture_mask |= btn_mask; +} + +static void _sapp_tc_win32_release_mouse(_sapp_tc_window_t* w, uint8_t btn_mask) { + if (w->capture_mask != 0) { + w->capture_mask &= (uint8_t)~btn_mask; + if (w->capture_mask == 0) { + ReleaseCapture(); + } + } +} + +static void _sapp_tc_win32_files_dropped(_sapp_tc_window_t* w, HDROP hdrop) { + if (!_sapp_tc.app.drop_enabled || !_sapp_tc.app.drop_buffer) { + DragFinish(hdrop); + return; + } + const int max_files = _sapp_tc.app.drop_max_files; + const int max_path = _sapp_tc.app.drop_max_path_length; + memset(_sapp_tc.app.drop_buffer, 0, (size_t)max_files * (size_t)max_path); + _sapp_tc.app.drop_num_files = 0; + int num = (int)DragQueryFileW(hdrop, 0xFFFFFFFF, NULL, 0); + if (num > max_files) num = max_files; + bool ok = (num > 0); + WCHAR* wpath = (WCHAR*)calloc((size_t)max_path, sizeof(WCHAR)); + if (!wpath) ok = false; + for (int i = 0; ok && (i < num); i++) { + if (0 == DragQueryFileW(hdrop, (UINT)i, wpath, (UINT)max_path)) { + ok = false; + break; + } + char* dst = _sapp_tc.app.drop_buffer + (size_t)i * (size_t)max_path; + if (!_sapp_tc_win32_wide_to_utf8(wpath, dst, max_path)) { + ok = false; /* path too long: drop everything */ + break; + } + } + POINT pt; + const BOOL in_client = DragQueryPoint(hdrop, &pt); + free(wpath); + DragFinish(hdrop); + if (!ok) { + memset(_sapp_tc.app.drop_buffer, 0, (size_t)max_files * (size_t)max_path); + return; + } + _sapp_tc.app.drop_num_files = num; + sapp_event e; + memset(&e, 0, sizeof(e)); + e.type = SAPP_EVENTTYPE_FILES_DROPPED; + if (in_client) { + e.mouse_x = (float)pt.x * w->mouse_scale; + e.mouse_y = (float)pt.y * w->mouse_scale; + } else { + e.mouse_x = w->mouse_x; + e.mouse_y = w->mouse_y; + } + e.modifiers = _sapp_tc_win32_mods(); + _sapp_tc_send(w, &e); +} + +/*-- fullscreen (main window; borderless style-swap, upstream parity) -------*/ +static void _sapp_tc_win32_set_fullscreen(bool fullscreen, UINT swp_flags) { + _sapp_tc_window_t* w = _sapp_tc.app.main; + if (!w) return; + HMONITOR monitor = MonitorFromWindow(w->hwnd, MONITOR_DEFAULTTONEAREST); + MONITORINFO minfo; + memset(&minfo, 0, sizeof(minfo)); + minfo.cbSize = sizeof(minfo); + if (!GetMonitorInfoW(monitor, &minfo)) return; + _sapp_tc.app.fullscreen = fullscreen; + RECT rect; + DWORD style; + if (fullscreen) { + GetWindowRect(w->hwnd, &w->stored_window_rect); + style = WS_POPUP | WS_SYSMENU | WS_VISIBLE; + rect = minfo.rcMonitor; + } else { + style = WS_CLIPSIBLINGS | WS_CLIPCHILDREN | WS_CAPTION | WS_SYSMENU | + WS_MINIMIZEBOX | WS_MAXIMIZEBOX | WS_SIZEBOX; + rect = w->stored_window_rect; + } + SetWindowLongPtrW(w->hwnd, GWL_STYLE, (LONG_PTR)style); + SetWindowPos(w->hwnd, HWND_TOP, rect.left, rect.top, + rect.right - rect.left, rect.bottom - rect.top, + swp_flags | SWP_FRAMECHANGED); +} + +/*-- per-window tick + pacing ------------------------------------------------ + Primary pacing: the swapchain's frame latency waitable signals when a new + frame can be queued (true per-display vsync). One credit is consumed per + successful zero-timeout wait in the due-check; a Present returns it via + the swapchain. A tick that ends WITHOUT a Present (skip_present, occluded, + minimized) keeps `credit_held` and is re-paced by `earliest_next` -- never + waiting on the waitable again until the credit is spent -- so neither + busy-spinning (waitable stays signaled) nor starvation (credits leak away) + can occur. Windows without a waitable (fallback) are purely timer-paced. */ +static void _sapp_tc_win32_pace(_sapp_tc_window_t* w, double seconds) { + w->earliest_next = _sapp_tc_now() + seconds; +} + +static bool _sapp_tc_win32_window_due(_sapp_tc_window_t* w) { + if (!w || !w->swap_chain || w->in_tick) return false; + if (w->earliest_next > _sapp_tc_now()) return false; + if (w->frame_wait && !w->credit_held) { + if (WaitForSingleObject(w->frame_wait, 0) != WAIT_OBJECT_0) return false; + } + return true; +} + +static void _sapp_tc_win32_tick(_sapp_tc_window_t* w, bool from_modal) { + if (w->in_tick || !w->swap_chain) return; + /* the due-check just consumed a waitable credit (unless one was already + held); hold it until a real Present returns it through the swapchain */ + if (w->frame_wait) w->credit_held = true; + _sapp_tc_timing_update(&w->timing); + w->frame_duration = w->timing.smooth_dt; + const int swap_interval = (_sapp_tc.app.desc.swap_interval > 0) ? _sapp_tc.app.desc.swap_interval : 1; + + if (w->is_main) { + /* resize is deliberately coalesced here, NOT in WM_SIZE (upstream: + per-WM_SIZE ResizeBuffers blows up memory on some drivers). During + a modal size-move loop the swapchain keeps its size (DXGI stretch) + and one resize lands on the first normal tick after the drag. */ + if (!from_modal && _sapp_tc_win32_update_dimensions(w)) { + _sapp_tc_d3d11_resize(w); + _sapp_tc_win32_app_event(w, SAPP_EVENTTYPE_RESIZED); + } + w->in_tick = true; + if (!_sapp_tc.app.init_called) { + _sapp_tc.app.init_called = true; + if (_sapp_tc.app.desc.init_cb) { + _sapp_tc.app.desc.init_cb(); + } else if (_sapp_tc.app.desc.init_userdata_cb) { + _sapp_tc.app.desc.init_userdata_cb(_sapp_tc.app.desc.user_data); + } + } + if (_sapp_tc.app.desc.frame_cb) { + _sapp_tc.app.desc.frame_cb(); + } else if (_sapp_tc.app.desc.frame_userdata_cb) { + _sapp_tc.app.desc.frame_userdata_cb(_sapp_tc.app.desc.user_data); + } + _sapp_tc.app.frame_count++; + w->in_tick = false; + bool presented = false; + if (_sapp_tc.app.skip_present) { + /* one-shot event-driven present suppression (TrussC patch: keeps + the last image on screen when a frame decides not to draw) */ + _sapp_tc.app.skip_present = false; + } else { + const UINT flags = from_modal ? DXGI_PRESENT_DO_NOT_WAIT : 0; + const HRESULT hr = w->swap_chain->Present((UINT)swap_interval, flags); + presented = SUCCEEDED(hr) && (hr != DXGI_STATUS_OCCLUDED); + } + if (presented) { + w->credit_held = false; + } + if (w->iconified) { + /* minimized: frame_cb keeps running, throttled (upstream Sleep(16) + / mac fallback-timer parity) */ + _sapp_tc_win32_pace(w, 0.0166 * (double)swap_interval); + } else if (presented && w->frame_wait) { + w->earliest_next = 0.0; /* vsync pacing via the waitable */ + } else { + _sapp_tc_win32_pace(w, w->refresh_period * (double)swap_interval); + } + } else { + /* secondary window */ + if (w->iconified) { + _sapp_tc_win32_pace(w, w->refresh_period); + return; + } + if (w->occluded) { + /* cheap visibility poll: a fully covered window costs one + present-test per pace period and never renders or stalls */ + const HRESULT hr = w->swap_chain->Present(0, DXGI_PRESENT_TEST); + if (hr == DXGI_STATUS_OCCLUDED) { + _sapp_tc_win32_pace(w, w->refresh_period); + return; + } + w->occluded = false; + _sapp_tc_win32_app_event(w, SAPP_EVENTTYPE_RESUMED); + } + if (!from_modal && _sapp_tc_win32_update_dimensions(w)) { + _sapp_tc_d3d11_resize(w); + _sapp_tc_win32_app_event(w, SAPP_EVENTTYPE_RESIZED); + } + w->in_tick = true; + if (w->desc.tick_cb) { + sapp_window handle = { w->win_id }; + w->desc.tick_cb(handle, w->desc.user_data); + } + w->in_tick = false; + const UINT flags = from_modal ? DXGI_PRESENT_DO_NOT_WAIT : 0; + const HRESULT hr = w->swap_chain->Present(1, flags); + if (hr == DXGI_STATUS_OCCLUDED) { + w->occluded = true; + _sapp_tc_win32_app_event(w, SAPP_EVENTTYPE_SUSPENDED); + _sapp_tc_win32_pace(w, w->refresh_period); + } else if (SUCCEEDED(hr)) { + w->credit_held = false; + if (w->frame_wait) { + w->earliest_next = 0.0; + } else { + _sapp_tc_win32_pace(w, w->refresh_period); + } + } else { + _sapp_tc_win32_pace(w, w->refresh_period); + } + } +} + +/*-- window procedure --------------------------------------------------------*/ +static LRESULT CALLBACK _sapp_tc_wndproc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) { + if (msg == WM_NCCREATE) { + /* stash the window struct passed via CreateWindowExW lpCreateParams */ + const CREATESTRUCTW* cs = (const CREATESTRUCTW*)lParam; + SetWindowLongPtrW(hwnd, GWLP_USERDATA, (LONG_PTR)cs->lpCreateParams); + return DefWindowProcW(hwnd, msg, wParam, lParam); + } + _sapp_tc_window_t* w = (_sapp_tc_window_t*)GetWindowLongPtrW(hwnd, GWLP_USERDATA); + if (!w || !w->ready) { + /* creation-time messages are swallowed (upstream in_create_window) */ + return DefWindowProcW(hwnd, msg, wParam, lParam); + } + switch (msg) { + case WM_CLOSE: + if (w->is_main) { + /* the cancellable quit dance (sokol_app.h parity): sapp_quit() + pre-sets quit_ordered (unvetoable); otherwise QUIT_REQUESTED + is delivered and a handler may sapp_cancel_quit() */ + if (!_sapp_tc.app.quit_ordered) { + _sapp_tc.app.quit_requested = true; + _sapp_tc_win32_app_event(w, SAPP_EVENTTYPE_QUIT_REQUESTED); + if (_sapp_tc.app.quit_requested) { + _sapp_tc.app.quit_ordered = true; + } + } + if (_sapp_tc.app.quit_ordered) { + PostQuitMessage(0); + } + } else { + /* user closed a secondary window: notify; the app accepts by + calling sapp_destroy_window() (w may be freed after this) */ + if (w->desc.close_cb) { + sapp_window handle = { w->win_id }; + w->desc.close_cb(handle, w->desc.user_data); + } + } + return 0; /* never let DefWindowProc destroy the window here */ + case WM_SYSCOMMAND: + switch (wParam & 0xFFF0) { + case SC_SCREENSAVE: + case SC_MONITORPOWER: + if (_sapp_tc.app.fullscreen) { + return 0; /* no screensaver/monitor-off in fullscreen */ + } + break; + case SC_KEYMENU: + return 0; /* don't let Alt freeze the render loop */ + default: + break; + } + break; + case WM_ERASEBKGND: + return 1; + case WM_SIZE: { + /* only iconify tracking here; RESIZED + ResizeBuffers are + coalesced into the tick (upstream parity) */ + const bool iconified = (wParam == SIZE_MINIMIZED); + if (iconified != w->iconified) { + w->iconified = iconified; + _sapp_tc_win32_app_event(w, iconified ? SAPP_EVENTTYPE_ICONIFIED + : SAPP_EVENTTYPE_RESTORED); + if (!iconified) { + w->earliest_next = 0.0; /* resume promptly */ + } + } + break; + } + case WM_SETFOCUS: + _sapp_tc_win32_app_event(w, SAPP_EVENTTYPE_FOCUSED); + break; + case WM_KILLFOCUS: + _sapp_tc_win32_app_event(w, SAPP_EVENTTYPE_UNFOCUSED); + break; + case WM_SETCURSOR: + if (LOWORD(lParam) == HTCLIENT) { + _sapp_tc_win32_apply_cursor(_sapp_tc.app.current_cursor, + _sapp_tc.app.mouse_shown, true); + return TRUE; + } + break; + case WM_DPICHANGED: { + /* per-monitor-v2 only: adopt the OS-proposed rect; scale and + sizes are re-sampled on the next tick */ + const RECT* r = (const RECT*)lParam; + SetWindowPos(hwnd, NULL, r->left, r->top, + r->right - r->left, r->bottom - r->top, + SWP_NOZORDER | SWP_NOACTIVATE); + _sapp_tc_win32_update_refresh(w); + break; + } + case WM_LBUTTONDOWN: + _sapp_tc_win32_mouse_update(w, lParam); + _sapp_tc_win32_mouse_event(w, SAPP_EVENTTYPE_MOUSE_DOWN, SAPP_MOUSEBUTTON_LEFT); + _sapp_tc_win32_capture_mouse(w, (uint8_t)(1 << SAPP_MOUSEBUTTON_LEFT)); + break; + case WM_LBUTTONUP: + _sapp_tc_win32_mouse_update(w, lParam); + _sapp_tc_win32_mouse_event(w, SAPP_EVENTTYPE_MOUSE_UP, SAPP_MOUSEBUTTON_LEFT); + _sapp_tc_win32_release_mouse(w, (uint8_t)(1 << SAPP_MOUSEBUTTON_LEFT)); + break; + case WM_RBUTTONDOWN: + _sapp_tc_win32_mouse_update(w, lParam); + _sapp_tc_win32_mouse_event(w, SAPP_EVENTTYPE_MOUSE_DOWN, SAPP_MOUSEBUTTON_RIGHT); + _sapp_tc_win32_capture_mouse(w, (uint8_t)(1 << SAPP_MOUSEBUTTON_RIGHT)); + break; + case WM_RBUTTONUP: + _sapp_tc_win32_mouse_update(w, lParam); + _sapp_tc_win32_mouse_event(w, SAPP_EVENTTYPE_MOUSE_UP, SAPP_MOUSEBUTTON_RIGHT); + _sapp_tc_win32_release_mouse(w, (uint8_t)(1 << SAPP_MOUSEBUTTON_RIGHT)); + break; + case WM_MBUTTONDOWN: + _sapp_tc_win32_mouse_update(w, lParam); + _sapp_tc_win32_mouse_event(w, SAPP_EVENTTYPE_MOUSE_DOWN, SAPP_MOUSEBUTTON_MIDDLE); + _sapp_tc_win32_capture_mouse(w, (uint8_t)(1 << SAPP_MOUSEBUTTON_MIDDLE)); + break; + case WM_MBUTTONUP: + _sapp_tc_win32_mouse_update(w, lParam); + _sapp_tc_win32_mouse_event(w, SAPP_EVENTTYPE_MOUSE_UP, SAPP_MOUSEBUTTON_MIDDLE); + _sapp_tc_win32_release_mouse(w, (uint8_t)(1 << SAPP_MOUSEBUTTON_MIDDLE)); + break; + case WM_MOUSEMOVE: + _sapp_tc_win32_mouse_update(w, lParam); + if (!w->mouse_tracked) { + w->mouse_tracked = true; + TRACKMOUSEEVENT tme; + memset(&tme, 0, sizeof(tme)); + tme.cbSize = sizeof(tme); + tme.dwFlags = TME_LEAVE; + tme.hwndTrack = w->hwnd; + TrackMouseEvent(&tme); + w->mouse_dx = 0.0f; + w->mouse_dy = 0.0f; + _sapp_tc_win32_mouse_event(w, SAPP_EVENTTYPE_MOUSE_ENTER, SAPP_MOUSEBUTTON_INVALID); + } + _sapp_tc_win32_mouse_event(w, SAPP_EVENTTYPE_MOUSE_MOVE, SAPP_MOUSEBUTTON_INVALID); + break; + case WM_MOUSELEAVE: + w->mouse_dx = 0.0f; + w->mouse_dy = 0.0f; + w->mouse_tracked = false; + _sapp_tc_win32_mouse_event(w, SAPP_EVENTTYPE_MOUSE_LEAVE, SAPP_MOUSEBUTTON_INVALID); + break; + case WM_MOUSEWHEEL: + _sapp_tc_win32_scroll_event(w, 0.0f, + (float)GET_WHEEL_DELTA_WPARAM(wParam) / (float)WHEEL_DELTA); + break; + case WM_MOUSEHWHEEL: + _sapp_tc_win32_scroll_event(w, + -(float)GET_WHEEL_DELTA_WPARAM(wParam) / (float)WHEEL_DELTA, 0.0f); + break; + case WM_CHAR: + _sapp_tc_win32_char_event(w, (uint32_t)wParam, 0 != (lParam & 0x40000000)); + break; + case WM_KEYDOWN: + case WM_SYSKEYDOWN: + /* 9-bit scancode incl. the extended bit (distinguishes L/R + Ctrl/Alt and keypad Enter); SYS variants fall through to + DefWindowProc below so Alt+F4 etc. keep working */ + _sapp_tc_win32_key_event(w, SAPP_EVENTTYPE_KEY_DOWN, + (int)(HIWORD(lParam) & 0x1FF), 0 != (lParam & 0x40000000)); + break; + case WM_KEYUP: + case WM_SYSKEYUP: + _sapp_tc_win32_key_event(w, SAPP_EVENTTYPE_KEY_UP, + (int)(HIWORD(lParam) & 0x1FF), false); + break; + case WM_ENTERSIZEMOVE: + /* the modal size-move loop blocks the outer message loop; a + coarse WM_TIMER (~64Hz) keeps all windows rendering meanwhile */ + SetTimer(hwnd, _SAPP_TC_MODAL_TIMER, USER_TIMER_MINIMUM, NULL); + break; + case WM_EXITSIZEMOVE: + KillTimer(hwnd, _SAPP_TC_MODAL_TIMER); + break; + case WM_TIMER: + if (wParam == _SAPP_TC_MODAL_TIMER) { + for (int i = 0; i < _SAPP_TC_MAX_WINDOWS; i++) { + _sapp_tc_window_t* tw = _sapp_tc.windows[i]; + if (_sapp_tc_win32_window_due(tw)) { + _sapp_tc_win32_tick(tw, true); + } + } + } + break; + case WM_NCLBUTTONDOWN: + /* grabbing the title bar stalls rendering for ~500ms; posting a + synthetic mouse-move breaks the wait (upstream workaround) */ + if (wParam == HTCAPTION) { + POINT point; + if (GetCursorPos(&point)) { + ScreenToClient(hwnd, &point); + PostMessageW(hwnd, WM_MOUSEMOVE, 0, + (LPARAM)(((uint32_t)point.x) | (((uint32_t)point.y) << 16))); + } + } + break; + case WM_DROPFILES: + _sapp_tc_win32_files_dropped(w, (HDROP)wParam); + break; + default: + break; + } + return DefWindowProcW(hwnd, msg, wParam, lParam); +} + +/*-- window creation / destruction ------------------------------------------*/ +static void _sapp_tc_win32_ensure_wndclass(void) { + if (_sapp_tc.wndclass_registered) return; + _sapp_tc.wndclass_registered = true; + WNDCLASSW wndclassw; + memset(&wndclassw, 0, sizeof(wndclassw)); + wndclassw.style = CS_HREDRAW | CS_VREDRAW | CS_OWNDC; + wndclassw.lpfnWndProc = _sapp_tc_wndproc; + wndclassw.hInstance = GetModuleHandleW(NULL); + wndclassw.hCursor = LoadCursor(NULL, IDC_ARROW); + wndclassw.hIcon = LoadIcon(NULL, IDI_WINLOGO); + wndclassw.lpszClassName = L"SOKOLAPP_TC"; + RegisterClassW(&wndclassw); +} + +/* create the HWND at the desc's logical size: first against 96 DPI, then -- + per-monitor-v2 -- resize the CLIENT area for the real DPI of the monitor + the window actually landed on (the standard create-then-resize dance) */ +static bool _sapp_tc_win32_create_native_window(_sapp_tc_window_t* w, const wchar_t* title, + DWORD style, DWORD ex_style, int x, int y, bool use_default_size) { + _sapp_tc_win32_ensure_wndclass(); + int outer_w = CW_USEDEFAULT; + int outer_h = CW_USEDEFAULT; + if (!use_default_size) { + RECT rect = { 0, 0, (LONG)w->desc.width, (LONG)w->desc.height }; + AdjustWindowRectEx(&rect, style, FALSE, ex_style); + outer_w = (int)(rect.right - rect.left); + outer_h = (int)(rect.bottom - rect.top); + } + w->hwnd = CreateWindowExW(ex_style, L"SOKOLAPP_TC", title, style, + (x >= 0) ? x : CW_USEDEFAULT, + (y >= 0) ? y : CW_USEDEFAULT, + outer_w, outer_h, + NULL, NULL, GetModuleHandleW(NULL), w); + if (!w->hwnd) return false; + w->hmonitor = MonitorFromWindow(w->hwnd, MONITOR_DEFAULTTONEAREST); + _sapp_tc_win32_update_refresh(w); + _sapp_tc_win32_update_scale(w); + if (!use_default_size && (w->window_scale != 1.0f)) { + const LONG pw = (LONG)((float)w->desc.width * w->window_scale + 0.5f); + const LONG ph = (LONG)((float)w->desc.height * w->window_scale + 0.5f); + RECT rect = { 0, 0, pw, ph }; + if (_sapp_tc.GetDpiForWindow && _sapp_tc.AdjustWindowRectExForDpi) { + _sapp_tc.AdjustWindowRectExForDpi(&rect, style, FALSE, ex_style, + _sapp_tc.GetDpiForWindow(w->hwnd)); + } else { + AdjustWindowRectEx(&rect, style, FALSE, ex_style); + } + SetWindowPos(w->hwnd, NULL, 0, 0, + (int)(rect.right - rect.left), (int)(rect.bottom - rect.top), + SWP_NOMOVE | SWP_NOZORDER | SWP_NOACTIVATE); + } + _sapp_tc_timing_reset(&w->timing); + return true; +} + +static void _sapp_tc_win32_destroy_window_resources(_sapp_tc_window_t* w) { + _sapp_tc_d3d11_destroy_render_targets(w); + if (w->frame_wait) { + CloseHandle(w->frame_wait); + w->frame_wait = NULL; + } + _SAPP_TC_RELEASE(w->swap_chain); + if (w->hwnd) { + KillTimer(w->hwnd, _SAPP_TC_MODAL_TIMER); + SetWindowLongPtrW(w->hwnd, GWLP_USERDATA, 0); /* wndproc goes inert */ + DestroyWindow(w->hwnd); + w->hwnd = NULL; + } +} + +static bool _sapp_tc_win32_create_main_window(void) { + const sapp_desc* d = &_sapp_tc.app.desc; + int slot = -1; + for (int i = 0; i < _SAPP_TC_MAX_WINDOWS; i++) { + if (_sapp_tc.windows[i] == 0) { slot = i; break; } + } + if (slot < 0) return false; + + _sapp_tc_window_t* w = new _sapp_tc_window_t(); + w->win_id = ++_sapp_tc.next_id; + w->is_main = true; + w->color_fmt = DXGI_FORMAT_R10G10B10A2_UNORM; /* TrussC 10-bit output */ + w->window_scale = w->dpi_scale = w->mouse_scale = 1.0f; + w->refresh_period = 1.0 / 60.0; + + /* per-window desc: the app callbacks are reached via the trampoline */ + w->desc.width = d->width; + w->desc.height = d->height; + w->desc.sample_count = d->sample_count; + w->desc.no_high_dpi = !d->high_dpi; + w->desc.event_cb = _sapp_tc_main_event_tramp; + + const DWORD style = WS_CLIPSIBLINGS | WS_CLIPCHILDREN | WS_CAPTION | WS_SYSMENU | + WS_MINIMIZEBOX | WS_MAXIMIZEBOX | WS_SIZEBOX; + const DWORD ex_style = WS_EX_APPWINDOW | WS_EX_WINDOWEDGE; + /* width/height 0: let the OS pick (CW_USEDEFAULT, upstream parity) */ + const bool use_default_size = (d->width <= 0) || (d->height <= 0); + if (!_sapp_tc_win32_create_native_window(w, _sapp_tc.app.window_title_wide, + style, ex_style, -1, -1, use_default_size)) { + delete w; + return false; + } + _sapp_tc.windows[slot] = w; + _sapp_tc.app.main = w; + w->ready = true; + _sapp_tc_win32_update_dimensions(w); /* silent startup sizing (no RESIZED) */ + if (d->fullscreen) { + _sapp_tc_win32_set_fullscreen(true, SWP_HIDEWINDOW); + _sapp_tc_win32_update_dimensions(w); + } + ShowWindow(w->hwnd, SW_SHOW); + /* drop target is always registered; the handler gates on enable_dragndrop */ + DragAcceptFiles(w->hwnd, TRUE); + if (!_sapp_tc_d3d11_create_swapchain(w)) { + return false; + } + _sapp_tc.app.valid = true; + return true; +} + +/*-- the run loop ------------------------------------------------------------- + Blocks in MsgWaitForMultipleObjectsEx on the due windows' frame latency + waitables (plus a timeout for timer-paced windows), drains ALL pending + messages, then ticks every due window. Replaces upstream's PeekMessage + busy-render-loop: the process sleeps whenever nothing is due. */ +static void _sapp_tc_win32_run_loop(void) { + bool done = false; + while (!done && !_sapp_tc.app.quit_ordered) { + HANDLE handles[_SAPP_TC_MAX_WINDOWS]; + DWORD num_handles = 0; + const double now = _sapp_tc_now(); + double wake_at = now + 0.1; /* robustness cap; messages wake us anyway */ + for (int i = 0; i < _SAPP_TC_MAX_WINDOWS; i++) { + _sapp_tc_window_t* w = _sapp_tc.windows[i]; + if (!w || !w->swap_chain || w->in_tick) continue; + if (w->earliest_next > now) { + if (w->earliest_next < wake_at) wake_at = w->earliest_next; + } else if (w->frame_wait && !w->credit_held) { + handles[num_handles++] = w->frame_wait; + } else { + wake_at = now; /* due immediately (timer-paced / credit held) */ + } + } + double timeout_s = wake_at - now; + if (timeout_s < 0.0) timeout_s = 0.0; + MsgWaitForMultipleObjectsEx(num_handles, handles, + (DWORD)(timeout_s * 1000.0), QS_ALLINPUT, MWMO_INPUTAVAILABLE); + MSG msg; + while (PeekMessageW(&msg, NULL, 0, 0, PM_REMOVE)) { + if (WM_QUIT == msg.message) { + done = true; + continue; + } + TranslateMessage(&msg); + DispatchMessageW(&msg); + } + if (done) break; + /* tick every due window (re-fetch each slot: a tick or a dispatched + message may have destroyed windows) */ + for (int i = 0; i < _SAPP_TC_MAX_WINDOWS; i++) { + _sapp_tc_window_t* w = _sapp_tc.windows[i]; + if (_sapp_tc_win32_window_due(w)) { + _sapp_tc_win32_tick(w, false); + } + } + /* route programmatic quits through the same WM_CLOSE dance so the + QUIT_REQUESTED semantics stay identical (upstream parity) */ + if (_sapp_tc.app.quit_requested && _sapp_tc.app.main) { + PostMessageW(_sapp_tc.app.main->hwnd, WM_CLOSE, 0, 0); + } + } +} + +/*-- public API ---------------------------------------------------------------*/ +extern "C" { + +sapp_window sapp_create_window(const sapp_window_desc* desc) { + sapp_window invalid = { 0 }; + if (!desc) return invalid; + if (!_sapp_tc.app.valid || !_sapp_tc.app.device) return invalid; + _sapp_tc_init_keytable(); + + int slot = -1; + for (int i = 0; i < _SAPP_TC_MAX_WINDOWS; i++) { + if (_sapp_tc.windows[i] == 0) { slot = i; break; } + } + if (slot < 0) return invalid; + + _sapp_tc_window_t* w = new _sapp_tc_window_t(); + w->win_id = ++_sapp_tc.next_id; + w->desc = *desc; + if (w->desc.width <= 0) w->desc.width = 640; + if (w->desc.height <= 0) w->desc.height = 480; + if (w->desc.sample_count <= 0) w->desc.sample_count = 1; + w->color_fmt = DXGI_FORMAT_B8G8R8A8_UNORM; /* secondary windows are BGRA8 (mac parity) */ + w->window_scale = w->dpi_scale = w->mouse_scale = 1.0f; + w->refresh_period = 1.0 / 60.0; + + DWORD style; + if (w->desc.borderless) { + style = WS_POPUP | WS_SYSMENU; + } else { + style = WS_CLIPSIBLINGS | WS_CLIPCHILDREN | WS_CAPTION | WS_SYSMENU | + WS_MINIMIZEBOX | WS_MAXIMIZEBOX | WS_SIZEBOX; + } + const DWORD ex_style = WS_EX_APPWINDOW | WS_EX_WINDOWEDGE; + WCHAR title[256]; + _sapp_tc_win32_utf8_to_wide(w->desc.title ? w->desc.title : "TrussC", title, 256); + const int px = (desc->x >= 0) ? desc->x : 120; + const int py = (desc->y >= 0) ? desc->y : 120; + if (!_sapp_tc_win32_create_native_window(w, title, style, ex_style, px, py, false)) { + delete w; + return invalid; + } + w->ready = true; + _sapp_tc_win32_update_dimensions(w); + ShowWindow(w->hwnd, SW_SHOW); + DragAcceptFiles(w->hwnd, TRUE); + if (!_sapp_tc_d3d11_create_swapchain(w)) { + _sapp_tc_win32_destroy_window_resources(w); + delete w; + return invalid; + } + _sapp_tc.windows[slot] = w; + sapp_window handle = { w->win_id }; + return handle; +} + +void sapp_destroy_window(sapp_window win) { + _sapp_tc_window_t* w = _sapp_tc_lookup(win); + if (!w || w->is_main) return; /* the main window is owned by sapp_run */ + for (int i = 0; i < _SAPP_TC_MAX_WINDOWS; i++) { + if (_sapp_tc.windows[i] == w) { _sapp_tc.windows[i] = 0; break; } + } + w->desc.close_cb = 0; + _sapp_tc_win32_destroy_window_resources(w); + delete w; +} + +bool sapp_window_valid(sapp_window win) { + return _sapp_tc_lookup(win) != 0; +} + +int sapp_window_width(sapp_window win) { + _sapp_tc_window_t* w = _sapp_tc_lookup(win); + return w ? w->win_width : 0; +} + +int sapp_window_height(sapp_window win) { + _sapp_tc_window_t* w = _sapp_tc_lookup(win); + return w ? w->win_height : 0; +} + +int sapp_window_framebuffer_width(sapp_window win) { + _sapp_tc_window_t* w = _sapp_tc_lookup(win); + return w ? w->fb_width : 0; +} + +int sapp_window_framebuffer_height(sapp_window win) { + _sapp_tc_window_t* w = _sapp_tc_lookup(win); + return w ? w->fb_height : 0; +} + +float sapp_window_dpi_scale(sapp_window win) { + _sapp_tc_window_t* w = _sapp_tc_lookup(win); + return w ? w->dpi_scale : 1.0f; +} + +int sapp_window_sample_count(sapp_window win) { + _sapp_tc_window_t* w = _sapp_tc_lookup(win); + return w ? w->desc.sample_count : 1; +} + +bool sapp_window_occluded(sapp_window win) { + _sapp_tc_window_t* w = _sapp_tc_lookup(win); + return w ? w->occluded : false; +} + +void sapp_window_set_title(sapp_window win, const char* title) { + _sapp_tc_window_t* w = _sapp_tc_lookup(win); + if (!w || !w->hwnd || !title) return; + WCHAR wide[256]; + if (_sapp_tc_win32_utf8_to_wide(title, wide, 256)) { + SetWindowTextW(w->hwnd, wide); + } +} + +double sapp_window_frame_duration(sapp_window win) { + _sapp_tc_window_t* w = _sapp_tc_lookup(win); + if (!w) return 1.0 / 60.0; + return (w->frame_duration > 0.0) ? w->frame_duration : w->refresh_period; +} + +/* Metal handles do not exist on Windows */ +const void* sapp_window_metal_current_drawable(sapp_window win) { (void)win; return 0; } +const void* sapp_window_metal_depth_stencil_texture(sapp_window win) { (void)win; return 0; } +const void* sapp_window_metal_msaa_color_texture(sapp_window win) { (void)win; return 0; } +const void* sapp_window_macos_get_window(sapp_window win) { (void)win; return 0; } + +const void* sapp_window_d3d11_render_view(sapp_window win) { + _sapp_tc_window_t* w = _sapp_tc_lookup(win); + if (!w || !w->in_tick) return 0; + return (w->desc.sample_count > 1) ? (const void*)w->msaa_rtv : (const void*)w->rtv; +} + +const void* sapp_window_d3d11_resolve_view(sapp_window win) { + _sapp_tc_window_t* w = _sapp_tc_lookup(win); + if (!w || !w->in_tick || (w->desc.sample_count <= 1)) return 0; + return (const void*)w->rtv; +} + +const void* sapp_window_d3d11_depth_stencil_view(sapp_window win) { + _sapp_tc_window_t* w = _sapp_tc_lookup(win); + if (!w || !w->in_tick) return 0; + return (const void*)w->dsv; +} + +const void* sapp_window_win32_get_hwnd(sapp_window win) { + _sapp_tc_window_t* w = _sapp_tc_lookup(win); + return w ? (const void*)w->hwnd : 0; +} + +/*-- sokol_app.h public API (Windows implementation lives here) --------------*/ + +void sapp_run(const sapp_desc* desc) { + if (!desc) return; + _sapp_tc.app.desc = *desc; + sapp_desc* d = &_sapp_tc.app.desc; + if (d->sample_count <= 0) d->sample_count = 1; + if (d->swap_interval <= 0) d->swap_interval = 1; + if (d->clipboard_size <= 0) d->clipboard_size = 8192; + if (d->max_dropped_files <= 0) d->max_dropped_files = 1; + if (d->max_dropped_file_path_length <= 0) d->max_dropped_file_path_length = 2048; + strncpy(_sapp_tc.app.window_title, d->window_title ? d->window_title : "sokol", + sizeof(_sapp_tc.app.window_title) - 1); + d->window_title = _sapp_tc.app.window_title; + _sapp_tc_win32_utf8_to_wide(_sapp_tc.app.window_title, + _sapp_tc.app.window_title_wide, + (int)(sizeof(_sapp_tc.app.window_title_wide) / sizeof(WCHAR))); + if (d->enable_clipboard) { + _sapp_tc.app.clipboard_enabled = true; + _sapp_tc.app.clipboard_size = d->clipboard_size; + _sapp_tc.app.clipboard = (char*)calloc(1, (size_t)d->clipboard_size); + } + if (d->enable_dragndrop) { + _sapp_tc.app.drop_enabled = true; + _sapp_tc.app.drop_max_files = d->max_dropped_files; + _sapp_tc.app.drop_max_path_length = d->max_dropped_file_path_length; + _sapp_tc.app.drop_buffer = (char*)calloc( + (size_t)d->max_dropped_files, (size_t)d->max_dropped_file_path_length); + } + _sapp_tc.app.mouse_shown = true; + _sapp_tc.app.current_cursor = SAPP_MOUSECURSOR_DEFAULT; + LARGE_INTEGER freq; + QueryPerformanceFrequency(&freq); + _sapp_tc.qpc_period = 1.0 / (double)freq.QuadPart; + _sapp_tc_win32_init_console(); + _sapp_tc_init_keytable(); + _sapp_tc_win32_init_dpi(); + _sapp_tc_win32_init_cursors(); + + if (_sapp_tc_d3d11_create_device()) { + if (_sapp_tc_win32_create_main_window()) { + _sapp_tc_win32_run_loop(); + } + } + + /* user cleanup runs BEFORE any GPU/window teardown (the app shuts down + sokol_gfx here, which still needs the device) */ + if (!_sapp_tc.app.cleanup_called) { + _sapp_tc.app.cleanup_called = true; + if (_sapp_tc.app.desc.cleanup_cb) { + _sapp_tc.app.desc.cleanup_cb(); + } else if (_sapp_tc.app.desc.cleanup_userdata_cb) { + _sapp_tc.app.desc.cleanup_userdata_cb(_sapp_tc.app.desc.user_data); + } + } + /* destroy any windows the app left open (secondary first, then main) */ + for (int i = 0; i < _SAPP_TC_MAX_WINDOWS; i++) { + _sapp_tc_window_t* w = _sapp_tc.windows[i]; + if (!w || w->is_main) continue; + _sapp_tc.windows[i] = 0; + _sapp_tc_win32_destroy_window_resources(w); + delete w; + } + if (_sapp_tc.app.main) { + _sapp_tc_window_t* w = _sapp_tc.app.main; + for (int i = 0; i < _SAPP_TC_MAX_WINDOWS; i++) { + if (_sapp_tc.windows[i] == w) { _sapp_tc.windows[i] = 0; break; } + } + _sapp_tc.app.main = 0; + _sapp_tc_win32_destroy_window_resources(w); + delete w; + } + _SAPP_TC_RELEASE(_sapp_tc.app.device_context); + _SAPP_TC_RELEASE(_sapp_tc.app.device); + if (_sapp_tc.wndclass_registered) { + UnregisterClassW(L"SOKOLAPP_TC", GetModuleHandleW(NULL)); + _sapp_tc.wndclass_registered = false; + } + for (int i = 0; i < _SAPP_MOUSECURSOR_NUM; i++) { + if (_sapp_tc.app.custom_cursor_bound[i] && _sapp_tc.app.custom_cursors[i]) { + DestroyIcon((HICON)_sapp_tc.app.custom_cursors[i]); + _sapp_tc.app.custom_cursors[i] = NULL; + _sapp_tc.app.custom_cursor_bound[i] = false; + } + } + _sapp_tc_win32_restore_console(); + if (_sapp_tc.app.clipboard) { free(_sapp_tc.app.clipboard); _sapp_tc.app.clipboard = 0; } + if (_sapp_tc.app.drop_buffer) { free(_sapp_tc.app.drop_buffer); _sapp_tc.app.drop_buffer = 0; } + _sapp_tc.app.valid = false; +} + +bool sapp_isvalid(void) { + return _sapp_tc.app.valid; +} + +int sapp_width(void) { + _sapp_tc_window_t* w = _sapp_tc.app.main; + return (w && w->fb_width > 0) ? w->fb_width : 1; +} + +float sapp_widthf(void) { + return (float)sapp_width(); +} + +int sapp_height(void) { + _sapp_tc_window_t* w = _sapp_tc.app.main; + return (w && w->fb_height > 0) ? w->fb_height : 1; +} + +float sapp_heightf(void) { + return (float)sapp_height(); +} + +int sapp_sample_count(void) { + return _sapp_tc.app.desc.sample_count > 0 ? _sapp_tc.app.desc.sample_count : 1; +} + +bool sapp_high_dpi(void) { + _sapp_tc_window_t* w = _sapp_tc.app.main; + return _sapp_tc.app.desc.high_dpi && w && (w->dpi_scale >= 1.5f); +} + +float sapp_dpi_scale(void) { + _sapp_tc_window_t* w = _sapp_tc.app.main; + return w ? w->dpi_scale : 1.0f; +} + +uint64_t sapp_frame_count(void) { + return _sapp_tc.app.frame_count; +} + +double sapp_frame_duration(void) { + _sapp_tc_window_t* w = _sapp_tc.app.main; + if (!w) return 1.0 / 60.0; + return (w->timing.smooth_dt > 0.0) ? w->timing.smooth_dt : w->refresh_period; +} + +void sapp_request_quit(void) { + _sapp_tc.app.quit_requested = true; +} + +void sapp_cancel_quit(void) { + _sapp_tc.app.quit_requested = false; +} + +void sapp_quit(void) { + _sapp_tc.app.quit_ordered = true; +} + +void sapp_consume_event(void) { + _sapp_tc.app.event_consumed = true; +} + +void sapp_skip_present(void) { + /* one-shot; HONORED on D3D11 (TrussC patch: event-driven present + suppression -- keeps the last image on screen, prevents flicker) */ + _sapp_tc.app.skip_present = true; +} + +void sapp_show_keyboard(bool show) { + (void)show; /* on-screen keyboard: mobile only */ +} + +bool sapp_keyboard_shown(void) { + return false; +} + +void sapp_show_mouse(bool show) { + if (_sapp_tc.app.mouse_shown != show) { + _sapp_tc_win32_apply_cursor(_sapp_tc.app.current_cursor, show, false); + } +} + +bool sapp_mouse_shown(void) { + return _sapp_tc.app.mouse_shown; +} + +void sapp_set_mouse_cursor(sapp_mouse_cursor cursor) { + if ((int)cursor < 0 || (int)cursor >= _SAPP_MOUSECURSOR_NUM) return; + if (cursor != _sapp_tc.app.current_cursor) { + _sapp_tc_win32_apply_cursor(cursor, _sapp_tc.app.mouse_shown, false); + } +} + +sapp_mouse_cursor sapp_get_mouse_cursor(void) { + return _sapp_tc.app.current_cursor; +} + +sapp_mouse_cursor sapp_bind_mouse_cursor_image(sapp_mouse_cursor cursor, const sapp_image_desc* desc) { + if ((int)cursor < 0 || (int)cursor >= _SAPP_MOUSECURSOR_NUM) return cursor; + if (!desc || !desc->pixels.ptr || desc->width <= 0 || desc->height <= 0) return cursor; + if (desc->pixels.size < (size_t)(desc->width * desc->height * 4)) return cursor; + sapp_unbind_mouse_cursor_image(cursor); + HCURSOR c = _sapp_tc_win32_create_cursor(desc); + if (!c) return cursor; + _sapp_tc.app.custom_cursors[cursor] = c; + _sapp_tc.app.custom_cursor_bound[cursor] = true; + if (_sapp_tc.app.current_cursor == cursor) { + _sapp_tc_win32_apply_cursor(cursor, _sapp_tc.app.mouse_shown, false); + } + return cursor; +} + +void sapp_unbind_mouse_cursor_image(sapp_mouse_cursor cursor) { + if ((int)cursor < 0 || (int)cursor >= _SAPP_MOUSECURSOR_NUM) return; + if (!_sapp_tc.app.custom_cursor_bound[cursor]) return; + _sapp_tc.app.custom_cursor_bound[cursor] = false; + HCURSOR c = _sapp_tc.app.custom_cursors[cursor]; + _sapp_tc.app.custom_cursors[cursor] = NULL; + if (_sapp_tc.app.current_cursor == cursor) { + _sapp_tc_win32_apply_cursor(cursor, _sapp_tc.app.mouse_shown, false); + } + if (c) DestroyIcon((HICON)c); +} + +void sapp_set_clipboard_string(const char* str) { + if (!_sapp_tc.app.clipboard_enabled || !_sapp_tc.app.clipboard || !str) return; + _sapp_tc_window_t* w = _sapp_tc.app.main; + if (!w) return; + const SIZE_T wchar_buf_size = (SIZE_T)_sapp_tc.app.clipboard_size * sizeof(wchar_t); + HANDLE object = GlobalAlloc(GMEM_MOVEABLE, wchar_buf_size); + if (!object) return; + wchar_t* wchar_buf = (wchar_t*)GlobalLock(object); + if (!wchar_buf) { + GlobalFree(object); + return; + } + if (!_sapp_tc_win32_utf8_to_wide(str, wchar_buf, (int)(wchar_buf_size / sizeof(wchar_t)))) { + GlobalUnlock(object); + GlobalFree(object); + return; + } + GlobalUnlock(object); + bool owned = false; + if (OpenClipboard(w->hwnd)) { + EmptyClipboard(); + /* the clipboard takes ownership of the global object on success */ + owned = (NULL != SetClipboardData(CF_UNICODETEXT, object)); + CloseClipboard(); + } + if (!owned) { + GlobalFree(object); + return; + } + strncpy(_sapp_tc.app.clipboard, str, (size_t)_sapp_tc.app.clipboard_size - 1); + _sapp_tc.app.clipboard[_sapp_tc.app.clipboard_size - 1] = 0; +} + +const char* sapp_get_clipboard_string(void) { + if (!_sapp_tc.app.clipboard_enabled || !_sapp_tc.app.clipboard) return ""; + _sapp_tc_window_t* w = _sapp_tc.app.main; + if (!w) return ""; + _sapp_tc.app.clipboard[0] = 0; + if (!OpenClipboard(w->hwnd)) { + return _sapp_tc.app.clipboard; + } + HANDLE object = GetClipboardData(CF_UNICODETEXT); + if (object) { + const wchar_t* wchar_buf = (const wchar_t*)GlobalLock(object); + if (wchar_buf) { + _sapp_tc_win32_wide_to_utf8(wchar_buf, _sapp_tc.app.clipboard, + _sapp_tc.app.clipboard_size); + GlobalUnlock(object); + } + } + CloseClipboard(); + return _sapp_tc.app.clipboard; +} + +void sapp_set_window_title(const char* str) { + if (!str) return; + strncpy(_sapp_tc.app.window_title, str, sizeof(_sapp_tc.app.window_title) - 1); + _sapp_tc_win32_utf8_to_wide(_sapp_tc.app.window_title, + _sapp_tc.app.window_title_wide, + (int)(sizeof(_sapp_tc.app.window_title_wide) / sizeof(WCHAR))); + _sapp_tc_window_t* w = _sapp_tc.app.main; + if (w && w->hwnd) { + SetWindowTextW(w->hwnd, _sapp_tc.app.window_title_wide); + } +} + +bool sapp_is_fullscreen(void) { + return _sapp_tc.app.fullscreen; +} + +void sapp_toggle_fullscreen(void) { + if (!_sapp_tc.app.main) return; + _sapp_tc_win32_set_fullscreen(!_sapp_tc.app.fullscreen, SWP_SHOWWINDOW); +} + +int sapp_get_num_dropped_files(void) { + return _sapp_tc.app.drop_enabled ? _sapp_tc.app.drop_num_files : 0; +} + +const char* sapp_get_dropped_file_path(int index) { + if (!_sapp_tc.app.drop_enabled || !_sapp_tc.app.drop_buffer) return ""; + if (index < 0 || index >= _sapp_tc.app.drop_num_files) return ""; + return _sapp_tc.app.drop_buffer + (size_t)index * (size_t)_sapp_tc.app.drop_max_path_length; +} + +sapp_pixel_format sapp_color_format(void) { + return SAPP_PIXELFORMAT_RGB10A2; /* TrussC 10-bit output on D3D11 */ +} + +sapp_pixel_format sapp_depth_format(void) { + return SAPP_PIXELFORMAT_DEPTH_STENCIL; +} + +const void* sapp_win32_get_hwnd(void) { + _sapp_tc_window_t* w = _sapp_tc.app.main; + return w ? (const void*)w->hwnd : 0; +} + +const void* sapp_d3d11_get_device(void) { + return (const void*)_sapp_tc.app.device; +} + +const void* sapp_d3d11_get_device_context(void) { + return (const void*)_sapp_tc.app.device_context; +} + +const void* sapp_d3d11_get_swap_chain(void) { + _sapp_tc_window_t* w = _sapp_tc.app.main; + return w ? (const void*)w->swap_chain : 0; +} + +sapp_environment sapp_get_environment(void) { + sapp_environment env; + memset(&env, 0, sizeof(env)); + env.defaults.color_format = sapp_color_format(); + env.defaults.depth_format = sapp_depth_format(); + env.defaults.sample_count = sapp_sample_count(); + env.d3d11.device = (const void*)_sapp_tc.app.device; + env.d3d11.device_context = (const void*)_sapp_tc.app.device_context; + return env; +} + +sapp_swapchain sapp_get_swapchain(void) { + /* unlike Metal there is no per-frame acquire: the flip-model swapchain + rotates internally, the views are created once and reused (resize + recreates them) -- calling this any number of times per frame is safe */ + sapp_swapchain sc; + memset(&sc, 0, sizeof(sc)); + _sapp_tc_window_t* w = _sapp_tc.app.main; + if (!w) return sc; + sc.width = sapp_width(); + sc.height = sapp_height(); + sc.sample_count = sapp_sample_count(); + sc.color_format = sapp_color_format(); + sc.depth_format = sapp_depth_format(); + if (w->desc.sample_count > 1) { + sc.d3d11.render_view = (const void*)w->msaa_rtv; + sc.d3d11.resolve_view = (const void*)w->rtv; + } else { + sc.d3d11.render_view = (const void*)w->rtv; + sc.d3d11.resolve_view = 0; + } + sc.d3d11.depth_stencil_view = (const void*)w->dsv; + return sc; +} + +} /* extern "C" */ + +#else /* other platforms */ +/*== stubs (explicit platform gap; the Linux/web/mobile ports replace these) */ #if defined(__cplusplus) extern "C" { #endif @@ -1697,7 +3674,11 @@ double sapp_window_frame_duration(sapp_window win) { (void)win; return 1.0 / 60. const void* sapp_window_metal_current_drawable(sapp_window win) { (void)win; return 0; } const void* sapp_window_metal_depth_stencil_texture(sapp_window win) { (void)win; return 0; } const void* sapp_window_metal_msaa_color_texture(sapp_window win) { (void)win; return 0; } +const void* sapp_window_d3d11_render_view(sapp_window win) { (void)win; return 0; } +const void* sapp_window_d3d11_resolve_view(sapp_window win) { (void)win; return 0; } +const void* sapp_window_d3d11_depth_stencil_view(sapp_window win) { (void)win; return 0; } const void* sapp_window_macos_get_window(sapp_window win) { (void)win; return 0; } +const void* sapp_window_win32_get_hwnd(sapp_window win) { (void)win; return 0; } #if defined(__cplusplus) } /* extern "C" */ #endif diff --git a/core/include/tc/app/tcWindow.h b/core/include/tc/app/tcWindow.h index d2260fc4..e26f4be9 100644 --- a/core/include/tc/app/tcWindow.h +++ b/core/include/tc/app/tcWindow.h @@ -7,21 +7,23 @@ // One App, many windows: the main window keeps the classic implicit lifecycle // (runApp / update / draw), a secondary Window owns its own Node tree, events // and render state (WindowContext) and is driven by its display's own vsync -// (macOS: one CADisplayLink per window, delivered on the main run loop). -// Everything runs synchronously on the main thread; a fully occluded window -// simply skips drawable acquisition (fps 0), so it can never stall the others. +// (macOS: one CADisplayLink per window, delivered on the main run loop; +// Windows: one DXGI frame latency waitable object per swapchain, serviced by +// the message loop). Everything runs synchronously on the main thread; a +// fully occluded window simply skips rendering (fps 0), so it can never +// stall the others. // // GPU resources (Texture / Fbo / Mesh / Font) live in the single shared // sokol_gfx context and can be used freely from any window. // -// Platform support: macOS only for now. On other platforms createWindow() +// Platform support: macOS and Windows. On other platforms createWindow() // logs an error and returns nullptr. // // This file is included from TrussC.h (after Node / CoreEvents / WindowSettings). namespace trussc { -class TC_PLATFORMS("macos") Window { +class TC_PLATFORMS("macos,windows") Window { public: ~Window(); @@ -97,10 +99,10 @@ class TC_PLATFORMS("macos") Window { Color clearColor_ = Color(0.05f, 0.05f, 0.08f, 1.0f); }; -// Create a secondary window. macOS only for now (nullptr elsewhere). +// Create a secondary window. macOS and Windows (nullptr elsewhere). // The returned shared_ptr keeps the window alive; closing the window (user or // close()) destroys the native side while the handle stays valid (isOpen()). -TC_PLATFORMS("macos") std::shared_ptr createWindow(const WindowSettings& settings = {}); +TC_PLATFORMS("macos,windows") std::shared_ptr createWindow(const WindowSettings& settings = {}); inline Window::Window() { ctx_.isMain = false; @@ -133,8 +135,9 @@ inline void Window::setApp(std::shared_ptr app) { ctx_.rootNode = app_.get(); } -#if !defined(__APPLE__) -// Non-macOS stubs. The real implementations live in platform/mac/tcWindowMac.mm. +#if !defined(__APPLE__) && !defined(_WIN32) +// Stubs for platforms without window glue. The real implementations live in +// platform/mac/tcWindowMac.mm and platform/win/tcWindowWin.cpp. // (iOS also defines __APPLE__ but has no window glue — iOS is not in the CI // build matrix; revisit if an iOS app ever links these symbols.) inline Window::~Window() {} @@ -143,7 +146,7 @@ inline void Window::setTitle(const std::string&) {} inline int Window::getWidth() const { return 0; } inline int Window::getHeight() const { return 0; } inline std::shared_ptr createWindow(const WindowSettings&) { - logError("Window") << "createWindow(): secondary windows are only supported on macOS for now"; + logError("Window") << "createWindow(): secondary windows are only supported on macOS and Windows for now"; return nullptr; } #endif diff --git a/core/platform/win/sokol_impl.cpp b/core/platform/win/sokol_impl.cpp index 156c1a73..857f1072 100644 --- a/core/platform/win/sokol_impl.cpp +++ b/core/platform/win/sokol_impl.cpp @@ -2,7 +2,6 @@ // sokol バックエンド実装 (Windows / Linux) // ============================================================================= -#define SOKOL_IMPL #define SOKOL_NO_ENTRY // main() を自分で定義するため #if defined(__GNUC__) || defined(__clang__) @@ -12,8 +11,20 @@ # pragma GCC diagnostic ignored "-Wmissing-field-initializers" #endif +#if defined(_WIN32) +// sokol_app.h: declarations only — the Windows implementation of the sapp_* +// API (main window, run loop, events) lives in sokol_app_tc.h below. +#include "sokol_app.h" +#define SOKOL_IMPL +#include "sokol_log.h" +#define SOKOL_APP_TC_IMPL +#include "sokol_app_tc.h" +#else +// Linux: still on the upstream sokol_app.h implementation (P2 port pending) +#define SOKOL_IMPL #include "sokol_log.h" #include "sokol_app.h" +#endif #include "sokol_gfx.h" #include "sokol_glue.h" #include "util/sokol_gl_tc.h" diff --git a/core/platform/win/tcWindowWin.cpp b/core/platform/win/tcWindowWin.cpp new file mode 100644 index 00000000..95813da4 --- /dev/null +++ b/core/platform/win/tcWindowWin.cpp @@ -0,0 +1,327 @@ +// ============================================================================= +// tcWindowWin.cpp - TrussC adapter for secondary windows (Windows) +// ============================================================================= +// The native windowing layer (HWND + D3D11 flip swapchain + per-window frame +// latency waitable ticks, occlusion-tested presents, sapp_event conversion +// with the full sokol_app scancode table) lives in sokol/sokol_app_tc.h and +// knows nothing about TrussC. This file maps its C API onto TrussC: +// tick_cb -> WindowContext switch + per-window dt + update/draw the tree +// event_cb -> CoreEvents + App hooks + Node-tree dispatch (same keycode +// semantics as the main window: key == SAPP_KEYCODE_*) +// close_cb -> Window::close() +// All rendering shares the one sokol_gfx context; each window gets its own +// sokol_gl context (same pattern as Fbo). Mirror of platform/mac/tcWindowMac.mm. + +#include "TrussC.h" + +#if defined(_WIN32) +#include "sokol_app_tc.h" // declarations only (impl lives in sokol_impl.cpp) + +using namespace trussc; + +namespace { + +struct AdapterState { + sapp_window win{0}; +}; + +// Swapchain provider handed to WindowContext::acquireSwapchain. Unlike Metal +// there is no per-frame drawable: the flip-model backbuffer views persist and +// are simply handed back (they are only valid during this window's tick). +sg_swapchain acquireSecondarySwapchain(void* user) { + auto* st = static_cast(user); + sg_swapchain sc = {}; + if (!st) return sc; + const void* renderView = sapp_window_d3d11_render_view(st->win); + if (!renderView) return sc; + sc.width = sapp_window_framebuffer_width(st->win); + sc.height = sapp_window_framebuffer_height(st->win); + sc.sample_count = sapp_window_sample_count(st->win); + sc.color_format = SG_PIXELFORMAT_BGRA8; + sc.depth_format = SG_PIXELFORMAT_DEPTH_STENCIL; + sc.d3d11.render_view = renderView; + sc.d3d11.resolve_view = sapp_window_d3d11_resolve_view(st->win); + sc.d3d11.depth_stencil_view = sapp_window_d3d11_depth_stencil_view(st->win); + return sc; +} + +// --- tick: drive this window's update/draw with its context active --------- +void windowTick(sapp_window swin, void* user) { + Window* win = static_cast(user); + if (!win) return; + auto& ctx = win->context(); + + const int fbw = sapp_window_framebuffer_width(swin); + const int fbh = sapp_window_framebuffer_height(swin); + if (fbw <= 0 || fbh <= 0) return; + ctx.fbWidth = fbw; + ctx.fbHeight = fbh; + ctx.dpiScale = sapp_window_dpi_scale(swin); + // Keep a RectNode root in sync with the window's logical size (same + // contract as the main App on SAPP_EVENTTYPE_RESIZED). + win->syncRootSize((float)sapp_window_width(swin), (float)sapp_window_height(swin)); + + // Per-window delta time: wall-clock between THIS window's processed + // ticks, so getDeltaTime() inside this window's update/draw is correct + // even when its display runs at a different refresh rate than the main + // window's. A long gap (occlusion) yields one large delta, exactly like + // an event-driven main window. + { + auto callNow = std::chrono::high_resolution_clock::now(); + if (!ctx.lastUpdateCallTimeInitialized) { + ctx.lastUpdateCallTimeInitialized = true; + ctx.updateDeltaTime = sapp_window_frame_duration(swin); + } else { + ctx.updateDeltaTime = std::chrono::duration(callNow - ctx.lastUpdateCallTime).count(); + } + ctx.lastUpdateCallTime = callNow; + } + + auto* prev = internal::currentWindowCtx; + internal::currentWindowCtx = &ctx; + + // Own sokol_gl context, created lazily on the first tick (sgl is set up + // by then). Same lifecycle caveats as Fbo contexts on sgl resize. + if (ctx.swapchainTarget.context.id == SG_INVALID_ID) { + sgl_context_desc_t cdesc = {}; + cdesc.max_vertices = 65536; + cdesc.max_commands = 16384; + cdesc.color_format = SG_PIXELFORMAT_BGRA8; + cdesc.depth_format = SG_PIXELFORMAT_DEPTH_STENCIL; + cdesc.sample_count = sapp_window_sample_count(swin); + ctx.swapchainTarget.context = sgl_make_context(&cdesc); + } + sgl_set_context(ctx.swapchainTarget.context); + sgl_defaults(); + + beginFrame(); + + win->events().update.notify(); + win->tickTree(); + + const Color& cc = win->clearColor_; + clear(cc.r, cc.g, cc.b, cc.a); + win->events().draw.notify(); + win->drawTreeNow(); + + present(); + win->events().afterFrame.notify(); + + sgl_set_context(sgl_default_context()); + internal::currentWindowCtx = prev; +} + +// --- events: map sapp_event onto CoreEvents / App hooks / tree dispatch ---- +void windowEvent(const sapp_event* ev, sapp_window swin, void* user) { + Window* win = static_cast(user); + if (!win) return; + auto& ctx = win->context(); + auto* prev = internal::currentWindowCtx; + internal::currentWindowCtx = &ctx; + + // Raw event pass-through (same hook addons use on the main window) + win->events().rawEvent.notify(*ev); + + // sapp coords arrive in framebuffer pixels; TrussC windows use logical + // points (secondary windows have no pixelPerfect mode for now) + const float dpi = sapp_window_dpi_scale(swin); + const float scale = (dpi > 0.0f) ? (1.0f / dpi) : 1.0f; + const bool hasShift = (ev->modifiers & SAPP_MODIFIER_SHIFT) != 0; + const bool hasCtrl = (ev->modifiers & SAPP_MODIFIER_CTRL) != 0; + const bool hasAlt = (ev->modifiers & SAPP_MODIFIER_ALT) != 0; + const bool hasSuper = (ev->modifiers & SAPP_MODIFIER_SUPER) != 0; + + switch (ev->type) { + case SAPP_EVENTTYPE_MOUSE_DOWN: { + const float x = ev->mouse_x * scale, y = ev->mouse_y * scale; + ctx.mouseX = x; ctx.mouseY = y; + ctx.mouseButton = (int)ev->mouse_button; + ctx.mousePressed = true; + MouseEventArgs e; + e.pos = e.globalPos = Vec2(x, y); + e.button = (int)ev->mouse_button; + e.shift = hasShift; e.ctrl = hasCtrl; e.alt = hasAlt; e.super = hasSuper; + e.syncLegacy(); + win->events().mousePressed.notify(e); + if (win->getApp()) win->getApp()->mousePressed(e); + win->dispatchMousePressToTree(e); + break; + } + case SAPP_EVENTTYPE_MOUSE_UP: { + const float x = ev->mouse_x * scale, y = ev->mouse_y * scale; + ctx.mouseX = x; ctx.mouseY = y; + ctx.mouseButton = -1; + ctx.mousePressed = false; + MouseEventArgs e; + e.pos = e.globalPos = Vec2(x, y); + e.button = (int)ev->mouse_button; + e.shift = hasShift; e.ctrl = hasCtrl; e.alt = hasAlt; e.super = hasSuper; + e.syncLegacy(); + win->events().mouseReleased.notify(e); + if (win->getApp()) win->getApp()->mouseReleased(e); + win->dispatchMouseReleaseToTree(e); + break; + } + case SAPP_EVENTTYPE_MOUSE_ENTER: + case SAPP_EVENTTYPE_MOUSE_MOVE: { + const float x = ev->mouse_x * scale, y = ev->mouse_y * scale; + ctx.pmouseX = ctx.mouseX; ctx.pmouseY = ctx.mouseY; + ctx.mouseX = x; ctx.mouseY = y; + internal::MouseEventRaw raw; + raw.pos = raw.globalPos = Vec2(x, y); + raw.delta = raw.globalDelta = Vec2(ev->mouse_dx * scale, ev->mouse_dy * scale); + raw.shift = hasShift; raw.ctrl = hasCtrl; raw.alt = hasAlt; raw.super = hasSuper; + if (ctx.mousePressed && ctx.mouseButton >= 0) { + raw.button = ctx.mouseButton; + MouseDragEventArgs e = internal::toDragArgs(raw); + win->events().mouseDragged.notify(e); + if (win->getApp()) win->getApp()->mouseDragged(e); + } else { + MouseMoveEventArgs e = internal::toMoveArgs(raw); + win->events().mouseMoved.notify(e); + if (win->getApp()) win->getApp()->mouseMoved(e); + } + break; + } + case SAPP_EVENTTYPE_MOUSE_LEAVE: { + // Park the cursor offscreen so the next tick's updateHoverState + // clears this window's hover (hoveredNode lives in ITS context). + ctx.pmouseX = ctx.mouseX; ctx.pmouseY = ctx.mouseY; + ctx.mouseX = -1.0f; ctx.mouseY = -1.0f; + break; + } + case SAPP_EVENTTYPE_MOUSE_SCROLL: { + ScrollEventArgs e; + e.pos = e.globalPos = Vec2(ctx.mouseX, ctx.mouseY); + e.scroll = Vec2(ev->scroll_x, ev->scroll_y); + e.shift = hasShift; e.ctrl = hasCtrl; e.alt = hasAlt; e.super = hasSuper; + e.syncLegacy(); + win->events().mouseScrolled.notify(e); + if (win->getApp()) win->getApp()->mouseScrolled(e); + break; + } + case SAPP_EVENTTYPE_KEY_DOWN: { + KeyEventArgs e; + e.key = ev->key_code; // SAPP_KEYCODE_*, identical to the main window + e.isRepeat = ev->key_repeat; + e.shift = hasShift; e.ctrl = hasCtrl; e.alt = hasAlt; e.super = hasSuper; + if (!ev->key_repeat) ctx.keysPressed.insert(e.key); + win->events().keyPressed.notify(e); + if (win->getApp()) win->getApp()->keyPressed(e); + break; + } + case SAPP_EVENTTYPE_KEY_UP: { + KeyEventArgs e; + e.key = ev->key_code; + e.isRepeat = false; + e.shift = hasShift; e.ctrl = hasCtrl; e.alt = hasAlt; e.super = hasSuper; + ctx.keysPressed.erase(e.key); + win->events().keyReleased.notify(e); + if (win->getApp()) win->getApp()->keyReleased(e); + break; + } + case SAPP_EVENTTYPE_SUSPENDED: + logNotice("Window") << "second window occluded - skipping frames (no stall)"; + break; + case SAPP_EVENTTYPE_RESUMED: + logNotice("Window") << "second window visible again - resuming ticks"; + break; + default: + // CHAR / RESIZED / FOCUSED / UNFOCUSED: rawEvent carries them; + // size sync happens per tick. + break; + } + + internal::currentWindowCtx = prev; +} + +void windowClosed(sapp_window swin, void* user) { + (void)swin; + Window* win = static_cast(user); + if (win) win->close(); // tears down native + app state +} + +} // namespace + +// --------------------------------------------------------------------------- +// Window methods (Windows implementations) +// --------------------------------------------------------------------------- +namespace trussc { + +Window::~Window() { close(); } + +void Window::close() { + auto* st = static_cast(native_); + if (!st) return; + native_ = nullptr; // re-entrancy guard (close_cb) + ctx_.acquireSwapchain = nullptr; + ctx_.acquireSwapchainUser = nullptr; + sapp_destroy_window(st->win); + if (ctx_.swapchainTarget.context.id != SG_INVALID_ID) { + sgl_destroy_context(ctx_.swapchainTarget.context); + ctx_.swapchainTarget.context.id = SG_INVALID_ID; + ctx_.swapchainTarget.cache.clear(); + } + delete st; + if (app_) { + app_->exit(); + app_->cleanup(); + internal::attachedApps.erase(app_.get()); + app_.reset(); + ctx_.rootNode = nullptr; + } +} + +void Window::setTitle(const std::string& title) { + auto* st = static_cast(native_); + if (st) sapp_window_set_title(st->win, title.c_str()); +} + +int Window::getWidth() const { + auto* st = static_cast(native_); + return st ? sapp_window_width(st->win) : 0; +} + +int Window::getHeight() const { + auto* st = static_cast(native_); + return st ? sapp_window_height(st->win) : 0; +} + +std::shared_ptr createWindow(const WindowSettings& settings) { + if (headless::isActive()) { + logError("Window") << "createWindow(): not available in headless mode"; + return nullptr; + } + auto win = std::shared_ptr(new Window()); + auto* st = new AdapterState(); + + sapp_window_desc d = {}; + d.x = -1; + d.y = -1; + d.width = settings.width; + d.height = settings.height; + d.title = settings.title.c_str(); + d.borderless = !settings.decorated; + d.no_high_dpi = !settings.highDpi; + d.sample_count = settings.sampleCount; + // (the shared D3D11 device is owned by sokol_app_tc.h; nothing to pass) + d.tick_cb = &windowTick; + d.event_cb = &windowEvent; + d.close_cb = &windowClosed; + d.user_data = win.get(); + st->win = sapp_create_window(&d); + if (st->win.id == 0) { + delete st; + logError("Window") << "createWindow(): native window creation failed"; + return nullptr; + } + + win->native_ = st; + win->ctx_.acquireSwapchain = &acquireSecondarySwapchain; + win->ctx_.acquireSwapchainUser = st; + return win; +} + +} // namespace trussc + +#endif // _WIN32 diff --git a/docs/dev/sokol_app_tc-design.md b/docs/dev/sokol_app_tc-design.md index 2f77144d..7c883547 100644 --- a/docs/dev/sokol_app_tc-design.md +++ b/docs/dev/sokol_app_tc-design.md @@ -151,6 +151,29 @@ can feed events manually, so imgui does not chain us to sokol_app. swapchain, WndProc, DPI v2 — audited good) + sokol_app's win32 keyboard tables; DXGI waitable ticks replace WM_TIMER (kills the 64 Hz cap); main window unified. The win branch is reference source, not merged as-is. + - **P1 code COMPLETE (compile-verification via CI):** the header implements + the sapp_* API on Windows (win32 + D3D11), mirroring the mac P0b shim + strategy — platform/win/sokol_impl.cpp includes sokol_app.h + declarations-only, TrussC.h untouched; NEW tcWindowWin.cpp adapter + mirrors tcWindowMac.mm (BGRA8 secondary swapchains, persistent D3D11 + views instead of per-tick drawables). Implementation contract: + docs/dev/sapp-win32-impl-spec.md + docs/dev/multi-window-win-audit.md. + Key decisions: every swapchain (main RGB10A2, secondary BGRA8) is + flip-discard with DXGI_SWAP_CHAIN_FLAG_FRAME_LATENCY_WAITABLE_OBJECT + (flag carried through every ResizeBuffers; RTV-unbind + Flush before + resizing); the run loop blocks in MsgWaitForMultipleObjectsEx on the + per-window waitables — a tick that ends without a Present (skip_present, + occluded, minimized) holds its waitable "credit" and is timer-re-paced + until the next real Present, so neither busy-spin nor waitable + starvation can occur; resize coalesced per tick (never in WM_SIZE); + WM_ENTERSIZEMOVE arms a ~64Hz WM_TIMER that keeps ALL windows ticking + through the modal drag; scancode-indexed keytable lifted verbatim + (fixes the win branch's key=vk gap) + WM_CHAR with surrogate pairs; + quit = WM_CLOSE dance + PostQuitMessage; skip_present HONORED on + Present (TrussC flicker patch); per-monitor-v2 DPI with the + create-then-resize dance; DXGI_PRESENT_TEST occlusion poll for + secondary windows (SUSPENDED/RESUMED). Runtime verification on real + Windows hardware still pending. - **P2 — Linux driver.** X11 + GLX shared context (multiwindow-glfw pattern), timer-paced ticks. - **P3 — web driver.** RAF tick, HTML5 event callbacks, WebGL context; From 317696bb77f5e2600268b0e964748bb228965630 Mon Sep 17 00:00:00 2001 From: tettou771 Date: Thu, 9 Jul 2026 18:43:13 +0900 Subject: [PATCH 19/87] fix: qualify X11 Window type as ::Window in tcPlatform_linux.cpp The multi-window branch introduced trussc::Window, which shadows the X11 Window typedef inside namespace trussc. First CI run of this base exposed the collision (linux/web/android jobs). --- core/platform/linux/tcPlatform_linux.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/core/platform/linux/tcPlatform_linux.cpp b/core/platform/linux/tcPlatform_linux.cpp index 54af34d9..ed285f60 100644 --- a/core/platform/linux/tcPlatform_linux.cpp +++ b/core/platform/linux/tcPlatform_linux.cpp @@ -20,7 +20,8 @@ namespace trussc { void bringWindowToFront() { Display* display = XOpenDisplay(nullptr); if (!display) return; - Window window = (Window)(uintptr_t)sapp_x11_get_window(); + // ::Window = the X11 window id type (trussc::Window shadows it in this ns) + ::Window window = (::Window)(uintptr_t)sapp_x11_get_window(); if (window) { XRaiseWindow(display, window); XSetInputFocus(display, window, RevertToParent, CurrentTime); @@ -82,7 +83,8 @@ void setWindowPosition(int x, int y) { void setWindowDecorated(bool decorated) { Display* display = XOpenDisplay(nullptr); if (!display) return; - Window window = (Window)(uintptr_t)sapp_x11_get_window(); + // ::Window = the X11 window id type (trussc::Window shadows it in this ns) + ::Window window = (::Window)(uintptr_t)sapp_x11_get_window(); if (window) { // Motif WM hints: clear the decorations flag to drop the WM-drawn frame. struct { From 0ea87537ff5a927cd8a4d025cb489c457c64050e Mon Sep 17 00:00:00 2001 From: tettou771 Date: Thu, 9 Jul 2026 18:43:13 +0900 Subject: [PATCH 20/87] fix: exclude archive member header metadata from hot-reload .def export dumpbin /LINKERMEMBER:1 member headers (' 10A2B4 size', mode/uid/gid) match the hex-address symbol regex once an object file reaches 1 MB (the size field grows to 6 hex digits). sokol_impl.obj crossed that line with the win32 sokol_app_tc section, putting a bogus 'size' export in trussc_exports.def (LNK4022 + LNK2001). --- core/cmake/trussc_app.cmake | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/core/cmake/trussc_app.cmake b/core/cmake/trussc_app.cmake index b41508ac..76950e1f 100644 --- a/core/cmake/trussc_app.cmake +++ b/core/cmake/trussc_app.cmake @@ -396,6 +396,12 @@ foreach(ENTRY \${ENTRIES}) # NULL thunk data elseif(SYM MATCHES \"^_RTC_\") # Internal symbol for runtime checks + elseif(SYM MATCHES \"^(size|mode|uid|gid)$\") + # Archive member header metadata, not a symbol. The header lines + # (' 2A4B8 size', ' 0 mode', ...) only match the + # hex-address regex once a member grows to a 6-hex-digit size + # field (>= 1 MB object file), which is why small libs never hit + # this. else() list(APPEND SYMBOLS \"\${SYM}\") endif() From 11b984f73bfff73a482e190db2d252e48d8143ef Mon Sep 17 00:00:00 2001 From: tettou771 Date: Thu, 9 Jul 2026 22:01:56 +0900 Subject: [PATCH 21/87] fix: silence -Wpass-failed on the snappy dependency under Emscripten snappy 1.2.1 builds itself with -Werror on Clang; its '#pragma clang loop' vectorization hints cannot be honored under wasm, so the newer emsdk clang fails the build with -Wpass-failed. Pre-existing on main (full=true web job reproduces it there); surfaced by this branch's full CI runs. --- addons/tcxHap/CMakeLists.txt | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/addons/tcxHap/CMakeLists.txt b/addons/tcxHap/CMakeLists.txt index 8695efbd..ab5e2bf2 100644 --- a/addons/tcxHap/CMakeLists.txt +++ b/addons/tcxHap/CMakeLists.txt @@ -24,6 +24,13 @@ set(SNAPPY_BUILD_TESTS OFF CACHE BOOL "" FORCE) set(SNAPPY_BUILD_BENCHMARKS OFF CACHE BOOL "" FORCE) set(SNAPPY_INSTALL OFF CACHE BOOL "" FORCE) FetchContent_MakeAvailable(snappy) +if(EMSCRIPTEN) + # snappy compiles itself with -Werror on Clang; its '#pragma clang loop' + # vectorization hints cannot be honored under wasm, and the resulting + # -Wpass-failed diagnostic would fail the build. Silence it on the + # dependency target only. + target_compile_options(snappy PRIVATE -Wno-pass-failed) +endif() # ----------------------------------------------------------------------------- # HAP reference decoder (BSD 2-Clause) From 82703e8c84d09e5fd206fb464b6a3457bc917cb3 Mon Sep 17 00:00:00 2001 From: tettou771 Date: Thu, 9 Jul 2026 23:46:14 +0900 Subject: [PATCH 22/87] fix: capture and echo test binary output in build_all.py Child-process stdout from trusscli-built test exes gets lost on the Windows CI runners (inherited handles), leaving failing tests with no diagnostics in the log. Capture the output and re-print it through the script's own flushed stdout, plus report the raw exit code and add a 600s timeout. --- examples/build_all.py | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/examples/build_all.py b/examples/build_all.py index f6f2c6e3..2d1c5a6c 100755 --- a/examples/build_all.py +++ b/examples/build_all.py @@ -169,6 +169,24 @@ def find_test_binary(test_dir, platform_info): return p return None +def run_test_binary(binary, cwd): + # Run a test executable, CAPTURE its output and echo it through our own + # (flushed) stdout. Inherited-handle child output gets lost or reordered + # on the Windows CI runners, which made failing tests undiagnosable. + try: + r = subprocess.run([binary], cwd=cwd, stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, timeout=600) + except subprocess.TimeoutExpired as e: + if e.stdout: + print(e.stdout.decode('utf-8', errors='replace'), flush=True) + print(" (test timed out after 600s)", flush=True) + return False + if r.stdout: + print(r.stdout.decode('utf-8', errors='replace'), flush=True) + if r.returncode != 0: + print(f" (exit code {r.returncode} / 0x{r.returncode & 0xFFFFFFFF:08X})", flush=True) + return r.returncode == 0 + def run_command(cmd, cwd, verbose=False): try: if verbose: @@ -222,7 +240,7 @@ def build_and_run_test(test_dir, pg_bin, platform_info, args): return False, "binary-missing" Colors.print(f" Running {os.path.relpath(binary, test_dir)} ...", Colors.YELLOW) - if not run_command([binary], cwd=test_dir, verbose=True): # always stream test output + if not run_test_binary(binary, cwd=test_dir): # captured + echoed (see run_test_binary) return False, "run" return True, None @@ -247,7 +265,7 @@ def build_and_run_unit_test(test_dir, pg_bin, platform_info, args): return False, "binary-missing" Colors.print(f" Running {os.path.relpath(binary, test_dir)} ...", Colors.YELLOW) - if not run_command([binary], cwd=test_dir, verbose=True): # always stream test output + if not run_test_binary(binary, cwd=test_dir): # captured + echoed (see run_test_binary) return False, "run" return True, None From a9cd45ab20608f83f179c0f8828036f6c63b9e55 Mon Sep 17 00:00:00 2001 From: tettou771 Date: Fri, 10 Jul 2026 01:21:37 +0900 Subject: [PATCH 23/87] fix: open text file utilities in binary mode (byte-faithful on Windows) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit loadTextFile/saveTextFile/appendToFile used text-mode streams, so on Windows every \n was written as \r\n: getFileSize() disagreed with the saved string length while load/save round-trips still matched (both sides converted). Binary mode makes the on-disk bytes identical across platforms. Caught by core/tests/filePath on its first Windows CI run ('getFileSize: Japanese name' — the body contains a \n). --- core/include/tc/utils/tcFile.h | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/core/include/tc/utils/tcFile.h b/core/include/tc/utils/tcFile.h index 69059af3..9c911888 100644 --- a/core/include/tc/utils/tcFile.h +++ b/core/include/tc/utils/tcFile.h @@ -130,9 +130,11 @@ inline int64_t getFileSize(const fs::path& path) { // ============================================================================= // Load entire text file into string +// (binary mode: the returned string is the file's exact bytes on every +// platform; Windows text mode would silently fold \r\n into \n) inline std::string loadTextFile(const fs::path& path) { fs::path fullPath = getDataPath(path); - std::ifstream file(fullPath); + std::ifstream file(fullPath, std::ios::binary); if (!file.is_open()) { logError() << "Cannot open file: " << path; return ""; @@ -144,9 +146,11 @@ inline std::string loadTextFile(const fs::path& path) { } // Save string to text file +// (binary mode: what you pass is what lands on disk on every platform; +// Windows text mode would expand \n to \r\n, changing the file size) inline bool saveTextFile(const fs::path& path, const std::string& content) { fs::path fullPath = getDataPath(path); - std::ofstream file(fullPath); + std::ofstream file(fullPath, std::ios::binary); if (!file.is_open()) { logError() << "Cannot create file: " << path; return false; @@ -159,7 +163,7 @@ inline bool saveTextFile(const fs::path& path, const std::string& content) { // Append string to text file inline bool appendToFile(const fs::path& path, const std::string& content) { fs::path fullPath = getDataPath(path); - std::ofstream file(fullPath, std::ios::app); + std::ofstream file(fullPath, std::ios::app | std::ios::binary); if (!file.is_open()) { logError() << "Cannot open file for append: " << path; return false; From ab084bd2dd0d6ab6d708d343b989a9c8b35d438b Mon Sep 17 00:00:00 2001 From: tettou771 Date: Fri, 10 Jul 2026 04:16:09 +0900 Subject: [PATCH 24/87] doc: record P1 full-CI-green status in the design doc --- docs/dev/sokol_app_tc-design.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/dev/sokol_app_tc-design.md b/docs/dev/sokol_app_tc-design.md index 7c883547..6f745edf 100644 --- a/docs/dev/sokol_app_tc-design.md +++ b/docs/dev/sokol_app_tc-design.md @@ -151,7 +151,9 @@ can feed events manually, so imgui does not chain us to sokol_app. swapchain, WndProc, DPI v2 — audited good) + sokol_app's win32 keyboard tables; DXGI waitable ticks replace WM_TIMER (kills the 64 Hz cap); main window unified. The win branch is reference source, not merged as-is. - - **P1 code COMPLETE (compile-verification via CI):** the header implements + - **P1 code COMPLETE — full CI green on all platforms (mac/win/linux/ + web/android incl. 108 example builds, addon tests, core tests 3/3 and + the HotReload smoke test on Windows):** the header implements the sapp_* API on Windows (win32 + D3D11), mirroring the mac P0b shim strategy — platform/win/sokol_impl.cpp includes sokol_app.h declarations-only, TrussC.h untouched; NEW tcWindowWin.cpp adapter From 5f1c2f1ef2fa3be9f22bf5b6bb56891270c89856 Mon Sep 17 00:00:00 2001 From: tettou771 Date: Mon, 13 Jul 2026 11:08:00 +0900 Subject: [PATCH 25/87] feat!: make VecN single-value (splat) constructors explicit + Vec4 component-wise ops MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BREAKING (0.7.0): Vec2/Vec3/Vec4(float) and IVec2/IVec3(int) fill constructors are now explicit. 'v + 2' no longer compiles — vector + scalar is not a mathematical operation; it only worked through the implicit splat conversion. Write v + Vec3(2) when component-wise adding is intended. Scalar multiply/divide (v * 2, v / 2, 2 * v) are real overloads and unaffected. Dimension-promoting constructors (Vec3(Vec2, z), Vec4(Vec3, w), ...) stay implicit. This also aligns the Vec family with Color, whose gray constructor was already explicit. Consistency: Vec4 gains component-wise operator*/operator/ (+ *=, /=), matching Vec2/Vec3. Impact measured: all 107 examples build clean against this core (build_all.py --clean, macOS). The Lua side never supported the splat conversion (sol2 has no implicit ctor knowledge), so no sketch breaks. NOTE for 0.7.0 landing: regenerate reference-data + luagen(-types) bindings then (Vec4's new operators change the AST); done at merge time to avoid conflicts with ongoing generated-file churn on dev. --- core/include/tcMath.h | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/core/include/tcMath.h b/core/include/tcMath.h index 82e19852..ad816167 100644 --- a/core/include/tcMath.h +++ b/core/include/tcMath.h @@ -28,7 +28,7 @@ struct Vec2 { // Constructors Vec2() = default; Vec2(float x_, float y_) : x(x_), y(y_) {} - Vec2(float v) : x(v), y(v) {} + explicit Vec2(float v) : x(v), y(v) {} Vec2(const Vec2&) = default; Vec2& operator=(const Vec2&) = default; @@ -145,7 +145,7 @@ struct Vec3 { // Constructors Vec3() = default; Vec3(float x_, float y_, float z_) : x(x_), y(y_), z(z_) {} - Vec3(float v) : x(v), y(v), z(v) {} + explicit Vec3(float v) : x(v), y(v), z(v) {} Vec3(const Vec2& v, float z_ = 0.0f) : x(v.x), y(v.y), z(z_) {} Vec3(const Vec3&) = default; Vec3& operator=(const Vec3&) = default; @@ -247,7 +247,7 @@ struct IVec2 { IVec2() = default; IVec2(int x_, int y_) : x(x_), y(y_) {} - IVec2(int v) : x(v), y(v) {} + explicit IVec2(int v) : x(v), y(v) {} IVec2 operator+(const IVec2& v) const { return IVec2(x + v.x, y + v.y); } IVec2 operator-(const IVec2& v) const { return IVec2(x - v.x, y - v.y); } @@ -277,7 +277,7 @@ struct IVec3 { IVec3() = default; IVec3(int x_, int y_, int z_) : x(x_), y(y_), z(z_) {} - IVec3(int v) : x(v), y(v), z(v) {} + explicit IVec3(int v) : x(v), y(v), z(v) {} IVec3(const IVec2& v, int z_ = 0) : x(v.x), y(v.y), z(z_) {} IVec3 operator+(const IVec3& v) const { return IVec3(x + v.x, y + v.y, z + v.z); } @@ -311,7 +311,7 @@ struct Vec4 { // Constructors Vec4() = default; Vec4(float x_, float y_, float z_, float w_) : x(x_), y(y_), z(z_), w(w_) {} - Vec4(float v) : x(v), y(v), z(v), w(v) {} + explicit Vec4(float v) : x(v), y(v), z(v), w(v) {} Vec4(const Vec3& v, float w_ = 1.0f) : x(v.x), y(v.y), z(v.z), w(w_) {} Vec4(const Vec2& v, float z_ = 0.0f, float w_ = 1.0f) : x(v.x), y(v.y), z(z_), w(w_) {} Vec4(const Vec4&) = default; @@ -328,6 +328,8 @@ struct Vec4 { // Arithmetic operators Vec4 operator+(const Vec4& v) const { return Vec4(x + v.x, y + v.y, z + v.z, w + v.w); } Vec4 operator-(const Vec4& v) const { return Vec4(x - v.x, y - v.y, z - v.z, w - v.w); } + Vec4 operator*(const Vec4& v) const { return Vec4(x * v.x, y * v.y, z * v.z, w * v.w); } + Vec4 operator/(const Vec4& v) const { return Vec4(x / v.x, y / v.y, z / v.z, w / v.w); } Vec4 operator*(float s) const { return Vec4(x * s, y * s, z * s, w * s); } Vec4 operator/(float s) const { return Vec4(x / s, y / s, z / s, w / s); } Vec4 operator-() const { return Vec4(-x, -y, -z, -w); } @@ -335,6 +337,8 @@ struct Vec4 { // Compound assignment operators Vec4& operator+=(const Vec4& v) { x += v.x; y += v.y; z += v.z; w += v.w; return *this; } Vec4& operator-=(const Vec4& v) { x -= v.x; y -= v.y; z -= v.z; w -= v.w; return *this; } + Vec4& operator*=(const Vec4& v) { x *= v.x; y *= v.y; z *= v.z; w *= v.w; return *this; } + Vec4& operator/=(const Vec4& v) { x /= v.x; y /= v.y; z /= v.z; w /= v.w; return *this; } Vec4& operator*=(float s) { x *= s; y *= s; z *= s; w *= s; return *this; } Vec4& operator/=(float s) { x /= s; y /= s; z /= s; w /= s; return *this; } From a9f100be4d8c2d6f03492ba23e032339b5ced26f Mon Sep 17 00:00:00 2001 From: tettou771 Date: Mon, 13 Jul 2026 14:39:28 +0900 Subject: [PATCH 26/87] =?UTF-8?q?doc:=20fix=20FOR=5FAI=20corpus=20?= =?UTF-8?q?=E2=80=94=20plain=20RectNode=20does=20not=20auto-enable=20event?= =?UTF-8?q?s=20(+=20regen=20API=20index)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/FOR_AI_ASSISTANT.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/FOR_AI_ASSISTANT.md b/docs/FOR_AI_ASSISTANT.md index d51287b9..d3a76fd1 100644 --- a/docs/FOR_AI_ASSISTANT.md +++ b/docs/FOR_AI_ASSISTANT.md @@ -1007,10 +1007,10 @@ So: setActive / setVisible to temporarily stop/hide, destroy when you're done wi ### Custom-looking button — is click handling provided? -You don't write event handling from scratch. **RectNode has subscribable mouse events built in**, so you only custom-draw the look. RectNode exposes public Event members `mousePressed` / `mouseReleased` / `mouseDragged` / `mouseScrolled` (`Event` etc.), uses rectangle hit-testing, and **has `enableEvents()` already called in its constructor (events on by default).** Two ways to use it: +You don't write event handling from scratch. **RectNode has subscribable mouse events built in**, so you only custom-draw the look. RectNode exposes public Event members `mousePressed` / `mouseReleased` / `mouseDragged` / `mouseScrolled` (`Event` etc.) and uses rectangle hit-testing. **One required step: call `enableEvents()`** — a plain RectNode does NOT receive events by default (only ready-made widgets like `RectNodeButton` and `ScrollContainer` enable it in their constructors); forget it and the node silently never gets a click. Two ways to use it: - **Subscribe from outside (no subclass)**: `listener_ = button->mousePressed.listen([this](MouseEventArgs& e){ ... });` (keep the returned `EventListener` as a member). - **Subclass and override**: `bool onMousePress(const MouseEventArgs& e) override { ...; return true; }`. -Draw freely in local coordinates around (0,0) — rounded corners, image, shader, fully your own. (A plain `Node`, not a RectNode, needs `enableEvents()`. For a ready-made look, `RectNodeButton` — a simple color-on-press button — is built in.) +Draw freely in local coordinates around (0,0) — rounded corners, image, shader, fully your own. (For a ready-made look, `RectNodeButton` — a simple color-on-press button with events pre-enabled — is built in.) ## Events (loose coupling) @@ -3930,14 +3930,15 @@ const std::string & VideoWriter::getPath() const // Resolved output file path const VideoRecordSettings & VideoWriter::getSettings() const // Encoder settings the writer was opened with int VideoWriter::getWidth() const // Encoder output width in pixels bool VideoWriter::isOpen() const // Check if the encoder is open and accepting frames -unsigned char * VideoWriter::lockFrame(int & strideOut) // Lock and return the encoder's frame buffer for zero-copy fills; strideOut receives the row stride. Pair with submitFrame +unsigned char * VideoWriter::lockFrame(int & strideOut) [macos] // Lock and return the encoder's frame buffer for zero-copy fills; strideOut receives the row stride. Pair with submitFrame bool VideoWriter::open(const std::string & path, int width, int height, const VideoRecordSettings & settings = {}) // Open the encoder at the given size (path resolved via getDataPath) -bool VideoWriter::submitFrame(double timeSec) // Append the previously locked frame at the given presentation time (seconds) +bool VideoWriter::submitFrame(double timeSec) [macos] // Append the previously locked frame at the given presentation time (seconds) ``` ### WindowSettings — Window configuration passed to the app at startup (size, title, DPI, MSAA, fullscreen, decoration, VSync). Setters chain ```cpp +WindowSettings & WindowSettings::reserveUniformBuffer(int bytes) // Reserve the per-frame GPU uniform buffer in bytes, vector::reserve style; 0 = backend default 4MB ≈ 8k draw calls. Metal/WebGPU/Vulkan only — GL/D3D11 have no such cap (chainable) WindowSettings & WindowSettings::setClipboardSize(int size) // Set clipboard buffer size in bytes (chainable) WindowSettings & WindowSettings::setDecorated(bool enabled) // false = borderless/chromeless window that can still take focus and be closed programmatically (chainable) WindowSettings & WindowSettings::setFullscreen(bool enabled) // Enable/disable fullscreen at startup (chainable) From c1628ffdf41c77f40f91f2152fe801cad14a3a2f Mon Sep 17 00:00:00 2001 From: tettou771 Date: Mon, 13 Jul 2026 14:39:28 +0900 Subject: [PATCH 27/87] =?UTF-8?q?doc:=20roadmap=20=E2=80=94=20plan=20mouse?= =?UTF-8?q?=20press/release/drag=20bubbling=20for=20v0.7.0=20(unify=20with?= =?UTF-8?q?=20scroll)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/ROADMAP.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 8ff29d8a..85dc06c0 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -23,6 +23,7 @@ | Audio device hot-plug handling | Detect device disconnect (USB DAC unplugged etc.) via miniaudio's `ma_device_notification_proc`. On detection, auto-fail-over to the system default device, then fire `AudioEngine::audioDeviceChanged` with the new device's info. Listeners can override the fallback by calling `init(settings)` themselves. Without this, calling `init(settings)` to switch devices after a hot-unplug will hang in `ma_device_uninit` waiting for the dead device's audio thread to join. Likely also wants a `cause` enum on `AudioDeviceChangedArgs` (InitialInit / UserRequest / DeviceDisconnect) so listeners can distinguish event sources. | Medium | | Custom-shader textured quad path (sgl-integrated) | A TrussC-level drawing path that issues `sg_apply_pipeline` + `sg_apply_bindings` + `sg_draw` with a user-supplied shader, while staying ordered with respect to surrounding `sokol_gl` commands. Built on the existing `suspendSwapchainPass()` / `resumeSwapchainPass()` machinery (already used by FBO), plus a partial-flush of the default sgl context so a `drawRect → font/customShader → drawCircle` sequence renders in submission order. Unlocks: R8 / RG8 / non-RGBA texture formats (current sgl shader forces RGBA8 with `(255,255,255,A)` padding), font atlas R8 storage (see below), sensor-data visualization (depth/IR/grayscale with palette mapping), SDF text, mask compositing, future Path glyph rendering. Probably surfaces as `Texture::drawWith(Shader&, ...)` or similar on the TrussC `gpu/` side; sgl_gl_tc fork ideally stays untouched. | High | | Font atlas R8 storage | Drop font atlas from RGBA8 to R8 (4× memory reduction — e.g. ~4 MB → ~1 MB on a large CJK atlas). Currently blocked on the custom-shader path above: the sgl built-in shader returns `(R, 0, 0, 1)` from an R8 sample, which renders as solid red instead of `(color.rgb, glyph.a)`. Once that path lands, swap the atlas pixel format + add a tiny font-specific fragment shader doing `out = vec4(uColor.rgb, sample.r * uColor.a)`. No quality loss (glyphs were already grayscale alpha stored as `(255,255,255,A)`). Sokol R8 is native on all current backends (Metal / D3D11 / GL 3.3 / GLES3 / WebGL2 / WebGPU — GLES2 / WebGL1 already dropped). Pair with this PR cycle's tategaki work to make Japanese / CJK use cases cheap on RAM. | Low (once shader path exists) | +| Mouse event bubbling (press/release/drag) — **v0.7.0, breaking** | Unify input propagation with the scroll path, which already bubbles ("return false to allow bubbling to parent", see `RectNode::onMouseScroll`). Today `dispatchMousePress` delivers to the front-most hit node ONLY — if its `fireMousePress` returns false the event silently dies (no ancestor walk, no occluded-sibling fallback), so press/release/drag and scroll follow different rules. Plan: when the hit node does not consume, walk the parent chain (localizing via `localizeMouse` per level, firing node + mod hooks), and the first consumer becomes `grabbedNode` for drag tracking. Keep `RectNode::onMousePress`'s `return true` (consume) default, so bubbling activates only where a handler explicitly returns false — a path that today just swallows the event, making the change near-compatible in practice but still semantically breaking (hence the 0.7.0 window). Kills the current anti-pattern where a child must know its parent to forward input (contradicts the "dependencies point downward only" rule) and unblocks drag-through-children cases (grab a window by its title bar across label children; drag-scroll a list over its buttons). Watch mouse enter/leave consistency along the chain. **Considered and deferred, revisit after bubbling lands:** flipping hit-testing to default-ON ("visible RectNode catches clicks", DOM/Unity-style, opt-out via `disableEvents()`). Only sane as a package with bubbling + consume-by-choice (make `RectNode::onMousePress` return whether a listener consumed instead of unconditional true); default-ON alone under single-target dispatch would make every decorative label/separator child swallow its parent widget's clicks. Perf is a non-issue either way: the hit-test walk already visits all active+visible nodes (Mat4 inverse + children snapshot per node) and `eventsEnabled_` only gates the final rect test. | Medium | | Font line wrapping (`enableWrap` + `setMaxLineLength`) | Opt-in text wrapping that flows long strings inside a length budget, default off so existing `drawString` behavior is unchanged. Staged rollout, all sharing the same internal "text → break-opportunity list → placement" split so later phases only refine the second half. **Phase 1**: basic wrap for horizontal text — break at ASCII spaces and between CJK ideographs (simplified UAX #14). **Phase 2**: Latin hyphenation — insert a `-` when a word has to be split mid-run, no dictionary, just last-resort. **Phase 3**: CJK `kinsoku` (行頭/行末禁則) opt-in via `setHangingPunctuation(true)` or similar — prevent `、。」』)` etc. from starting a line, either by hanging past the edge (ぶら下げ) or pulling back into the previous line (追い込み). Use a small kinsoku character set, not a full table. **Phase 4**: reuse the same pipeline for vertical writing — `maxLineLength` then means column height, columns break right→left as already implemented, kinsoku tables carry over. Justification / Knuth-Plass / advanced Western typography stays out of scope. Differentiator versus other creative-coding frameworks (oF / Cinder / Processing) where wrap is either absent or western-only. Likely lives on its own branch (`feat/font-wrap`) after the tategaki PR lands so the diffs stay reviewable. | Medium | ### Medium Priority From f8f81054fd338bc00d76e59d04771519d0223ebe Mon Sep 17 00:00:00 2001 From: tettou771 Date: Mon, 13 Jul 2026 14:47:59 +0900 Subject: [PATCH 28/87] add: emit-sketch-reference.js (Lua-flavored reference data) + implicit-conv audit tool emit-sketch-reference.js emits trussc.org/generated/trusssketch-ref.js: same schema and global (TrussCAPI) as trussc-api.js so the main reference renderer is reused unchanged, but filtered to the Lua-bound surface with Lua-flavored signatures (colon methods, dot statics, number/boolean/string type names, Lua-ified defaults) and a hand-written Tasks category (spawn/wait/forever, en/ja/ko). implicit-conv-audit.js cross-references reference-data args x type constructors (explicit recovered by header grep) to list every bound arg site relying on a C++ implicit conversion Lua cannot perform. --- addons/tcxLua/tools/implicit-conv-audit.js | 133 ++++++++ docs/scripts/emit-sketch-reference.js | 367 +++++++++++++++++++++ 2 files changed, 500 insertions(+) create mode 100644 addons/tcxLua/tools/implicit-conv-audit.js create mode 100644 docs/scripts/emit-sketch-reference.js diff --git a/addons/tcxLua/tools/implicit-conv-audit.js b/addons/tcxLua/tools/implicit-conv-audit.js new file mode 100644 index 00000000..246c7c6e --- /dev/null +++ b/addons/tcxLua/tools/implicit-conv-audit.js @@ -0,0 +1,133 @@ +#!/usr/bin/env node +/** + * implicit-conv-audit.js — enumerate every bound function/method argument that + * relies on a C++ implicit conversion Lua cannot perform. + * + * A C++ call like drawLine(vec2a, vec2b) against drawLine(Vec3, Vec3) works + * because Vec3 has a non-explicit Vec3(const Vec2&) constructor. sol2 knows + * nothing about C++ converting constructors: with safeties off the wrong-type + * argument is UB (garbage reinterpret for userdata, NULL deref for + * non-userdata like numbers/strings). This script cross-references + * reference-data.json (all bound signatures x all type constructors) to list + * every such site, so adapters (see tcxLuaPathAdapter.h) can be added where + * the conversion is worth supporting. + * + * node implicit-conv-audit.js [path/to/reference-data.json] + * + * `explicit` is not captured in the AST extraction, so it is recovered by + * scanning core/include headers for `explicit TypeName(...)` and matching the + * first parameter's base type. + */ +const fs = require('fs'); +const path = require('path'); +const { execSync } = require('child_process'); + +const RDJ = process.argv[2] || + path.join(__dirname, '../../../docs/reference/reference-data.json'); +const CORE_INCLUDE = path.join(__dirname, '../../../core/include'); +const data = JSON.parse(fs.readFileSync(RDJ, 'utf8')); + +// ---- helpers ----------------------------------------------------------------- +const baseType = (t) => (t || '') + .replace(/&&?/g, '').replace(/\bconst\b/g, '').replace(/\btrussc::/g, '') + .replace(/\bstd::filesystem::/g, 'fs::').trim(); +const NUMBER = new Set(['float', 'double', 'int', 'unsigned', 'unsigned int', + 'long', 'size_t', 'int8_t', 'int16_t', 'int32_t', 'int64_t', + 'uint8_t', 'uint16_t', 'uint32_t', 'uint64_t', 'short', 'char']); +const STRING = new Set(['std::string', 'string', 'std::string_view', + 'string_view', 'const char *', 'char *']); +const luaKind = (bt) => NUMBER.has(bt) ? 'number' + : STRING.has(bt) ? 'string' + : bt === 'bool' ? 'bool' + : bt === 'fs::path' || bt === 'path' ? 'path' + : null; // null = not natively representable (class type / other) + +// ---- collect bound top-level types + their converting constructors ------------ +const types = {}; // name -> entry +for (const id in data) { + const e = data[id]; + if (e.kind === 'type' && !e.owner && !e.ns && !e.hidden) types[e.name] = e; +} +const enums = new Set(); +for (const id in data) { + const e = data[id]; + if (e.kind === 'enum' && !e.owner && !e.ns) enums.add(e.name); +} + +// explicit-ctor recovery: "explicit TypeName(" + first param base type +const explicitSet = new Set(); // "Type<-FirstParamBase" +try { + const out = execSync( + `grep -rhoE "explicit +[A-Za-z_][A-Za-z0-9_]* *\\([^,)]*" ${JSON.stringify(CORE_INCLUDE)}`, + { encoding: 'utf8', maxBuffer: 1 << 24 }); + for (const line of out.split('\n')) { + const m = line.match(/explicit +([A-Za-z_][A-Za-z0-9_]*) *\(([^,)]*)/); + if (!m) continue; + const first = baseType(m[2].replace(/[A-Za-z_][A-Za-z0-9_]*$/, '').trim() || m[2]); + explicitSet.add(`${m[1]}<-${first}`); + } +} catch (e) { /* grep found nothing */ } + +// converting ctors: exactly 1 required arg, source != self +const convSources = {}; // TypeName -> [{from, fromKind, params, explicit}] +for (const name in types) { + for (const c of (types[name].constructors || [])) { + const args = c.args || []; + const required = args.filter(a => !a.hasDefault); + if (required.length !== 1) continue; + const from = baseType(required[0].type); + if (from === name) continue; // copy/move + const kind = luaKind(from) || (types[from] ? 'usertype' : (enums.has(from) ? 'enum' : 'other')); + const explicit = explicitSet.has(`${name}<-${from}`); + (convSources[name] ||= []).push({ from, fromKind: kind, params: c.params, explicit }); + } +} + +// ---- walk every bound callable's args ----------------------------------------- +// Mirrors luagen bindability: skip template sigs, pointer/array args. +const sites = {}; // "To<-From" -> {explicit, examples: [], count} +let usertypeArgSites = 0; // context stat for the SOL-safeties decision +function scanSignature(ownerLabel, fnName, sig) { + if (sig.tmpl) return; + for (const a of (sig.args || [])) { + if (a.isPointer || a.isArray) return; // sig not bound at all + } + for (const a of (sig.args || [])) { + const bt = baseType(a.type); + if (!types[bt]) continue; // not a class-type arg + usertypeArgSites++; + for (const cs of (convSources[bt] || [])) { + if (cs.fromKind === 'other') continue; // not reachable from Lua anyway + const key = `${bt} <- ${cs.from}${cs.explicit ? ' [explicit in C++]' : ''}`; + const s = (sites[key] ||= { count: 0, examples: [], explicit: cs.explicit }); + s.count++; + if (s.examples.length < 8) + s.examples.push(`${ownerLabel}${fnName}(${(sig.args || []).map(x => baseType(x.type)).join(', ')})`); + } + } +} +for (const id in data) { + const e = data[id]; + if (e.hidden || e.deprecated) continue; + if (e.kind === 'func' && !e.owner && !e.ns) { + for (const sig of (e.signatures || [])) scanSignature('', e.name, sig); + } else if (e.kind === 'method' && e.owner) { + if (e.access && e.access !== 'public') continue; + const owner = data[e.owner]; + if (!owner || owner.hidden) continue; + for (const sig of (e.signatures || [])) scanSignature(owner.name + ':', e.name, sig); + } +} + +// ---- report -------------------------------------------------------------------- +const entries = Object.entries(sites).sort((a, b) => b[1].count - a[1].count); +console.log('== Implicit-conversion reliance sites (bound args whose C++ type has a'); +console.log(' converting ctor from a Lua-representable source) ==\n'); +for (const [key, s] of entries) { + console.log(`${key} — ${s.count} arg site(s)`); + for (const ex of s.examples) console.log(` ${ex}`); + if (s.count > s.examples.length) console.log(` ... +${s.count - s.examples.length} more`); + console.log(''); +} +console.log(`(context: ${usertypeArgSites} bound arg sites take a usertype at all — every one`); +console.log(` of them is UB on a wrong-typed argument while sol safeties are off)`); diff --git a/docs/scripts/emit-sketch-reference.js b/docs/scripts/emit-sketch-reference.js new file mode 100644 index 00000000..af75c6b9 --- /dev/null +++ b/docs/scripts/emit-sketch-reference.js @@ -0,0 +1,367 @@ +#!/usr/bin/env node +/** + * emit-sketch-reference.js — emit the TrussSketch REFERENCE PAGE data + * (trussc.org/generated/trusssketch-ref.js, global `TrussCAPI`). + * + * This is a LUA-flavored twin of the C++ web reference emitted by emit-web.js. + * It deliberately reuses the SAME global name (`TrussCAPI`) and the SAME output + * schema so that trussc.org/reference/reference.js can render it UNCHANGED — the + * only difference is the CONTENT: + * + * • Surface — only what tcxLua actually binds, mirroring the bindability rules + * in emit-sketch-api.js (argBindable + UNBOUND_TYPES + hidden / + * deprecated skips). + * • Signatures — rendered as Lua call forms (no C++ types / no `const&`): + * free fns `drawRect(x, y, w, h)`, instance methods with colon + * syntax (`mesh:draw()`), statics/fields with dot syntax + * (`Color.fromHex`, `vec2.x`), enums accessed as `BlendMode.Add`. + * • Return types — mapped to Lua-facing names (number/boolean/string/(nothing) + * or the TrussC type name). + * • Macros are dropped (no C preprocessor in Lua). A hand-written "Tasks" + * category advertises the playground host API (spawn / wait / forever). + * + * Prose (desc/desc_ja/desc_ko) + keywords are copied VERBATIM from reference-data + * so search and i18n inherit for free. + * + * Usage: node emit-sketch-reference.js + * Output: ../../../trussc.org/generated/trusssketch-ref.js + */ + +const fs = require('fs'); +const path = require('path'); +const { execSync } = require('child_process'); + +// --- paths ------------------------------------------------------------------- +const REF_DATA = path.join(__dirname, '../reference/reference-data.json'); +const CATEGORIES = path.join(__dirname, '../reference/categories.json'); +const COLORS = path.join(__dirname, '../reference/colors.json'); +const EXTRAS = path.join(__dirname, '../reference/extras.json'); +const OUT = path.join(__dirname, '../../../trussc.org/generated/trusssketch-ref.js'); + +const REF = JSON.parse(fs.readFileSync(REF_DATA, 'utf8')); +const CATS = JSON.parse(fs.readFileSync(CATEGORIES, 'utf8')); +const CAT_BY_ID = new Map(CATS.map(c => [c.id, c])); +const colorsData = JSON.parse(fs.readFileSync(COLORS, 'utf8')); +const extras = JSON.parse(fs.readFileSync(EXTRAS, 'utf8')); + +// ---- bindability rules (mirrors emit-sketch-api.js — advertise only what's bound) +const PRIM = new Set(['void', 'bool', 'char', 'short', 'int', 'long', 'float', 'double', 'unsigned', 'signed', 'size_t', 'int8_t', 'int16_t', 'int32_t', 'int64_t', 'uint8_t', 'uint16_t', 'uint32_t', 'uint64_t']); +function argBindable(a) { + if (a.isArray || a.isPointer) return false; + if (/\binternal::/.test(a.type)) return false; + if (a.isRef && !a.isConst && !/&&/.test(a.type)) { + const base = a.type.replace(/[&*]/g, '').replace(/\bconst\b/g, '').trim(); + const allPrim = base.split(/\s+/).every(w => PRIM.has(w)); + const stdValue = base.includes('std::') || /\b(string|basic_string|vector|map|set|pair|tuple|function)\b/.test(base); + if (allPrim || stdValue) return false; + } + return true; +} +// types NOT bound (mirror emit-sketch-api / luagen-types EXCLUDE of internal::-poisoned ones) +const UNBOUND_TYPES = new Set(['Thread', 'SoundSource', 'Environment', 'VideoPlayerBase']); + +// ---- small helpers ---------------------------------------------------------- +const en = d => (d && d.en) || ''; +const ja = d => (d && d.ja) || ''; +const ko = d => (d && d.ko) || ''; +const cleanName = n => (n || '').replace(/_+$/, ''); +// receiver variable name for instance calls: Vec2 -> vec2, Color -> color. +const recvName = t => t ? t.charAt(0).toLowerCase() + t.slice(1) : t; + +// Map a C++ return/parameter type to a Lua-facing name. +// float/double/int/size_t/... -> number, bool -> boolean, +// std::string / fs::path -> string, void -> (nothing), +// containers -> table / function, TrussC types keep their (cleaned) name. +const NUM = new Set(['char', 'short', 'int', 'long', 'float', 'double', 'unsigned', 'signed', 'size_t', + 'int8_t', 'int16_t', 'int32_t', 'int64_t', 'uint8_t', 'uint16_t', 'uint32_t', 'uint64_t']); +function mapType(t) { + if (t == null) return '(nothing)'; + let s = String(t).trim(); + // strip reference / pointer / const qualifiers + s = s.replace(/&&/g, '').replace(/[&*]/g, '').replace(/\bconst\b/g, '').replace(/\bconstexpr\b/g, '').trim(); + if (s === '' || s === 'void') return '(nothing)'; + if (s === 'bool') return 'boolean'; + // strip a leading namespace chain (std::, tc::, fs::, ...) for the base check + const bare = s.replace(/^(?:[A-Za-z_]\w*::)+/, ''); + if (bare === 'string' || bare === 'basic_string' || bare === 'path' || bare === 'string_view') return 'string'; + if (/^vector\b/.test(bare) || /^(map|set|unordered_map|unordered_set|array)\b/.test(bare)) return 'table'; + if (/^function\b/.test(bare)) return 'function'; + // primitive number words (possibly multi-word like "unsigned int") + const words = s.split(/\s+/); + if (words.every(w => NUM.has(w))) return 'number'; + // a TrussC / user type — keep its bare name (drop namespace + template args) + return bare.replace(/<.*$/, '') || s; +} + +// Convert a C++ default-value expression into a Lua-facing literal. +// nullptr -> nil, std::string("x") -> "x", std::function<…>(…) -> nil, +// empty std:: containers -> {}, X::Y -> X.Y, 1.0f -> 1.0. +function luaDefault(d) { + let s = String(d).trim(); + if (s === 'nullptr' || s === 'NULL') return 'nil'; + let m = s.match(/^std::string\((.*)\)$/); + if (m) { const inner = m[1].trim(); return inner === '' ? '""' : inner; } + m = s.match(/^"(.*)"s?$/); + if (m) return `"${m[1]}"`; + if (/^std::function\b/.test(s)) return 'nil'; + if (/^std::(vector|map|set|unordered_map|unordered_set|array)\b.*\(\s*\)$/.test(s)) return '{}'; + if (/^std::.*\(\s*\)$/.test(s)) return 'nil'; + if (/^-?\d*\.?\d+f$/.test(s)) return s.replace(/f$/, ''); // strip float suffix + return s.replace(/::/g, '.'); // X::Y (enum/const) -> X.Y +} + +// Lua parameter string from a signature's args (no types). Optional args (those +// with a C++ default) render Lua-style as `name = default`. +function luaParams(args, { withDefaults = true } = {}) { + return (args || []).map((a, i) => { + const nm = cleanName(a.name) || ('a' + i); + if (withDefaults && a.hasDefault && a.default != null) return `${nm} = ${luaDefault(a.default)}`; + return nm; + }).join(', '); +} + +// A signature is bindable when it is not a template and every arg is bindable. +function sigBindable(sig) { + if (!sig || sig.tmpl) return false; + return (sig.args || []).every(argBindable); +} + +function getVersion() { + try { + return execSync('git describe --tags --abbrev=0', { cwd: path.join(__dirname, '../..') }) + .toString().trim(); + } catch { return 'unknown'; } +} + +// ============================================================================= +// Free functions -> categories (grouped + ordered via categories.json) +// ============================================================================= +const OTHER_ID = '__other__'; +const funcsByCat = new Map(); // catId -> [func entry, …] +for (const id in REF) { + const e = REF[id]; + if (e.kind !== 'func' || e.owner || e.ns || e.hidden || e.deprecated) continue; + if (e.tparams && e.tparams.length) continue; + if (/^operator/.test(e.name)) continue; + const bindSigs = (e.signatures || []).filter(sigBindable); + if (!bindSigs.length) continue; + + const catId = e.category && CAT_BY_ID.has(e.category) ? e.category : OTHER_ID; + if (!funcsByCat.has(catId)) funcsByCat.set(catId, []); + const bucket = funcsByCat.get(catId); + // one entry per bindable overload signature (legacy-flattened shape) + for (const sig of bindSigs) { + bucket.push({ + name: e.name, + params: luaParams(sig.args, { withDefaults: false }), + params_typed: luaParams(sig.args, { withDefaults: true }), + return_type: mapType(sig.ret), + desc: en(e.description), + keywords: Array.isArray(e.keywords) ? e.keywords : [], + desc_ja: ja(e.description), + desc_ko: ko(e.description), + }); + } +} + +// Emit categories in categories.json order, then leftover ids, then Other. +const orderedCatIds = [ + ...CATS.slice().sort((a, b) => (a.order ?? 1e9) - (b.order ?? 1e9)).map(c => c.id).filter(id => funcsByCat.has(id)), + ...[...funcsByCat.keys()].filter(id => id !== OTHER_ID && !CAT_BY_ID.has(id)), + ...(funcsByCat.has(OTHER_ID) ? [OTHER_ID] : []), +]; +const catDisplay = (id) => { + if (id === OTHER_ID) return { name: 'Other', name_ja: 'その他', name_ko: '기타' }; + const c = CAT_BY_ID.get(id); + return c ? { name: c.name, name_ja: c.name_ja || '', name_ko: c.name_ko || '' } : { name: id, name_ja: '', name_ko: '' }; +}; + +const categories = []; + +// Hand-written "Tasks" category — the playground host API (cooperative tasks). +// These functions are NOT in reference-data; they are provided by the sketch runtime. +categories.push({ + name: 'Tasks', + name_ja: 'タスク', + name_ko: '태스크', + functions: [ + { + name: 'spawn', params: 'fn', params_typed: 'fn', return_type: '(nothing)', + desc: 'Start a cooperative task; runs until fn returns.', + desc_ja: '協調タスクを開始。fnが終わるまで動く', + desc_ko: '협조 태스크를 시작; fn이 반환될 때까지 실행됨.', + keywords: ['coroutine', 'task', 'async', 'thread', 'concurrent'], + }, + { + name: 'wait', params: 'seconds', params_typed: 'seconds', return_type: '(nothing)', + desc: 'Pause the current task for the given number of seconds.', + desc_ja: '現在のタスクを指定秒だけ一時停止する', + desc_ko: '현재 태스크를 주어진 초만큼 일시정지한다.', + keywords: ['sleep', 'delay', 'pause', 'yield', 'task'], + }, + { + name: 'forever', params: 'fn', params_typed: 'fn', return_type: '(nothing)', + desc: 'Repeat fn every frame, with an implicit wait(0) between iterations.', + desc_ja: '毎フレームfnを繰り返す(暗黙のwait(0)付き)', + desc_ko: '매 프레임 fn을 반복하며, 반복마다 암묵적으로 wait(0)을 넣는다.', + keywords: ['loop', 'repeat', 'every', 'frame', 'task'], + }, + ], +}); + +for (const catId of orderedCatIds) { + const functions = funcsByCat.get(catId); + if (!functions || !functions.length) continue; + const d = catDisplay(catId); + categories.push({ name: d.name, functions, name_ja: d.name_ja, name_ko: d.name_ko }); +} + +// ============================================================================= +// Types (with Lua-flavored constructor / properties / methods / static methods) +// ============================================================================= +const types = []; +for (const id in REF) { + const e = REF[id]; + if (e.kind !== 'type' || e.owner || e.ns || e.hidden) continue; + if (UNBOUND_TYPES.has(e.name)) continue; + if (e.tparams && e.tparams.length && !(e.lua_bind && e.lua_bind.length)) continue; + + const typeName = e.name; + const recv = recvName(typeName); + const t = { + name: typeName, + desc: en(e.description), + keywords: Array.isArray(e.keywords) ? e.keywords : [], + desc_ja: ja(e.description), + desc_ko: ko(e.description), + }; + if (Array.isArray(e.related) && e.related.length) t.related = e.related; + + // constructor: every bindable ctor except the copy ctor (single self-typed arg). + const ctors = (e.constructors || []).filter(c => (c.args || []).every(argBindable)) + .filter(c => { const a = c.args || []; return !(a.length === 1 && a[0].type.replace(/[&]/g, '').replace(/\bconst\b/g, '').trim() === typeName); }); + if (ctors.length) { + // Type(...) call form (sol __call). Render every signature. + t.constructor = { signatures: ctors.map(c => luaParams(c.args)) }; + } + + const properties = [], methods = [], statics = []; + for (const mid in REF) { + const m = REF[mid]; + if (m.owner !== e.id) continue; + if (m.access && m.access !== 'public') continue; + if (m.hidden || m.deprecated) continue; + if (/^operator/.test(m.name)) continue; + if (m.kind === 'field') { + const ft = m.type || ''; + if (!ft || /\binternal::/.test(ft) || /\*/.test(ft) || /\[/.test(ft)) continue; + // fields accessed with dot syntax on an instance: `vec2.x` + properties.push({ + name: `${recv}.${m.name}`, + type: mapType(ft), + desc: en(m.description), + desc_ja: ja(m.description), + desc_ko: ko(m.description), + }); + } else if (m.kind === 'method') { + const bindSigs = (m.signatures || []).filter(sigBindable); + if (!bindSigs.length) continue; + const isStatic = !!m.static; + // instance -> colon syntax `recv:method`; static -> dot syntax `Type.method` + const dispName = isStatic ? `${typeName}.${m.name}` : `${recv}:${m.name}`; + (isStatic ? statics : methods).push({ + name: dispName, + return: mapType(bindSigs[0].ret), + signatures: bindSigs.map(s => luaParams(s.args)), + desc: en(m.description), + desc_ja: ja(m.description), + desc_ko: ko(m.description), + }); + } + } + if (properties.length) t.properties = properties; + if (methods.length) t.methods = methods; + if (statics.length) t.static_methods = statics; + types.push(t); +} +types.sort((a, b) => a.name.localeCompare(b.name)); + +// ============================================================================= +// Enums (values accessed in Lua as `BlendMode.Add`) +// ============================================================================= +const enums = []; +for (const id in REF) { + const e = REF[id]; + if (e.kind !== 'enum' || e.owner || e.ns || e.hidden) continue; + if (!e.members || !e.members.length) continue; + const out = { + name: e.name, + desc: en(e.description), + keywords: Array.isArray(e.keywords) ? e.keywords : [], + values: e.members.map(m => ({ + name: m.name, value: m.value, + desc: '', desc_ja: '', desc_ko: '', + })), + desc_ja: ja(e.description), + desc_ko: ko(e.description), + }; + if (Array.isArray(e.related) && e.related.length) out.related = e.related; + enums.push(out); +} +enums.sort((a, b) => a.name.localeCompare(b.name)); + +// ============================================================================= +// Constants (kind:var, non-hidden — matches the generated bindings) +// ============================================================================= +const constants = []; +for (const id in REF) { + const e = REF[id]; + if (e.kind !== 'var' || e.owner || e.ns || e.hidden) continue; + constants.push({ + name: e.name, + value: extras.constants[e.id] ?? extras.constants[e.name] ?? '', + desc: en(e.description), + desc_ja: ja(e.description), + desc_ko: ko(e.description), + keywords: Array.isArray(e.keywords) ? e.keywords : [], + }); +} +constants.sort((a, b) => a.name.localeCompare(b.name)); + +// ============================================================================= +// Emit — SAME global (`TrussCAPI`) + SAME schema as generated/trussc-api.js. +// `macros` is intentionally omitted (empty) — Lua has no preprocessor. +// ============================================================================= +const output = { + version: getVersion() + ' (Lua)', + lang: 'all', + categories, + constants, + keywords: extras.keywords, + types, + enums, + macros: [], + colors: colorsData, +}; + +const js = `// TrussSketch Reference (Lua) +// Lua-flavored twin of the C++ API reference. SAME global (\`TrussCAPI\`) and +// schema as generated/trussc-api.js so /reference/reference.js renders it unchanged. +// +// AUTO-GENERATED by docs/scripts/emit-sketch-reference.js +// Source: docs/reference/reference-data.json (bound surface only) + colors.json. +// Do not edit directly. + +const TrussCAPI = ${JSON.stringify(output, null, 4)}; + +// Export for different environments +if (typeof module !== 'undefined' && module.exports) { + module.exports = TrussCAPI; +} +`; + +fs.writeFileSync(OUT, js); +const fnCount = categories.reduce((n, c) => n + c.functions.length, 0); +console.log(`[emit-sketch-reference] wrote ${path.relative(process.cwd(), OUT)}`); +console.log(` version: ${output.version}`); +console.log(` categories: ${categories.length} functions(overload rows): ${fnCount} types: ${types.length} enums: ${enums.length} constants: ${constants.length} colorGroups: ${colorsData.length}`); From 2cd7ecba1e7773ade60c975587255b816e0200d0 Mon Sep 17 00:00:00 2001 From: tettou771 Date: Mon, 13 Jul 2026 15:13:31 +0900 Subject: [PATCH 29/87] fix: mark trusssketch-ref.js as lang:'lua' (renderer picks '.' scope separator) --- docs/scripts/emit-sketch-reference.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/scripts/emit-sketch-reference.js b/docs/scripts/emit-sketch-reference.js index af75c6b9..de9c7560 100644 --- a/docs/scripts/emit-sketch-reference.js +++ b/docs/scripts/emit-sketch-reference.js @@ -334,7 +334,7 @@ constants.sort((a, b) => a.name.localeCompare(b.name)); // ============================================================================= const output = { version: getVersion() + ' (Lua)', - lang: 'all', + lang: 'lua', // renderer switches scope separator ('.' not '::') on this categories, constants, keywords: extras.keywords, From 1c827107b84845f6a2eb158d0e466fa1a61fb71d Mon Sep 17 00:00:00 2001 From: tettou771 Date: Mon, 13 Jul 2026 15:48:01 +0900 Subject: [PATCH 30/87] fix(tcxLua): enable Type(...) constructor call form via sol::call_constructor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Vec3(1,2,3) / Color(1,0,0) / Fbo() all failed with "attempt to call a table value": positional sol::constructors<> only provides Type.new(), not __call on the usertype table. This was never caught because every example and every bindcheck call check used the .new() form — the Type(...) form documented in the sketch reference/completions never worked anywhere. luagen-types now emits sol::call_constructor alongside the positional list for every generated usertype (regenerated), and all 17 hand-written registrations (Fbo/Image/Mat3/Mat4/Shader/Texture/Json/Xml/... + the Tween template) get the same pair. bindcheck: +2 regression checks calling the CALL form for a generated type (Vec3/Color) and hand-written ones (Fbo/Mat4) — 31 checks green. --- .../tcxLua/bindcheck/bin/data/bindcheck.lua | 13 ++ .../src/generated/trussctype_generated.cpp | 120 ++++++++++++------ addons/tcxLua/src/tcxLua.cpp | 43 +++++-- addons/tcxLua/src/tcxLua.h | 6 + .../tcxLua/tools/luagen-types/luagen-types.js | 9 +- 5 files changed, 145 insertions(+), 46 deletions(-) diff --git a/addons/tcxLua/bindcheck/bin/data/bindcheck.lua b/addons/tcxLua/bindcheck/bin/data/bindcheck.lua index 0ecad903..b10829b5 100644 --- a/addons/tcxLua/bindcheck/bin/data/bindcheck.lua +++ b/addons/tcxLua/bindcheck/bin/data/bindcheck.lua @@ -105,6 +105,19 @@ try("fs::path to Lua string", function() -- tcxLuaPathAdapter: path -> stri local p = getDataPath('bindcheck_probe.txt') return type(p) == 'string' and #p > 0 end) +try("ctor CALL form (generated)", function() -- Type(...) via sol::call_constructor + -- .new() alone passing is NOT enough: without call_constructor the + -- usertype table has no __call and Vec3(1,2,3) dies with + -- "attempt to call a table value". Regression check for both worlds. + local v = Vec3(1, 2, 3) + local c = Color(1, 0, 0) + return approx(v.z, 3) and approx(c.r, 1) +end) +try("ctor CALL form (hand-written)", function() + local f = Fbo() + local m = Mat4() + return f ~= nil and m ~= nil +end) -- Emit machine-parseable summary via print (base lib; goes to stdout). print("##BINDCHECK_BEGIN##") diff --git a/addons/tcxLua/src/generated/trussctype_generated.cpp b/addons/tcxLua/src/generated/trussctype_generated.cpp index 62ad8eb8..533d9a4a 100644 --- a/addons/tcxLua/src/generated/trussctype_generated.cpp +++ b/addons/tcxLua/src/generated/trussctype_generated.cpp @@ -12,6 +12,7 @@ void tcxLua::setGeneratedTypeBindings(const std::shared_ptr& lua) { { sol::usertype t = lua->new_usertype("Vec2", sol::constructors(), + sol::call_constructor, sol::constructors(), sol::meta_function::index, [](const trussc::Vec2& a, int b){ return a[b]; }, sol::meta_function::addition, [](const trussc::Vec2& a, const trussc::Vec2 & b){ return a + b; }, sol::meta_function::subtraction, [](const trussc::Vec2& a, const trussc::Vec2 & b){ return a - b; }, @@ -42,6 +43,7 @@ void tcxLua::setGeneratedTypeBindings(const std::shared_ptr& lua) { { sol::usertype t = lua->new_usertype("Vec3", sol::constructors(), + sol::call_constructor, sol::constructors(), sol::meta_function::index, [](const trussc::Vec3& a, int b){ return a[b]; }, sol::meta_function::addition, [](const trussc::Vec3& a, const trussc::Vec3 & b){ return a + b; }, sol::meta_function::subtraction, [](const trussc::Vec3& a, const trussc::Vec3 & b){ return a - b; }, @@ -69,6 +71,7 @@ void tcxLua::setGeneratedTypeBindings(const std::shared_ptr& lua) { { sol::usertype t = lua->new_usertype("IVec2", sol::constructors(), + sol::call_constructor, sol::constructors(), sol::meta_function::addition, [](const trussc::IVec2& a, const trussc::IVec2 & b){ return a + b; }, sol::meta_function::subtraction, [](const trussc::IVec2& a, const trussc::IVec2 & b){ return a - b; }, sol::meta_function::unary_minus, [](const trussc::IVec2& a){ return -a; }, @@ -81,6 +84,7 @@ void tcxLua::setGeneratedTypeBindings(const std::shared_ptr& lua) { { sol::usertype t = lua->new_usertype("IVec3", sol::constructors(), + sol::call_constructor, sol::constructors(), sol::meta_function::addition, [](const trussc::IVec3& a, const trussc::IVec3 & b){ return a + b; }, sol::meta_function::subtraction, [](const trussc::IVec3& a, const trussc::IVec3 & b){ return a - b; }, sol::meta_function::unary_minus, [](const trussc::IVec3& a){ return -a; }, @@ -95,6 +99,7 @@ void tcxLua::setGeneratedTypeBindings(const std::shared_ptr& lua) { { sol::usertype t = lua->new_usertype("Vec4", sol::constructors(), + sol::call_constructor, sol::constructors(), sol::meta_function::index, [](const trussc::Vec4& a, int b){ return a[b]; }, sol::meta_function::addition, [](const trussc::Vec4& a, const trussc::Vec4 & b){ return a + b; }, sol::meta_function::subtraction, [](const trussc::Vec4& a, const trussc::Vec4 & b){ return a - b; }, @@ -119,6 +124,7 @@ void tcxLua::setGeneratedTypeBindings(const std::shared_ptr& lua) { { sol::usertype t = lua->new_usertype("Quaternion", sol::constructors(), + sol::call_constructor, sol::constructors(), sol::meta_function::equal_to, [](const trussc::Quaternion& a, const trussc::Quaternion & b){ return a == b; }, sol::meta_function::multiplication, [](const trussc::Quaternion& a, const trussc::Quaternion & b){ return a * b; }); t["w"] = &trussc::Quaternion::w; @@ -151,7 +157,8 @@ void tcxLua::setGeneratedTypeBindings(const std::shared_ptr& lua) { } { sol::usertype t = lua->new_usertype("Rect", - sol::constructors()); + sol::constructors(), + sol::call_constructor, sol::constructors()); t["x"] = &trussc::Rect::x; t["y"] = &trussc::Rect::y; t["width"] = &trussc::Rect::width; @@ -167,7 +174,8 @@ void tcxLua::setGeneratedTypeBindings(const std::shared_ptr& lua) { } { sol::usertype t = lua->new_usertype("Ray", - sol::constructors()); + sol::constructors(), + sol::call_constructor, sol::constructors()); t["origin"] = &trussc::Ray::origin; t["direction"] = &trussc::Ray::direction; t["at"] = &trussc::Ray::at; @@ -186,20 +194,23 @@ void tcxLua::setGeneratedTypeBindings(const std::shared_ptr& lua) { } { sol::usertype t = lua->new_usertype("EventListener", - sol::constructors()); + sol::constructors(), + sol::call_constructor, sol::constructors()); t["disconnect"] = &trussc::EventListener::disconnect; t["isConnected"] = &trussc::EventListener::isConnected; } { sol::usertype t = lua->new_usertype("LogEventArgs", - sol::constructors()); + sol::constructors(), + sol::call_constructor, sol::constructors()); t["level"] = &trussc::LogEventArgs::level; t["message"] = &trussc::LogEventArgs::message; t["timestamp"] = &trussc::LogEventArgs::timestamp; } { sol::usertype t = lua->new_usertype("Logger", - sol::constructors()); + sol::constructors(), + sol::call_constructor, sol::constructors()); t["onLog"] = &trussc::Logger::onLog; t["log"] = &trussc::Logger::log; t["setConsoleLogLevel"] = &trussc::Logger::setConsoleLogLevel; @@ -213,11 +224,13 @@ void tcxLua::setGeneratedTypeBindings(const std::shared_ptr& lua) { } { sol::usertype t = lua->new_usertype("LogStream", - sol::constructors()); + sol::constructors(), + sol::call_constructor, sol::constructors()); } { sol::usertype t = lua->new_usertype("Color", sol::constructors(), + sol::call_constructor, sol::constructors(), sol::meta_function::addition, [](const trussc::Color& a, const trussc::Color & b){ return a + b; }, sol::meta_function::subtraction, [](const trussc::Color& a, const trussc::Color & b){ return a - b; }, sol::meta_function::multiplication, [](const trussc::Color& a, float b){ return a * b; }, @@ -250,6 +263,7 @@ void tcxLua::setGeneratedTypeBindings(const std::shared_ptr& lua) { { sol::usertype t = lua->new_usertype("ColorLinear", sol::constructors(), + sol::call_constructor, sol::constructors(), sol::meta_function::addition, [](const trussc::ColorLinear& a, const trussc::ColorLinear & b){ return a + b; }, sol::meta_function::subtraction, [](const trussc::ColorLinear& a, const trussc::ColorLinear & b){ return a - b; }, sol::meta_function::multiplication, sol::overload([](const trussc::ColorLinear& a, float b){ return a * b; }, [](const trussc::ColorLinear& a, const trussc::ColorLinear & b){ return a * b; }), @@ -269,7 +283,8 @@ void tcxLua::setGeneratedTypeBindings(const std::shared_ptr& lua) { } { sol::usertype t = lua->new_usertype("ColorHSB", - sol::constructors()); + sol::constructors(), + sol::call_constructor, sol::constructors()); t["h"] = &trussc::ColorHSB::h; t["s"] = &trussc::ColorHSB::s; t["b"] = &trussc::ColorHSB::b; @@ -282,7 +297,8 @@ void tcxLua::setGeneratedTypeBindings(const std::shared_ptr& lua) { } { sol::usertype t = lua->new_usertype("ColorOKLab", - sol::constructors()); + sol::constructors(), + sol::call_constructor, sol::constructors()); t["L"] = &trussc::ColorOKLab::L; t["a"] = &trussc::ColorOKLab::a; t["b"] = &trussc::ColorOKLab::b; @@ -295,7 +311,8 @@ void tcxLua::setGeneratedTypeBindings(const std::shared_ptr& lua) { } { sol::usertype t = lua->new_usertype("ColorOKLCH", - sol::constructors()); + sol::constructors(), + sol::call_constructor, sol::constructors()); t["L"] = &trussc::ColorOKLCH::L; t["C"] = &trussc::ColorOKLCH::C; t["H"] = &trussc::ColorOKLCH::H; @@ -489,7 +506,8 @@ void tcxLua::setGeneratedTypeBindings(const std::shared_ptr& lua) { } { sol::usertype t = lua->new_usertype("SoundStream", - sol::constructors()); + sol::constructors(), + sol::call_constructor, sol::constructors()); t["loadStream"] = sol::overload([](trussc::SoundStream& self, const std::string & path) { return self.loadStream(path); }, [](trussc::SoundStream& self, const std::string & path, int maxPolyphony) { return self.loadStream(path, maxPolyphony); }); t["getDuration"] = &trussc::SoundStream::getDuration; t["getPath"] = &trussc::SoundStream::getPath; @@ -546,7 +564,8 @@ void tcxLua::setGeneratedTypeBindings(const std::shared_ptr& lua) { } { sol::usertype t = lua->new_usertype("Sound", - sol::constructors()); + sol::constructors(), + sol::call_constructor, sol::constructors()); t["load"] = &trussc::Sound::load; #if (defined(__APPLE__) && (!defined(TARGET_OS_IPHONE) || !TARGET_OS_IPHONE)) || defined(_WIN32) || (defined(__linux__) && !defined(__ANDROID__)) || defined(__ANDROID__) || (defined(__APPLE__) && defined(TARGET_OS_IPHONE) && TARGET_OS_IPHONE) t["loadStream"] = sol::overload([](trussc::Sound& self, const std::string & path) { return self.loadStream(path); }, [](trussc::Sound& self, const std::string & path, int maxPolyphony) { return self.loadStream(path, maxPolyphony); }); @@ -594,7 +613,8 @@ void tcxLua::setGeneratedTypeBindings(const std::shared_ptr& lua) { } { sol::usertype t = lua->new_usertype("JsonReadReflector", - sol::constructors()); + sol::constructors(), + sol::call_constructor, sol::constructors()); t["applied"] = &trussc::JsonReadReflector::applied; t["skipped"] = &trussc::JsonReadReflector::skipped; t["readOnly"] = &trussc::JsonReadReflector::readOnly; @@ -603,7 +623,8 @@ void tcxLua::setGeneratedTypeBindings(const std::shared_ptr& lua) { } { sol::usertype t = lua->new_usertype("FileWriter", - sol::constructors()); + sol::constructors(), + sol::call_constructor, sol::constructors()); t["open"] = sol::overload([](trussc::FileWriter& self, const std::string & path) { return self.open(path); }, [](trussc::FileWriter& self, const std::string & path, bool append) { return self.open(path, append); }); t["close"] = &trussc::FileWriter::close; t["isOpen"] = &trussc::FileWriter::isOpen; @@ -613,7 +634,8 @@ void tcxLua::setGeneratedTypeBindings(const std::shared_ptr& lua) { } { sol::usertype t = lua->new_usertype("FileReader", - sol::constructors()); + sol::constructors(), + sol::call_constructor, sol::constructors()); t["open"] = &trussc::FileReader::open; t["close"] = &trussc::FileReader::close; t["isOpen"] = &trussc::FileReader::isOpen; @@ -676,6 +698,7 @@ void tcxLua::setGeneratedTypeBindings(const std::shared_ptr& lua) { { sol::usertype t = lua->new_usertype("Path", sol::constructors &), trussc::Path(const std::vector &)>(), + sol::call_constructor, sol::constructors &), trussc::Path(const std::vector &)>(), sol::meta_function::index, [](const trussc::Path& a, int b){ return a[b]; }); t["addVertex"] = sol::overload([](trussc::Path& self, float x, float y) { return self.addVertex(x, y); }, [](trussc::Path& self, float x, float y, float z) { return self.addVertex(x, y, z); }, [](trussc::Path& self, const trussc::Vec2 & v) { return self.addVertex(v); }, [](trussc::Path& self, const trussc::Vec3 & v) { return self.addVertex(v); }); t["addVertices"] = sol::overload([](trussc::Path& self, const std::vector & verts) { return self.addVertices(verts); }, [](trussc::Path& self, const std::vector & verts) { return self.addVertices(verts); }); @@ -706,7 +729,8 @@ void tcxLua::setGeneratedTypeBindings(const std::shared_ptr& lua) { } { sol::usertype t = lua->new_usertype("IesProfile", - sol::constructors()); + sol::constructors(), + sol::call_constructor, sol::constructors()); t["load"] = &trussc::IesProfile::load; t["loadFromString"] = &trussc::IesProfile::loadFromString; t["isLoaded"] = &trussc::IesProfile::isLoaded; @@ -735,7 +759,8 @@ void tcxLua::setGeneratedTypeBindings(const std::shared_ptr& lua) { } { sol::usertype t = lua->new_usertype("Mesh", - sol::constructors()); + sol::constructors(), + sol::call_constructor, sol::constructors()); t["setMode"] = &trussc::Mesh::setMode; t["getMode"] = &trussc::Mesh::getMode; t["addVertex"] = sol::overload([](trussc::Mesh& self, float x, float y) -> decltype(auto) { return self.addVertex(x, y); }, [](trussc::Mesh& self, float x, float y, float z) -> decltype(auto) { return self.addVertex(x, y, z); }, [](trussc::Mesh& self, const trussc::Vec2 & v) -> decltype(auto) { return self.addVertex(v); }, [](trussc::Mesh& self, const trussc::Vec3 & v) -> decltype(auto) { return self.addVertex(v); }); @@ -802,7 +827,8 @@ void tcxLua::setGeneratedTypeBindings(const std::shared_ptr& lua) { } { sol::usertype t = lua->new_usertype("StrokeMesh", - sol::constructors()); + sol::constructors(), + sol::call_constructor, sol::constructors()); t["setWidth"] = &trussc::StrokeMesh::setWidth; t["setColor"] = &trussc::StrokeMesh::setColor; t["setCapType"] = &trussc::StrokeMesh::setCapType; @@ -821,7 +847,8 @@ void tcxLua::setGeneratedTypeBindings(const std::shared_ptr& lua) { } { sol::usertype t = lua->new_usertype("Font", - sol::constructors()); + sol::constructors(), + sol::call_constructor, sol::constructors()); t["load"] = &trussc::Font::load; t["isLoaded"] = &trussc::Font::isLoaded; t["setAlign"] = sol::overload([](trussc::Font& self, trussc::Direction h, trussc::Direction v) { return self.setAlign(h, v); }, [](trussc::Font& self, trussc::Direction h) { return self.setAlign(h); }); @@ -869,7 +896,8 @@ void tcxLua::setGeneratedTypeBindings(const std::shared_ptr& lua) { } { sol::usertype t = lua->new_usertype("FullscreenShader", - sol::constructors()); + sol::constructors(), + sol::call_constructor, sol::constructors()); t["draw"] = &trussc::FullscreenShader::draw; } { @@ -889,7 +917,8 @@ void tcxLua::setGeneratedTypeBindings(const std::shared_ptr& lua) { #if (defined(__APPLE__) && (!defined(TARGET_OS_IPHONE) || !TARGET_OS_IPHONE)) || defined(_WIN32) || (defined(__linux__) && !defined(__ANDROID__)) || (defined(__APPLE__) && defined(TARGET_OS_IPHONE) && TARGET_OS_IPHONE) || defined(__EMSCRIPTEN__) { sol::usertype t = lua->new_usertype("VideoGrabber", - sol::constructors()); + sol::constructors(), + sol::call_constructor, sol::constructors()); t["listDevices"] = &trussc::VideoGrabber::listDevices; t["setDeviceID"] = &trussc::VideoGrabber::setDeviceID; t["getDeviceID"] = &trussc::VideoGrabber::getDeviceID; @@ -922,7 +951,8 @@ void tcxLua::setGeneratedTypeBindings(const std::shared_ptr& lua) { #if (defined(__APPLE__) && (!defined(TARGET_OS_IPHONE) || !TARGET_OS_IPHONE)) || defined(_WIN32) || (defined(__linux__) && !defined(__ANDROID__)) || (defined(__APPLE__) && defined(TARGET_OS_IPHONE) && TARGET_OS_IPHONE) || defined(__EMSCRIPTEN__) { sol::usertype t = lua->new_usertype("VideoPlayer", - sol::constructors()); + sol::constructors(), + sol::call_constructor, sol::constructors()); t["load"] = &trussc::VideoPlayer::load; t["close"] = &trussc::VideoPlayer::close; t["update"] = &trussc::VideoPlayer::update; @@ -973,7 +1003,8 @@ void tcxLua::setGeneratedTypeBindings(const std::shared_ptr& lua) { #if (defined(__APPLE__) && (!defined(TARGET_OS_IPHONE) || !TARGET_OS_IPHONE)) || defined(_WIN32) || (defined(__linux__) && !defined(__ANDROID__)) || defined(__ANDROID__) || (defined(__APPLE__) && defined(TARGET_OS_IPHONE) && TARGET_OS_IPHONE) { sol::usertype t = lua->new_usertype("VideoWriter", - sol::constructors()); + sol::constructors(), + sol::call_constructor, sol::constructors()); t["open"] = sol::overload([](trussc::VideoWriter& self, const std::string & path, int width, int height) { return self.open(path, width, height); }, [](trussc::VideoWriter& self, const std::string & path, int width, int height, const trussc::VideoRecordSettings & settings) { return self.open(path, width, height, settings); }); t["close"] = &trussc::VideoWriter::close; t["isOpen"] = &trussc::VideoWriter::isOpen; @@ -993,7 +1024,8 @@ void tcxLua::setGeneratedTypeBindings(const std::shared_ptr& lua) { #if (defined(__APPLE__) && (!defined(TARGET_OS_IPHONE) || !TARGET_OS_IPHONE)) || defined(_WIN32) || (defined(__linux__) && !defined(__ANDROID__)) || defined(__ANDROID__) || (defined(__APPLE__) && defined(TARGET_OS_IPHONE) && TARGET_OS_IPHONE) { sol::usertype t = lua->new_usertype("ScreenRecorder", - sol::constructors()); + sol::constructors(), + sol::call_constructor, sol::constructors()); t["start"] = sol::overload([](trussc::ScreenRecorder& self, const std::string & path) { return self.start(path); }, [](trussc::ScreenRecorder& self, const std::string & path, const trussc::VideoRecordSettings & settings) { return self.start(path, settings); }, [](trussc::ScreenRecorder& self, const trussc::Fbo & fbo, const std::string & path) { return self.start(fbo, path); }, [](trussc::ScreenRecorder& self, const trussc::Fbo & fbo, const std::string & path, const trussc::VideoRecordSettings & settings) { return self.start(fbo, path, settings); }, [](trussc::ScreenRecorder& self, const std::string & path, float durationSec) { return self.start(path, durationSec); }, [](trussc::ScreenRecorder& self, const trussc::Fbo & fbo, const std::string & path, float durationSec) { return self.start(fbo, path, durationSec); }); t["stop"] = &trussc::ScreenRecorder::stop; t["isRecording"] = &trussc::ScreenRecorder::isRecording; @@ -1016,7 +1048,8 @@ void tcxLua::setGeneratedTypeBindings(const std::shared_ptr& lua) { #if (defined(__APPLE__) && (!defined(TARGET_OS_IPHONE) || !TARGET_OS_IPHONE)) || defined(_WIN32) || (defined(__linux__) && !defined(__ANDROID__)) || defined(__ANDROID__) || (defined(__APPLE__) && defined(TARGET_OS_IPHONE) && TARGET_OS_IPHONE) { sol::usertype t = lua->new_usertype("UdpSocket", - sol::constructors()); + sol::constructors(), + sol::call_constructor, sol::constructors()); t["onReceive"] = &trussc::UdpSocket::onReceive; t["onError"] = &trussc::UdpSocket::onError; t["create"] = &trussc::UdpSocket::create; @@ -1070,7 +1103,8 @@ void tcxLua::setGeneratedTypeBindings(const std::shared_ptr& lua) { #if (defined(__APPLE__) && (!defined(TARGET_OS_IPHONE) || !TARGET_OS_IPHONE)) || defined(_WIN32) || (defined(__linux__) && !defined(__ANDROID__)) || defined(__ANDROID__) || (defined(__APPLE__) && defined(TARGET_OS_IPHONE) && TARGET_OS_IPHONE) { sol::usertype t = lua->new_usertype("TcpClient", - sol::constructors()); + sol::constructors(), + sol::call_constructor, sol::constructors()); t["onConnect"] = &trussc::TcpClient::onConnect; t["onReceive"] = &trussc::TcpClient::onReceive; t["onDisconnect"] = &trussc::TcpClient::onDisconnect; @@ -1121,7 +1155,8 @@ void tcxLua::setGeneratedTypeBindings(const std::shared_ptr& lua) { #if (defined(__APPLE__) && (!defined(TARGET_OS_IPHONE) || !TARGET_OS_IPHONE)) || defined(_WIN32) || (defined(__linux__) && !defined(__ANDROID__)) || defined(__ANDROID__) { sol::usertype t = lua->new_usertype("TcpServer", - sol::constructors()); + sol::constructors(), + sol::call_constructor, sol::constructors()); t["onClientConnect"] = &trussc::TcpServer::onClientConnect; t["onReceive"] = &trussc::TcpServer::onReceive; t["onClientDisconnect"] = &trussc::TcpServer::onClientDisconnect; @@ -1169,7 +1204,8 @@ void tcxLua::setGeneratedTypeBindings(const std::shared_ptr& lua) { #if (defined(__APPLE__) && (!defined(TARGET_OS_IPHONE) || !TARGET_OS_IPHONE)) || defined(_WIN32) || (defined(__linux__) && !defined(__ANDROID__)) || defined(__ANDROID__) { sol::usertype t = lua->new_usertype("Serial", - sol::constructors()); + sol::constructors(), + sol::call_constructor, sol::constructors()); t["getDeviceList"] = &trussc::Serial::getDeviceList; t["setup"] = sol::overload([](trussc::Serial& self, const std::string & portName, int baudRate) { return self.setup(portName, baudRate); }, [](trussc::Serial& self, int deviceIndex, int baudRate) { return self.setup(deviceIndex, baudRate); }); t["close"] = &trussc::Serial::close; @@ -1189,7 +1225,8 @@ void tcxLua::setGeneratedTypeBindings(const std::shared_ptr& lua) { #endif { sol::usertype t = lua->new_usertype("ChipSoundNote", - sol::constructors()); + sol::constructors(), + sol::call_constructor, sol::constructors()); t["wave"] = &trussc::ChipSoundNote::wave; t["hz"] = &trussc::ChipSoundNote::hz; t["volume"] = &trussc::ChipSoundNote::volume; @@ -1213,15 +1250,18 @@ void tcxLua::setGeneratedTypeBindings(const std::shared_ptr& lua) { } { sol::usertype> t = lua->new_usertype>("Tween_float", - sol::constructors(), trussc::Tween(float, float, float), trussc::Tween(float, float, float, trussc::EaseType), trussc::Tween(float, float, float, trussc::EaseType, trussc::EaseMode)>()); + sol::constructors(), trussc::Tween(float, float, float), trussc::Tween(float, float, float, trussc::EaseType), trussc::Tween(float, float, float, trussc::EaseType, trussc::EaseMode)>(), + sol::call_constructor, sol::constructors(), trussc::Tween(float, float, float), trussc::Tween(float, float, float, trussc::EaseType), trussc::Tween(float, float, float, trussc::EaseType, trussc::EaseMode)>()); } { sol::usertype> t = lua->new_usertype>("Tween_Vec2", - sol::constructors(), trussc::Tween(trussc::Vec2, trussc::Vec2, float), trussc::Tween(trussc::Vec2, trussc::Vec2, float, trussc::EaseType), trussc::Tween(trussc::Vec2, trussc::Vec2, float, trussc::EaseType, trussc::EaseMode)>()); + sol::constructors(), trussc::Tween(trussc::Vec2, trussc::Vec2, float), trussc::Tween(trussc::Vec2, trussc::Vec2, float, trussc::EaseType), trussc::Tween(trussc::Vec2, trussc::Vec2, float, trussc::EaseType, trussc::EaseMode)>(), + sol::call_constructor, sol::constructors(), trussc::Tween(trussc::Vec2, trussc::Vec2, float), trussc::Tween(trussc::Vec2, trussc::Vec2, float, trussc::EaseType), trussc::Tween(trussc::Vec2, trussc::Vec2, float, trussc::EaseType, trussc::EaseMode)>()); } { sol::usertype> t = lua->new_usertype>("Tween_Vec3", - sol::constructors(), trussc::Tween(trussc::Vec3, trussc::Vec3, float), trussc::Tween(trussc::Vec3, trussc::Vec3, float, trussc::EaseType), trussc::Tween(trussc::Vec3, trussc::Vec3, float, trussc::EaseType, trussc::EaseMode)>()); + sol::constructors(), trussc::Tween(trussc::Vec3, trussc::Vec3, float), trussc::Tween(trussc::Vec3, trussc::Vec3, float, trussc::EaseType), trussc::Tween(trussc::Vec3, trussc::Vec3, float, trussc::EaseType, trussc::EaseMode)>(), + sol::call_constructor, sol::constructors(), trussc::Tween(trussc::Vec3, trussc::Vec3, float), trussc::Tween(trussc::Vec3, trussc::Vec3, float, trussc::EaseType), trussc::Tween(trussc::Vec3, trussc::Vec3, float, trussc::EaseType, trussc::EaseMode)>()); } { sol::usertype t = lua->new_usertype("Mod"); @@ -1229,7 +1269,8 @@ void tcxLua::setGeneratedTypeBindings(const std::shared_ptr& lua) { } { sol::usertype t = lua->new_usertype("Node", - sol::constructors()); + sol::constructors(), + sol::call_constructor, sol::constructors()); t["localMatrixChanged"] = &trussc::Node::localMatrixChanged; t["setup"] = &trussc::Node::setup; t["update"] = &trussc::Node::update; @@ -1353,7 +1394,8 @@ void tcxLua::setGeneratedTypeBindings(const std::shared_ptr& lua) { } { sol::usertype t = lua->new_usertype("RectNodeButton", - sol::constructors()); + sol::constructors(), + sol::call_constructor, sol::constructors()); t["normalColor"] = &trussc::RectNodeButton::normalColor; t["hoverColor"] = &trussc::RectNodeButton::hoverColor; t["pressColor"] = &trussc::RectNodeButton::pressColor; @@ -1363,7 +1405,8 @@ void tcxLua::setGeneratedTypeBindings(const std::shared_ptr& lua) { } { sol::usertype t = lua->new_usertype("LayoutMod", - sol::constructors()); + sol::constructors(), + sol::call_constructor, sol::constructors()); t["getDirection"] = &trussc::LayoutMod::getDirection; t["setDirection"] = &trussc::LayoutMod::setDirection; t["getSpacing"] = &trussc::LayoutMod::getSpacing; @@ -1385,7 +1428,8 @@ void tcxLua::setGeneratedTypeBindings(const std::shared_ptr& lua) { } { sol::usertype t = lua->new_usertype("ScrollContainer", - sol::constructors()); + sol::constructors(), + sol::call_constructor, sol::constructors()); t["setContent"] = &trussc::ScrollContainer::setContent; t["getContent"] = &trussc::ScrollContainer::getContent; t["getContentRect"] = &trussc::ScrollContainer::getContentRect; @@ -1418,7 +1462,8 @@ void tcxLua::setGeneratedTypeBindings(const std::shared_ptr& lua) { } { sol::usertype t = lua->new_usertype("TweenMod", - sol::constructors()); + sol::constructors(), + sol::call_constructor, sol::constructors()); t["complete"] = &trussc::TweenMod::complete; t["moveTo"] = sol::overload([](trussc::TweenMod& self, float x, float y) -> decltype(auto) { return self.moveTo(x, y); }, [](trussc::TweenMod& self, float x, float y, float z) -> decltype(auto) { return self.moveTo(x, y, z); }, [](trussc::TweenMod& self, const trussc::Vec3 & pos) -> decltype(auto) { return self.moveTo(pos); }, [](trussc::TweenMod& self, const trussc::Vec2 & pos) -> decltype(auto) { return self.moveTo(pos); }); t["moveBy"] = sol::overload([](trussc::TweenMod& self, float dx, float dy) -> decltype(auto) { return self.moveBy(dx, dy); }, [](trussc::TweenMod& self, float dx, float dy, float dz) -> decltype(auto) { return self.moveBy(dx, dy, dz); }, [](trussc::TweenMod& self, const trussc::Vec3 & delta) -> decltype(auto) { return self.moveBy(delta); }, [](trussc::TweenMod& self, const trussc::Vec2 & delta) -> decltype(auto) { return self.moveBy(delta); }); @@ -1454,7 +1499,8 @@ void tcxLua::setGeneratedTypeBindings(const std::shared_ptr& lua) { } { sol::usertype t = lua->new_usertype("App", - sol::constructors()); + sol::constructors(), + sol::call_constructor, sol::constructors()); t["setSize"] = &trussc::App::setSize; t["requestExit"] = &trussc::App::requestExit; t["isExitRequested"] = &trussc::App::isExitRequested; diff --git a/addons/tcxLua/src/tcxLua.cpp b/addons/tcxLua/src/tcxLua.cpp index df536466..93e73f83 100644 --- a/addons/tcxLua/src/tcxLua.cpp +++ b/addons/tcxLua/src/tcxLua.cpp @@ -233,6 +233,12 @@ void tcxLua::setTypeBindings(const std::shared_ptr& lua){ float m20, float m21, float m22, float m23, float m30, float m31, float m32, float m33), Mat4(const Mat4&)>(), + sol::call_constructor, sol::constructors(), sol::meta_function::multiplication, sol::overload( [](const Mat4& a, const Mat4& b){ return a * b; }, @@ -270,6 +276,11 @@ void tcxLua::setTypeBindings(const std::shared_ptr& lua){ float m10, float m11, float m12, float m20, float m21, float m22), Mat3(const Mat3&)>(), + sol::call_constructor, sol::constructors(), sol::meta_function::multiplication, sol::overload( [](const Mat3& a, const Mat3& b){ return a * b; }, @@ -322,7 +333,8 @@ void tcxLua::setTypeBindings(const std::shared_ptr& lua){ sol::usertype fbo_type = lua->new_usertype("Fbo", - sol::constructors() // FIXME: move constructor? + sol::constructors(), + sol::call_constructor, sol::constructors() // FIXME: move constructor? ); fbo_type["allocate"] = sol::overload( @@ -362,7 +374,8 @@ void tcxLua::setTypeBindings(const std::shared_ptr& lua){ fbo_type["getSampler"] = &Fbo::getSampler; sol::usertype tex_type = lua->new_usertype("Texture", - sol::constructors() // FIXME: move constructor? + sol::constructors(), + sol::call_constructor, sol::constructors() // FIXME: move constructor? ); tex_type["allocate"] = sol::overload( [](Texture& f, int w, int h){ return f.allocate(w, h); }, @@ -428,7 +441,8 @@ void tcxLua::setTypeBindings(const std::shared_ptr& lua){ sol::usertype img_type = lua->new_usertype("Image", - sol::constructors() // FIXME: move constructor? + sol::constructors(), + sol::call_constructor, sol::constructors() // FIXME: move constructor? ); img_type["load"] = sol::overload( &Image::load, @@ -461,7 +475,8 @@ void tcxLua::setTypeBindings(const std::shared_ptr& lua){ img_type["getTexture"] = [](Image& f) -> Texture& { return f.getTexture(); }; sol::usertype pix_type = lua->new_usertype("Pixels", - sol::constructors() // FIXME: move constructor? + sol::constructors(), + sol::call_constructor, sol::constructors() // FIXME: move constructor? ); pix_type["allocate"] = sol::overload( [](Pixels& f, int w, int h){ return f.allocate(w, h); }, @@ -506,7 +521,8 @@ void tcxLua::setTypeBindings(const std::shared_ptr& lua){ sol::usertype shader_type = lua->new_usertype("Shader", - sol::constructors() // FIXME: move constructor? + sol::constructors(), + sol::call_constructor, sol::constructors() // FIXME: move constructor? ); shader_type["load"] = &Shader::load; shader_type["clear"] = &Shader::clear; @@ -533,7 +549,8 @@ void tcxLua::setTypeBindings(const std::shared_ptr& lua){ shader_type["submitVertices"] = &Shader::submitVertices; sol::usertype easycam_t = lua->new_usertype("EasyCam", - sol::constructors() + sol::constructors(), + sol::call_constructor, sol::constructors() ); easycam_t["begin"] = &EasyCam::begin; @@ -572,7 +589,8 @@ void tcxLua::setTypeBindings(const std::shared_ptr& lua){ easycam_t["mouseScrolled"] = &EasyCam::mouseScrolled; sol::usertype light_t = lua->new_usertype("Light", - sol::constructors() + sol::constructors(), + sol::call_constructor, sol::constructors() ); light_t["setDirectional"] = sol::overload( [](Light& m, float x, float y, float z){ return m.setDirectional(x, y, z); }, @@ -649,7 +667,8 @@ void tcxLua::setTypeBindings(const std::shared_ptr& lua){ sol::usertype material_t = lua->new_usertype("Material", - sol::constructors() + sol::constructors(), + sol::call_constructor, sol::constructors() ); material_t["getBaseColor"] = &Material::getBaseColor; material_t["setBaseColor"] = sol::overload( @@ -710,6 +729,7 @@ void tcxLua::setTypeBindings(const std::shared_ptr& lua){ // luagen in trussc_generated.cpp. Only the Json usertype is hand-written here. sol::usertype json_t = lua->new_usertype("Json", sol::constructors(), + sol::call_constructor, sol::constructors(), "get_string", [](Json& j){ return j.get(); }, "get_double", [](Json& j){ return j.get(); }, "get_float", [](Json& j){ return j.get(); }, @@ -749,6 +769,7 @@ void tcxLua::setTypeBindings(const std::shared_ptr& lua){ // loadXml/parseXml are free functions -> generated by luagen. Xml usertype is hand-written. sol::usertype xml_t = lua->new_usertype("Xml", sol::constructors(), + sol::call_constructor, sol::constructors(), "load", &Xml::load, "parse", &Xml::parse, "save", sol::overload( @@ -773,6 +794,7 @@ void tcxLua::setTypeBindings(const std::shared_ptr& lua){ sol::usertype xmlattr_t = lua->new_usertype("XmlAttribute", sol::constructors(), + sol::call_constructor, sol::constructors(), "set", sol::overload( // WORKAROUND [](XmlAttribute& x, pugi::string_view_t s){ return (x = s); }, [](XmlAttribute& x, const pugi::char_t* s){ return (x = s); }, @@ -788,6 +810,7 @@ void tcxLua::setTypeBindings(const std::shared_ptr& lua){ sol::usertype xmltext_t = lua->new_usertype("XmlText", sol::constructors(), + sol::call_constructor, sol::constructors(), "set", sol::overload( // WORKAROUND [](XmlText& x, pugi::string_view_t s){ return (x = s); }, [](XmlText& x, const pugi::char_t* s){ return (x = s); }, @@ -800,6 +823,7 @@ void tcxLua::setTypeBindings(const std::shared_ptr& lua){ sol::usertype xmlnode_t = lua->new_usertype("XmlNode", sol::constructors(), + sol::call_constructor, sol::constructors(), "append_attribute", sol::overload( [](XmlNode& x, pugi::string_view_t n){ return x.append_attribute(n); }, [](XmlNode& x, const pugi::char_t* n){ return x.append_attribute(n); } @@ -871,6 +895,8 @@ void tcxLua::setTypeBindings(const std::shared_ptr& lua){ lua->new_usertype("SoundBuffer", sol::constructors(), + sol::call_constructor, sol::constructors(), sol::base_classes, sol::bases(), "loadOgg", &SoundBuffer::loadOgg, "loadWav", &SoundBuffer::loadWav, @@ -918,6 +944,7 @@ void tcxLua::setTypeBindings(const std::shared_ptr& lua){ lua->new_usertype("MicInput", sol::constructors(), + sol::call_constructor, sol::constructors(), // MicInput(const MicInput&), MicInput(MicInput&&)>(), "start", &MicInput::start, "stop", &MicInput::stop, diff --git a/addons/tcxLua/src/tcxLua.h b/addons/tcxLua/src/tcxLua.h index 5f64cbd3..5deae013 100644 --- a/addons/tcxLua/src/tcxLua.h +++ b/addons/tcxLua/src/tcxLua.h @@ -61,6 +61,12 @@ class tcxLua { TweenT(TweenValue, TweenValue, float, EaseType, EaseMode) // FIXME: move constructor? >(), + sol::call_constructor, sol::constructors< + TweenT(), + TweenT(TweenValue, TweenValue, float), + TweenT(TweenValue, TweenValue, float, EaseType), + TweenT(TweenValue, TweenValue, float, EaseType, EaseMode) + >(), "from", &TweenT::from, "to", &TweenT::to, "duration", &TweenT::duration, diff --git a/addons/tcxLua/tools/luagen-types/luagen-types.js b/addons/tcxLua/tools/luagen-types/luagen-types.js index c22dee66..aa6bf2ff 100644 --- a/addons/tcxLua/tools/luagen-types/luagen-types.js +++ b/addons/tcxLua/tools/luagen-types/luagen-types.js @@ -181,7 +181,14 @@ function emitType(typeEntry, cppType, luaName, T) { } let s = ` {\n sol::usertype<${Q}> t = lua->new_usertype<${Q}>("${luaName}"`; - if (ctorTypes.length) s += `,\n sol::constructors<${ctorTypes.join(', ')}>()`; + // Positional constructors provide Type.new(...); sol::call_constructor + // additionally enables the Type(...) call form (without it the usertype + // table has no __call and e.g. `Vec3(1, 2, 3)` fails with "attempt to + // call a table value"). + if (ctorTypes.length) { + s += `,\n sol::constructors<${ctorTypes.join(', ')}>()`; + s += `,\n sol::call_constructor, sol::constructors<${ctorTypes.join(', ')}>()`; + } for (const meta in ops) { const lams = [...new Set(ops[meta])]; s += `,\n sol::meta_function::${meta}, ` + (lams.length === 1 ? lams[0] : `sol::overload(${lams.join(', ')})`); From cb28ad5654e479918fb6f73cef70b1962f18d539 Mon Sep 17 00:00:00 2001 From: tettou771 Date: Mon, 13 Jul 2026 16:29:43 +0900 Subject: [PATCH 31/87] fix(sokol_app_tc): stop the render loop stalling after the first frame (waitable double-wait) On Windows the render loop froze permanently after presenting the first frame. The frame latency waitable is an auto-reset object: the run loop's MsgWaitForMultipleObjectsEx consumes its signal, and then window_due's WaitForSingleObject on the same handle always missed it, so no window was ever "due" again and tick() stopped running (only input events kept flowing). - add a waitable_ready flag to the window struct - the run loop maps the satisfied MsgWait handle back to its window and records the consumed signal in waitable_ready - window_due checks waitable_ready instead of re-waiting the handle - during a modal size/move loop the run loop is blocked, so poll the waitable from the WM_TIMER handler too (fixes drawing stalling while dragging/resizing) Verified on hardware: all windowed apps render at ~60fps again, CPU <1%, with mouse tracking, resize, multi-window and minimize/restore all working. --- core/include/sokol/sokol_app_tc.h | 41 ++++++++++++++++++++++++++----- 1 file changed, 35 insertions(+), 6 deletions(-) diff --git a/core/include/sokol/sokol_app_tc.h b/core/include/sokol/sokol_app_tc.h index 9978abf7..faefa075 100644 --- a/core/include/sokol/sokol_app_tc.h +++ b/core/include/sokol/sokol_app_tc.h @@ -1777,6 +1777,10 @@ typedef struct _sapp_tc_window_t { IDXGISwapChain1* swap_chain; HANDLE frame_wait; /* frame latency waitable (NULL: timer-paced fallback) */ bool credit_held; /* a waitable credit was consumed without a Present yet */ + bool waitable_ready; /* the run-loop's MsgWait consumed this window's frame + latency waitable signal (auto-reset): the due-check + must NOT re-wait the same handle (it would miss the + already-consumed signal and stall forever) */ ID3D11Texture2D* rt; /* swapchain backbuffer */ ID3D11RenderTargetView* rtv; ID3D11Texture2D* msaa_rt; /* MSAA color (sample_count > 1; flip model can't MSAA the backbuffer) */ @@ -2654,8 +2658,13 @@ static void _sapp_tc_win32_pace(_sapp_tc_window_t* w, double seconds) { static bool _sapp_tc_win32_window_due(_sapp_tc_window_t* w) { if (!w || !w->swap_chain || w->in_tick) return false; if (w->earliest_next > _sapp_tc_now()) return false; + /* Waitable pacing: the frame latency waitable is an auto-reset object that + the run-loop's MsgWaitForMultipleObjectsEx already waits on. That wait + CONSUMES the signal, so we must NOT re-wait the handle here (doing so + missed the consumed signal and stalled the render loop after frame 1). + Instead the run-loop records the consumed signal in waitable_ready. */ if (w->frame_wait && !w->credit_held) { - if (WaitForSingleObject(w->frame_wait, 0) != WAIT_OBJECT_0) return false; + if (!w->waitable_ready) return false; } return true; } @@ -2663,8 +2672,10 @@ static bool _sapp_tc_win32_window_due(_sapp_tc_window_t* w) { static void _sapp_tc_win32_tick(_sapp_tc_window_t* w, bool from_modal) { if (w->in_tick || !w->swap_chain) return; /* the due-check just consumed a waitable credit (unless one was already - held); hold it until a real Present returns it through the swapchain */ - if (w->frame_wait) w->credit_held = true; + held); hold it until a real Present returns it through the swapchain. + Clear waitable_ready: this signal is now spent (the next one comes from + the run-loop's MsgWait after this frame is presented). */ + if (w->frame_wait) { w->credit_held = true; w->waitable_ready = false; } _sapp_tc_timing_update(&w->timing); w->frame_duration = w->timing.smooth_dt; const int swap_interval = (_sapp_tc.app.desc.swap_interval > 0) ? _sapp_tc.app.desc.swap_interval : 1; @@ -2941,6 +2952,15 @@ static LRESULT CALLBACK _sapp_tc_wndproc(HWND hwnd, UINT msg, WPARAM wParam, LPA if (wParam == _SAPP_TC_MODAL_TIMER) { for (int i = 0; i < _SAPP_TC_MAX_WINDOWS; i++) { _sapp_tc_window_t* tw = _sapp_tc.windows[i]; + if (!tw) continue; + /* the outer run-loop (which records the waitable signal in + waitable_ready) is blocked in this modal size/move loop, so + poll the frame latency waitable here instead -- otherwise the + due-check never sees a credit and drag/resize stops rendering */ + if (tw->frame_wait && !tw->credit_held && !tw->waitable_ready && + WaitForSingleObject(tw->frame_wait, 0) == WAIT_OBJECT_0) { + tw->waitable_ready = true; + } if (_sapp_tc_win32_window_due(tw)) { _sapp_tc_win32_tick(tw, true); } @@ -3098,6 +3118,7 @@ static void _sapp_tc_win32_run_loop(void) { bool done = false; while (!done && !_sapp_tc.app.quit_ordered) { HANDLE handles[_SAPP_TC_MAX_WINDOWS]; + _sapp_tc_window_t* handle_owner[_SAPP_TC_MAX_WINDOWS]; DWORD num_handles = 0; const double now = _sapp_tc_now(); double wake_at = now + 0.1; /* robustness cap; messages wake us anyway */ @@ -3106,16 +3127,24 @@ static void _sapp_tc_win32_run_loop(void) { if (!w || !w->swap_chain || w->in_tick) continue; if (w->earliest_next > now) { if (w->earliest_next < wake_at) wake_at = w->earliest_next; - } else if (w->frame_wait && !w->credit_held) { + } else if (w->frame_wait && !w->credit_held && !w->waitable_ready) { + handle_owner[num_handles] = w; handles[num_handles++] = w->frame_wait; } else { - wake_at = now; /* due immediately (timer-paced / credit held) */ + wake_at = now; /* due immediately (timer-paced / credit or signal held) */ } } double timeout_s = wake_at - now; if (timeout_s < 0.0) timeout_s = 0.0; - MsgWaitForMultipleObjectsEx(num_handles, handles, + DWORD wr = MsgWaitForMultipleObjectsEx(num_handles, handles, (DWORD)(timeout_s * 1000.0), QS_ALLINPUT, MWMO_INPUTAVAILABLE); + /* If a frame latency waitable satisfied the wait, it was auto-reset here. + Record it so the due-check consumes THIS signal instead of re-waiting + the (now-unsignaled) handle. Only one handle is reported per wait; the + rest stay signaled and are caught on the next loop. */ + if (wr >= WAIT_OBJECT_0 && wr < WAIT_OBJECT_0 + num_handles) { + handle_owner[wr - WAIT_OBJECT_0]->waitable_ready = true; + } MSG msg; while (PeekMessageW(&msg, NULL, 0, 0, PM_REMOVE)) { if (WM_QUIT == msg.message) { From d62ce3302cb6291700a9970968d1b91e9eb28dfa Mon Sep 17 00:00:00 2001 From: tettou771 Date: Mon, 13 Jul 2026 16:33:29 +0900 Subject: [PATCH 32/87] fix(docs): sketch emitters derive hand-written types from the ACTUAL bound surface MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The sketch reference/completions were emitting the raw C++ AST for the 15 hand-written usertypes: methods that are not bound (Fbo::lifetimeToken, 16 unbound EasyCam members, ...) and Lua-invalid names (fbo:end — 'end' is reserved; the real binding is end_fbo). New docs/scripts/lib/lua-bound-surface.js parses tcxLua.cpp/tcxLua.h the same way bindcheck/regen_expected.sh does, member-level: registered Lua names, their C++ targets (for prose matching), constructors, and the defineTween template (emitted as TweenFloat/TweenVec2/TweenVec3/ TweenColor). Renames honored: end_fbo/end_shader/end_cam, Mat3/Mat4 set<-at. Generated types unchanged (byte-identical shapes). --- docs/scripts/emit-sketch-api.js | 106 +++++++-- docs/scripts/emit-sketch-reference.js | 140 ++++++++--- docs/scripts/lib/lua-bound-surface.js | 319 ++++++++++++++++++++++++++ 3 files changed, 509 insertions(+), 56 deletions(-) create mode 100644 docs/scripts/lib/lua-bound-surface.js diff --git a/docs/scripts/emit-sketch-api.js b/docs/scripts/emit-sketch-api.js index 47a64b95..53b461a5 100644 --- a/docs/scripts/emit-sketch-api.js +++ b/docs/scripts/emit-sketch-api.js @@ -28,6 +28,16 @@ const OUT = path.join(__dirname, '../../../trussc.org/generated/trusssketch-api. const data = JSON.parse(fs.readFileSync(RDJ, 'utf8')); const colorsData = JSON.parse(fs.readFileSync(COLORS, 'utf8')); +// Actual Lua binding surface of the HAND-WRITTEN tcxLua usertypes (see the lib +// header): bound member names (end_fbo not end), no unbound members, plus the +// four value-typed Tween instances. Hand types are advertised from this instead +// of the raw C++ AST so completions never suggest unusable/unbound members. +const { getLuaBoundSurface, LUA_RESERVED, TWEEN_CTOR_PARAMS, TWEEN_METHOD_META } = + require('./lib/lua-bound-surface.js'); +const SURFACE = getLuaBoundSurface(); +const HAND = SURFACE.types; +const dedup = a => [...new Set(a)]; + // ---- mirrors of the luagen bindability rules (advertise only what's bound) -- const PRIM = new Set(['void','bool','char','short','int','long','float','double','unsigned','signed','size_t','int8_t','int16_t','int32_t','int64_t','uint8_t','uint16_t','uint32_t','uint64_t']); function argBindable(a) { @@ -83,35 +93,69 @@ for (const id in data) { const e = data[id]; if (e.kind !== 'type' || e.owner || e.ns || e.hidden) continue; if (UNBOUND_TYPES.has(e.name)) continue; + // Tween template: emitted as four value-typed instances below. + if (e.name === 'Tween') continue; if (e.tparams && e.tparams.length && !(e.lua_bind && e.lua_bind.length)) continue; const t = { name: e.name, desc: en(e.description) }; - // constructor: the richest ctor, callable as Type(...) (sol __call) - const ctors = (e.constructors || []).filter(c => (c.args || []).every(argBindable)) + // bindable ctors (for named params), excluding the copy ctor. + const bindCtors = (e.constructors || []).filter(c => (c.args || []).every(argBindable)) .filter(c => { const a = c.args || []; return !(a.length === 1 && a[0].type.replace(/[&]/g, '').replace(/\bconst\b/g, '').trim() === e.name); }); - if (ctors.length) { - const best = ctors.reduce((x, y) => ((y.args || []).length > (x.args || []).length ? y : x)); - t.constructor = { snippet: snippetOf(e.name, best.args || []) }; - } const properties = [], methods = [], statics = []; - for (const mid in data) { - const m = data[mid]; - if (m.owner !== e.id) continue; - if (m.access && m.access !== 'public') continue; - if (m.hidden || m.deprecated) continue; - if (/^operator/.test(m.name)) continue; - if (m.kind === 'field') { - const ft = m.type || ''; - if (!ft || /\binternal::/.test(ft) || /\*/.test(ft) || /\[/.test(ft)) continue; - properties.push({ name: m.name, type: ft, desc: en(m.description) }); - } else if (m.kind === 'method') { - const fb = firstBindableSig(m); - if (!fb) continue; - (m.static ? statics : methods).push({ - name: m.name, snippet: snippetOf(m.name, fb.args), - return: fb.sig.ret || 'void', desc: en(m.description), + + if (HAND.has(e.name)) { + // Hand-written usertype — advertise ONLY the bound Lua surface. + const surf = HAND.get(e.name); + const cppMembers = new Map(); + for (const mid in data) { const m = data[mid]; if (m.owner === e.id) cppMembers.set(m.name, m); } + + // constructor snippet: the richest bound ctor, named via reference-data. + if (surf.ctors.length) { + const best = surf.ctors.reduce((x, y) => (y.count > x.count ? y : x)); + const rc = bindCtors.find(c => (c.args || []).length === best.count); + const args = rc ? rc.args : Array.from({ length: best.count }, (_, i) => ({ name: 'a' + i })); + t.constructor = { snippet: snippetOf(e.name, args) }; + } + + for (const mem of surf.members) { + if (LUA_RESERVED.has(mem.lua)) continue; // unusable Lua spelling; alias kept + const m = mem.cpp ? cppMembers.get(mem.cpp) : null; + if (m && m.kind === 'field') { + properties.push({ name: mem.lua, type: m.type || '', desc: en(m.description) }); + continue; + } + const isStatic = !!(m && m.static); + const fb = m ? firstBindableSig(m) : null; + (isStatic ? statics : methods).push({ + name: mem.lua, snippet: snippetOf(mem.lua, fb ? fb.args : []), + return: fb ? (fb.sig.ret || 'void') : '', desc: m ? en(m.description) : '', }); } + } else { + // Generated usertype — reference-data IS the binding source. + if (bindCtors.length) { + const best = bindCtors.reduce((x, y) => ((y.args || []).length > (x.args || []).length ? y : x)); + t.constructor = { snippet: snippetOf(e.name, best.args || []) }; + } + for (const mid in data) { + const m = data[mid]; + if (m.owner !== e.id) continue; + if (m.access && m.access !== 'public') continue; + if (m.hidden || m.deprecated) continue; + if (/^operator/.test(m.name)) continue; + if (m.kind === 'field') { + const ft = m.type || ''; + if (!ft || /\binternal::/.test(ft) || /\*/.test(ft) || /\[/.test(ft)) continue; + properties.push({ name: m.name, type: ft, desc: en(m.description) }); + } else if (m.kind === 'method') { + const fb = firstBindableSig(m); + if (!fb) continue; + (m.static ? statics : methods).push({ + name: m.name, snippet: snippetOf(m.name, fb.args), + return: fb.sig.ret || 'void', desc: en(m.description), + }); + } + } } if (properties.length) t.properties = properties; if (methods.length) t.methods = methods; @@ -119,6 +163,24 @@ for (const id in data) { types.push(t); } +// ---- Tween: one usertype per value type (TweenFloat/Vec2/Vec3/Color) ---------- +{ + const tw = SURFACE.tween; + const tref = Object.values(data).find(x => x && x.kind === 'type' && x.name === 'Tween' && !x.owner && !x.ns) || {}; + // richest ctor snippet: Name(from, to, duration) + const maxAr = Math.min(3, Math.max(0, ...tw.ctorArities)); + for (const vt of tw.valueTypes) { + const tt = { name: vt.name, desc: en(tref.description) }; + tt.constructor = { snippet: snippetOf(vt.name, TWEEN_CTOR_PARAMS.slice(0, maxAr).map(n => ({ name: n }))) }; + tt.methods = tw.memberNames.map(nm => { + const meta = TWEEN_METHOD_META[nm] || { params: [], ret: '' }; + const ret = meta.ret === 'self' ? vt.name : meta.ret === 'value' ? vt.value : (meta.ret || ''); + return { name: nm, snippet: snippetOf(nm, meta.params.map(p => ({ name: p }))), return: ret, desc: '' }; + }); + types.push(tt); + } +} + // ---- enums as types (values via static access: BlendMode.Add) ---------------- for (const id in data) { const e = data[id]; diff --git a/docs/scripts/emit-sketch-reference.js b/docs/scripts/emit-sketch-reference.js index de9c7560..e80d9c21 100644 --- a/docs/scripts/emit-sketch-reference.js +++ b/docs/scripts/emit-sketch-reference.js @@ -38,6 +38,15 @@ const COLORS = path.join(__dirname, '../reference/colors.json'); const EXTRAS = path.join(__dirname, '../reference/extras.json'); const OUT = path.join(__dirname, '../../../trussc.org/generated/trusssketch-ref.js'); +// Actual Lua binding surface of the HAND-WRITTEN tcxLua usertypes (see the lib +// header). Hand types are advertised from THIS (bound member names, reserved- +// word aliases like end_fbo, no unbound members) instead of the raw C++ AST. +const { getLuaBoundSurface, LUA_RESERVED, TWEEN_CTOR_PARAMS, TWEEN_METHOD_META } = + require('./lib/lua-bound-surface.js'); +const SURFACE = getLuaBoundSurface(); +const HAND = SURFACE.types; // luaTypeName -> { cppType, ctors, members } +const dedup = a => [...new Set(a)]; + const REF = JSON.parse(fs.readFileSync(REF_DATA, 'utf8')); const CATS = JSON.parse(fs.readFileSync(CATEGORIES, 'utf8')); const CAT_BY_ID = new Map(CATS.map(c => [c.id, c])); @@ -224,6 +233,9 @@ for (const id in REF) { const e = REF[id]; if (e.kind !== 'type' || e.owner || e.ns || e.hidden) continue; if (UNBOUND_TYPES.has(e.name)) continue; + // Tween is a template: reference-data holds prose only. Its four value-typed + // Lua instances are emitted separately (below) from the defineTween surface. + if (e.name === 'Tween') continue; if (e.tparams && e.tparams.length && !(e.lua_bind && e.lua_bind.length)) continue; const typeName = e.name; @@ -237,53 +249,113 @@ for (const id in REF) { }; if (Array.isArray(e.related) && e.related.length) t.related = e.related; - // constructor: every bindable ctor except the copy ctor (single self-typed arg). - const ctors = (e.constructors || []).filter(c => (c.args || []).every(argBindable)) + // bindable ctors from reference-data (used for named params), excluding the + // copy ctor (single self-typed arg). + const bindCtors = (e.constructors || []).filter(c => (c.args || []).every(argBindable)) .filter(c => { const a = c.args || []; return !(a.length === 1 && a[0].type.replace(/[&]/g, '').replace(/\bconst\b/g, '').trim() === typeName); }); - if (ctors.length) { - // Type(...) call form (sol __call). Render every signature. - t.constructor = { signatures: ctors.map(c => luaParams(c.args)) }; - } const properties = [], methods = [], statics = []; - for (const mid in REF) { - const m = REF[mid]; - if (m.owner !== e.id) continue; - if (m.access && m.access !== 'public') continue; - if (m.hidden || m.deprecated) continue; - if (/^operator/.test(m.name)) continue; - if (m.kind === 'field') { - const ft = m.type || ''; - if (!ft || /\binternal::/.test(ft) || /\*/.test(ft) || /\[/.test(ft)) continue; - // fields accessed with dot syntax on an instance: `vec2.x` - properties.push({ - name: `${recv}.${m.name}`, - type: mapType(ft), - desc: en(m.description), - desc_ja: ja(m.description), - desc_ko: ko(m.description), - }); - } else if (m.kind === 'method') { - const bindSigs = (m.signatures || []).filter(sigBindable); - if (!bindSigs.length) continue; - const isStatic = !!m.static; - // instance -> colon syntax `recv:method`; static -> dot syntax `Type.method` - const dispName = isStatic ? `${typeName}.${m.name}` : `${recv}:${m.name}`; + + if (HAND.has(typeName)) { + // Hand-written usertype — advertise ONLY the bound Lua surface. + const surf = HAND.get(typeName); + const cppMembers = new Map(); // C++ member name -> reference-data entry + for (const mid in REF) { const m = REF[mid]; if (m.owner === e.id) cppMembers.set(m.name, m); } + + // constructors: the bound ctor list; params named via reference-data + // where the arg count matches, else generic (a0, a1, …). + const sigs = dedup(surf.ctors.map(bc => { + const rc = bindCtors.find(c => (c.args || []).length === bc.count); + return rc ? luaParams(rc.args) : Array.from({ length: bc.count }, (_, i) => 'a' + i).join(', '); + })); + if (sigs.length) t.constructor = { signatures: sigs }; + + for (const mem of surf.members) { + if (LUA_RESERVED.has(mem.lua)) continue; // unusable Lua spelling; alias kept + const m = mem.cpp ? cppMembers.get(mem.cpp) : null; + if (m && m.kind === 'field') { + properties.push({ + name: `${recv}.${mem.lua}`, type: mapType(m.type), + desc: en(m.description), desc_ja: ja(m.description), desc_ko: ko(m.description), + }); + continue; + } + const isStatic = !!(m && m.static); + let msigs = m ? (m.signatures || []).filter(sigBindable) : []; + if (m && !msigs.length) msigs = (m.signatures || []).filter(s => !s.tmpl); + const dispName = isStatic ? `${typeName}.${mem.lua}` : `${recv}:${mem.lua}`; (isStatic ? statics : methods).push({ name: dispName, - return: mapType(bindSigs[0].ret), - signatures: bindSigs.map(s => luaParams(s.args)), - desc: en(m.description), - desc_ja: ja(m.description), - desc_ko: ko(m.description), + return: msigs.length ? mapType(msigs[0].ret) : '', + signatures: msigs.length ? msigs.map(s => luaParams(s.args)) : [''], + desc: m ? en(m.description) : '', desc_ja: m ? ja(m.description) : '', desc_ko: m ? ko(m.description) : '', }); } + } else { + // Generated usertype — reference-data IS the binding source (luagen-types). + if (bindCtors.length) t.constructor = { signatures: bindCtors.map(c => luaParams(c.args)) }; + for (const mid in REF) { + const m = REF[mid]; + if (m.owner !== e.id) continue; + if (m.access && m.access !== 'public') continue; + if (m.hidden || m.deprecated) continue; + if (/^operator/.test(m.name)) continue; + if (m.kind === 'field') { + const ft = m.type || ''; + if (!ft || /\binternal::/.test(ft) || /\*/.test(ft) || /\[/.test(ft)) continue; + // fields accessed with dot syntax on an instance: `vec2.x` + properties.push({ + name: `${recv}.${m.name}`, + type: mapType(ft), + desc: en(m.description), + desc_ja: ja(m.description), + desc_ko: ko(m.description), + }); + } else if (m.kind === 'method') { + const bindSigs = (m.signatures || []).filter(sigBindable); + if (!bindSigs.length) continue; + const isStatic = !!m.static; + // instance -> colon syntax `recv:method`; static -> dot syntax `Type.method` + const dispName = isStatic ? `${typeName}.${m.name}` : `${recv}:${m.name}`; + (isStatic ? statics : methods).push({ + name: dispName, + return: mapType(bindSigs[0].ret), + signatures: bindSigs.map(s => luaParams(s.args)), + desc: en(m.description), + desc_ja: ja(m.description), + desc_ko: ko(m.description), + }); + } + } } + if (properties.length) t.properties = properties; if (methods.length) t.methods = methods; if (statics.length) t.static_methods = statics; types.push(t); } + +// Tween — one usertype per value type (TweenFloat/Vec2/Vec3/Color). Prose from +// reference-data's `Tween`; ctor/method signatures from the defineTween surface. +{ + const tw = SURFACE.tween; + const tref = Object.values(REF).find(x => x && x.kind === 'type' && x.name === 'Tween' && !x.owner && !x.ns) || {}; + for (const vt of tw.valueTypes) { + const rv = recvName(vt.name); + const tt = { + name: vt.name, + desc: en(tref.description), keywords: Array.isArray(tref.keywords) ? tref.keywords : [], + desc_ja: ja(tref.description), desc_ko: ko(tref.description), + }; + tt.constructor = { signatures: dedup(tw.ctorArities.map(n => TWEEN_CTOR_PARAMS.slice(0, n).join(', '))) }; + tt.methods = tw.memberNames.map(nm => { + const meta = TWEEN_METHOD_META[nm] || { params: [], ret: '' }; + const ret = meta.ret === 'self' ? vt.name : meta.ret === 'value' ? vt.value : (meta.ret || ''); + return { name: `${rv}:${nm}`, return: ret, signatures: [meta.params.join(', ')], desc: '', desc_ja: '', desc_ko: '' }; + }); + types.push(tt); + } +} types.sort((a, b) => a.name.localeCompare(b.name)); // ============================================================================= diff --git a/docs/scripts/lib/lua-bound-surface.js b/docs/scripts/lib/lua-bound-surface.js new file mode 100644 index 00000000..f134e51f --- /dev/null +++ b/docs/scripts/lib/lua-bound-surface.js @@ -0,0 +1,319 @@ +/** + * lua-bound-surface.js — extract the ACTUAL Lua binding surface of the + * HAND-WRITTEN tcxLua usertypes, so the sketch emitters advertise what Lua can + * really call (not the raw C++ AST). + * + * Why: hand usertypes in addons/tcxLua/src/tcxLua.cpp register members under + * LUA names that can differ from the C++ member (`fbo["end_fbo"] = &Fbo::end` + * because `end` is a Lua reserved word) and DROP members that reference-data + * lists but nothing binds (`Fbo::lifetimeToken`). Driving the emitters off + * reference-data alone therefore emits methods that are either unusable Lua + * (`fbo:end`) or simply not there. This module parses the binding sources — + * the same files bindcheck/regen_expected.sh scans — and returns, per hand + * usertype, the registered LUA names + the C++ member each one targets, plus + * the Tween template surface (instantiated as TweenFloat/Vec2/Vec3/Color). + * + * Parsing approach (mirrors regen_expected.sh, extended to member level): + * • `new_usertype("LuaName", …)` opens a usertype block; the call + * args yield the Lua name (first arg), the sol::constructors<…> list, and — + * for the INLINE registration style — `"member", value` pairs. + * • The POST-ASSIGNMENT style (`var["member"] = value;`) is collected by + * scanning `var["…"] = …;` statements for the block's variable. + * • For each registered member the referenced C++ name is recovered from + * `&Type::member` (pointer) or the lambda body's `recv.method(` call, so + * prose can be matched from reference-data by C++ name. + * • sol meta_functions / call_constructor / base_classes are not members. + * + * This module does NO reference-data lookup and NO rendering — it only reports + * the raw bound surface. The emitters join it against reference-data. + */ +'use strict'; +const fs = require('fs'); +const path = require('path'); + +const SRC = path.join(__dirname, '../../../addons/tcxLua/src'); +const CPP = path.join(SRC, 'tcxLua.cpp'); +const HDR = path.join(SRC, 'tcxLua.h'); + +// Lua reserved words — a usertype member registered under one of these is +// unusable as `obj:word()` / `Type.word` in Lua (syntax error), so a usable +// alias (e.g. end_fbo) is always registered alongside. We drop the reserved +// spelling and keep the alias. +const LUA_RESERVED = new Set(['and', 'break', 'do', 'else', 'elseif', 'end', + 'false', 'for', 'function', 'goto', 'if', 'in', 'local', 'nil', 'not', + 'or', 'repeat', 'return', 'then', 'until', 'while']); + +// --- low-level scanning helpers --------------------------------------------- + +// Return the index of the ')' that matches the '(' at `open` (paren-aware, +// string-aware). Angle brackets are NOT tracked here (irrelevant for paren +// matching, and `->` would corrupt an angle counter). +function matchParen(s, open) { + let depth = 0; + for (let i = open; i < s.length; i++) { + const c = s[i]; + if (c === '"' || c === "'") { i = skipString(s, i); continue; } + if (c === '(') depth++; + else if (c === ')') { if (--depth === 0) return i; } + } + return -1; +} + +// Return the index of the '>' that matches the '<' at `open`. Guards against +// the arrow `->` (a '>' preceded by '-' is not an angle close). +function matchAngle(s, open) { + let depth = 0; + for (let i = open; i < s.length; i++) { + const c = s[i]; + if (c === '"' || c === "'") { i = skipString(s, i); continue; } + if (c === '<') depth++; + else if (c === '>' && s[i - 1] !== '-') { if (--depth === 0) return i; } + } + return -1; +} + +// Strip C/C++ comments (string-literal aware) so commented-out bindings — e.g. +// the disabled StdPath usertype and `// FIXME` notes — are not parsed as real +// registrations. Mirrors regen_expected.sh's `grep -vE '^\s*//'` intent. +function stripComments(s) { + let out = ''; + for (let i = 0; i < s.length; i++) { + const c = s[i], n = s[i + 1]; + if (c === '"' || c === "'") { const e = skipString(s, i); out += s.slice(i, e + 1); i = e; continue; } + if (c === '/' && n === '/') { while (i < s.length && s[i] !== '\n') i++; out += '\n'; continue; } + if (c === '/' && n === '*') { i += 2; while (i < s.length && !(s[i] === '*' && s[i + 1] === '/')) i++; i++; out += ' '; continue; } + out += c; + } + return out; +} + +// Advance past a string/char literal starting at `i` (i points at the quote). +function skipString(s, i) { + const q = s[i]; + for (let j = i + 1; j < s.length; j++) { + if (s[j] === '\\') { j++; continue; } + if (s[j] === q) return j; + } + return s.length; +} + +// Split a comma-separated arg list at depth 0. Tracks () [] {} and <> +// (with the `->` guard), and skips string literals. +function splitTopLevel(s) { + const out = []; + let depth = 0, angle = 0, start = 0; + for (let i = 0; i < s.length; i++) { + const c = s[i]; + if (c === '"' || c === "'") { i = skipString(s, i); continue; } + else if (c === '(' || c === '[' || c === '{') depth++; + else if (c === ')' || c === ']' || c === '}') depth--; + else if (c === '<') angle++; + else if (c === '>' && s[i - 1] !== '-') { if (angle > 0) angle--; } + else if (c === ',' && depth === 0 && angle === 0) { + out.push(s.slice(start, i)); start = i + 1; + } + } + const tail = s.slice(start); + if (tail.trim() !== '') out.push(tail); + return out.map(x => x.trim()); +} + +const isStringLit = t => /^"([^"]*)"$/.test(t.trim()); +const unquote = t => t.trim().replace(/^"/, '').replace(/"$/, ''); + +// Recover the C++ member name a bound value expression refers to: +// &Type::member -> member (pointer-to-member) +// [](T& f){ return f.method(...); } -> method (lambda body call) +// Returns null when nothing recognizable is referenced (pure workaround +// lambdas with no member call). +function extractCpp(expr) { + // pointer-to-member: require at least one `Ident::` so lambda params like + // `Fbo& f` (a reference `&`, not a member pointer) are not misread. + let m = expr.match(/&\s*(?:[A-Za-z_]\w*\s*::\s*)+([A-Za-z_]\w*)/); + if (m) return m[1]; + // lambda body: first `recv.method(` call (skips `lua->create_table`, `->`). + m = expr.match(/\b[A-Za-z_]\w*\s*\.\s*([A-Za-z_]\w*)\s*\(/); + if (m) return m[1]; + return null; +} + +// Read from `i` up to the depth-0 ';' (statement end). Paren/brace/bracket and +// string aware; angle brackets are ignored (`->` would corrupt them). +function readStatement(s, i) { + let depth = 0; + for (; i < s.length; i++) { + const c = s[i]; + if (c === '"' || c === "'") { i = skipString(s, i); continue; } + else if (c === '(' || c === '[' || c === '{') depth++; + else if (c === ')' || c === ']' || c === '}') depth--; + else if (c === ';' && depth === 0) return s.slice(0, i); + } + return s; +} + +// Parse a `sol::constructors< … >` list into arg-type arrays, excluding the +// copy/move constructor (a single self-typed arg). `contents` is the text +// inside the angle brackets. +function parseCtorList(contents, cppType) { + const ctors = []; + for (const entry of splitTopLevel(contents)) { + // `Type` (no parens) == default ctor; `Type(a, b)` -> capture args. + const paren = entry.indexOf('('); + let args = []; + if (paren >= 0) { + const close = matchParen(entry, paren); + const inner = entry.slice(paren + 1, close).trim(); + if (inner !== '') args = splitTopLevel(inner); + } + // copy/move: single arg whose base type is the usertype itself. + if (args.length === 1) { + const base = args[0].replace(/[&*]/g, '').replace(/\bconst\b/g, '') + .replace(/\b\w+$/, '').replace(/[<>].*$/, '').trim() || args[0]; + const baseType = args[0].replace(/[&*]/g, '').replace(/\bconst\b/g, '').trim().split(/\s+/)[0]; + if (baseType === cppType || base === cppType || /&&/.test(args[0])) continue; + } + ctors.push({ count: args.length, args }); + } + return ctors; +} + +// Parse inline `"member", value` pairs and the first sol::constructors from a +// new_usertype(...) call's arg text. args[0] is the Lua type name (skipped). +function parseCallArgs(callArgs, cppType) { + const parts = splitTopLevel(callArgs); + const members = []; + let ctors = []; + for (let i = 1; i < parts.length; i++) { + const p = parts[i]; + if (isStringLit(p)) { + const value = parts[i + 1] || ''; + members.push({ lua: unquote(p), cpp: extractCpp(value) }); + i++; // consume the value token + continue; + } + if (!ctors.length && /^sol::constructors\s* VAR =` prefix captures the block variable used + // by the post-assignment style. + const re = /(?:sol::usertype<[^>]+>\s+(\w+)\s*=\s*)?lua->new_usertype<([^>]*)>\s*\(/g; + let m; + while ((m = re.exec(text))) { + const varName = m[1]; + const cppType = m[2].trim(); + const open = m.index + m[0].length - 1; // index of '(' + const close = matchParen(text, open); + if (close < 0) continue; + const callArgs = text.slice(open + 1, close); + const parts = splitTopLevel(callArgs); + if (!parts.length || !isStringLit(parts[0])) continue; + const luaName = unquote(parts[0]); + + const { members, ctors } = parseCallArgs(callArgs, cppType); + if (varName) members.push(...collectAssignments(text, varName)); + + // De-dup members by Lua name (last write wins, matching C++ semantics). + const byLua = new Map(); + for (const mem of members) byLua.set(mem.lua, mem); + types.set(luaName, { cppType, ctors, members: [...byLua.values()] }); + } + return types; +} + +// Collect `VAR["member"] = value;` post-assignments for one block variable. +function collectAssignments(text, varName) { + const members = []; + const re = new RegExp(varName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + + '\\s*\\[\\s*"([^"]+)"\\s*\\]\\s*=', 'g'); + let m; + while ((m = re.exec(text))) { + const expr = readStatement(text, re.lastIndex); + members.push({ lua: m[1], cpp: extractCpp(expr.slice(re.lastIndex)) }); + } + return members; +} + +// --- Tween template ---------------------------------------------------------- + +// The Tween usertype is registered by a template (defineTween in tcxLua.h) and +// instantiated four times in tcxLua.cpp. We read the bound member NAMES + ctor +// overloads from the template, and the value types from the instantiations. +function parseTween(cppText, hdrText) { + // instantiations: defineTween, float>(lua, "TweenFloat") + const valueTypes = []; + const reInst = /defineTween<[^,]+,\s*([\w:]+)\s*>\s*\(\s*lua\s*,\s*"([^"]+)"/g; + let m; + while ((m = reInst.exec(cppText))) valueTypes.push({ name: m[2], value: m[1].trim() }); + + // template body: the new_usertype call inside defineTween. + const nu = hdrText.indexOf('lua->new_usertype'); + let memberNames = [], ctorArities = []; + if (nu >= 0) { + const open = hdrText.indexOf('(', nu); + const close = matchParen(hdrText, open); + const callArgs = hdrText.slice(open + 1, close); + // every string literal in the template call is a bound member name + // (the first call arg is the `name` VARIABLE, not a literal). + const parts = splitTopLevel(callArgs); + for (let i = 0; i < parts.length; i++) { + if (isStringLit(parts[i])) { memberNames.push(unquote(parts[i])); i++; } + } + // ctor overloads -> arities (arg counts), for named-param rendering. + const ci = callArgs.indexOf('sol::constructors<'); + if (ci >= 0) { + const a = callArgs.indexOf('<', ci); + const b = matchAngle(callArgs, a); + for (const entry of splitTopLevel(callArgs.slice(a + 1, b))) { + const paren = entry.indexOf('('); + if (paren < 0) { ctorArities.push(0); continue; } + const inner = entry.slice(paren + 1, matchParen(entry, paren)).trim(); + ctorArities.push(inner === '' ? 0 : splitTopLevel(inner).length); + } + } + } + // unique arities, ascending + ctorArities = [...new Set(ctorArities)].sort((x, y) => x - y); + return { valueTypes, memberNames, ctorArities }; +} + +// --- public API -------------------------------------------------------------- + +function getLuaBoundSurface() { + const cppText = stripComments(fs.readFileSync(CPP, 'utf8')); + const hdrText = stripComments(fs.readFileSync(HDR, 'utf8')); + return { + types: parseHandTypes(cppText), + tween: parseTween(cppText, hdrText), + }; +} + +// Rendering metadata for the Tween usertype. reference-data's `Tween` entry has +// prose but ZERO members (the members live on the template), so signatures / +// returns are supplied here from tcTween.h. `ret`: 'self' -> the Tween* type +// (chainable setters), 'value' -> the tween's value type (float/Vec2/…). +const TWEEN_CTOR_PARAMS = ['from', 'to', 'duration', 'ease', 'mode']; +const TWEEN_METHOD_META = { + from: { params: ['value'], ret: 'self' }, to: { params: ['value'], ret: 'self' }, + duration: { params: ['seconds'], ret: 'self' }, ease: { params: ['type', 'mode'], ret: 'self' }, + loop: { params: ['count'], ret: 'self' }, yoyo: { params: ['enable'], ret: 'self' }, + delay: { params: ['seconds'], ret: 'self' }, start: { params: [], ret: 'self' }, + pause: { params: [], ret: 'self' }, resume: { params: [], ret: 'self' }, + reset: { params: [], ret: 'self' }, finish: { params: [], ret: 'self' }, + getValue: { params: [], ret: 'value' }, getProgress: { params: [], ret: 'number' }, + getElapsed: { params: [], ret: 'number' }, getDuration: { params: [], ret: 'number' }, + isPlaying: { params: [], ret: 'boolean' }, isComplete: { params: [], ret: 'boolean' }, + getStart: { params: [], ret: 'value' }, getEnd: { params: [], ret: 'value' }, + getLoopCount: { params: [], ret: 'number' }, +}; + +module.exports = { getLuaBoundSurface, LUA_RESERVED, TWEEN_CTOR_PARAMS, TWEEN_METHOD_META }; From f1be7540eac7414930aa8d5757c4600a45b0e843 Mon Sep 17 00:00:00 2001 From: tettou771 Date: Mon, 13 Jul 2026 16:34:36 +0900 Subject: [PATCH 33/87] doc: record the waitable single-consumer contract + Win11 runtime verification --- docs/dev/sokol_app_tc-design.md | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/docs/dev/sokol_app_tc-design.md b/docs/dev/sokol_app_tc-design.md index 6f745edf..7cbd29ad 100644 --- a/docs/dev/sokol_app_tc-design.md +++ b/docs/dev/sokol_app_tc-design.md @@ -167,15 +167,28 @@ can feed events manually, so imgui does not chain us to sokol_app. per-window waitables — a tick that ends without a Present (skip_present, occluded, minimized) holds its waitable "credit" and is timer-re-paced until the next real Present, so neither busy-spin nor waitable - starvation can occur; resize coalesced per tick (never in WM_SIZE); + starvation can occur. The waitable is an AUTO-RESET object with exactly + ONE consumer: the run loop's MsgWait consumes the signal and records it + in the window's `waitable_ready` flag (handle→window map; MsgWait + resets only the one satisfied handle), and the due-check reads that + flag — never re-wait the handle (the original code did, stalling + rendering after the first frame; caught on real hardware, cb28ad56 — + CI is compile+headless only, the render loop needs a machine). During + a modal size/move loop the blocked run loop's role passes to the + WM_TIMER handler, which polls the waitable itself. Win11 DWM note: + Present never returns DXGI_STATUS_OCCLUDED there, so the SUSPENDED/ + RESUMED occlusion path is dormant on modern Windows (hidden windows + pause naturally when their waitable goes quiet); kept for other + configs; resize coalesced per tick (never in WM_SIZE); WM_ENTERSIZEMOVE arms a ~64Hz WM_TIMER that keeps ALL windows ticking through the modal drag; scancode-indexed keytable lifted verbatim (fixes the win branch's key=vk gap) + WM_CHAR with surrogate pairs; quit = WM_CLOSE dance + PostQuitMessage; skip_present HONORED on Present (TrussC flicker patch); per-monitor-v2 DPI with the create-then-resize dance; DXGI_PRESENT_TEST occlusion poll for - secondary windows (SUSPENDED/RESUMED). Runtime verification on real - Windows hardware still pending. + secondary windows (SUSPENDED/RESUMED). Runtime VERIFIED on real Win11 + hardware 2026-07-13: ~60fps, CPU <1%, resize/fullscreen/minimize/ + second-window/clean-exit/drag-render all pass. - **P2 — Linux driver.** X11 + GLX shared context (multiwindow-glfw pattern), timer-paced ticks. - **P3 — web driver.** RAF tick, HTML5 event callbacks, WebGL context; From 9a5f9ae9930944f23ab3828d04b574d5d005aa63 Mon Sep 17 00:00:00 2001 From: tettou771 Date: Mon, 13 Jul 2026 16:58:44 +0900 Subject: [PATCH 34/87] fix: restore the real screen camera after FullscreenShader::draw MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The post-draw cleanup wrote a corner-origin ortho sized in physical pixels (sapp_width). The engine's screen convention is the camera from setupScreenFovWithSize — a CENTERED projection plus a lookat modelview — so any 2D drawing after a fullscreen pass on the swapchain ended up shifted by (-W/2, -H/2) and half-scaled on retina displays. Re-run the real screen setup with the current view params instead (and keep the corner ortho only inside Fbo passes, matching Fbo::begin). --- core/include/tc/gpu/tcShader.h | 29 +++++++++++++++++++++++------ 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/core/include/tc/gpu/tcShader.h b/core/include/tc/gpu/tcShader.h index 5a3cbf1c..1d3859a7 100644 --- a/core/include/tc/gpu/tcShader.h +++ b/core/include/tc/gpu/tcShader.h @@ -611,13 +611,30 @@ class FullscreenShader : public Shader { sg_draw(0, 6, 1); - // Restore sokol_gl state + // Restore sokol_gl state to what the rest of the frame expects. + // + // Inside an Fbo pass that is the corner-origin ortho Fbo::begin set up. + // On the SWAPCHAIN the screen convention is NOT a corner ortho — it is + // the screen camera from setupScreenFovWithSize (a CENTERED projection + // plus a lookat modelview; perspective when defaultScreenFov > 0). A + // plain ortho here leaves a mixed state: as soon as the engine + // re-applies the camera modelview, later 2D draws land shifted by + // (-W/2, -H/2) at the wrong scale. (Historic bug: this used + // sapp_width(), physical px, which additionally halved everything on + // retina.) Re-run the real setup with the CURRENT view params instead. sg_reset_state_cache(); - sgl_defaults(); - sgl_matrix_mode_projection(); - sgl_ortho(0.0f, (float)sapp_width(), (float)sapp_height(), 0.0f, -10000.0f, 10000.0f); - sgl_matrix_mode_modelview(); - sgl_load_identity(); + if (internal::inFboPass) { + sgl_defaults(); + internal::loadPipeline(internal::activeFill2D()); + sgl_matrix_mode_projection(); + sgl_ortho(0.0f, internal::currentViewW, internal::currentViewH, 0.0f, -10000.0f, 10000.0f); + sgl_matrix_mode_modelview(); + sgl_load_identity(); + } else { + internal::setupScreenFovWithSize(internal::currentScreenFov, + internal::currentViewW, internal::currentViewH, + 0.0f, 0.0f); + } } protected: From dc5c0351b1983ccc093642e51127a70b8d3a6c8b Mon Sep 17 00:00:00 2001 From: tettou771 Date: Mon, 13 Jul 2026 17:27:12 +0900 Subject: [PATCH 35/87] doc: x11/GLX implementation contract + corrected P2 tick model (P2 kickoff) --- docs/dev/sapp-x11-impl-spec.md | 766 ++++++++++++++++++++++++++++++++ docs/dev/sokol_app_tc-design.md | 26 ++ 2 files changed, 792 insertions(+) create mode 100644 docs/dev/sapp-x11-impl-spec.md diff --git a/docs/dev/sapp-x11-impl-spec.md b/docs/dev/sapp-x11-impl-spec.md new file mode 100644 index 00000000..898ed02d --- /dev/null +++ b/docs/dev/sapp-x11-impl-spec.md @@ -0,0 +1,766 @@ +# sokol_app.h X11 (GLX / OpenGL Core) Backend — Behavioral Specification + +Implementation contract for replacing sokol_app.h's X11/Linux main-window / +app-lifecycle with `sokol_app_tc.h` (project "P2 Linux driver"). All line +references are into `core/include/sokol/sokol_app.h` (14602 lines, this +worktree). Focus: `_SAPP_LINUX` + `_SAPP_GLX` (desktop OpenGL via GLX). + +**Which #ifdef path is live in TrussC.** `core/CMakeLists.txt:277` compiles the +Linux target with `SOKOL_GLCORE` (and links `X11`, `Xi`, `Xcursor`, `OpenGL`). +sokol_app resolves that at 2361–2366: `SOKOL_GLCORE && !SOKOL_FORCE_EGL` → +`#define _SAPP_GLX (1)`. So on TrussC/Linux the live paths are **`_SAPP_GLX`** +for the renderer and **`_SAPP_X11`** for windowing. The `_SAPP_EGL` path +(2363/2369, selected by `SOKOL_GLES3` or `SOKOL_FORCE_EGL`) is **dead on +desktop Linux** — it is the live path only on Android/Raspbian (`SOKOL_GLES3`, +different CMake branches). `SOKOL_VULKAN`/`SOKOL_WGPU` on Linux are also dead. +This document documents GLX and ignores EGL/Vulkan/WGPU except where a shared +X11 code path forks on them (marked inline). + +This is the third sibling of `sapp-mac-impl-spec.md` (event-driven `[NSApp run]`) +and `sapp-win32-impl-spec.md` (owned PeekMessage loop). Like Win32, the X11 +backend is an **explicit owned event+render loop** (`_sapp_linux_run`). Unlike +both, its frame pacing is **entirely delegated to blocking `glXSwapBuffers` +with the swap interval** — there is no timer, no CADisplayLink, no `Sleep`, and +crucially **no RandR usage anywhere** (see §11). + +**Per-window vs process-global (read this first — the replacement makes windows plural):** +Almost everything is single-window global state baked into `static _sapp_t _sapp`. +The truly *process-global* pieces (must stay global / open once regardless of +window count) are: +- **`Display* display`** — the single X11 server connection (`XOpenDisplay(NULL)`, + §1). One connection multiplexes all windows; open once, close last. **All X11 + calls take this `Display*`**, so it is the spine every per-window struct hangs off. +- **`int screen` / `Window root`** — default screen + its root window (§1). Screen-wide. +- **Interned atoms** (`UTF8_STRING`, `CLIPBOARD`, `TARGETS`, `WM_PROTOCOLS`, + `WM_DELETE_WINDOW`, `WM_STATE`, `NET_WM_*`, all XDND atoms) — atom identity is + per-`Display`, so intern once, share across windows (§4). +- **`_sapp.glx` function pointers + extension flags** — the whole dlopen'd libGL + vtable and the `EXT/MESA_swap_control`, `ARB_create_context*` booleans (§3). + Loaded once. **The `GLXContext ctx` inside it is also effectively global** — + the multiwindow plan uses ONE shared context (see §3). +- **`_sapp.x11.xi`** (XInput2 opcode/version) — extension is per-`Display`, query once (§5). +- **`standard_cursors[]` / `hidden_cursor`** — `Cursor` XIDs; shareable across + windows on the same `Display` (§9). +- **`_sapp.keycodes[256]`** — the X11-keycode→sapp-keycode table (§6). Global, fill once. +- **`dpi`** — currently one display-wide DPI value (§1, §4-DPI). + +Everything else is **per-window** and must move into a per-window struct when +windows go plural: **`Window window`** (the X11 window XID), **`Colormap colormap`**, +**`GLXWindow glx.window`** (the GLX drawable — one per X window), the sizes +(`window_width/height`, `framebuffer_width/height`), `window_state`, +`mouse_buttons`, `custom_cursors[]`, `key_repeat[256]`, and the `xdnd` in-flight +drag state. Note the **DPI is currently a single `_sapp.x11.dpi`** shared by all — +per-monitor DPI is not implemented (§4-DPI, §11). + +--- + +## 0. Global state touched (the shared contract) + +Single global `static _sapp_t _sapp;` (definition ~3264) embeds `_sapp_x11_t x11;` +(3314) and, under GLX, `_sapp_glx_t glx;` (3316). + +### `_sapp_x11_t` (3150–3179) +```c +typedef struct { + uint8_t mouse_buttons; // bitmask of held buttons (enter/leave suppression during drag) + Display* display; // THE server connection (process-global) + int screen; // default screen index + Window root; // root window of screen + Colormap colormap; // per-window (created against the chosen visual) + Window window; // per-window (the X11 window XID) + Cursor hidden_cursor; // blank cursor for hide / mouse-lock + Cursor standard_cursors[_SAPP_MOUSECURSOR_NUM]; + Cursor custom_cursors[_SAPP_MOUSECURSOR_NUM]; + int window_state; // NormalState / IconicState / WithdrawnState (from WM_STATE) + float dpi; // display-wide DPI (Xft.dpi or 96.0 fallback) + unsigned char error_code; // set by the temporary X error handler + Atom UTF8_STRING, CLIPBOARD, TARGETS, WM_PROTOCOLS, WM_DELETE_WINDOW, + WM_STATE, NET_WM_NAME, NET_WM_ICON_NAME, NET_WM_ICON, + NET_WM_STATE, NET_WM_STATE_FULLSCREEN; + _sapp_xi_t xi; // XInput2 (raw mouse in lock mode) + _sapp_xdnd_t xdnd; // XDND drag&drop protocol state + bool key_repeat[256]; // per-X11-keycode repeat tracking (see §6) +} _sapp_x11_t; +``` +Note: **there is NO XIM/XIC (X Input Method / Input Context) anywhere in this +struct or the whole backend.** See §6 — this is a major surprise vs the task +brief, which assumed XIM/XIC handling. + +### `_sapp_xi_t` (3125–3132) — XInput2 raw motion (mouse-lock only) +```c +typedef struct { bool available; int major_opcode, event_base, error_base, major, minor; } _sapp_xi_t; +``` + +### `_sapp_xdnd_t` (3134–3148) — XDND protocol state +```c +typedef struct { + int version; Window source; Atom format; + Atom XdndAware, XdndEnter, XdndPosition, XdndStatus, XdndActionCopy, + XdndDrop, XdndFinished, XdndSelection, XdndTypeList, text_uri_list; +} _sapp_xdnd_t; +``` + +### `_sapp_glx_t` (3183–3222, `_SAPP_GLX` only) +```c +typedef struct { + void* libgl; // dlopen("libGL.so.1") + int major, minor; // GLX version + int event_base, error_base; + GLXContext ctx; // THE GL context (shared in multiwindow plan) + GLXWindow window; // per-window GLX drawable + /* GLX 1.3 fn ptrs */ GetFBConfigs, GetFBConfigAttrib, GetClientString, + QueryExtension, QueryVersion, DestroyContext, MakeCurrent, SwapBuffers, + QueryExtensionsString, GetVisualFromFBConfig, CreateWindow, DestroyWindow; + /* GLX 1.4 / ext */ GetProcAddress, GetProcAddressARB, SwapIntervalEXT, + SwapIntervalMESA, CreateContextAttribsARB; + void (*GetIntegerv)(uint32_t, int32_t*); + bool EXT_swap_control, MESA_swap_control, ARB_multisample, + ARB_create_context, ARB_create_context_profile; +} _sapp_glx_t; +``` + +Cross-platform `_sapp` fields the X11 path reads/writes are the same set as the +other backends (`desc`, `valid`, `first_frame`, `quit_requested`, `quit_ordered`, +`fullscreen`, `window_width/height`, `framebuffer_width/height`, `dpi_scale`, +`sample_count`, `swap_interval`, `timing`, `mouse.*`, `clipboard.*`, `drop.*`, +`event`, `keycodes[]`, `custom_cursor_bound[]`, `skip_present`) **plus** +`window_title` (a plain `char[]`, UTF-8 — no wide/UTF-16 companion like Win32, +since X11 is UTF-8 native via `UTF8_STRING`). Also `_sapp.gl.framebuffer` +(`_sapp_gl_t`, 3234–3238) = the default framebuffer object id captured after +`glXMakeCurrent` (§3, §7). + +`_sapp_init_state(desc)` runs first inside `_sapp_linux_run` (same as the others): +applies `_sapp_desc_defaults` (sample_count→1, swap_interval→1, clipboard_size→8192, +max_dropped_files→1, path_length→2048, window_title→"sokol", **and on GLCORE the +GL version defaults to 4.3 core** — 3529–3536: major=4, minor=3 on non-Apple), +sets `first_frame=true`, `dpi_scale=1.0`, `mouse.shown=true`, +`fullscreen=desc.fullscreen`, copies width/height verbatim (**may be 0** — the +X11 backend resolves 0 to 4/5 of the display, §4). + +--- + +## 1. Entry / run loop (`_sapp_linux_run`, 13836–13922) + +### Entry points (13924–13957) +- Default entry is a plain **`int main(int argc, char* argv[])`** (13925): calls + `sokol_main(argc,argv)` → `_sapp_linux_run(&desc)`. **argv is passed straight + to `sokol_main` as UTF-8** — no conversion dance (contrast Win32's + `CommandLineToArgvW`). +- **TrussC compiles `SOKOL_NO_ENTRY`** (`core/include/TrussC.h:15`, and + `core/platform/linux/sokol_impl.cpp`), so sokol's `main` is compiled out. The + live path is `sapp_run(desc)` (13941) → `_sapp_linux_run(desc)` (13952), called + from TrussC's own `runApp()` (`TrussC.h:2636`). + +### Init order (13845–13887) — load-bearing, exact +1. `_sapp_init_state(desc)`; then `_sapp.x11.window_state = NormalState`. +2. **`XInitThreads()`** — must be called before any other Xlib call to make the + connection thread-safe (TrussC runs off-thread work; keep this). +3. `XrmInitialize()` — init the resource-manager subsystem (for Xft.dpi query). +4. `_sapp.x11.display = XOpenDisplay(NULL)` — PANIC `LINUX_X11_OPEN_DISPLAY_FAILED` + on null (no DISPLAY / no X server). +5. `screen = DefaultScreen(display)`, `root = DefaultRootWindow(display)`. +6. `_sapp_x11_query_system_dpi()` (§4-DPI) → then + **`_sapp.dpi_scale = _sapp.x11.dpi / 96.0f`** (13858). Comment (13857): "on + Linux system-window-size to frame-buffer-size mapping is always 1:1" — X11 has + no separate backing-scale; DPI scaling is applied to the framebuffer directly. +7. `_sapp_x11_init_extensions()` — intern all atoms + query XInput2 (§4/§5). +8. `_sapp_x11_create_standard_cursors()` — build all `Cursor`s incl. hidden (§9). +9. **`XkbSetDetectableAutoRepeat(display, true, NULL)`** — enable detectable + auto-repeat so key-repeat does NOT generate spurious KeyRelease/KeyPress pairs + (§6). Process/connection-global. +10. `_sapp_x11_init_keytable()` — fill `_sapp.keycodes[]` via XKB names (§6). +11. **GLX bring-up** (13863–13870, `_SAPP_GLX` block): + - `_sapp_glx_init()` — dlopen libGL, resolve fn ptrs, query extensions (§3). + - `_sapp_glx_choose_visual(&visual, &depth)` — pick fbconfig → `XVisualInfo` (§3). + - `_sapp_x11_create_window(visual, depth)` — create the X window on that visual (§4). + - `_sapp_glx_create_context()` — create context + `GLXWindow` drawable, make current (§3). + - `_sapp_glx_swapinterval(_sapp.swap_interval)` — set vsync on the drawable (§3). +12. `sapp_set_icon(&desc->icon)` (13880) → `_sapp_x11_set_icon` sets `_NET_WM_ICON` (§10). +13. `_sapp.valid = true;` (13881). +14. `_sapp_x11_show_window()` — `XMapWindow` + wait for `VisibilityNotify` + raise (§4). +15. If `_sapp.fullscreen`: `_sapp_x11_set_fullscreen(true)` — **must be after map** (§4). +16. `XFlush(display)` — flush the whole setup batch to the server, then enter the loop. + +There is no `applicationDidFinishLaunching` equivalent; the run function does all +setup inline (like Win32), then loops. + +### The main loop (13888–13907) +```c +XFlush(_sapp.x11.display); +while (!_sapp.quit_ordered) { + _sapp_timing_update(&_sapp.timing, 0.0); // measure dt (external_now=0 → internal clock) + int count = XPending(_sapp.x11.display); // # events already in the queue + while (count--) { + XEvent event; + XNextEvent(_sapp.x11.display, &event); // dequeue (won't block: count bounded) + _sapp_x11_process_event(&event); // dispatch (§5) + } + _sapp_linux_frame(); // render + swap (BLOCKS on vsync, §1-frame) + XFlush(_sapp.x11.display); // push any X requests the frame made + if (_sapp.quit_requested && !_sapp.quit_ordered) {// quit dance (§8) + _sapp_x11_app_event(SAPP_EVENTTYPE_QUIT_REQUESTED); + if (_sapp.quit_requested) _sapp.quit_ordered = true; + } +} +``` +Key structural facts, contrasted with the other backends and the task brief: +- **`XPending`-snapshot-then-drain, NOT blocking `XNextEvent` at the top.** + `XPending` returns the count of already-queued events (flushing output first); + the inner loop drains *exactly that many* so it can't block waiting for input. + `frame_cb` runs inside `_sapp_linux_frame()` **once per outer iteration**, not + per event. This is the same shape as Win32's PeekMessage drain. +- **The loop has NO explicit sleep/timer.** It runs as fast as + `_sapp_linux_frame` returns, and that function **blocks inside + `glXSwapBuffers` for vsync** when `swap_interval >= 1`. That blocking swap IS + the frame pacing (see §1-frame and §11). With `swap_interval == 0` the loop + becomes a busy spin (uncapped fps). +- **Loop exit is single-flag:** the condition is only `!_sapp.quit_ordered`. + There is no `WM_QUIT`-style sentinel event. `quit_ordered` is set either by the + quit dance in the loop body (from a WM_DELETE_WINDOW ClientMessage or a + programmatic `sapp_request_quit`) or directly by `sapp_quit()` (§8). + +### Cleanup order (13908–13921) — mirrors the other backends' guarantee +1. `_sapp_call_cleanup()` (user `cleanup_cb`, once) — **before** any GL teardown. +2. `_sapp_glx_destroy_context()` — `glXDestroyWindow` + `glXDestroyContext` (§3). +3. `_sapp_x11_destroy_window()` — `XUnmapWindow` + `XDestroyWindow` + `XFreeColormap` (§4). +4. `_sapp_x11_destroy_standard_cursors()` — free all `Cursor`s (§9). +5. `XCloseDisplay(display)`. +6. `_sapp_discard_state()` — free clipboard/drop/icon buffers, zero `_sapp`. + +### Frame function (`_sapp_linux_frame`, 13818–13834) +```c +_sapp_x11_update_dimensions_from_window_size(); // sample window size EVERY frame (§4) +/* GLX branch: */ +_sapp_frame(); // runs init_cb (first frame) then frame_cb +_sapp_glx_swap_buffers(); // BLOCKS on vsync; honors skip_present (§3) +``` +- **Resize is polled here every frame**, not event-driven: + `_sapp_x11_update_dimensions_from_window_size()` does `XGetWindowAttributes` and + recomputes framebuffer size, firing RESIZED if it changed (§4). ConfigureNotify + is *not* separately handled (§5) — this poll is the size authority. Contrast + Win32 (loop-tail `update_dimensions`) and mac (delegate notification). +- The `_SAPP_EGL` fork here (13828–13832) calls `eglSwapBuffers` with the same + `skip_present` guard — **dead on desktop, live on Android/Raspbian.** + +### Frame timing source +`_sapp.timing` is the platform-agnostic filtered EMA timer (identical config to +the other backends: dt_min=1µs, dt_max=100ms, threshold=4ms, alpha=0.025, seed +1/60), fed by `_sapp_timing_update(&_sapp.timing, 0.0)` at the top of every loop +iteration (13889). `external_now==0` → it samples the internal monotonic clock. +**There is no display-refresh timestamp source** (no CADisplayLink, no RandR +mode query) — X11/GLX always uses the measured EMA filter. `sapp_frame_duration()` +→ `_sapp_timing_get` (smoothed); `sapp_frame_duration_unfiltered()` → raw `t->dt`. + +--- + +## 2. GLX renderer bring-up (`_sapp_glx_*`, 12319–12579) + +### libGL load + entry points (`_sapp_glx_init`, 12359–12426) +- dlopen cascade `"libGL.so.1"` then `"libGL.so"` (RTLD_LAZY|RTLD_GLOBAL); PANIC + `LINUX_GLX_LOAD_LIBGL_FAILED` if neither loads. +- `dlsym` all GLX 1.3 entry points (list in the struct above); PANIC + `LINUX_GLX_LOAD_ENTRY_POINTS_FAILED` if any core one is missing. +- `QueryExtension` / `QueryVersion` → PANIC on failure; **require GLX ≥ 1.3** + (`LINUX_GLX_VERSION_TOO_LOW`). +- Query the extension string and set booleans + resolve ext fn ptrs: + - `GLX_EXT_swap_control` → `SwapIntervalEXT` (per-drawable vsync). + - `GLX_MESA_swap_control` → `SwapIntervalMESA` (context-wide vsync). + - `GLX_ARB_multisample` → allow reading `GLX_SAMPLES` on fbconfigs. + - `GLX_ARB_create_context` → `CreateContextAttribsARB`. + - `GLX_ARB_create_context_profile` → flag. +- `getprocaddr` order: `glXGetProcAddress` → `glXGetProcAddressARB` → `dlsym` + (12348–12357). + +### fbconfig selection (`_sapp_glx_choosefbconfig`, 12434–12505) +`glXGetFBConfigs` for the screen → filter to RGBA + window-drawable configs +(with a Chromium/VirtualBox `GLX_VENDOR == "Chromium"` hack, 12442–12448, that +distrusts the window bit) → score each against a **desired config: 8/8/8/8 RGBA, +24 depth, 8 stencil, doublebuffer, `samples = sample_count>1 ? sample_count : 0`** +using the shared `_sapp_gl_choose_fbconfig` scorer. PANIC +`LINUX_GLX_NO_SUITABLE_GLXFBCONFIG` if none. `_sapp_glx_choose_visual` +(12507–12519) turns the chosen fbconfig into an `XVisualInfo` (visual + depth) +via `glXGetVisualFromFBConfig` — those are handed to `XCreateWindow` so the X +window matches the GL pixel format. + +### Context + drawable (`_sapp_glx_create_context`, 12526–12552) +- Re-chooses the fbconfig (must match the window's visual). +- **Requires `ARB_create_context && ARB_create_context_profile`** (else PANIC + `LINUX_GLX_REQUIRED_EXTENSIONS_MISSING`) — legacy `glXCreateContext` is never used. +- `CreateContextAttribsARB` with attribs: + `MAJOR_VERSION = desc.gl.major_version` (4), `MINOR_VERSION = minor` (3), + `PROFILE_MASK = CORE_PROFILE_BIT`, `FLAGS = FORWARD_COMPATIBLE_BIT`. + Wrapped in `_sapp_x11_grab_error_handler()` / `_release_error_handler()` so a + GLX BadMatch surfaces as a controlled PANIC (`LINUX_GLX_CREATE_CONTEXT_FAILED`), + not an Xlib abort. +- **`GLXWindow window = glXCreateWindow(display, fbconfig, x11.window, NULL)`** — + the GLX drawable wrapping the X window. **This is the per-window object in the + multiwindow plan** (one `GLXWindow` per X window). PANIC `LINUX_GLX_CREATE_WINDOW_FAILED`. +- `_sapp_glx_make_current()` (12521–12524): `glXMakeCurrent(display, glx.window, + glx.ctx)` then `glGetIntegerv(GL_FRAMEBUFFER_BINDING, &_sapp.gl.framebuffer)` — + captures the default FBO id (0) that `sapp_get_swapchain` later reports (§7). + +### Swap + swap interval +- `_sapp_glx_swap_buffers` (12565–12569): **honors `skip_present`** + (TrussC patch, 12566–12567 — one-shot consume, returns without swapping), else + `glXSwapBuffers(display, glx.window)`. **This call blocks until vsync** when the + swap interval is ≥1 — the loop's only pacing mechanism. +- `_sapp_glx_swapinterval` (12571–12577): prefer `SwapIntervalEXT(display, + glx.window, interval)` (**per-drawable** — so each window in the multiwindow + plan can set its own), else fall back to `SwapIntervalMESA(interval)` + (**context-wide** — cannot be per-window). If neither ext exists, vsync is + whatever the driver defaults to. + +### What a SECOND window needs (multiwindow-glfw pattern) +Per the design doc P2 entry (X11+GLX shared context): keep **one** `GLXContext` +(shared), and per window create its own `GLXWindow` drawable + `Colormap` + +`Window`. Before rendering window N: `glXMakeCurrent(display, glxWindowN, ctx)`, +render, `glXSwapBuffers(display, glxWindowN)`. Swap interval is per-drawable via +`SwapIntervalEXT` (set once per drawable). **Caveat for pacing:** with N vsync'd +drawables you block on N swaps per loop iteration → the frame rate can degrade to +refresh/N if the swaps don't overlap. See §11. + +--- + +## 3. Window creation (`_sapp_x11_create_window`, 12939–13002) + +- **Visual:** on GLX, the visual+depth from `_sapp_glx_choose_visual`; if null + (Vulkan/WGPU dead paths) falls back to `DefaultVisual`/`DefaultDepth`. +- **Colormap:** `XCreateColormap(display, root, visual, AllocNone)` — **per-window** + (must match the visual). Stored in `x11.colormap`, freed in destroy. +- **Attributes / event mask** (`XSetWindowAttributes`, mask + `CWBorderPixel|CWColormap|CWEventMask`): + ``` + event_mask = StructureNotifyMask | KeyPressMask | KeyReleaseMask | + PointerMotionMask | ButtonPressMask | ButtonReleaseMask | + ExposureMask | FocusChangeMask | VisibilityChangeMask | + EnterWindowMask | LeaveWindowMask | PropertyChangeMask; + ``` + (This is the complete set of events the window opts into. Note `ExposureMask` + and `VisibilityChangeMask` are selected but Expose is **not** handled in + `process_event` — see §5.) +- **Initial size:** `x11_window_width = round(window_width * dpi/96)`. If + `window_width == 0` → **4/5 of `DisplayWidth`** (12961–12966; same 4/5 rule as + mac, unlike Win32's `CW_USEDEFAULT`). Position is `0,0` (WM will place it). +- `XCreateWindow(...)` wrapped in grab/release error handler; PANIC + `LINUX_X11_CREATE_WINDOW_FAILED` on null. +- **WM protocols:** `XSetWMProtocols(WM_DELETE_WINDOW)` — the close-button + protocol. **Note: only `WM_DELETE_WINDOW` is registered; `_NET_WM_PING` is NOT** + (grep confirms zero `NET_WM_PING` in the file) — contrary to the task brief. +- **Size hints:** `XAllocSizeHints` with only `flags = PWinGravity; + win_gravity = CenterGravity`. Comment (12988): "PPosition and PSize are obsolete + and ignored." **No `PMinSize`/`PMaxSize`/`PResizeInc` are set** — there is no + min/max-size or non-resizable option (contrary to the task brief's `PMinSize`). +- **XDND advertise:** if `drop.enabled`, set `XdndAware = _SAPP_X11_XDND_VERSION (5)` + on the window (12996–12999). TrussC always sets `desc.enable_dragndrop = true` + (`TrussC.h`), so this is always on. +- **Decorations (`_MOTIF_WM_HINTS`):** **NOT set by sokol at all.** sokol has no + decoration flag in `sapp_desc`. TrussC applies it *itself* after the window + exists — `core/platform/linux/tcPlatform_linux.cpp:83 setWindowDecorated()` sets + `_MOTIF_WM_HINTS` via `sapp_x11_get_window()`/its own `XOpenDisplay`. **The + replacement must keep exposing `sapp_x11_get_window()` and `sapp_x11_get_display()`** + or TrussC's decoration/bring-to-front code breaks (§12). +- Tail: `_sapp_x11_update_window_title()` (§10) + `_sapp_x11_update_dimensions_from_window_size()`. + +`_sapp_x11_show_window` (13023–13031): `XMapWindow` → `_sapp_x11_wait_for_event( +VisibilityNotify, 0.1s)` (poll-with-timeout on the connection fd, 12599–12610) → +`XRaiseWindow` → `XFlush`. `_sapp_x11_destroy_window` (13004–13015): unmap + +destroy window + free colormap + flush. + +--- + +## 4. DPI, dimensions, fullscreen + +### DPI (`_sapp_x11_query_system_dpi`, 12283–12317) +- Reads **`Xft.dpi`** from the X resource-manager string: `XResourceManagerString` + → `XrmGetStringDatabase` → `XrmGetResource(db, "Xft.dpi", "Xft.Dpi", ...)`, + `atof` the value. This matches Qt/GTK behaviour. +- **Fallback: 96.0** (with `_SAPP_WARN(LINUX_X11_QUERY_SYSTEM_DPI_FAILED)`) if the + resource is absent. The commented-out alternative (12289) shows the physical + `DisplayWidthMM` formula sokol deliberately does *not* use. +- **Single display-wide value** (`_sapp.x11.dpi`); no per-monitor query, no RandR. + `high_dpi` in `desc` does not gate the scale on X11 the way it does on mac — + `dpi_scale` is unconditionally `dpi/96` (see §11 for the surprise). + +### Dimensions (`_sapp_x11_update_dimensions`, 12619–12641) +- **`window_scale = _sapp.x11.dpi / 96.0` — NOT `dpi_scale`** (explicit comment + 12620): `window_width = round(x11_px / window_scale)` (logical points), + `framebuffer_width = round(window_width * dpi_scale)` (device pixels). Because + `dpi_scale == window_scale` on X11 (both `dpi/96`), framebuffer ≈ raw X11 px. +- Fires RESIZED only if the framebuffer size changed **and** `!first_frame` + (12636 — so the startup poll is silent, same guard as the other backends). +- `_sapp_x11_update_dimensions_from_window_size` (12643–12647): `XGetWindowAttributes` + → feed width/height. Called **every frame** from `_sapp_linux_frame` (§1) and + after fullscreen toggle. This is the resize authority. + +### Fullscreen (`_sapp_x11_set_fullscreen`, 12649–12667) +- **EWMH `_NET_WM_STATE` client message** to the root window (via + `_sapp_x11_send_event`, 12581–12597, `SubstructureNotify|Redirect` mask), adding + (`_NET_WM_STATE_ADD=1`) or removing (`REMOVE=0`) `_NET_WM_STATE_FULLSCREEN`. + **Must be called after `XMapWindow`** (comment 12650) — hence the ordering in §1. +- `sapp_toggle_fullscreen` → `_sapp_x11_toggle_fullscreen` (12758–12762): flip + `_sapp.fullscreen`, send the state message, re-poll dimensions. `_sapp.fullscreen` + is the single source of truth (no OS notification correction like mac; the WM + may reject the request and there is no reconcile). + +--- + +## 5. Event handling (`_sapp_x11_process_event`, 13637–13685) + +Dispatch is a `switch(event->type)`. **Complete list of handled X event types:** + +| X event | handler | behaviour | +|---|---|---| +| `GenericEvent` | `_on_genericevent` 13329 | XInput2 `XI_RawMotion` — **only in mouse-lock**; reads raw dx/dy from valuators, fires MOUSE_MOVE. Uses `XGetEventData`/`XFreeEventData` cookie dance. | +| `FocusIn` | `_on_focusin` 13353 | FOCUSED (ignores `NotifyGrab`/`NotifyUngrab` modes, like GLFW). | +| `FocusOut` | `_on_focusout` 13360 | if mouse-locked → unlock; then UNFOCUSED (same grab/ungrab filter). | +| `KeyPress` | `_on_keypress` 13371 | KEY_DOWN + CHAR (§6). | +| `KeyRelease` | `_on_keyrelease` 13390 | KEY_UP (§6). | +| `ButtonPress` | `_on_buttonpress` 13402 | MOUSE_DOWN for buttons 1–3; **buttons 4/5 = vertical wheel, 6/7 = horizontal wheel** → MOUSE_SCROLL (§5-mouse). | +| `ButtonRelease` | `_on_buttonrelease` 13422 | MOUSE_UP for buttons 1–3 (wheel buttons ignored on release). | +| `EnterNotify` | `_on_enternotify` 13434 | MOUSE_ENTER — **suppressed while any button held** (`mouse_buttons != 0`). | +| `LeaveNotify` | `_on_leavenotify` 13442 | MOUSE_LEAVE — same button-held suppression. | +| `MotionNotify` | `_on_motionnotify` 13449 | MOUSE_MOVE — **only when `!mouse.locked`** (locked motion comes via GenericEvent). | +| `PropertyNotify` | `_on_propertynotify` 13456 | watches `WM_STATE` → fires ICONIFIED (IconicState) / RESTORED (NormalState) on change. This is how iconify is detected — **there is no dedicated map/unmap event handling.** | +| `SelectionNotify` | `_on_selectionnotify` 13472 | XDND drop data arrival (§8) + clipboard-paste reply (§7). | +| `SelectionRequest` | `_on_selectionrequest` 13596 | another app reads our clipboard (§7). | +| `ClientMessage` | `_on_clientmessage` 13510 | `WM_DELETE_WINDOW` → `quit_requested=true`; XDND Enter/Position/Drop (§8). | +| `DestroyNotify` | (13678) | **explicitly a no-op** ("not a bug" comment). | + +**Events selected but NOT handled:** `Expose` (ExposureMask is set but there is no +`case Expose:` — rendering is loop-driven, not paint-driven, exactly like Win32 +ignoring WM_PAINT), `ConfigureNotify` (resize is polled per-frame, §4, not +event-driven), `VisibilityNotify` (only consumed synchronously by +`_sapp_x11_wait_for_event` during map, not in the dispatch), `MapNotify`/`UnmapNotify`. + +### Mouse coordinates + scaling (`_sapp_x11_mouse_update`, 13136–13151) +- Coordinates come straight from `event->xbutton.x/y` / `xmotion.x/y` / + `xcrossing.x/y` — **raw X11 window pixels, NO dpi scaling applied** (the app + sees pixel coords; framebuffer==window in px on X11 so this is consistent). + `dx/dy` computed from the previous position when `pos_valid`; `clear_dxdy` zeroes + them on enter/leave to avoid a jump. Skipped entirely when `mouse.locked` (raw + motion path owns dx/dy then). +- Wheel: buttons 4/5 → `scroll_y = +1/-1`, buttons 6/7 → `scroll_x = +1/-1` + (13413–13418). Values are unit notches, not pixel deltas. + +### Modifier translation (`_sapp_x11_mods`, 13101–13125) +Maps the X11 state mask: `ShiftMask→SHIFT`, `ControlMask→CTRL`, `Mod1Mask→ALT`, +`Mod4Mask→SUPER`, `Button1/2/3Mask→LMB/MMB/RMB`. **Left/right is NOT distinguished +in the modifier mask** (only combined SHIFT/CTRL/ALT/SUPER) — L/R identity flows +through `key_code` only (§6). Because X11 does not set the modifier bit on the +*same* event that presses/releases the modifier key, the code **emulates** it: +key-down ORs in `_sapp_x11_key_modifier_bit(key)` (13378), key-up ANDs it out +(13397); button-down/up do the same with `_sapp_x11_button_modifier_bit`. + +Event helpers `_mouse_event`/`_scroll_event`/`_key_event`/`_char_event` +(13153–13199) all gate on `_sapp_events_enabled()` (event_cb set AND init_called), +so no events fire before the first frame's init — identical to the other backends. + +--- + +## 6. Keyboard + +### Keycode table (`_sapp_x11_init_keytable`, 12104–12281) — XKB physical-key names +- Indexing: **by X11 keycode** (range [8,255]); `_sapp.keycodes[256]`. +- Approach: **`XkbGetMap` + `XkbGetNames`**, then match each keycode's **XKB + physical key name** (`desc->names->keys[scancode].name`, 4-char strings like + `"AC01"`, `"TLDE"`, `"SPCE"`, `"RALT"`) against a hardcoded name→sapp_keycode + table (12116–12238), with a key-alias fallback. This is **layout-independent** + (physical location, not the produced symbol). `RALT`/`LVL3`/`MDSW` all map to + `RIGHT_ALT`. +- **Fallback for still-INVALID keycodes:** `XGetKeyboardMapping` + the + layout-dependent `_sapp_x11_translate_keysyms` (11939+), which switches on the + keysym (used only where the XKB name lookup failed; noted as a US-layout-biased + fallback). So the primary mechanism is **XKB key names**, NOT + `XkbKeycodeToKeysym`. +- `_sapp_x11_translate_key(scancode)` (13201–13207): bounds-check + table lookup. + +### Key repeat (`_sapp_x11_keypress_repeat`, 13242–13255) — software-tracked +- **`XkbSetDetectableAutoRepeat(true)` is set at startup** (§1 step 9) so held + keys emit repeated `KeyPress` without paired `KeyRelease` — NOT the classic + event-peek trick. Repeat detection is then a **per-keycode bool array**: + `key_repeat[keycode]` returns its prior value and sets it true on each KeyPress; + KeyRelease clears it. So the FIRST KeyPress reports `repeat=false`, subsequent + ones `repeat=true` until release. + +### CHAR events (`_on_keypress`, 13371–13388) — **XLookupString, NOT XIM/XIC** +- **This is the single biggest surprise vs the task brief.** There is **no + XOpenIM / XCreateIC / Xutf8LookupString / XmbLookupString** anywhere. (The only + `XI*` symbols in the file are `XIMaskLen`/`XIMaskIsSet` — XInput2 raw-motion + masks, unrelated to input methods.) +- CHAR is produced by: `XLookupString(&event->xkey, NULL, 0, &keysym, NULL)` to + get the raw `KeySym`, then **`_sapp_x11_keysym_to_unicode(keysym)`** + (13209+) — a Latin-1 fast path plus a **binary search over a static + keysym→Unicode table** (`_sapp_x11_keysymtab`, defined ~11050) plus the + directly-encoded 24-bit UCS keysym range. If it yields `chr > 0`, fire CHAR. +- **Consequences:** no compose-key sequences, no dead-key composition, no + CJK/IME input, no locale-aware multibyte input. Only characters expressible as a + single keysym→codepoint mapping work. **If TrussC needs IME/dead-key/CJK text + input, the port must ADD XIM/XIC (or better, xkbcommon-compose) — it cannot be + lifted from sokol because sokol never implemented it.** (This matches + `tcxIME` being a separate addon; core text input is intentionally minimal.) + +### CLIPBOARD_PASTED +Detected inside `_sapp_x11_key_event` (13179–13188): on KEY_DOWN with +`modifiers == SAPP_MODIFIER_CTRL` (exact match) and `key_code == V`, fire +CLIPBOARD_PASTED reusing the event. Same idea as Win32 (Ctrl+V), not mac (Cmd+V). +The app must call `sapp_get_clipboard_string()` in its handler (buffer not pre-filled). + +--- + +## 7. Clipboard (X selection ownership) + +Uses the **`CLIPBOARD`** selection (not `PRIMARY`), owned by our `x11.window`. +Enabled only if `desc.enable_clipboard` (TrussC always sets it true). + +- **Set (`_sapp_x11_set_clipboard_string`, 12823–12832):** copies into + `clipboard.buffer` implicitly via the caller, then `XSetSelectionOwner(CLIPBOARD, + window)` and verifies ownership (`LINUX_X11_FAILED_TO_BECOME_OWNER_OF_CLIPBOARD` + on failure). Errors `CLIPBOARD_STRING_TOO_BIG` if the string exceeds `buf_size`. +- **Get (`_sapp_x11_get_clipboard_string`, 12834–12886):** if we already own the + selection, return the internal buffer directly (avoid a round-trip). Else + `XConvertSelection(CLIPBOARD, UTF8_STRING, ...)` into a scratch property, block + on `_sapp_x11_wait_for_event(SelectionNotify, 0.1s)`, then `XGetWindowProperty` + the data. **INCR (incremental) transfers are explicitly REJECTED** (12878, + `actualType == incremental` → `CLIPBOARD_STRING_TOO_BIG`, return NULL) — large + pastes beyond `buf_size` fail rather than chunk. Only `UTF8_STRING` is requested. +- **Serving other apps (`_on_selectionrequest`, 13596–13635):** responds to + `SelectionRequest` on `CLIPBOARD` only. For `target == UTF8_STRING` → hand over + `clipboard.buffer`; for `target == TARGETS` → reply with the single-element list + `{UTF8_STRING}`; anything else → `property = None` (refuse). No INCR serving. +- **Receiving our own paste reply (`_on_selectionnotify`, 13472+):** the same + handler is shared with XDND — it only acts on the XDND selection there; clipboard + gets are synchronous via `wait_for_event`. + +--- + +## 8. Drag & drop (XDND protocol, `_on_clientmessage` + `_on_selectionnotify`) + +Implements **XDND version 5** (`_SAPP_X11_XDND_VERSION`). Only active when +`drop.enabled` (advertised via `XdndAware` on the window, §3; atoms interned in +`init_extensions` only when drop enabled, §4). + +- **`XdndEnter` (13519–13543):** record `source` window and negotiated `version` + (rejects versions > 5). Scan the offered type list (inline 3 types, or the + `XdndTypeList` property when the list bit is set) for **`text/uri-list`**; store + it as the accepted `format`. +- **`XdndPosition` (13571–13593):** reply with `XdndStatus`, accepting (`data.l[1]=1`, + `XdndActionCopy`) iff we found a usable format. Mouse position during drag is + **NOT tracked** (FIXME comment 13573 — parity with other backends). +- **`XdndDrop` (13544–13570):** if we have a format, `XConvertSelection(XdndSelection, + text/uri-list)` (with the v≥1 timestamp) to request the data; else (v≥2) reply + `XdndFinished` with rejected. +- **`SelectionNotify` for `XdndSelection` (13472–13508):** the drop data arrives; + `_sapp_x11_parse_dropped_files_list` (13257–13327) parses the (percent-encoded, + `\r\n`-separated, `file://`-prefixed) URI list into `drop.buffer` + (`num_files`/`max_path_length` bounded; `DROPPED_FILE_PATH_TOO_LONG` / + `LINUX_X11_DROPPED_FILE_URI_WRONG_SCHEME` errors). On success fires + **FILES_DROPPED** (with `modifiers` left 0 — FIXME 13484: XSelection has no state + and `XQueryKeymap` returns zeros). Then sends `XdndFinished` to the source (v≥2). + +--- + +## 9. Mouse cursor (Xcursor lib + font-cursor fallback) + +### Standard cursors (`_sapp_x11_create_standard_cursors`, 12698–12713) +Per enum, try the themed cursor first via **`XcursorLibraryLoadImage(name, theme, +size)`** (theme from `XcursorGetTheme`, size from `XcursorGetDefaultSize`) → +`XcursorImageLoadCursor`; on failure fall back to **`XCreateFontCursor(fallback)`** +(the classic X font cursors, e.g. `XC_left_ptr`, `XC_xterm`, `XC_crosshair`, +`XC_hand2`, `XC_sb_h_double_arrow`, `XC_fleur`). Names are the CSS/freedesktop +cursor names (`"default"`, `"text"`, `"pointer"`, `"ew-resize"`, `"not-allowed"`, +…). Some (NWSE/NESW/NOT_ALLOWED) have **no font fallback (fallback 0)** so they +only appear if the theme provides them. + +### Hidden cursor (`_sapp_x11_create_hidden_cursor`, 12669–12681) +A 16×16 fully-transparent `XcursorImage` loaded as a `Cursor` — used both for +`sapp_show_mouse(false)` and for mouse-lock. (No blank pixmap trick; Xcursor is a +hard dependency, linked in CMake.) + +### Apply (`_sapp_x11_update_cursor`, 12764–12780) +If shown: `XDefineCursor(window, custom_cursors[c] | standard_cursors[c])` (custom +wins when `custom_cursor_bound[c]`), or `XUndefineCursor` if the standard slot is +empty. If not shown: `XDefineCursor(window, hidden_cursor)`. Then `XFlush`. + +### Custom cursors (`_sapp_x11_make_custom_mouse_cursor`, 12729–12748) +Builds an `XcursorImage` at the requested size with the hotspot, **copies RGBA→BGRA** +(Xcursor is BGRA/ARGB32, 12739–12743), loads via `XcursorImageLoadCursor`. Destroy +via `XFreeCursor` (12750–12756). Public `sapp_bind/unbind_mouse_cursor_image` route here. + +### Mouse lock (`_sapp_x11_lock_mouse`, 12782–12821) +`XGrabPointer` confined to the window with the hidden cursor + (if XInput2 present) +`XISelectEvents(XI_RawMotion)` on the root for raw deltas; unlock re-warps the +pointer back and ungrabs. Not a TrussC main-path concern but state is per-window. + +--- + +## 10. Title, icon, misc window props + +- **Title (`_sapp_x11_update_window_title`, 12888–12904):** sets it three ways for + WM compatibility — `Xutf8SetWMProperties` (legacy `WM_NAME`/`WM_ICON_NAME`), + plus `_NET_WM_NAME` and `_NET_WM_ICON_NAME` as `UTF8_STRING` properties (the + EWMH way). UTF-8 throughout; no wide-string conversion. `sapp_set_window_title` + copies into `_sapp.window_title` then calls this. +- **Icon (`_sapp_x11_set_icon`, 12906–12937):** packs all images into a single + `_NET_WM_ICON` `CARDINAL` array — each image is `width, height, then w*h` + pixels in **ARGB `long`** order (`A<<24 | R<<16 | G<<8 | B`), `XChangeProperty` + replace. Optional for rendering; a real taskbar/titlebar icon. +- **`sapp_x11_get_window()` (14566) / `sapp_x11_get_display()` (14574):** return + the `Window` XID / `Display*`. **TrussC depends on both** in + `tcPlatform_linux.cpp` (decoration, bring-to-front) — the replacement MUST keep + them (§12). +- **`sapp_high_dpi`, `sapp_show_keyboard`:** `show_keyboard` is a no-op on X11 + (`onscreen_keyboard_shown` stays false). + +--- + +## 11. sapp_get_environment / sapp_get_swapchain (GL) + contradictions vs the design doc + +### GL environment / swapchain (14387–14486) +- `sapp_color_format()` (14018–14046): on the `#else` (GL) branch returns + **`SAPP_PIXELFORMAT_RGBA8`** (14044). **The 10-bit `RGB10A2` patch is + Metal/D3D11 ONLY** (14040–14042) — the Linux GL path is plain 8-bit RGBA8. So + none of the win32/mac 10-bit patch machinery applies here. +- `sapp_depth_format()` → `DEPTH_STENCIL` (14048). +- `sapp_get_swapchain()` fills, for `_SAPP_ANY_GL` (14477–14478), + **`res.gl.framebuffer = _sapp.gl.framebuffer`** — the default FBO id (0) + captured in `_sapp_glx_make_current`. That's the *entire* GL swapchain contract: + the glue's `sglue_swapchain()` reads `sc.gl.framebuffer` + width/height/formats/ + sample_count. No render_view/drawable/textures like D3D11/Metal. +- `sapp_get_environment()` sets `defaults.{color,depth}_format/sample_count`. There + is no device handle to pass for GL (the context is current on the thread). + +### Contradictions / surprises vs the design doc's P2 assumptions + +The design doc P2 entry says: *"X11 + GLX shared context (multiwindow-glfw +pattern), timer-paced ticks"*, and the table row reads *"Linux | timer paced to +RandR refresh rate (clock_nanosleep) | best-effort; GLX has no per-window vsync +callback."* Against the actual upstream code: + +1. **No RandR, no `clock_nanosleep`, no timer.** Upstream sokol paces frames + purely by **blocking inside `glXSwapBuffers` with the swap interval** (§1-frame, + §2). It never queries the display's refresh rate (zero RandR/XRR calls in the + file) and never sleeps. If the P2 port wants explicit timer pacing to a known + refresh rate, it must **add** RandR (`XRRGetScreenResources`/`XRRGetCrtcInfo`) + or a `clock_nanosleep` loop itself — there is nothing to lift. The simplest + faithful port keeps the blocking-swap model. +2. **Per-window vsync DOES exist** via `GLX_EXT_swap_control` + (`glXSwapIntervalEXT(display, drawable, interval)` is per-drawable, §2) — so + the doc's "GLX has no per-window vsync callback" is only half-true: there's no + *callback*, but there IS per-drawable interval control. The real + multi-window pacing hazard is the opposite: **blocking on N vsync'd swaps per + iteration can serialize to refresh/N fps.** The port likely wants to vsync only + the primary drawable (interval 1) and set interval 0 on the others, or swap all + then rely on one blocking swap. +3. **Shared context is correct** and already how upstream is shaped (one + `glx.ctx`, per-window `glx.window` `GLXWindow`). The per-window tick plan works + as long as each window does `glXMakeCurrent(display, itsDrawable, sharedCtx)` + before rendering — the current code only ever makes-current once because there's + one window. +4. **XIM/XIC does not exist** (§6) — the task/plan assumption of "XIM/XIC input + method handling to lift" is false. Text input is `XLookupString` + + keysym→Unicode table only. Dead keys / compose / IME are absent and must be + added if required (via `tcxIME` or xkbcommon-compose), not ported. +5. **`_NET_WM_PING`, `_MOTIF_WM_HINTS`, `PMinSize`/size constraints are absent** + from sokol (§3). Decoration control is TrussC's own code atop + `sapp_x11_get_window()`. Keep those getters. +6. **DPI is display-wide and unconditional** (`dpi_scale = Xft.dpi/96` regardless + of `desc.high_dpi`, §4-DPI) — there's no per-monitor DPI and no `high_dpi=false` + opt-out on X11, unlike mac/win32. A multiwindow port spanning monitors of + different DPI has no upstream story here. +7. **Resize is per-frame polled, not event-driven** (no `ConfigureNotify` + handler, §5) — a per-window tick that renders each window will naturally re-poll + that window's size each tick, which fits, but the port must poll *per window*, + not once globally. + +--- + +## 12. What TrussC actually uses (don't port what nothing calls) + +TrussC on Linux is `SOKOL_GLCORE` + `SOKOL_NO_ENTRY`, driving sapp via +`sapp_run()` → `_sapp_linux_run()`. `buildAppDescriptor` (`TrussC.h:2504`) always +sets `enable_dragndrop=true`, `enable_clipboard=true`, `high_dpi` from settings, +`sample_count` (default 4), `swap_interval` (default 1), `fullscreen`, title, icon. + +**Used and must be preserved:** +- The whole GLX bring-up + the owned XPending/frame/swap loop. +- `skip_present` in `_sapp_glx_swap_buffers` (TrussC event-driven-render patch). +- Clipboard (CLIPBOARD/UTF8_STRING), drag&drop (XDND v5, `file://` list), + fullscreen (`_NET_WM_STATE`), standard+custom+hidden cursors, `_NET_WM_ICON`, + UTF-8 title, per-frame resize poll + RESIZED, iconify/restore via WM_STATE, + focus, all mouse/key/scroll/char events. +- **Public getters `sapp_x11_get_window()` and `sapp_x11_get_display()`** — + `core/platform/linux/tcPlatform_linux.cpp` calls `sapp_x11_get_window()` for + `setWindowDecorated` (`_MOTIF_WM_HINTS`) and `bringWindowToFront` (`XRaiseWindow` + via its own `XOpenDisplay`). Dropping these breaks TrussC's window management. +- `sapp_width()/sapp_height()` (used by `tcPlatform_linux` screenshot path). + +**Present in sokol but UNUSED by TrussC / skippable in the port:** +- **The entire `_SAPP_EGL` fork** (13687–13816, and the swap fork at 13828) — + dead on desktop (only `SOKOL_GLES3` = Android/Raspbian). Don't port for desktop. +- **Vulkan / WGPU Linux forks** — dead (`SOKOL_GLCORE` only). +- **Mouse lock / XInput2 raw motion** (`_on_genericevent`, `_lock_mouse`, `xi`) — + TrussC's main path doesn't lock the pointer; low priority, but small. +- **`XkbSetDetectableAutoRepeat` is required** (not skippable) — without it the + software repeat tracking mis-detects (KeyRelease/KeyPress pairs). +- **Legacy `Xutf8SetWMProperties`** in title-setting — harmless, could keep just + the EWMH `_NET_WM_NAME` path, but cheap to keep for old-WM compatibility. +- Icon (`_NET_WM_ICON`) is used but non-load-bearing for rendering. + +--- + +## Appendix: function → line index (GLX/X11-relevant) + +| function | line | +|---|---| +| `_sapp_x11_error_handler` / `_grab` / `_release` | 11884 / 11890 / 11895 | +| `_sapp_x11_init_extensions` (atoms + XInput2) | 11900 | +| `_sapp_x11_translate_keysyms` (fallback) | 11939 | +| `_sapp_x11_keysymtab` (keysym→Unicode table) | 11050 | +| `_sapp_x11_init_keytable` (XKB names) | 12104 | +| `_sapp_x11_query_system_dpi` (Xft.dpi) | 12283 | +| `_sapp_glx_has_ext` / `_extsupported` / `_getprocaddr` | 12321 / 12340 / 12348 | +| `_sapp_glx_init` | 12359 | +| `_sapp_glx_attrib` / `_choosefbconfig` / `_choose_visual` | 12428 / 12434 / 12507 | +| `_sapp_glx_make_current` | 12521 | +| `_sapp_glx_create_context` / `_destroy_context` | 12526 / 12554 | +| `_sapp_glx_swap_buffers` (skip_present patch) | 12565 | +| `_sapp_glx_swapinterval` | 12571 | +| `_sapp_x11_send_event` / `_wait_for_event` | 12581 / 12599 | +| `_sapp_x11_app_event` | 12612 | +| `_sapp_x11_update_dimensions` / `_from_window_size` | 12619 / 12643 | +| `_sapp_x11_set_fullscreen` / `_toggle_fullscreen` | 12649 / 12758 | +| `_sapp_x11_create_hidden_cursor` | 12669 | +| `_sapp_x11_create_standard_cursor(s)` | 12683 / 12698 | +| `_sapp_x11_destroy_standard_cursors` | 12715 | +| `_sapp_x11_make/destroy_custom_mouse_cursor` | 12729 / 12750 | +| `_sapp_x11_update_cursor` | 12764 | +| `_sapp_x11_lock_mouse` | 12782 | +| `_sapp_x11_set/get_clipboard_string` | 12823 / 12834 | +| `_sapp_x11_update_window_title` | 12888 | +| `_sapp_x11_set_icon` (_NET_WM_ICON) | 12906 | +| `_sapp_x11_create_window` / `_destroy_window` | 12939 / 13004 | +| `_sapp_x11_window_visible` / `_show_window` / `_hide_window` | 13017 / 13023 / 13033 | +| `_sapp_x11_get_window_property` / `_get_window_state` | 13038 / 13057 | +| `_sapp_x11_key_modifier_bit` / `_button_modifier_bit` / `_mods` | 13073 / 13092 / 13101 | +| `_sapp_x11_translate_button` / `_mouse_update` | 13127 / 13136 | +| `_sapp_x11_mouse/scroll/key/char_event` | 13153 / 13162 / 13172 / 13191 | +| `_sapp_x11_translate_key` / `_keysym_to_unicode` | 13201 / 13209 | +| `_sapp_x11_keypress_repeat` / `_keyrelease_repeat` | 13242 / 13251 | +| `_sapp_x11_parse_dropped_files_list` | 13257 | +| `_on_genericevent` (XI raw motion) | 13329 | +| `_on_focusin` / `_on_focusout` | 13353 / 13360 | +| `_on_keypress` / `_on_keyrelease` | 13371 / 13390 | +| `_on_buttonpress` / `_on_buttonrelease` | 13402 / 13422 | +| `_on_enternotify` / `_on_leavenotify` / `_on_motionnotify` | 13434 / 13442 / 13449 | +| `_on_propertynotify` (WM_STATE iconify) | 13456 | +| `_on_selectionnotify` (XDND drop / clipboard) | 13472 | +| `_on_clientmessage` (WM_DELETE / XDND) | 13510 | +| `_on_selectionrequest` (serve clipboard) | 13596 | +| `_sapp_x11_process_event` (dispatch) | 13637 | +| `_sapp_linux_frame` | 13818 | +| `_sapp_linux_run` | 13836 | +| `main` | 13925 | +| `sapp_run` (SOKOL_NO_ENTRY dispatch) | 13941 | +| `sapp_color_format` (GL → RGBA8) | 14018 | +| `sapp_get_environment` / `sapp_get_swapchain` (gl.framebuffer) | 14387 / 14417 | +| `sapp_x11_get_window` / `sapp_x11_get_display` | 14566 / 14574 | diff --git a/docs/dev/sokol_app_tc-design.md b/docs/dev/sokol_app_tc-design.md index 7cbd29ad..87e2317a 100644 --- a/docs/dev/sokol_app_tc-design.md +++ b/docs/dev/sokol_app_tc-design.md @@ -191,6 +191,32 @@ can feed events manually, so imgui does not chain us to sokol_app. second-window/clean-exit/drag-render all pass. - **P2 — Linux driver.** X11 + GLX shared context (multiwindow-glfw pattern), timer-paced ticks. + - **KICKOFF READY — implementation contract: docs/dev/sapp-x11-impl-spec.md** + (opus-extracted). Two original design assumptions were WRONG and the + plan is corrected as follows: + 1. Upstream has NO timer/RandR pacing to lift — it paces purely by + blocking inside a vsync'd glXSwapBuffers. And GLX DOES have + per-drawable swap intervals (glXSwapIntervalEXT), just no vsync + callback. Corrected tick model: the MAIN window keeps upstream + parity (blocking vsync'd swap = the loop's pacing source); + SECONDARY windows use swap interval 0 + timer due-checks inside + the same loop (RandR-queried refresh as the period). Never vsync + more than one drawable — N blocking swaps per iteration would + serialize to refresh/N fps. Mixed-Hz fidelity is therefore coarser + than mac/win (secondary jitter ≤ one main-frame); accepted, + documented. + 2. There is NO XIM/XIC upstream: CHAR events are XLookupString + a + static keysym→unicode table, no compose/dead keys/IME. Port as-is + (parity), IME stays tcxIME territory. + Other load-bearing facts: GL swapchain is RGBA8 (the 10-bit patch does + not apply on GLX); resize is polled per frame via XGetWindowAttributes + (no ConfigureNotify handler) — poll per window per tick; keytable uses + XKB physical key names (XkbGetNames) with software repeat tracking that + REQUIRES XkbSetDetectableAutoRepeat at startup; keep the public + sapp_x11_get_window()/sapp_x11_get_display() getters (tcPlatform_linux + applies Motif decoration hints itself); shared GLXContext + per-window + GLXWindow/Colormap, glXMakeCurrent(display, drawable, ctx) before each + window's render. - **P3 — web driver.** RAF tick, HTML5 event callbacks, WebGL context; port the canvas-keyboard patch natively. - **P4 — iOS driver.** UIWindow + CAMetalLayer + CADisplayLink + touch. From 5c5881e194954d01a2b342698eff2f72d3eb98ab Mon Sep 17 00:00:00 2001 From: tettou771 Date: Mon, 13 Jul 2026 18:22:45 +0900 Subject: [PATCH 36/87] =?UTF-8?q?doc:=20roadmap=20=E2=80=94=20retire=20imp?= =?UTF-8?q?lemented=20entries=20(audio,=20font=20wrap),=20add=20feature=20?= =?UTF-8?q?candidates?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Retired (already implemented): real-time audio events (audioOut/audioIn, 2026-05-26), AudioSettings init, Sound::loadStream, and font line wrapping (enableWrap/setMaxLineLength + hyphenation + kinsoku + vertical, all phases). Hot-plug detection stays (audioDeviceChanged only fires from init today). Added candidates: coroutine sequencing (Lua wait() + C++20 co_await), ScreenRecorder audio track, audio analysis (onset/beat/Mel), GPU compute (sokol ready; WebGL2 excluded), input record & replay, frame vector export (SVG/PDF), kiosk/installation mode, spring/smoothDamp, and an Addon Candidates section (tcxTimeline). --- docs/ROADMAP.md | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 85dc06c0..9d0e378b 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -17,14 +17,18 @@ | Area lights | Rectangle / disc / line area light sources | High | | Cascaded shadow maps | CSM for directional lights (large outdoor scenes) | High | | Unified `LoadResult` API | Replace `bool` return of `Image::load` / `SoundBuffer::load` / `Video::load` / `Font::load` / `Shader::load` with a shared `trussc::LoadResult` carrying `LoadError` enum + message + raw code. Use `explicit operator bool()` so existing `if (x.load(...))` keeps working. Needs an audit of error taxonomy first (file-not-found / invalid-format / permission-denied / decoder-failure / etc.) and a per-domain inventory of what error sources exist (stb_image, AVFoundation, mbedTLS, miniaudio, etc.). | High | -| Real-time audio stream API | Expose the audio callback so user code can synthesize / process samples per buffer (typical 256–512 frames at 96 kHz). oF-style listener pattern: `App` gets an overridable `audioOut(float* out, size_t frames, int channels)` (and `audioIn(const float* in, ...)`); plus a free-function or `SoundStream` class for non-App-bound code. Currently the only way to produce sound is pre-baked `SoundBuffer` + `Sound::play()`, which can't handle dynamic synthesis (sine generators with live parameter changes, granular synthesis, software synths, audio effects, etc.). Output runs at engine native rate (96 kHz, no rateRatio path) — document that. Likely paired with `tc::AudioSettings` (sample rate / buffer size / channel count / max polyphony) passed to a new `AudioEngine::init(settings)` overload, so the engine config is exposed at the same time. | High | -| `AudioEngine::init(AudioSettings)` overload | Currently `AudioEngine::SAMPLE_RATE` / `NUM_CHANNELS` / `MAX_PLAYING_SOUNDS` are `static constexpr`. Wrap them in a `tc::AudioSettings` struct so `AudioEngine::init(settings)` (called at the very start of `setup()` before any `Sound::load()`) can opt into a different sample rate (48 kHz for mobile CPU savings, 192 kHz for hi-res), buffer size, polyphony, etc. Existing zero-config callers keep working — `init()` without args = current defaults. Late callers (after auto-init) get a warning + ignored settings (silent re-init is too disruptive to already-playing sounds). Move the constexpr references in tcVideoPlayer_linux.cpp etc. to a runtime `getSampleRate()` accessor. Often implemented in the same PR as the real-time stream API entry above. | Medium | -| Streaming audio playback | New `Sound::loadStream(path)` (sibling of `Sound::load`) that keeps the underlying `ma_decoder` alive and reads from disk on demand via a worker-thread-fed ring buffer (~16 KB prefetch). Cuts memory cost for long files (multi-minute BGM, podcasts) from "full PCM in RAM" (e.g. ~80 MB for a 4-minute stereo track decoded to float) to a fixed-size prefetch. WAV / MP3 / FLAC ride miniaudio's incremental `ma_decoder_read_pcm_frames`; OGG path mirrors with stb_vorbis incremental reads. AAC stays on-memory (platform decoders buffer internally). Short SFX should keep using on-memory `load()` — streaming is opt-in. Seek requires re-fill of prefetch and ~10 ms blackout, acceptable for music. | Medium | -| Audio device hot-plug handling | Detect device disconnect (USB DAC unplugged etc.) via miniaudio's `ma_device_notification_proc`. On detection, auto-fail-over to the system default device, then fire `AudioEngine::audioDeviceChanged` with the new device's info. Listeners can override the fallback by calling `init(settings)` themselves. Without this, calling `init(settings)` to switch devices after a hot-unplug will hang in `ma_device_uninit` waiting for the dead device's audio thread to join. Likely also wants a `cause` enum on `AudioDeviceChangedArgs` (InitialInit / UserRequest / DeviceDisconnect) so listeners can distinguish event sources. | Medium | +| Audio device hot-plug handling | Detect device disconnect (USB DAC unplugged etc.) via miniaudio's `ma_device_notification_proc`. On detection, auto-fail-over to the system default device, then fire `AudioEngine::audioDeviceChanged` with the new device's info (today that event only fires from `init()` — initial + user re-init — not from unplug). Listeners can override the fallback by calling `init(settings)` themselves. Without this, calling `init(settings)` to switch devices after a hot-unplug will hang in `ma_device_uninit` waiting for the dead device's audio thread to join. Likely also wants a `cause` enum on `AudioDeviceChangedArgs` (InitialInit / UserRequest / DeviceDisconnect) so listeners can distinguish event sources. | Medium | | Custom-shader textured quad path (sgl-integrated) | A TrussC-level drawing path that issues `sg_apply_pipeline` + `sg_apply_bindings` + `sg_draw` with a user-supplied shader, while staying ordered with respect to surrounding `sokol_gl` commands. Built on the existing `suspendSwapchainPass()` / `resumeSwapchainPass()` machinery (already used by FBO), plus a partial-flush of the default sgl context so a `drawRect → font/customShader → drawCircle` sequence renders in submission order. Unlocks: R8 / RG8 / non-RGBA texture formats (current sgl shader forces RGBA8 with `(255,255,255,A)` padding), font atlas R8 storage (see below), sensor-data visualization (depth/IR/grayscale with palette mapping), SDF text, mask compositing, future Path glyph rendering. Probably surfaces as `Texture::drawWith(Shader&, ...)` or similar on the TrussC `gpu/` side; sgl_gl_tc fork ideally stays untouched. | High | | Font atlas R8 storage | Drop font atlas from RGBA8 to R8 (4× memory reduction — e.g. ~4 MB → ~1 MB on a large CJK atlas). Currently blocked on the custom-shader path above: the sgl built-in shader returns `(R, 0, 0, 1)` from an R8 sample, which renders as solid red instead of `(color.rgb, glyph.a)`. Once that path lands, swap the atlas pixel format + add a tiny font-specific fragment shader doing `out = vec4(uColor.rgb, sample.r * uColor.a)`. No quality loss (glyphs were already grayscale alpha stored as `(255,255,255,A)`). Sokol R8 is native on all current backends (Metal / D3D11 / GL 3.3 / GLES3 / WebGL2 / WebGPU — GLES2 / WebGL1 already dropped). Pair with this PR cycle's tategaki work to make Japanese / CJK use cases cheap on RAM. | Low (once shader path exists) | | Mouse event bubbling (press/release/drag) — **v0.7.0, breaking** | Unify input propagation with the scroll path, which already bubbles ("return false to allow bubbling to parent", see `RectNode::onMouseScroll`). Today `dispatchMousePress` delivers to the front-most hit node ONLY — if its `fireMousePress` returns false the event silently dies (no ancestor walk, no occluded-sibling fallback), so press/release/drag and scroll follow different rules. Plan: when the hit node does not consume, walk the parent chain (localizing via `localizeMouse` per level, firing node + mod hooks), and the first consumer becomes `grabbedNode` for drag tracking. Keep `RectNode::onMousePress`'s `return true` (consume) default, so bubbling activates only where a handler explicitly returns false — a path that today just swallows the event, making the change near-compatible in practice but still semantically breaking (hence the 0.7.0 window). Kills the current anti-pattern where a child must know its parent to forward input (contradicts the "dependencies point downward only" rule) and unblocks drag-through-children cases (grab a window by its title bar across label children; drag-scroll a list over its buttons). Watch mouse enter/leave consistency along the chain. **Considered and deferred, revisit after bubbling lands:** flipping hit-testing to default-ON ("visible RectNode catches clicks", DOM/Unity-style, opt-out via `disableEvents()`). Only sane as a package with bubbling + consume-by-choice (make `RectNode::onMousePress` return whether a listener consumed instead of unconditional true); default-ON alone under single-target dispatch would make every decorative label/separator child swallow its parent widget's clicks. Perf is a non-issue either way: the hit-test walk already visits all active+visible nodes (Mat4 inverse + children snapshot per node) and `eventsEnabled_` only gates the final rect test. | Medium | -| Font line wrapping (`enableWrap` + `setMaxLineLength`) | Opt-in text wrapping that flows long strings inside a length budget, default off so existing `drawString` behavior is unchanged. Staged rollout, all sharing the same internal "text → break-opportunity list → placement" split so later phases only refine the second half. **Phase 1**: basic wrap for horizontal text — break at ASCII spaces and between CJK ideographs (simplified UAX #14). **Phase 2**: Latin hyphenation — insert a `-` when a word has to be split mid-run, no dictionary, just last-resort. **Phase 3**: CJK `kinsoku` (行頭/行末禁則) opt-in via `setHangingPunctuation(true)` or similar — prevent `、。」』)` etc. from starting a line, either by hanging past the edge (ぶら下げ) or pulling back into the previous line (追い込み). Use a small kinsoku character set, not a full table. **Phase 4**: reuse the same pipeline for vertical writing — `maxLineLength` then means column height, columns break right→left as already implemented, kinsoku tables carry over. Justification / Knuth-Plass / advanced Western typography stays out of scope. Differentiator versus other creative-coding frameworks (oF / Cinder / Processing) where wrap is either absent or western-only. Likely lives on its own branch (`feat/font-wrap`) after the tategaki PR lands so the diffs stay reviewable. | Medium | +| Coroutine sequencing (`wait` / `tween` / `event` awaiters) | Write time-spanning procedures as procedures: `move(); wait(1); move();` instead of callback chains or hand-rolled state machines in `update()`. Two layers, cheapest first. **Lua (TrussSketch)**: Lua coroutines are native — the host only needs a scheduler that resumes yielded coroutines next frame / after N seconds; `wait(1)` = sugar over `coroutine.yield(1)`. Scratch-style sequencing lands in sketches within days of work. **C++20**: TrussC already requires C++20, so `co_await` is available — a `Task` type + frame-loop-driven awaiters (`co_await wait(1.0)`, `co_await tween(...)`, `co_await event(node.mousePressed)` = "suspend until clicked"). Big win for installation flow control (idle → attract → interact → reset). Ship Lua first, stabilize the C++ API shape against real sketch usage. Purely additive — no existing path changes. | Medium (Lua) / High (C++) | +| ScreenRecorder audio track | Record audio (app output and/or mic) into the video file alongside the frames. **The one item in this batch with real regression surface**: it modifies the working per-platform encode pipelines (AVFoundation / Media Foundation / ...) to mux an audio track with correct A/V sync — so it should ship as its own release, verified on real hardware per platform, not bundled with other work. Pairs naturally with the real-time audio events (`audioOut` already exposes the exact buffers to record). | Medium-High | +| Audio analysis: onset / beat / Mel bands | FFT already exists (`tcFFT.h`); layer the standard music-visualization trio on top: **onset detection** (spectral flux — the moment a drum hit / note attack happens), **beat tracking** (period estimation over the onset train → BPM + phase), **Mel-band energies** (perceptual frequency bands, the usual input for reactive visuals). Makes "visuals that react tightly to music" a one-liner — a workshop staple that currently requires hand-rolling. Additive on the existing FFT chain. | Medium | +| GPU compute (`tc::ComputeShader`) | The vendored sokol_gfx already supports compute passes (`sg_begin_pass({.compute=true})` + storage buffers/images + dispatch; sokol-shdc cross-compiles `@cs` shaders). Wrap it TrussC-style and ship a flagship GPU-particle example (1M particles). **Backend caveat**: compute works on Metal (macOS/iOS), D3D11 (Windows), GL/GLES3.1+ (Linux/Android), and **WebGPU only on web — WebGL2 is excluded**, so compute examples can't run in the current WebGL2-based web player (mark `webSupported: false`, or revisit when the player moves to WebGPU). Desktop/mobile are fully ready today. | Medium-High | +| Input record & replay (`tc::InputRecorder`) | Record timestamped input events (mouse / key / MIDI / OSC) to a file and replay them deterministically through the same event bus MCP injection already uses. Three uses for the price of one: **attract mode** for unattended exhibits (replay a canned interaction), **repro debugging** (capture the "happens sometimes" and take it home), **regression tests** (record an interaction once, assert on the result in CI or via MCP). Additive — replay enters through the existing injection path. | Medium | +| Frame vector export (SVG / PDF) | `beginVectorCapture("frame.svg")` records the frame's draw calls (rects, circles, paths, text-as-paths) as vectors instead of pixels. Serves the pen-plotter (AxiDraw) and print/generative-poster crowd — a real community that today stays on Processing for its PDF export; oF's ofxCairo path is effectively unmaintained. TrussC's thin unified draw API makes the interception layer tractable. Opt-in capture: zero cost when off (verify that). Raster-only features (shaders, textures) are out of scope or rasterized-on-import. | Medium-High | +| Kiosk / installation mode (trusscli) | First-class answer to "museum runs an app for 3 months": `trusscli kiosk` (or app-side `InstallationSettings`) = watchdog auto-restart on crash/hang, log rotation, crash-report collection (.ips etc.), scheduled start/stop, and a tiny remote health endpoint (HTTP status: fps, uptime, last error). Every long-running oF installation reinvents this with shell loops; nobody ships it as a framework feature. Mostly trusscli / process-level — near-zero core regression risk. | Medium | +| Spring / smoothDamp utilities | `smoothDamp(current, target, velocity, smoothTime)` (critically damped, Unity-style) + a tiny spring type. "Follow the mouse with nice easing" is hand-rolled by every user today; two functions in tcMath end that. Zero risk, high daily-use value. | Low | ### Medium Priority @@ -39,6 +43,14 @@ | Dear ImGui 1.92.6 → latest | Currently on 1.92.6 WIP. 1.92.8 (2026-05-12) introduced a notable breaking change: `AddRect()` / `AddPolyline()` / `PathStroke()` swapped their `flags` and `thickness` arg positions. TrussC core does not touch these, but user code that does will need updates. Inline redirection keeps source compatible unless `IMGUI_DISABLE_OBSOLETE_FUNCTIONS` is on. Plan a single bump to the latest 1.92.x. | Low | +--- + +## Addon Candidates + +| Addon | Description | +|:------|:------------| +| tcxTimeline | Keyframe timeline editor for any `TC_REFLECT`ed property — the building blocks all exist in TrussC already (property enumeration via reflection, tcxImGui for the editor UI, JSON serialization for saving tracks). Spiritual successor to ofxTimeline / Duration (James George, developed with YCAM InterLab), which is effectively unmaintained — its users (installation / show-control / VJ work) have nowhere current to go. Also the natural showcase for the reflection system. | + --- ## oF Compatibility Gap From dd874ffa2ec89ccf66dbe550767f732f6e341dbd Mon Sep 17 00:00:00 2001 From: tettou771 Date: Mon, 13 Jul 2026 18:59:02 +0900 Subject: [PATCH 37/87] feat: implement Linux X11/GLX driver in sokol_app_tc.h (P2) Replace sokol_app.h's Linux implementation with sokol_app_tc.h on desktop Linux (SOKOL_GLCORE; Raspberry Pi stays on upstream GLES3/EGL): - One shared GLXContext, one GLXWindow drawable + Colormap per window. - Main window keeps upstream's pacing model: the run loop blocks inside its vsync'd glXSwapBuffers (interval 1 via GLX_EXT_swap_control per drawable). If the swap stops blocking (iconified, skip_present, MESA context-wide fallback) the tick self-heals to timer pacing, so no busy-spin is possible. - Secondary windows swap with interval 0 (vsyncing N drawables would serialize the loop to refresh/N) and are timer-paced to a refresh estimate measured from the main window's presents. - The loop sleeps in poll() on the X connection fd when nothing is due. - XKB physical-key-name keytable + XkbSetDetectableAutoRepeat software repeat, XLookupString + keysym table CHAR events (lifted verbatim from sokol_app.h), CLIPBOARD selection, XDND v5 on every window, EWMH fullscreen, Xcursor cursors, _MOTIF_WM_HINTS borderless, per-tick size polling, WM_DELETE quit dance, skip_present honored. - GLX entry points called directly (libGL is always linked on Linux); only extensions go through glXGetProcAddressARB. No dlopen. - New cross-platform getters: sapp_window_gl_framebuffer() and sapp_window_x11_get_window() (zero stubs on mac/win/other). - tcWindowLinux.cpp: TrussC adapter mirroring tcWindowWin.cpp (GL swapchain = default framebuffer, RGBA8). - tcWindow.h: Linux joins TC_PLATFORMS; stubs stay for web/Android/RasPi. --- core/include/sokol/sokol_app_tc.h | 3298 ++++++++++++++++++++++++- core/include/tc/app/tcWindow.h | 28 +- core/platform/linux/sokol_impl.cpp | 16 +- core/platform/linux/tcWindowLinux.cpp | 326 +++ 4 files changed, 3643 insertions(+), 25 deletions(-) create mode 100644 core/platform/linux/tcWindowLinux.cpp diff --git a/core/include/sokol/sokol_app_tc.h b/core/include/sokol/sokol_app_tc.h index faefa075..6fe6e023 100644 --- a/core/include/sokol/sokol_app_tc.h +++ b/core/include/sokol/sokol_app_tc.h @@ -18,12 +18,12 @@ WHAT IT IS ========== - Native multi-window support with per-window vsync ticks. On macOS and - Windows this header additionally IMPLEMENTS the public single-window - sokol_app.h API (sapp_run, sapp_width, sapp_dpi_scale, ...): the "main - window" is simply window #0, created from the sapp_desc, and the app's - OS run loop is owned here. sokol_app.h must then be included WITHOUT its - implementation (declarations only) in the implementation TU: + Native multi-window support with per-window vsync ticks. On macOS, + Windows and Linux this header additionally IMPLEMENTS the public + single-window sokol_app.h API (sapp_run, sapp_width, sapp_dpi_scale, ...): + the "main window" is simply window #0, created from the sapp_desc, and the + app's OS run loop is owned here. sokol_app.h must then be included WITHOUT + its implementation (declarations only) in the implementation TU: #include "sokol_app.h" // no SOKOL_IMPL / SOKOL_APP_IMPL here #define SOKOL_IMPL @@ -98,8 +98,11 @@ macOS: implemented (NSWindow + CAMetalLayer + CADisplayLink, macOS 14+). Windows: implemented (HWND + D3D11 + DXGI flip swapchain, one frame latency waitable object per window; Windows 10+). - Others: stubs that return an invalid handle (explicit platform gap); - Linux (X11 + shared GL) is the next port. + Linux: implemented (X11 + GLX, one shared GL context with a GLXWindow + drawable per window; main window paced by its blocking vsync'd + glXSwapBuffers, secondary windows swap interval 0 + timer-paced). + Others (web/mobile): stubs that return an invalid handle (explicit + platform gap). */ #define SOKOL_APP_TC_INCLUDED (1) #include @@ -165,9 +168,14 @@ SOKOL_APP_API_DECL const void* sapp_window_d3d11_render_view(sapp_window win); SOKOL_APP_API_DECL const void* sapp_window_d3d11_resolve_view(sapp_window win); SOKOL_APP_API_DECL const void* sapp_window_d3d11_depth_stencil_view(sapp_window win); -/* native escape hatches (macOS: NSWindow*; Windows: HWND) */ +/* GL swapchain handle -- the default-framebuffer id (Linux/GLX: rendering + targets the current drawable, which is this window's during its tick_cb) */ +SOKOL_APP_API_DECL uint32_t sapp_window_gl_framebuffer(sapp_window win); + +/* native escape hatches (macOS: NSWindow*; Windows: HWND; Linux: X11 Window XID) */ SOKOL_APP_API_DECL const void* sapp_window_macos_get_window(sapp_window win); SOKOL_APP_API_DECL const void* sapp_window_win32_get_hwnd(sapp_window win); +SOKOL_APP_API_DECL const void* sapp_window_x11_get_window(sapp_window win); #if defined(__cplusplus) } /* extern "C" */ @@ -1377,11 +1385,13 @@ const void* sapp_window_macos_get_window(sapp_window win) { return w ? (__bridge const void*)w->window : 0; } -/* D3D11 / Win32 handles do not exist on macOS */ +/* D3D11 / Win32 / GL / X11 handles do not exist on macOS */ const void* sapp_window_d3d11_render_view(sapp_window win) { (void)win; return 0; } const void* sapp_window_d3d11_resolve_view(sapp_window win) { (void)win; return 0; } const void* sapp_window_d3d11_depth_stencil_view(sapp_window win) { (void)win; return 0; } const void* sapp_window_win32_get_hwnd(sapp_window win) { (void)win; return 0; } +const void* sapp_window_x11_get_window(sapp_window win) { (void)win; return 0; } +uint32_t sapp_window_gl_framebuffer(sapp_window win) { (void)win; return 0; } /*-- sokol_app.h public API (macOS implementation lives here) ---------------*/ @@ -3320,6 +3330,10 @@ const void* sapp_window_win32_get_hwnd(sapp_window win) { return w ? (const void*)w->hwnd : 0; } +/* GL / X11 handles do not exist on Windows (D3D11 backend) */ +const void* sapp_window_x11_get_window(sapp_window win) { (void)win; return 0; } +uint32_t sapp_window_gl_framebuffer(sapp_window win) { (void)win; return 0; } + /*-- sokol_app.h public API (Windows implementation lives here) --------------*/ void sapp_run(const sapp_desc* desc) { @@ -3683,8 +3697,3268 @@ sapp_swapchain sapp_get_swapchain(void) { } /* extern "C" */ +#elif defined(__linux__) && !defined(__ANDROID__) && !defined(__EMSCRIPTEN__) +/*== Linux (X11 + GLX, desktop GL core) ===================================== + Implements the public sokol_app.h API on Linux plus the multi-window API. + Structure mirrors sokol_app.h's X11/GLX backend (quit dance on + WM_DELETE_WINDOW, per-frame size polling instead of ConfigureNotify, XKB + physical-key-name keytable + XkbSetDetectableAutoRepeat software repeat, + XLookupString + keysym table CHAR events, CLIPBOARD selection, XDND v5, + EWMH fullscreen, Xcursor cursors, TrussC skip_present patch). + + Frame pacing: ONE GLXContext is shared by all windows, each window has its + own GLXWindow drawable. The MAIN window keeps upstream's model: the run + loop blocks inside its vsync'd glXSwapBuffers (swap interval 1 via + GLX_EXT_swap_control on its drawable) -- that blocking swap IS the pacing. + SECONDARY windows swap with interval 0 (never block; vsyncing two or more + drawables would serialize the loop to refresh/N) and are timer-paced to a + refresh-period estimate measured from the main window's presents. If the + main swap stops blocking (iconified, skip_present, no usable swap-control + extension), the tick self-heals to timer pacing, so the loop can never + busy-spin. + + Unlike upstream, GLX entry points are called directly (TrussC's Linux + build always links libGL for sokol_gfx's GLCORE backend, so the dlopen + dance is unnecessary); only extension functions go through + glXGetProcAddressARB. */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#if defined(SOKOL_APP_IMPL_INCLUDED) +#error "sokol_app_tc.h owns the Linux implementation of the sapp_* API; include sokol_app.h WITHOUT an implementation define in this TU" +#endif + +#ifndef SOKOL_ASSERT +#include +#define SOKOL_ASSERT(c) assert(c) +#endif + +/* GLX extension constants (present in on any current system; + defined here so an exotic glx.h cannot break the build) */ +#ifndef GLX_CONTEXT_MAJOR_VERSION_ARB +#define GLX_CONTEXT_MAJOR_VERSION_ARB 0x2091 +#endif +#ifndef GLX_CONTEXT_MINOR_VERSION_ARB +#define GLX_CONTEXT_MINOR_VERSION_ARB 0x2092 +#endif +#ifndef GLX_CONTEXT_PROFILE_MASK_ARB +#define GLX_CONTEXT_PROFILE_MASK_ARB 0x9126 +#endif +#ifndef GLX_CONTEXT_CORE_PROFILE_BIT_ARB +#define GLX_CONTEXT_CORE_PROFILE_BIT_ARB 0x00000001 +#endif +#ifndef GLX_CONTEXT_FLAGS_ARB +#define GLX_CONTEXT_FLAGS_ARB 0x2094 +#endif +#ifndef GLX_CONTEXT_FORWARD_COMPATIBLE_BIT_ARB +#define GLX_CONTEXT_FORWARD_COMPATIBLE_BIT_ARB 0x00000002 +#endif +#ifndef GLX_SAMPLES +#define GLX_SAMPLES 0x186A1 +#endif + +#define _SAPP_TC_MAX_WINDOWS (32) +#define _SAPP_X11_MAX_X11_KEYCODES (256) +#define _SAPP_TC_XDND_VERSION (5) + +/* EMA-filtered frame timer, port of sokol_app.h's _sapp_timing_* (X11 has no + display-link timestamps, so this measured filter is the only dt source) */ +typedef struct { + double last; /* previous sample time, 0 = none yet */ + double ema; + double smooth_dt; + double dt; /* last raw (clamped) delta */ +} _sapp_tc_timing_t; + +typedef struct _sapp_tc_window_t { + uint32_t win_id; + sapp_window_desc desc; + bool is_main; /* window #0: created by sapp_run, drives the app callbacks */ + bool ready; /* creation finished; event dispatch may deliver events */ + Window xwin; /* the X11 window XID */ + Colormap colormap; /* per-window (created against the shared visual) */ + GLXWindow glx_win; /* the GLX drawable wrapping xwin */ + int fb_width, fb_height; /* framebuffer pixels (== X11 window pixels) */ + int win_width, win_height; /* logical points */ + float dpi_scale; /* framebuffer px per point (Xft.dpi / 96) */ + double earliest_next; /* CLOCK_MONOTONIC seconds; window is not due before this */ + double last_present; /* time of the previous real present (main pacing probe) */ + double frame_duration; + _sapp_tc_timing_t timing; + bool iconified; /* WM_STATE == IconicState */ + bool occluded; /* VisibilityFullyObscured (secondary windows only) */ + bool in_tick; + bool mouse_pos_valid; + float mouse_x, mouse_y; /* last position in event coordinates (raw px) */ + float mouse_dx, mouse_dy; + uint8_t mouse_buttons; /* held-button bitmask (enter/leave suppression) */ + bool key_repeat[_SAPP_X11_MAX_X11_KEYCODES]; /* software repeat tracking */ +} _sapp_tc_window_t; + +static struct { + bool keytable_valid; + sapp_keycode keycodes[SAPP_MAX_KEYCODES]; + uint32_t next_id; + _sapp_tc_window_t* windows[_SAPP_TC_MAX_WINDOWS]; + + /* ---- X11 process-globals (one server connection for all windows) ----- */ + Display* display; + int screen; + Window root; + float dpi; /* Xft.dpi, 96.0 fallback */ + unsigned char error_code; /* set by the temporary X error handler */ + XErrorHandler prev_error_handler; + Atom UTF8_STRING; + Atom CLIPBOARD; + Atom TARGETS; + Atom WM_PROTOCOLS; + Atom WM_DELETE_WINDOW; + Atom WM_STATE; + Atom NET_WM_NAME; + Atom NET_WM_ICON_NAME; + Atom NET_WM_STATE; + Atom NET_WM_STATE_FULLSCREEN; + Atom MOTIF_WM_HINTS; + Atom SAPP_TC_SELECTION; /* scratch property for clipboard transfers */ + struct { + int version; + Window source; + Atom format; + _sapp_tc_window_t* over; /* which of OUR windows the drag targets */ + Atom XdndAware, XdndEnter, XdndPosition, XdndStatus, XdndActionCopy, + XdndDrop, XdndFinished, XdndSelection, XdndTypeList, text_uri_list; + } xdnd; + Cursor hidden_cursor; + Cursor standard_cursors[_SAPP_MOUSECURSOR_NUM]; + Cursor custom_cursors[_SAPP_MOUSECURSOR_NUM]; + + /* ---- GLX (one shared context; per-window GLXWindow drawables) -------- */ + GLXFBConfig fbconfig; + GLXContext ctx; + uint32_t gl_framebuffer; /* default-FBO id captured after make-current */ + void (*SwapIntervalEXT)(Display*, GLXDrawable, int); /* per-drawable */ + int (*SwapIntervalMESA)(int); /* context-wide */ + GLXContext (*CreateContextAttribsARB)(Display*, GLXFBConfig, GLXContext, Bool, const int*); + double refresh_period; /* estimate from the main window's vsync'd presents */ + + /* ---- app / main-window state (this header owns sapp_run on Linux) ---- */ + struct { + sapp_desc desc; /* sanitized copy; window_title points at the buffer below */ + char window_title[256]; + bool valid; + bool init_called; + bool cleanup_called; + bool quit_requested; + bool quit_ordered; + bool fullscreen; + bool event_consumed; + bool skip_present; /* one-shot; honored on glXSwapBuffers (TrussC flicker patch) */ + uint64_t frame_count; + _sapp_tc_window_t* main; + /* mouse cursor */ + bool mouse_shown; + sapp_mouse_cursor current_cursor; + bool custom_cursor_bound[_SAPP_MOUSECURSOR_NUM]; + /* clipboard */ + bool clipboard_enabled; + int clipboard_size; + char* clipboard; + /* drag & drop */ + bool drop_enabled; + int drop_max_files; + int drop_max_path_length; + int drop_num_files; + char* drop_buffer; + } app; +} _sapp_tc; + +static void _sapp_tc_x11_tick(_sapp_tc_window_t* w); +static bool _sapp_tc_x11_update_dimensions(_sapp_tc_window_t* w); +static void _sapp_tc_x11_apply_cursor(void); + +/*-- timing -----------------------------------------------------------------*/ +static double _sapp_tc_now(void) { + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + return (double)ts.tv_sec + (double)ts.tv_nsec * 1.0e-9; +} + +static void _sapp_tc_timing_reset(_sapp_tc_timing_t* t) { + t->last = 0.0; + t->ema = t->smooth_dt = t->dt = 1.0 / 60.0; +} + +static double _sapp_tc_timing_clamp(double dt) { + if (dt < 0.000001) return 0.000001; + if (dt > 0.1) return 0.1; + return dt; +} + +static void _sapp_tc_timing_update(_sapp_tc_timing_t* t) { + const double now = _sapp_tc_now(); + if (t->last > 0.0) { + double dt = _sapp_tc_timing_clamp(now - t->last); + t->dt = dt; + /* big jump: reset the filter; otherwise EMA-smooth */ + if (fabs(dt - t->smooth_dt) > 0.004) { + t->ema = t->smooth_dt = dt; + } else { + t->ema += 0.025 * (dt - t->ema); + t->smooth_dt = _sapp_tc_timing_clamp(t->ema); + } + } + t->last = now; +} + +/*-- window lookup ----------------------------------------------------------*/ +static _sapp_tc_window_t* _sapp_tc_lookup(sapp_window win) { + if (win.id == 0) return 0; + for (int i = 0; i < _SAPP_TC_MAX_WINDOWS; i++) { + _sapp_tc_window_t* w = _sapp_tc.windows[i]; + if (w && w->win_id == win.id) return w; + } + return 0; +} + +static _sapp_tc_window_t* _sapp_tc_lookup_xwin(Window xwin) { + if (!xwin) return 0; + for (int i = 0; i < _SAPP_TC_MAX_WINDOWS; i++) { + _sapp_tc_window_t* w = _sapp_tc.windows[i]; + if (w && w->ready && w->xwin == xwin) return w; + } + return 0; +} + +/*-- event plumbing ---------------------------------------------------------*/ +static void _sapp_tc_send(_sapp_tc_window_t* w, sapp_event* ev) { + if (!w->desc.event_cb) return; + ev->frame_count = _sapp_tc.app.frame_count; + ev->window_width = w->win_width; + ev->window_height = w->win_height; + ev->framebuffer_width = w->fb_width; + ev->framebuffer_height = w->fb_height; + sapp_window handle = { w->win_id }; + w->desc.event_cb(ev, handle, w->desc.user_data); +} + +/*-- main window: event routing to the sapp_desc callbacks ------------------*/ +static bool _sapp_tc_app_events_enabled(void) { + /* same gate as sokol_app.h: nothing fires before the first tick's init_cb */ + return _sapp_tc.app.init_called && + (_sapp_tc.app.desc.event_cb || _sapp_tc.app.desc.event_userdata_cb); +} + +static void _sapp_tc_main_event_tramp(const sapp_event* ev, sapp_window win, void* user) { + (void)win; (void)user; + if (!_sapp_tc_app_events_enabled()) return; + _sapp_tc.app.event_consumed = false; + if (_sapp_tc.app.desc.event_cb) { + _sapp_tc.app.desc.event_cb(ev); + } else { + _sapp_tc.app.desc.event_userdata_cb(ev, _sapp_tc.app.desc.user_data); + } +} + +/* see GLFW's xkb_unicode.c */ +static const struct _sapp_tc_x11_codepair { + uint16_t keysym; + uint16_t ucs; +} _sapp_tc_x11_keysymtab[] = { + { 0x01a1, 0x0104 }, + { 0x01a2, 0x02d8 }, + { 0x01a3, 0x0141 }, + { 0x01a5, 0x013d }, + { 0x01a6, 0x015a }, + { 0x01a9, 0x0160 }, + { 0x01aa, 0x015e }, + { 0x01ab, 0x0164 }, + { 0x01ac, 0x0179 }, + { 0x01ae, 0x017d }, + { 0x01af, 0x017b }, + { 0x01b1, 0x0105 }, + { 0x01b2, 0x02db }, + { 0x01b3, 0x0142 }, + { 0x01b5, 0x013e }, + { 0x01b6, 0x015b }, + { 0x01b7, 0x02c7 }, + { 0x01b9, 0x0161 }, + { 0x01ba, 0x015f }, + { 0x01bb, 0x0165 }, + { 0x01bc, 0x017a }, + { 0x01bd, 0x02dd }, + { 0x01be, 0x017e }, + { 0x01bf, 0x017c }, + { 0x01c0, 0x0154 }, + { 0x01c3, 0x0102 }, + { 0x01c5, 0x0139 }, + { 0x01c6, 0x0106 }, + { 0x01c8, 0x010c }, + { 0x01ca, 0x0118 }, + { 0x01cc, 0x011a }, + { 0x01cf, 0x010e }, + { 0x01d0, 0x0110 }, + { 0x01d1, 0x0143 }, + { 0x01d2, 0x0147 }, + { 0x01d5, 0x0150 }, + { 0x01d8, 0x0158 }, + { 0x01d9, 0x016e }, + { 0x01db, 0x0170 }, + { 0x01de, 0x0162 }, + { 0x01e0, 0x0155 }, + { 0x01e3, 0x0103 }, + { 0x01e5, 0x013a }, + { 0x01e6, 0x0107 }, + { 0x01e8, 0x010d }, + { 0x01ea, 0x0119 }, + { 0x01ec, 0x011b }, + { 0x01ef, 0x010f }, + { 0x01f0, 0x0111 }, + { 0x01f1, 0x0144 }, + { 0x01f2, 0x0148 }, + { 0x01f5, 0x0151 }, + { 0x01f8, 0x0159 }, + { 0x01f9, 0x016f }, + { 0x01fb, 0x0171 }, + { 0x01fe, 0x0163 }, + { 0x01ff, 0x02d9 }, + { 0x02a1, 0x0126 }, + { 0x02a6, 0x0124 }, + { 0x02a9, 0x0130 }, + { 0x02ab, 0x011e }, + { 0x02ac, 0x0134 }, + { 0x02b1, 0x0127 }, + { 0x02b6, 0x0125 }, + { 0x02b9, 0x0131 }, + { 0x02bb, 0x011f }, + { 0x02bc, 0x0135 }, + { 0x02c5, 0x010a }, + { 0x02c6, 0x0108 }, + { 0x02d5, 0x0120 }, + { 0x02d8, 0x011c }, + { 0x02dd, 0x016c }, + { 0x02de, 0x015c }, + { 0x02e5, 0x010b }, + { 0x02e6, 0x0109 }, + { 0x02f5, 0x0121 }, + { 0x02f8, 0x011d }, + { 0x02fd, 0x016d }, + { 0x02fe, 0x015d }, + { 0x03a2, 0x0138 }, + { 0x03a3, 0x0156 }, + { 0x03a5, 0x0128 }, + { 0x03a6, 0x013b }, + { 0x03aa, 0x0112 }, + { 0x03ab, 0x0122 }, + { 0x03ac, 0x0166 }, + { 0x03b3, 0x0157 }, + { 0x03b5, 0x0129 }, + { 0x03b6, 0x013c }, + { 0x03ba, 0x0113 }, + { 0x03bb, 0x0123 }, + { 0x03bc, 0x0167 }, + { 0x03bd, 0x014a }, + { 0x03bf, 0x014b }, + { 0x03c0, 0x0100 }, + { 0x03c7, 0x012e }, + { 0x03cc, 0x0116 }, + { 0x03cf, 0x012a }, + { 0x03d1, 0x0145 }, + { 0x03d2, 0x014c }, + { 0x03d3, 0x0136 }, + { 0x03d9, 0x0172 }, + { 0x03dd, 0x0168 }, + { 0x03de, 0x016a }, + { 0x03e0, 0x0101 }, + { 0x03e7, 0x012f }, + { 0x03ec, 0x0117 }, + { 0x03ef, 0x012b }, + { 0x03f1, 0x0146 }, + { 0x03f2, 0x014d }, + { 0x03f3, 0x0137 }, + { 0x03f9, 0x0173 }, + { 0x03fd, 0x0169 }, + { 0x03fe, 0x016b }, + { 0x047e, 0x203e }, + { 0x04a1, 0x3002 }, + { 0x04a2, 0x300c }, + { 0x04a3, 0x300d }, + { 0x04a4, 0x3001 }, + { 0x04a5, 0x30fb }, + { 0x04a6, 0x30f2 }, + { 0x04a7, 0x30a1 }, + { 0x04a8, 0x30a3 }, + { 0x04a9, 0x30a5 }, + { 0x04aa, 0x30a7 }, + { 0x04ab, 0x30a9 }, + { 0x04ac, 0x30e3 }, + { 0x04ad, 0x30e5 }, + { 0x04ae, 0x30e7 }, + { 0x04af, 0x30c3 }, + { 0x04b0, 0x30fc }, + { 0x04b1, 0x30a2 }, + { 0x04b2, 0x30a4 }, + { 0x04b3, 0x30a6 }, + { 0x04b4, 0x30a8 }, + { 0x04b5, 0x30aa }, + { 0x04b6, 0x30ab }, + { 0x04b7, 0x30ad }, + { 0x04b8, 0x30af }, + { 0x04b9, 0x30b1 }, + { 0x04ba, 0x30b3 }, + { 0x04bb, 0x30b5 }, + { 0x04bc, 0x30b7 }, + { 0x04bd, 0x30b9 }, + { 0x04be, 0x30bb }, + { 0x04bf, 0x30bd }, + { 0x04c0, 0x30bf }, + { 0x04c1, 0x30c1 }, + { 0x04c2, 0x30c4 }, + { 0x04c3, 0x30c6 }, + { 0x04c4, 0x30c8 }, + { 0x04c5, 0x30ca }, + { 0x04c6, 0x30cb }, + { 0x04c7, 0x30cc }, + { 0x04c8, 0x30cd }, + { 0x04c9, 0x30ce }, + { 0x04ca, 0x30cf }, + { 0x04cb, 0x30d2 }, + { 0x04cc, 0x30d5 }, + { 0x04cd, 0x30d8 }, + { 0x04ce, 0x30db }, + { 0x04cf, 0x30de }, + { 0x04d0, 0x30df }, + { 0x04d1, 0x30e0 }, + { 0x04d2, 0x30e1 }, + { 0x04d3, 0x30e2 }, + { 0x04d4, 0x30e4 }, + { 0x04d5, 0x30e6 }, + { 0x04d6, 0x30e8 }, + { 0x04d7, 0x30e9 }, + { 0x04d8, 0x30ea }, + { 0x04d9, 0x30eb }, + { 0x04da, 0x30ec }, + { 0x04db, 0x30ed }, + { 0x04dc, 0x30ef }, + { 0x04dd, 0x30f3 }, + { 0x04de, 0x309b }, + { 0x04df, 0x309c }, + { 0x05ac, 0x060c }, + { 0x05bb, 0x061b }, + { 0x05bf, 0x061f }, + { 0x05c1, 0x0621 }, + { 0x05c2, 0x0622 }, + { 0x05c3, 0x0623 }, + { 0x05c4, 0x0624 }, + { 0x05c5, 0x0625 }, + { 0x05c6, 0x0626 }, + { 0x05c7, 0x0627 }, + { 0x05c8, 0x0628 }, + { 0x05c9, 0x0629 }, + { 0x05ca, 0x062a }, + { 0x05cb, 0x062b }, + { 0x05cc, 0x062c }, + { 0x05cd, 0x062d }, + { 0x05ce, 0x062e }, + { 0x05cf, 0x062f }, + { 0x05d0, 0x0630 }, + { 0x05d1, 0x0631 }, + { 0x05d2, 0x0632 }, + { 0x05d3, 0x0633 }, + { 0x05d4, 0x0634 }, + { 0x05d5, 0x0635 }, + { 0x05d6, 0x0636 }, + { 0x05d7, 0x0637 }, + { 0x05d8, 0x0638 }, + { 0x05d9, 0x0639 }, + { 0x05da, 0x063a }, + { 0x05e0, 0x0640 }, + { 0x05e1, 0x0641 }, + { 0x05e2, 0x0642 }, + { 0x05e3, 0x0643 }, + { 0x05e4, 0x0644 }, + { 0x05e5, 0x0645 }, + { 0x05e6, 0x0646 }, + { 0x05e7, 0x0647 }, + { 0x05e8, 0x0648 }, + { 0x05e9, 0x0649 }, + { 0x05ea, 0x064a }, + { 0x05eb, 0x064b }, + { 0x05ec, 0x064c }, + { 0x05ed, 0x064d }, + { 0x05ee, 0x064e }, + { 0x05ef, 0x064f }, + { 0x05f0, 0x0650 }, + { 0x05f1, 0x0651 }, + { 0x05f2, 0x0652 }, + { 0x06a1, 0x0452 }, + { 0x06a2, 0x0453 }, + { 0x06a3, 0x0451 }, + { 0x06a4, 0x0454 }, + { 0x06a5, 0x0455 }, + { 0x06a6, 0x0456 }, + { 0x06a7, 0x0457 }, + { 0x06a8, 0x0458 }, + { 0x06a9, 0x0459 }, + { 0x06aa, 0x045a }, + { 0x06ab, 0x045b }, + { 0x06ac, 0x045c }, + { 0x06ae, 0x045e }, + { 0x06af, 0x045f }, + { 0x06b0, 0x2116 }, + { 0x06b1, 0x0402 }, + { 0x06b2, 0x0403 }, + { 0x06b3, 0x0401 }, + { 0x06b4, 0x0404 }, + { 0x06b5, 0x0405 }, + { 0x06b6, 0x0406 }, + { 0x06b7, 0x0407 }, + { 0x06b8, 0x0408 }, + { 0x06b9, 0x0409 }, + { 0x06ba, 0x040a }, + { 0x06bb, 0x040b }, + { 0x06bc, 0x040c }, + { 0x06be, 0x040e }, + { 0x06bf, 0x040f }, + { 0x06c0, 0x044e }, + { 0x06c1, 0x0430 }, + { 0x06c2, 0x0431 }, + { 0x06c3, 0x0446 }, + { 0x06c4, 0x0434 }, + { 0x06c5, 0x0435 }, + { 0x06c6, 0x0444 }, + { 0x06c7, 0x0433 }, + { 0x06c8, 0x0445 }, + { 0x06c9, 0x0438 }, + { 0x06ca, 0x0439 }, + { 0x06cb, 0x043a }, + { 0x06cc, 0x043b }, + { 0x06cd, 0x043c }, + { 0x06ce, 0x043d }, + { 0x06cf, 0x043e }, + { 0x06d0, 0x043f }, + { 0x06d1, 0x044f }, + { 0x06d2, 0x0440 }, + { 0x06d3, 0x0441 }, + { 0x06d4, 0x0442 }, + { 0x06d5, 0x0443 }, + { 0x06d6, 0x0436 }, + { 0x06d7, 0x0432 }, + { 0x06d8, 0x044c }, + { 0x06d9, 0x044b }, + { 0x06da, 0x0437 }, + { 0x06db, 0x0448 }, + { 0x06dc, 0x044d }, + { 0x06dd, 0x0449 }, + { 0x06de, 0x0447 }, + { 0x06df, 0x044a }, + { 0x06e0, 0x042e }, + { 0x06e1, 0x0410 }, + { 0x06e2, 0x0411 }, + { 0x06e3, 0x0426 }, + { 0x06e4, 0x0414 }, + { 0x06e5, 0x0415 }, + { 0x06e6, 0x0424 }, + { 0x06e7, 0x0413 }, + { 0x06e8, 0x0425 }, + { 0x06e9, 0x0418 }, + { 0x06ea, 0x0419 }, + { 0x06eb, 0x041a }, + { 0x06ec, 0x041b }, + { 0x06ed, 0x041c }, + { 0x06ee, 0x041d }, + { 0x06ef, 0x041e }, + { 0x06f0, 0x041f }, + { 0x06f1, 0x042f }, + { 0x06f2, 0x0420 }, + { 0x06f3, 0x0421 }, + { 0x06f4, 0x0422 }, + { 0x06f5, 0x0423 }, + { 0x06f6, 0x0416 }, + { 0x06f7, 0x0412 }, + { 0x06f8, 0x042c }, + { 0x06f9, 0x042b }, + { 0x06fa, 0x0417 }, + { 0x06fb, 0x0428 }, + { 0x06fc, 0x042d }, + { 0x06fd, 0x0429 }, + { 0x06fe, 0x0427 }, + { 0x06ff, 0x042a }, + { 0x07a1, 0x0386 }, + { 0x07a2, 0x0388 }, + { 0x07a3, 0x0389 }, + { 0x07a4, 0x038a }, + { 0x07a5, 0x03aa }, + { 0x07a7, 0x038c }, + { 0x07a8, 0x038e }, + { 0x07a9, 0x03ab }, + { 0x07ab, 0x038f }, + { 0x07ae, 0x0385 }, + { 0x07af, 0x2015 }, + { 0x07b1, 0x03ac }, + { 0x07b2, 0x03ad }, + { 0x07b3, 0x03ae }, + { 0x07b4, 0x03af }, + { 0x07b5, 0x03ca }, + { 0x07b6, 0x0390 }, + { 0x07b7, 0x03cc }, + { 0x07b8, 0x03cd }, + { 0x07b9, 0x03cb }, + { 0x07ba, 0x03b0 }, + { 0x07bb, 0x03ce }, + { 0x07c1, 0x0391 }, + { 0x07c2, 0x0392 }, + { 0x07c3, 0x0393 }, + { 0x07c4, 0x0394 }, + { 0x07c5, 0x0395 }, + { 0x07c6, 0x0396 }, + { 0x07c7, 0x0397 }, + { 0x07c8, 0x0398 }, + { 0x07c9, 0x0399 }, + { 0x07ca, 0x039a }, + { 0x07cb, 0x039b }, + { 0x07cc, 0x039c }, + { 0x07cd, 0x039d }, + { 0x07ce, 0x039e }, + { 0x07cf, 0x039f }, + { 0x07d0, 0x03a0 }, + { 0x07d1, 0x03a1 }, + { 0x07d2, 0x03a3 }, + { 0x07d4, 0x03a4 }, + { 0x07d5, 0x03a5 }, + { 0x07d6, 0x03a6 }, + { 0x07d7, 0x03a7 }, + { 0x07d8, 0x03a8 }, + { 0x07d9, 0x03a9 }, + { 0x07e1, 0x03b1 }, + { 0x07e2, 0x03b2 }, + { 0x07e3, 0x03b3 }, + { 0x07e4, 0x03b4 }, + { 0x07e5, 0x03b5 }, + { 0x07e6, 0x03b6 }, + { 0x07e7, 0x03b7 }, + { 0x07e8, 0x03b8 }, + { 0x07e9, 0x03b9 }, + { 0x07ea, 0x03ba }, + { 0x07eb, 0x03bb }, + { 0x07ec, 0x03bc }, + { 0x07ed, 0x03bd }, + { 0x07ee, 0x03be }, + { 0x07ef, 0x03bf }, + { 0x07f0, 0x03c0 }, + { 0x07f1, 0x03c1 }, + { 0x07f2, 0x03c3 }, + { 0x07f3, 0x03c2 }, + { 0x07f4, 0x03c4 }, + { 0x07f5, 0x03c5 }, + { 0x07f6, 0x03c6 }, + { 0x07f7, 0x03c7 }, + { 0x07f8, 0x03c8 }, + { 0x07f9, 0x03c9 }, + { 0x08a1, 0x23b7 }, + { 0x08a2, 0x250c }, + { 0x08a3, 0x2500 }, + { 0x08a4, 0x2320 }, + { 0x08a5, 0x2321 }, + { 0x08a6, 0x2502 }, + { 0x08a7, 0x23a1 }, + { 0x08a8, 0x23a3 }, + { 0x08a9, 0x23a4 }, + { 0x08aa, 0x23a6 }, + { 0x08ab, 0x239b }, + { 0x08ac, 0x239d }, + { 0x08ad, 0x239e }, + { 0x08ae, 0x23a0 }, + { 0x08af, 0x23a8 }, + { 0x08b0, 0x23ac }, + { 0x08bc, 0x2264 }, + { 0x08bd, 0x2260 }, + { 0x08be, 0x2265 }, + { 0x08bf, 0x222b }, + { 0x08c0, 0x2234 }, + { 0x08c1, 0x221d }, + { 0x08c2, 0x221e }, + { 0x08c5, 0x2207 }, + { 0x08c8, 0x223c }, + { 0x08c9, 0x2243 }, + { 0x08cd, 0x21d4 }, + { 0x08ce, 0x21d2 }, + { 0x08cf, 0x2261 }, + { 0x08d6, 0x221a }, + { 0x08da, 0x2282 }, + { 0x08db, 0x2283 }, + { 0x08dc, 0x2229 }, + { 0x08dd, 0x222a }, + { 0x08de, 0x2227 }, + { 0x08df, 0x2228 }, + { 0x08ef, 0x2202 }, + { 0x08f6, 0x0192 }, + { 0x08fb, 0x2190 }, + { 0x08fc, 0x2191 }, + { 0x08fd, 0x2192 }, + { 0x08fe, 0x2193 }, + { 0x09e0, 0x25c6 }, + { 0x09e1, 0x2592 }, + { 0x09e2, 0x2409 }, + { 0x09e3, 0x240c }, + { 0x09e4, 0x240d }, + { 0x09e5, 0x240a }, + { 0x09e8, 0x2424 }, + { 0x09e9, 0x240b }, + { 0x09ea, 0x2518 }, + { 0x09eb, 0x2510 }, + { 0x09ec, 0x250c }, + { 0x09ed, 0x2514 }, + { 0x09ee, 0x253c }, + { 0x09ef, 0x23ba }, + { 0x09f0, 0x23bb }, + { 0x09f1, 0x2500 }, + { 0x09f2, 0x23bc }, + { 0x09f3, 0x23bd }, + { 0x09f4, 0x251c }, + { 0x09f5, 0x2524 }, + { 0x09f6, 0x2534 }, + { 0x09f7, 0x252c }, + { 0x09f8, 0x2502 }, + { 0x0aa1, 0x2003 }, + { 0x0aa2, 0x2002 }, + { 0x0aa3, 0x2004 }, + { 0x0aa4, 0x2005 }, + { 0x0aa5, 0x2007 }, + { 0x0aa6, 0x2008 }, + { 0x0aa7, 0x2009 }, + { 0x0aa8, 0x200a }, + { 0x0aa9, 0x2014 }, + { 0x0aaa, 0x2013 }, + { 0x0aae, 0x2026 }, + { 0x0aaf, 0x2025 }, + { 0x0ab0, 0x2153 }, + { 0x0ab1, 0x2154 }, + { 0x0ab2, 0x2155 }, + { 0x0ab3, 0x2156 }, + { 0x0ab4, 0x2157 }, + { 0x0ab5, 0x2158 }, + { 0x0ab6, 0x2159 }, + { 0x0ab7, 0x215a }, + { 0x0ab8, 0x2105 }, + { 0x0abb, 0x2012 }, + { 0x0abc, 0x2329 }, + { 0x0abe, 0x232a }, + { 0x0ac3, 0x215b }, + { 0x0ac4, 0x215c }, + { 0x0ac5, 0x215d }, + { 0x0ac6, 0x215e }, + { 0x0ac9, 0x2122 }, + { 0x0aca, 0x2613 }, + { 0x0acc, 0x25c1 }, + { 0x0acd, 0x25b7 }, + { 0x0ace, 0x25cb }, + { 0x0acf, 0x25af }, + { 0x0ad0, 0x2018 }, + { 0x0ad1, 0x2019 }, + { 0x0ad2, 0x201c }, + { 0x0ad3, 0x201d }, + { 0x0ad4, 0x211e }, + { 0x0ad6, 0x2032 }, + { 0x0ad7, 0x2033 }, + { 0x0ad9, 0x271d }, + { 0x0adb, 0x25ac }, + { 0x0adc, 0x25c0 }, + { 0x0add, 0x25b6 }, + { 0x0ade, 0x25cf }, + { 0x0adf, 0x25ae }, + { 0x0ae0, 0x25e6 }, + { 0x0ae1, 0x25ab }, + { 0x0ae2, 0x25ad }, + { 0x0ae3, 0x25b3 }, + { 0x0ae4, 0x25bd }, + { 0x0ae5, 0x2606 }, + { 0x0ae6, 0x2022 }, + { 0x0ae7, 0x25aa }, + { 0x0ae8, 0x25b2 }, + { 0x0ae9, 0x25bc }, + { 0x0aea, 0x261c }, + { 0x0aeb, 0x261e }, + { 0x0aec, 0x2663 }, + { 0x0aed, 0x2666 }, + { 0x0aee, 0x2665 }, + { 0x0af0, 0x2720 }, + { 0x0af1, 0x2020 }, + { 0x0af2, 0x2021 }, + { 0x0af3, 0x2713 }, + { 0x0af4, 0x2717 }, + { 0x0af5, 0x266f }, + { 0x0af6, 0x266d }, + { 0x0af7, 0x2642 }, + { 0x0af8, 0x2640 }, + { 0x0af9, 0x260e }, + { 0x0afa, 0x2315 }, + { 0x0afb, 0x2117 }, + { 0x0afc, 0x2038 }, + { 0x0afd, 0x201a }, + { 0x0afe, 0x201e }, + { 0x0ba3, 0x003c }, + { 0x0ba6, 0x003e }, + { 0x0ba8, 0x2228 }, + { 0x0ba9, 0x2227 }, + { 0x0bc0, 0x00af }, + { 0x0bc2, 0x22a5 }, + { 0x0bc3, 0x2229 }, + { 0x0bc4, 0x230a }, + { 0x0bc6, 0x005f }, + { 0x0bca, 0x2218 }, + { 0x0bcc, 0x2395 }, + { 0x0bce, 0x22a4 }, + { 0x0bcf, 0x25cb }, + { 0x0bd3, 0x2308 }, + { 0x0bd6, 0x222a }, + { 0x0bd8, 0x2283 }, + { 0x0bda, 0x2282 }, + { 0x0bdc, 0x22a2 }, + { 0x0bfc, 0x22a3 }, + { 0x0cdf, 0x2017 }, + { 0x0ce0, 0x05d0 }, + { 0x0ce1, 0x05d1 }, + { 0x0ce2, 0x05d2 }, + { 0x0ce3, 0x05d3 }, + { 0x0ce4, 0x05d4 }, + { 0x0ce5, 0x05d5 }, + { 0x0ce6, 0x05d6 }, + { 0x0ce7, 0x05d7 }, + { 0x0ce8, 0x05d8 }, + { 0x0ce9, 0x05d9 }, + { 0x0cea, 0x05da }, + { 0x0ceb, 0x05db }, + { 0x0cec, 0x05dc }, + { 0x0ced, 0x05dd }, + { 0x0cee, 0x05de }, + { 0x0cef, 0x05df }, + { 0x0cf0, 0x05e0 }, + { 0x0cf1, 0x05e1 }, + { 0x0cf2, 0x05e2 }, + { 0x0cf3, 0x05e3 }, + { 0x0cf4, 0x05e4 }, + { 0x0cf5, 0x05e5 }, + { 0x0cf6, 0x05e6 }, + { 0x0cf7, 0x05e7 }, + { 0x0cf8, 0x05e8 }, + { 0x0cf9, 0x05e9 }, + { 0x0cfa, 0x05ea }, + { 0x0da1, 0x0e01 }, + { 0x0da2, 0x0e02 }, + { 0x0da3, 0x0e03 }, + { 0x0da4, 0x0e04 }, + { 0x0da5, 0x0e05 }, + { 0x0da6, 0x0e06 }, + { 0x0da7, 0x0e07 }, + { 0x0da8, 0x0e08 }, + { 0x0da9, 0x0e09 }, + { 0x0daa, 0x0e0a }, + { 0x0dab, 0x0e0b }, + { 0x0dac, 0x0e0c }, + { 0x0dad, 0x0e0d }, + { 0x0dae, 0x0e0e }, + { 0x0daf, 0x0e0f }, + { 0x0db0, 0x0e10 }, + { 0x0db1, 0x0e11 }, + { 0x0db2, 0x0e12 }, + { 0x0db3, 0x0e13 }, + { 0x0db4, 0x0e14 }, + { 0x0db5, 0x0e15 }, + { 0x0db6, 0x0e16 }, + { 0x0db7, 0x0e17 }, + { 0x0db8, 0x0e18 }, + { 0x0db9, 0x0e19 }, + { 0x0dba, 0x0e1a }, + { 0x0dbb, 0x0e1b }, + { 0x0dbc, 0x0e1c }, + { 0x0dbd, 0x0e1d }, + { 0x0dbe, 0x0e1e }, + { 0x0dbf, 0x0e1f }, + { 0x0dc0, 0x0e20 }, + { 0x0dc1, 0x0e21 }, + { 0x0dc2, 0x0e22 }, + { 0x0dc3, 0x0e23 }, + { 0x0dc4, 0x0e24 }, + { 0x0dc5, 0x0e25 }, + { 0x0dc6, 0x0e26 }, + { 0x0dc7, 0x0e27 }, + { 0x0dc8, 0x0e28 }, + { 0x0dc9, 0x0e29 }, + { 0x0dca, 0x0e2a }, + { 0x0dcb, 0x0e2b }, + { 0x0dcc, 0x0e2c }, + { 0x0dcd, 0x0e2d }, + { 0x0dce, 0x0e2e }, + { 0x0dcf, 0x0e2f }, + { 0x0dd0, 0x0e30 }, + { 0x0dd1, 0x0e31 }, + { 0x0dd2, 0x0e32 }, + { 0x0dd3, 0x0e33 }, + { 0x0dd4, 0x0e34 }, + { 0x0dd5, 0x0e35 }, + { 0x0dd6, 0x0e36 }, + { 0x0dd7, 0x0e37 }, + { 0x0dd8, 0x0e38 }, + { 0x0dd9, 0x0e39 }, + { 0x0dda, 0x0e3a }, + { 0x0ddf, 0x0e3f }, + { 0x0de0, 0x0e40 }, + { 0x0de1, 0x0e41 }, + { 0x0de2, 0x0e42 }, + { 0x0de3, 0x0e43 }, + { 0x0de4, 0x0e44 }, + { 0x0de5, 0x0e45 }, + { 0x0de6, 0x0e46 }, + { 0x0de7, 0x0e47 }, + { 0x0de8, 0x0e48 }, + { 0x0de9, 0x0e49 }, + { 0x0dea, 0x0e4a }, + { 0x0deb, 0x0e4b }, + { 0x0dec, 0x0e4c }, + { 0x0ded, 0x0e4d }, + { 0x0df0, 0x0e50 }, + { 0x0df1, 0x0e51 }, + { 0x0df2, 0x0e52 }, + { 0x0df3, 0x0e53 }, + { 0x0df4, 0x0e54 }, + { 0x0df5, 0x0e55 }, + { 0x0df6, 0x0e56 }, + { 0x0df7, 0x0e57 }, + { 0x0df8, 0x0e58 }, + { 0x0df9, 0x0e59 }, + { 0x0ea1, 0x3131 }, + { 0x0ea2, 0x3132 }, + { 0x0ea3, 0x3133 }, + { 0x0ea4, 0x3134 }, + { 0x0ea5, 0x3135 }, + { 0x0ea6, 0x3136 }, + { 0x0ea7, 0x3137 }, + { 0x0ea8, 0x3138 }, + { 0x0ea9, 0x3139 }, + { 0x0eaa, 0x313a }, + { 0x0eab, 0x313b }, + { 0x0eac, 0x313c }, + { 0x0ead, 0x313d }, + { 0x0eae, 0x313e }, + { 0x0eaf, 0x313f }, + { 0x0eb0, 0x3140 }, + { 0x0eb1, 0x3141 }, + { 0x0eb2, 0x3142 }, + { 0x0eb3, 0x3143 }, + { 0x0eb4, 0x3144 }, + { 0x0eb5, 0x3145 }, + { 0x0eb6, 0x3146 }, + { 0x0eb7, 0x3147 }, + { 0x0eb8, 0x3148 }, + { 0x0eb9, 0x3149 }, + { 0x0eba, 0x314a }, + { 0x0ebb, 0x314b }, + { 0x0ebc, 0x314c }, + { 0x0ebd, 0x314d }, + { 0x0ebe, 0x314e }, + { 0x0ebf, 0x314f }, + { 0x0ec0, 0x3150 }, + { 0x0ec1, 0x3151 }, + { 0x0ec2, 0x3152 }, + { 0x0ec3, 0x3153 }, + { 0x0ec4, 0x3154 }, + { 0x0ec5, 0x3155 }, + { 0x0ec6, 0x3156 }, + { 0x0ec7, 0x3157 }, + { 0x0ec8, 0x3158 }, + { 0x0ec9, 0x3159 }, + { 0x0eca, 0x315a }, + { 0x0ecb, 0x315b }, + { 0x0ecc, 0x315c }, + { 0x0ecd, 0x315d }, + { 0x0ece, 0x315e }, + { 0x0ecf, 0x315f }, + { 0x0ed0, 0x3160 }, + { 0x0ed1, 0x3161 }, + { 0x0ed2, 0x3162 }, + { 0x0ed3, 0x3163 }, + { 0x0ed4, 0x11a8 }, + { 0x0ed5, 0x11a9 }, + { 0x0ed6, 0x11aa }, + { 0x0ed7, 0x11ab }, + { 0x0ed8, 0x11ac }, + { 0x0ed9, 0x11ad }, + { 0x0eda, 0x11ae }, + { 0x0edb, 0x11af }, + { 0x0edc, 0x11b0 }, + { 0x0edd, 0x11b1 }, + { 0x0ede, 0x11b2 }, + { 0x0edf, 0x11b3 }, + { 0x0ee0, 0x11b4 }, + { 0x0ee1, 0x11b5 }, + { 0x0ee2, 0x11b6 }, + { 0x0ee3, 0x11b7 }, + { 0x0ee4, 0x11b8 }, + { 0x0ee5, 0x11b9 }, + { 0x0ee6, 0x11ba }, + { 0x0ee7, 0x11bb }, + { 0x0ee8, 0x11bc }, + { 0x0ee9, 0x11bd }, + { 0x0eea, 0x11be }, + { 0x0eeb, 0x11bf }, + { 0x0eec, 0x11c0 }, + { 0x0eed, 0x11c1 }, + { 0x0eee, 0x11c2 }, + { 0x0eef, 0x316d }, + { 0x0ef0, 0x3171 }, + { 0x0ef1, 0x3178 }, + { 0x0ef2, 0x317f }, + { 0x0ef3, 0x3181 }, + { 0x0ef4, 0x3184 }, + { 0x0ef5, 0x3186 }, + { 0x0ef6, 0x318d }, + { 0x0ef7, 0x318e }, + { 0x0ef8, 0x11eb }, + { 0x0ef9, 0x11f0 }, + { 0x0efa, 0x11f9 }, + { 0x0eff, 0x20a9 }, + { 0x13a4, 0x20ac }, + { 0x13bc, 0x0152 }, + { 0x13bd, 0x0153 }, + { 0x13be, 0x0178 }, + { 0x20ac, 0x20ac }, + { 0xfe50, '`' }, + { 0xfe51, 0x00b4 }, + { 0xfe52, '^' }, + { 0xfe53, '~' }, + { 0xfe54, 0x00af }, + { 0xfe55, 0x02d8 }, + { 0xfe56, 0x02d9 }, + { 0xfe57, 0x00a8 }, + { 0xfe58, 0x02da }, + { 0xfe59, 0x02dd }, + { 0xfe5a, 0x02c7 }, + { 0xfe5b, 0x00b8 }, + { 0xfe5c, 0x02db }, + { 0xfe5d, 0x037a }, + { 0xfe5e, 0x309b }, + { 0xfe5f, 0x309c }, + { 0xfe63, '/' }, + { 0xfe64, 0x02bc }, + { 0xfe65, 0x02bd }, + { 0xfe66, 0x02f5 }, + { 0xfe67, 0x02f3 }, + { 0xfe68, 0x02cd }, + { 0xfe69, 0xa788 }, + { 0xfe6a, 0x02f7 }, + { 0xfe6e, ',' }, + { 0xfe6f, 0x00a4 }, + { 0xfe80, 'a' }, /* XK_dead_a */ + { 0xfe81, 'A' }, /* XK_dead_A */ + { 0xfe82, 'e' }, /* XK_dead_e */ + { 0xfe83, 'E' }, /* XK_dead_E */ + { 0xfe84, 'i' }, /* XK_dead_i */ + { 0xfe85, 'I' }, /* XK_dead_I */ + { 0xfe86, 'o' }, /* XK_dead_o */ + { 0xfe87, 'O' }, /* XK_dead_O */ + { 0xfe88, 'u' }, /* XK_dead_u */ + { 0xfe89, 'U' }, /* XK_dead_U */ + { 0xfe8a, 0x0259 }, + { 0xfe8b, 0x018f }, + { 0xfe8c, 0x00b5 }, + { 0xfe90, '_' }, + { 0xfe91, 0x02c8 }, + { 0xfe92, 0x02cc }, + { 0xff80 /*XKB_KEY_KP_Space*/, ' ' }, + { 0xff95 /*XKB_KEY_KP_7*/, 0x0037 }, + { 0xff96 /*XKB_KEY_KP_4*/, 0x0034 }, + { 0xff97 /*XKB_KEY_KP_8*/, 0x0038 }, + { 0xff98 /*XKB_KEY_KP_6*/, 0x0036 }, + { 0xff99 /*XKB_KEY_KP_2*/, 0x0032 }, + { 0xff9a /*XKB_KEY_KP_9*/, 0x0039 }, + { 0xff9b /*XKB_KEY_KP_3*/, 0x0033 }, + { 0xff9c /*XKB_KEY_KP_1*/, 0x0031 }, + { 0xff9d /*XKB_KEY_KP_5*/, 0x0035 }, + { 0xff9e /*XKB_KEY_KP_0*/, 0x0030 }, + { 0xffaa /*XKB_KEY_KP_Multiply*/, '*' }, + { 0xffab /*XKB_KEY_KP_Add*/, '+' }, + { 0xffac /*XKB_KEY_KP_Separator*/, ',' }, + { 0xffad /*XKB_KEY_KP_Subtract*/, '-' }, + { 0xffae /*XKB_KEY_KP_Decimal*/, '.' }, + { 0xffaf /*XKB_KEY_KP_Divide*/, '/' }, + { 0xffb0 /*XKB_KEY_KP_0*/, 0x0030 }, + { 0xffb1 /*XKB_KEY_KP_1*/, 0x0031 }, + { 0xffb2 /*XKB_KEY_KP_2*/, 0x0032 }, + { 0xffb3 /*XKB_KEY_KP_3*/, 0x0033 }, + { 0xffb4 /*XKB_KEY_KP_4*/, 0x0034 }, + { 0xffb5 /*XKB_KEY_KP_5*/, 0x0035 }, + { 0xffb6 /*XKB_KEY_KP_6*/, 0x0036 }, + { 0xffb7 /*XKB_KEY_KP_7*/, 0x0037 }, + { 0xffb8 /*XKB_KEY_KP_8*/, 0x0038 }, + { 0xffb9 /*XKB_KEY_KP_9*/, 0x0039 }, + { 0xffbd /*XKB_KEY_KP_Equal*/, '=' } +}; + +// translate the X11 KeySyms for a key to sokol-app key code +// NOTE: this is only used as a fallback, in case the XBK method fails +// it is layout-dependent and will fail partially on most non-US layouts. +// +static sapp_keycode _sapp_tc_x11_translate_keysyms(const KeySym* keysyms, int width) { + if (width > 1) { + switch (keysyms[1]) { + case XK_KP_0: return SAPP_KEYCODE_KP_0; + case XK_KP_1: return SAPP_KEYCODE_KP_1; + case XK_KP_2: return SAPP_KEYCODE_KP_2; + case XK_KP_3: return SAPP_KEYCODE_KP_3; + case XK_KP_4: return SAPP_KEYCODE_KP_4; + case XK_KP_5: return SAPP_KEYCODE_KP_5; + case XK_KP_6: return SAPP_KEYCODE_KP_6; + case XK_KP_7: return SAPP_KEYCODE_KP_7; + case XK_KP_8: return SAPP_KEYCODE_KP_8; + case XK_KP_9: return SAPP_KEYCODE_KP_9; + case XK_KP_Separator: + case XK_KP_Decimal: return SAPP_KEYCODE_KP_DECIMAL; + case XK_KP_Equal: return SAPP_KEYCODE_KP_EQUAL; + case XK_KP_Enter: return SAPP_KEYCODE_KP_ENTER; + default: break; + } + } + + switch (keysyms[0]) { + case XK_Escape: return SAPP_KEYCODE_ESCAPE; + case XK_Tab: return SAPP_KEYCODE_TAB; + case XK_Shift_L: return SAPP_KEYCODE_LEFT_SHIFT; + case XK_Shift_R: return SAPP_KEYCODE_RIGHT_SHIFT; + case XK_Control_L: return SAPP_KEYCODE_LEFT_CONTROL; + case XK_Control_R: return SAPP_KEYCODE_RIGHT_CONTROL; + case XK_Meta_L: + case XK_Alt_L: return SAPP_KEYCODE_LEFT_ALT; + case XK_Mode_switch: // Mapped to Alt_R on many keyboards + case XK_ISO_Level3_Shift: // AltGr on at least some machines + case XK_Meta_R: + case XK_Alt_R: return SAPP_KEYCODE_RIGHT_ALT; + case XK_Super_L: return SAPP_KEYCODE_LEFT_SUPER; + case XK_Super_R: return SAPP_KEYCODE_RIGHT_SUPER; + case XK_Menu: return SAPP_KEYCODE_MENU; + case XK_Num_Lock: return SAPP_KEYCODE_NUM_LOCK; + case XK_Caps_Lock: return SAPP_KEYCODE_CAPS_LOCK; + case XK_Print: return SAPP_KEYCODE_PRINT_SCREEN; + case XK_Scroll_Lock: return SAPP_KEYCODE_SCROLL_LOCK; + case XK_Pause: return SAPP_KEYCODE_PAUSE; + case XK_Delete: return SAPP_KEYCODE_DELETE; + case XK_BackSpace: return SAPP_KEYCODE_BACKSPACE; + case XK_Return: return SAPP_KEYCODE_ENTER; + case XK_Home: return SAPP_KEYCODE_HOME; + case XK_End: return SAPP_KEYCODE_END; + case XK_Page_Up: return SAPP_KEYCODE_PAGE_UP; + case XK_Page_Down: return SAPP_KEYCODE_PAGE_DOWN; + case XK_Insert: return SAPP_KEYCODE_INSERT; + case XK_Left: return SAPP_KEYCODE_LEFT; + case XK_Right: return SAPP_KEYCODE_RIGHT; + case XK_Down: return SAPP_KEYCODE_DOWN; + case XK_Up: return SAPP_KEYCODE_UP; + case XK_F1: return SAPP_KEYCODE_F1; + case XK_F2: return SAPP_KEYCODE_F2; + case XK_F3: return SAPP_KEYCODE_F3; + case XK_F4: return SAPP_KEYCODE_F4; + case XK_F5: return SAPP_KEYCODE_F5; + case XK_F6: return SAPP_KEYCODE_F6; + case XK_F7: return SAPP_KEYCODE_F7; + case XK_F8: return SAPP_KEYCODE_F8; + case XK_F9: return SAPP_KEYCODE_F9; + case XK_F10: return SAPP_KEYCODE_F10; + case XK_F11: return SAPP_KEYCODE_F11; + case XK_F12: return SAPP_KEYCODE_F12; + case XK_F13: return SAPP_KEYCODE_F13; + case XK_F14: return SAPP_KEYCODE_F14; + case XK_F15: return SAPP_KEYCODE_F15; + case XK_F16: return SAPP_KEYCODE_F16; + case XK_F17: return SAPP_KEYCODE_F17; + case XK_F18: return SAPP_KEYCODE_F18; + case XK_F19: return SAPP_KEYCODE_F19; + case XK_F20: return SAPP_KEYCODE_F20; + case XK_F21: return SAPP_KEYCODE_F21; + case XK_F22: return SAPP_KEYCODE_F22; + case XK_F23: return SAPP_KEYCODE_F23; + case XK_F24: return SAPP_KEYCODE_F24; + case XK_F25: return SAPP_KEYCODE_F25; + + // numeric keypad + case XK_KP_Divide: return SAPP_KEYCODE_KP_DIVIDE; + case XK_KP_Multiply: return SAPP_KEYCODE_KP_MULTIPLY; + case XK_KP_Subtract: return SAPP_KEYCODE_KP_SUBTRACT; + case XK_KP_Add: return SAPP_KEYCODE_KP_ADD; + + // these should have been detected in secondary keysym test above! + case XK_KP_Insert: return SAPP_KEYCODE_KP_0; + case XK_KP_End: return SAPP_KEYCODE_KP_1; + case XK_KP_Down: return SAPP_KEYCODE_KP_2; + case XK_KP_Page_Down: return SAPP_KEYCODE_KP_3; + case XK_KP_Left: return SAPP_KEYCODE_KP_4; + case XK_KP_Right: return SAPP_KEYCODE_KP_6; + case XK_KP_Home: return SAPP_KEYCODE_KP_7; + case XK_KP_Up: return SAPP_KEYCODE_KP_8; + case XK_KP_Page_Up: return SAPP_KEYCODE_KP_9; + case XK_KP_Delete: return SAPP_KEYCODE_KP_DECIMAL; + case XK_KP_Equal: return SAPP_KEYCODE_KP_EQUAL; + case XK_KP_Enter: return SAPP_KEYCODE_KP_ENTER; + + // last resort: Check for printable keys (should not happen if the XKB + // extension is available). This will give a layout dependent mapping + // (which is wrong, and we may miss some keys, especially on non-US + // keyboards), but it's better than nothing... + case XK_a: return SAPP_KEYCODE_A; + case XK_b: return SAPP_KEYCODE_B; + case XK_c: return SAPP_KEYCODE_C; + case XK_d: return SAPP_KEYCODE_D; + case XK_e: return SAPP_KEYCODE_E; + case XK_f: return SAPP_KEYCODE_F; + case XK_g: return SAPP_KEYCODE_G; + case XK_h: return SAPP_KEYCODE_H; + case XK_i: return SAPP_KEYCODE_I; + case XK_j: return SAPP_KEYCODE_J; + case XK_k: return SAPP_KEYCODE_K; + case XK_l: return SAPP_KEYCODE_L; + case XK_m: return SAPP_KEYCODE_M; + case XK_n: return SAPP_KEYCODE_N; + case XK_o: return SAPP_KEYCODE_O; + case XK_p: return SAPP_KEYCODE_P; + case XK_q: return SAPP_KEYCODE_Q; + case XK_r: return SAPP_KEYCODE_R; + case XK_s: return SAPP_KEYCODE_S; + case XK_t: return SAPP_KEYCODE_T; + case XK_u: return SAPP_KEYCODE_U; + case XK_v: return SAPP_KEYCODE_V; + case XK_w: return SAPP_KEYCODE_W; + case XK_x: return SAPP_KEYCODE_X; + case XK_y: return SAPP_KEYCODE_Y; + case XK_z: return SAPP_KEYCODE_Z; + case XK_1: return SAPP_KEYCODE_1; + case XK_2: return SAPP_KEYCODE_2; + case XK_3: return SAPP_KEYCODE_3; + case XK_4: return SAPP_KEYCODE_4; + case XK_5: return SAPP_KEYCODE_5; + case XK_6: return SAPP_KEYCODE_6; + case XK_7: return SAPP_KEYCODE_7; + case XK_8: return SAPP_KEYCODE_8; + case XK_9: return SAPP_KEYCODE_9; + case XK_0: return SAPP_KEYCODE_0; + case XK_space: return SAPP_KEYCODE_SPACE; + case XK_minus: return SAPP_KEYCODE_MINUS; + case XK_equal: return SAPP_KEYCODE_EQUAL; + case XK_bracketleft: return SAPP_KEYCODE_LEFT_BRACKET; + case XK_bracketright: return SAPP_KEYCODE_RIGHT_BRACKET; + case XK_backslash: return SAPP_KEYCODE_BACKSLASH; + case XK_semicolon: return SAPP_KEYCODE_SEMICOLON; + case XK_apostrophe: return SAPP_KEYCODE_APOSTROPHE; + case XK_grave: return SAPP_KEYCODE_GRAVE_ACCENT; + case XK_comma: return SAPP_KEYCODE_COMMA; + case XK_period: return SAPP_KEYCODE_PERIOD; + case XK_slash: return SAPP_KEYCODE_SLASH; + case XK_less: return SAPP_KEYCODE_WORLD_1; // At least in some layouts... + default: break; + } + + // no matching translation was found + return SAPP_KEYCODE_INVALID; +} +static void _sapp_tc_x11_init_keytable(void) { + for (int i = 0; i < SAPP_MAX_KEYCODES; i++) { + _sapp_tc.keycodes[i] = SAPP_KEYCODE_INVALID; + } + // use XKB to determine physical key locations independently of the current keyboard layout + XkbDescPtr desc = XkbGetMap(_sapp_tc.display, 0, XkbUseCoreKbd); + SOKOL_ASSERT(desc); + XkbGetNames(_sapp_tc.display, XkbKeyNamesMask | XkbKeyAliasesMask, desc); + + const int scancode_min = desc->min_key_code; + const int scancode_max = desc->max_key_code; + + const struct { sapp_keycode key; const char* name; } keymap[] = { + { SAPP_KEYCODE_GRAVE_ACCENT, "TLDE" }, + { SAPP_KEYCODE_1, "AE01" }, + { SAPP_KEYCODE_2, "AE02" }, + { SAPP_KEYCODE_3, "AE03" }, + { SAPP_KEYCODE_4, "AE04" }, + { SAPP_KEYCODE_5, "AE05" }, + { SAPP_KEYCODE_6, "AE06" }, + { SAPP_KEYCODE_7, "AE07" }, + { SAPP_KEYCODE_8, "AE08" }, + { SAPP_KEYCODE_9, "AE09" }, + { SAPP_KEYCODE_0, "AE10" }, + { SAPP_KEYCODE_MINUS, "AE11" }, + { SAPP_KEYCODE_EQUAL, "AE12" }, + { SAPP_KEYCODE_Q, "AD01" }, + { SAPP_KEYCODE_W, "AD02" }, + { SAPP_KEYCODE_E, "AD03" }, + { SAPP_KEYCODE_R, "AD04" }, + { SAPP_KEYCODE_T, "AD05" }, + { SAPP_KEYCODE_Y, "AD06" }, + { SAPP_KEYCODE_U, "AD07" }, + { SAPP_KEYCODE_I, "AD08" }, + { SAPP_KEYCODE_O, "AD09" }, + { SAPP_KEYCODE_P, "AD10" }, + { SAPP_KEYCODE_LEFT_BRACKET, "AD11" }, + { SAPP_KEYCODE_RIGHT_BRACKET, "AD12" }, + { SAPP_KEYCODE_A, "AC01" }, + { SAPP_KEYCODE_S, "AC02" }, + { SAPP_KEYCODE_D, "AC03" }, + { SAPP_KEYCODE_F, "AC04" }, + { SAPP_KEYCODE_G, "AC05" }, + { SAPP_KEYCODE_H, "AC06" }, + { SAPP_KEYCODE_J, "AC07" }, + { SAPP_KEYCODE_K, "AC08" }, + { SAPP_KEYCODE_L, "AC09" }, + { SAPP_KEYCODE_SEMICOLON, "AC10" }, + { SAPP_KEYCODE_APOSTROPHE, "AC11" }, + { SAPP_KEYCODE_Z, "AB01" }, + { SAPP_KEYCODE_X, "AB02" }, + { SAPP_KEYCODE_C, "AB03" }, + { SAPP_KEYCODE_V, "AB04" }, + { SAPP_KEYCODE_B, "AB05" }, + { SAPP_KEYCODE_N, "AB06" }, + { SAPP_KEYCODE_M, "AB07" }, + { SAPP_KEYCODE_COMMA, "AB08" }, + { SAPP_KEYCODE_PERIOD, "AB09" }, + { SAPP_KEYCODE_SLASH, "AB10" }, + { SAPP_KEYCODE_BACKSLASH, "BKSL" }, + { SAPP_KEYCODE_WORLD_1, "LSGT" }, + { SAPP_KEYCODE_SPACE, "SPCE" }, + { SAPP_KEYCODE_ESCAPE, "ESC" }, + { SAPP_KEYCODE_ENTER, "RTRN" }, + { SAPP_KEYCODE_TAB, "TAB" }, + { SAPP_KEYCODE_BACKSPACE, "BKSP" }, + { SAPP_KEYCODE_INSERT, "INS" }, + { SAPP_KEYCODE_DELETE, "DELE" }, + { SAPP_KEYCODE_RIGHT, "RGHT" }, + { SAPP_KEYCODE_LEFT, "LEFT" }, + { SAPP_KEYCODE_DOWN, "DOWN" }, + { SAPP_KEYCODE_UP, "UP" }, + { SAPP_KEYCODE_PAGE_UP, "PGUP" }, + { SAPP_KEYCODE_PAGE_DOWN, "PGDN" }, + { SAPP_KEYCODE_HOME, "HOME" }, + { SAPP_KEYCODE_END, "END" }, + { SAPP_KEYCODE_CAPS_LOCK, "CAPS" }, + { SAPP_KEYCODE_SCROLL_LOCK, "SCLK" }, + { SAPP_KEYCODE_NUM_LOCK, "NMLK" }, + { SAPP_KEYCODE_PRINT_SCREEN, "PRSC" }, + { SAPP_KEYCODE_PAUSE, "PAUS" }, + { SAPP_KEYCODE_F1, "FK01" }, + { SAPP_KEYCODE_F2, "FK02" }, + { SAPP_KEYCODE_F3, "FK03" }, + { SAPP_KEYCODE_F4, "FK04" }, + { SAPP_KEYCODE_F5, "FK05" }, + { SAPP_KEYCODE_F6, "FK06" }, + { SAPP_KEYCODE_F7, "FK07" }, + { SAPP_KEYCODE_F8, "FK08" }, + { SAPP_KEYCODE_F9, "FK09" }, + { SAPP_KEYCODE_F10, "FK10" }, + { SAPP_KEYCODE_F11, "FK11" }, + { SAPP_KEYCODE_F12, "FK12" }, + { SAPP_KEYCODE_F13, "FK13" }, + { SAPP_KEYCODE_F14, "FK14" }, + { SAPP_KEYCODE_F15, "FK15" }, + { SAPP_KEYCODE_F16, "FK16" }, + { SAPP_KEYCODE_F17, "FK17" }, + { SAPP_KEYCODE_F18, "FK18" }, + { SAPP_KEYCODE_F19, "FK19" }, + { SAPP_KEYCODE_F20, "FK20" }, + { SAPP_KEYCODE_F21, "FK21" }, + { SAPP_KEYCODE_F22, "FK22" }, + { SAPP_KEYCODE_F23, "FK23" }, + { SAPP_KEYCODE_F24, "FK24" }, + { SAPP_KEYCODE_F25, "FK25" }, + { SAPP_KEYCODE_KP_0, "KP0" }, + { SAPP_KEYCODE_KP_1, "KP1" }, + { SAPP_KEYCODE_KP_2, "KP2" }, + { SAPP_KEYCODE_KP_3, "KP3" }, + { SAPP_KEYCODE_KP_4, "KP4" }, + { SAPP_KEYCODE_KP_5, "KP5" }, + { SAPP_KEYCODE_KP_6, "KP6" }, + { SAPP_KEYCODE_KP_7, "KP7" }, + { SAPP_KEYCODE_KP_8, "KP8" }, + { SAPP_KEYCODE_KP_9, "KP9" }, + { SAPP_KEYCODE_KP_DECIMAL, "KPDL" }, + { SAPP_KEYCODE_KP_DIVIDE, "KPDV" }, + { SAPP_KEYCODE_KP_MULTIPLY, "KPMU" }, + { SAPP_KEYCODE_KP_SUBTRACT, "KPSU" }, + { SAPP_KEYCODE_KP_ADD, "KPAD" }, + { SAPP_KEYCODE_KP_ENTER, "KPEN" }, + { SAPP_KEYCODE_KP_EQUAL, "KPEQ" }, + { SAPP_KEYCODE_LEFT_SHIFT, "LFSH" }, + { SAPP_KEYCODE_LEFT_CONTROL, "LCTL" }, + { SAPP_KEYCODE_LEFT_ALT, "LALT" }, + { SAPP_KEYCODE_LEFT_SUPER, "LWIN" }, + { SAPP_KEYCODE_RIGHT_SHIFT, "RTSH" }, + { SAPP_KEYCODE_RIGHT_CONTROL, "RCTL" }, + { SAPP_KEYCODE_RIGHT_ALT, "RALT" }, + { SAPP_KEYCODE_RIGHT_ALT, "LVL3" }, + { SAPP_KEYCODE_RIGHT_ALT, "MDSW" }, + { SAPP_KEYCODE_RIGHT_SUPER, "RWIN" }, + { SAPP_KEYCODE_MENU, "MENU" } + }; + const int num_keymap_items = (int)(sizeof(keymap) / sizeof(keymap[0])); + + // find X11 keycode to sokol-app key code mapping + for (int scancode = scancode_min; scancode <= scancode_max; scancode++) { + sapp_keycode key = SAPP_KEYCODE_INVALID; + for (int i = 0; i < num_keymap_items; i++) { + if (strncmp(desc->names->keys[scancode].name, keymap[i].name, XkbKeyNameLength) == 0) { + key = keymap[i].key; + break; + } + } + + // fall back to key aliases in case the key name did not match + for (int i = 0; i < desc->names->num_key_aliases; i++) { + if (key != SAPP_KEYCODE_INVALID) { + break; + } + if (strncmp(desc->names->key_aliases[i].real, desc->names->keys[scancode].name, XkbKeyNameLength) != 0) { + continue; + } + for (int j = 0; j < num_keymap_items; j++) { + if (strncmp(desc->names->key_aliases[i].alias, keymap[j].name, XkbKeyNameLength) == 0) { + key = keymap[j].key; + break; + } + } + } + _sapp_tc.keycodes[scancode] = key; + } + XkbFreeNames(desc, XkbKeyNamesMask, True); + XkbFreeKeyboard(desc, 0, True); + + int width = 0; + KeySym* keysyms = XGetKeyboardMapping(_sapp_tc.display, scancode_min, scancode_max - scancode_min + 1, &width); + for (int scancode = scancode_min; scancode <= scancode_max; scancode++) { + // translate untranslated key codes using the traditional X11 KeySym lookups + if (_sapp_tc.keycodes[scancode] == SAPP_KEYCODE_INVALID) { + const size_t base = (size_t)((scancode - scancode_min) * width); + _sapp_tc.keycodes[scancode] = _sapp_tc_x11_translate_keysyms(&keysyms[base], width); + } + } + XFree(keysyms); +} +static int32_t _sapp_tc_x11_keysym_to_unicode(KeySym keysym) { + int min = 0; + int max = sizeof(_sapp_tc_x11_keysymtab) / sizeof(struct _sapp_tc_x11_codepair) - 1; + int mid; + + /* First check for Latin-1 characters (1:1 mapping) */ + if ((keysym >= 0x0020 && keysym <= 0x007e) || + (keysym >= 0x00a0 && keysym <= 0x00ff)) + { + return keysym; + } + + /* Also check for directly encoded 24-bit UCS characters */ + if ((keysym & 0xff000000) == 0x01000000) { + return keysym & 0x00ffffff; + } + + /* Binary search in table */ + while (max >= min) { + mid = (min + max) / 2; + if (_sapp_tc_x11_keysymtab[mid].keysym < keysym) { + min = mid + 1; + } else if (_sapp_tc_x11_keysymtab[mid].keysym > keysym) { + max = mid - 1; + } else { + return _sapp_tc_x11_keysymtab[mid].ucs; + } + } + + /* No matching Unicode value found */ + return -1; +} + +/*-- keycode helpers (per-window software key repeat) ------------------------*/ +static sapp_keycode _sapp_tc_translate_key(int scancode) { + if ((scancode >= 0) && (scancode < _SAPP_X11_MAX_X11_KEYCODES)) { + return _sapp_tc.keycodes[scancode]; + } + return SAPP_KEYCODE_INVALID; +} + +static bool _sapp_tc_x11_keypress_repeat(_sapp_tc_window_t* w, int keycode) { + /* XkbSetDetectableAutoRepeat is enabled at startup, so a held key emits + repeated KeyPress without paired KeyRelease; the first press reports + repeat=false, subsequent ones true until release */ + bool repeat = false; + if ((keycode >= 0) && (keycode < _SAPP_X11_MAX_X11_KEYCODES)) { + repeat = w->key_repeat[keycode]; + w->key_repeat[keycode] = true; + } + return repeat; +} + +static void _sapp_tc_x11_keyrelease_repeat(_sapp_tc_window_t* w, int keycode) { + if ((keycode >= 0) && (keycode < _SAPP_X11_MAX_X11_KEYCODES)) { + w->key_repeat[keycode] = false; + } +} + +/*-- X error handling (controlled failure instead of an Xlib abort) ----------*/ +static int _sapp_tc_x11_error_handler(Display* display, XErrorEvent* event) { + (void)display; + _sapp_tc.error_code = event->error_code; + return 0; +} + +static void _sapp_tc_x11_grab_error_handler(void) { + _sapp_tc.error_code = 0; + _sapp_tc.prev_error_handler = XSetErrorHandler(_sapp_tc_x11_error_handler); +} + +static void _sapp_tc_x11_release_error_handler(void) { + XSync(_sapp_tc.display, False); + XSetErrorHandler(_sapp_tc.prev_error_handler); + _sapp_tc.prev_error_handler = 0; +} + +/*-- window properties -------------------------------------------------------*/ +static unsigned long _sapp_tc_x11_get_window_property(Window xwin, Atom property, Atom type, unsigned char** value) { + Atom actual_type; + int actual_format; + unsigned long item_count, bytes_after; + XGetWindowProperty(_sapp_tc.display, xwin, property, + 0, LONG_MAX, False, type, + &actual_type, &actual_format, &item_count, &bytes_after, value); + return item_count; +} + +static int _sapp_tc_x11_get_window_state(_sapp_tc_window_t* w) { + int result = WithdrawnState; + struct { + CARD32 state; + Window icon; + } *state = NULL; + if (_sapp_tc_x11_get_window_property(w->xwin, _sapp_tc.WM_STATE, _sapp_tc.WM_STATE, (unsigned char**)&state) >= 2) { + result = (int)state->state; + } + if (state) { + XFree(state); + } + return result; +} + +static void _sapp_tc_x11_send_event(Window xwin, Atom type, long a, long b, long c, long d, long e) { + XEvent event; + memset(&event, 0, sizeof(event)); + event.type = ClientMessage; + event.xclient.window = xwin; + event.xclient.format = 32; + event.xclient.message_type = type; + event.xclient.data.l[0] = a; + event.xclient.data.l[1] = b; + event.xclient.data.l[2] = c; + event.xclient.data.l[3] = d; + event.xclient.data.l[4] = e; + XSendEvent(_sapp_tc.display, _sapp_tc.root, False, + SubstructureNotifyMask | SubstructureRedirectMask, &event); +} + +/*-- atoms / extensions -------------------------------------------------------*/ +static void _sapp_tc_x11_init_extensions(void) { + Display* d = _sapp_tc.display; + _sapp_tc.UTF8_STRING = XInternAtom(d, "UTF8_STRING", False); + _sapp_tc.CLIPBOARD = XInternAtom(d, "CLIPBOARD", False); + _sapp_tc.TARGETS = XInternAtom(d, "TARGETS", False); + _sapp_tc.WM_PROTOCOLS = XInternAtom(d, "WM_PROTOCOLS", False); + _sapp_tc.WM_DELETE_WINDOW = XInternAtom(d, "WM_DELETE_WINDOW", False); + _sapp_tc.WM_STATE = XInternAtom(d, "WM_STATE", False); + _sapp_tc.NET_WM_NAME = XInternAtom(d, "_NET_WM_NAME", False); + _sapp_tc.NET_WM_ICON_NAME = XInternAtom(d, "_NET_WM_ICON_NAME", False); + _sapp_tc.NET_WM_STATE = XInternAtom(d, "_NET_WM_STATE", False); + _sapp_tc.NET_WM_STATE_FULLSCREEN = XInternAtom(d, "_NET_WM_STATE_FULLSCREEN", False); + _sapp_tc.MOTIF_WM_HINTS = XInternAtom(d, "_MOTIF_WM_HINTS", False); + _sapp_tc.SAPP_TC_SELECTION = XInternAtom(d, "SAPP_TC_SELECTION", False); + if (_sapp_tc.app.drop_enabled) { + _sapp_tc.xdnd.XdndAware = XInternAtom(d, "XdndAware", False); + _sapp_tc.xdnd.XdndEnter = XInternAtom(d, "XdndEnter", False); + _sapp_tc.xdnd.XdndPosition = XInternAtom(d, "XdndPosition", False); + _sapp_tc.xdnd.XdndStatus = XInternAtom(d, "XdndStatus", False); + _sapp_tc.xdnd.XdndActionCopy = XInternAtom(d, "XdndActionCopy", False); + _sapp_tc.xdnd.XdndDrop = XInternAtom(d, "XdndDrop", False); + _sapp_tc.xdnd.XdndFinished = XInternAtom(d, "XdndFinished", False); + _sapp_tc.xdnd.XdndSelection = XInternAtom(d, "XdndSelection", False); + _sapp_tc.xdnd.XdndTypeList = XInternAtom(d, "XdndTypeList", False); + _sapp_tc.xdnd.text_uri_list = XInternAtom(d, "text/uri-list", False); + } +} + +/*-- DPI (Xft.dpi resource, Qt/GTK-compatible; 96 fallback) -------------------*/ +static void _sapp_tc_x11_query_system_dpi(void) { + _sapp_tc.dpi = 96.0f; + const char* rms = XResourceManagerString(_sapp_tc.display); + if (rms) { + XrmDatabase db = XrmGetStringDatabase(rms); + if (db) { + XrmValue value; + char* type = NULL; + if (XrmGetResource(db, "Xft.dpi", "Xft.Dpi", &type, &value)) { + if (type && (0 == strcmp(type, "String")) && value.addr) { + _sapp_tc.dpi = (float)atof(value.addr); + if (_sapp_tc.dpi <= 0.0f) { + _sapp_tc.dpi = 96.0f; + } + } + } + XrmDestroyDatabase(db); + } + } +} + +/*-- mouse cursors (Xcursor themed, X font cursor fallback) -------------------*/ +static void _sapp_tc_x11_create_hidden_cursor(void) { + XcursorImage* img = XcursorImageCreate(16, 16); + if (!img) return; + img->xhot = 0; + img->yhot = 0; + memset(img->pixels, 0, (size_t)(16 * 16) * sizeof(XcursorPixel)); + _sapp_tc.hidden_cursor = XcursorImageLoadCursor(_sapp_tc.display, img); + XcursorImageDestroy(img); +} + +static void _sapp_tc_x11_create_standard_cursor(sapp_mouse_cursor cursor, const char* name, const char* theme, int size, uint32_t fallback_native) { + if (theme) { + XcursorImage* img = XcursorLibraryLoadImage(name, theme, size); + if (img) { + _sapp_tc.standard_cursors[cursor] = XcursorImageLoadCursor(_sapp_tc.display, img); + XcursorImageDestroy(img); + } + } + if ((0 == _sapp_tc.standard_cursors[cursor]) && (0 != fallback_native)) { + _sapp_tc.standard_cursors[cursor] = XCreateFontCursor(_sapp_tc.display, fallback_native); + } +} + +static void _sapp_tc_x11_init_cursors(void) { + const char* theme = XcursorGetTheme(_sapp_tc.display); + const int size = XcursorGetDefaultSize(_sapp_tc.display); + _sapp_tc_x11_create_standard_cursor(SAPP_MOUSECURSOR_DEFAULT, "default", theme, size, XC_left_ptr); + _sapp_tc_x11_create_standard_cursor(SAPP_MOUSECURSOR_ARROW, "default", theme, size, XC_left_ptr); + _sapp_tc_x11_create_standard_cursor(SAPP_MOUSECURSOR_IBEAM, "text", theme, size, XC_xterm); + _sapp_tc_x11_create_standard_cursor(SAPP_MOUSECURSOR_CROSSHAIR, "crosshair", theme, size, XC_crosshair); + _sapp_tc_x11_create_standard_cursor(SAPP_MOUSECURSOR_POINTING_HAND, "pointer", theme, size, XC_hand2); + _sapp_tc_x11_create_standard_cursor(SAPP_MOUSECURSOR_RESIZE_EW, "ew-resize", theme, size, XC_sb_h_double_arrow); + _sapp_tc_x11_create_standard_cursor(SAPP_MOUSECURSOR_RESIZE_NS, "ns-resize", theme, size, XC_sb_v_double_arrow); + _sapp_tc_x11_create_standard_cursor(SAPP_MOUSECURSOR_RESIZE_NWSE, "nwse-resize", theme, size, 0); + _sapp_tc_x11_create_standard_cursor(SAPP_MOUSECURSOR_RESIZE_NESW, "nesw-resize", theme, size, 0); + _sapp_tc_x11_create_standard_cursor(SAPP_MOUSECURSOR_RESIZE_ALL, "all-scroll", theme, size, XC_fleur); + _sapp_tc_x11_create_standard_cursor(SAPP_MOUSECURSOR_NOT_ALLOWED, "not-allowed", theme, size, 0); + _sapp_tc_x11_create_hidden_cursor(); +} + +static void _sapp_tc_x11_destroy_cursors(void) { + if (_sapp_tc.hidden_cursor) { + XFreeCursor(_sapp_tc.display, _sapp_tc.hidden_cursor); + _sapp_tc.hidden_cursor = 0; + } + for (int i = 0; i < _SAPP_MOUSECURSOR_NUM; i++) { + if (_sapp_tc.standard_cursors[i]) { + XFreeCursor(_sapp_tc.display, _sapp_tc.standard_cursors[i]); + _sapp_tc.standard_cursors[i] = 0; + } + if (_sapp_tc.custom_cursors[i]) { + XFreeCursor(_sapp_tc.display, _sapp_tc.custom_cursors[i]); + _sapp_tc.custom_cursors[i] = 0; + _sapp_tc.app.custom_cursor_bound[i] = false; + } + } +} + +static void _sapp_tc_x11_apply_cursor(void) { + /* app-global cursor (same model as sokol_app.h), applied to every window */ + const sapp_mouse_cursor cur = _sapp_tc.app.current_cursor; + Cursor c = 0; + if (_sapp_tc.app.mouse_shown) { + c = _sapp_tc.app.custom_cursor_bound[cur] ? _sapp_tc.custom_cursors[cur] + : _sapp_tc.standard_cursors[cur]; + } else { + c = _sapp_tc.hidden_cursor; + } + for (int i = 0; i < _SAPP_TC_MAX_WINDOWS; i++) { + _sapp_tc_window_t* w = _sapp_tc.windows[i]; + if (!w || !w->xwin) continue; + if (c) { + XDefineCursor(_sapp_tc.display, w->xwin, c); + } else { + XUndefineCursor(_sapp_tc.display, w->xwin); + } + } + XFlush(_sapp_tc.display); +} + +/*-- title / decorations ------------------------------------------------------*/ +static void _sapp_tc_x11_update_window_title(_sapp_tc_window_t* w, const char* title) { + Xutf8SetWMProperties(_sapp_tc.display, w->xwin, title, title, NULL, 0, NULL, NULL, NULL); + XChangeProperty(_sapp_tc.display, w->xwin, _sapp_tc.NET_WM_NAME, _sapp_tc.UTF8_STRING, + 8, PropModeReplace, (unsigned char*)title, (int)strlen(title)); + XChangeProperty(_sapp_tc.display, w->xwin, _sapp_tc.NET_WM_ICON_NAME, _sapp_tc.UTF8_STRING, + 8, PropModeReplace, (unsigned char*)title, (int)strlen(title)); + XFlush(_sapp_tc.display); +} + +static void _sapp_tc_x11_set_decorated(_sapp_tc_window_t* w, bool decorated) { + struct { + unsigned long flags; + unsigned long functions; + unsigned long decorations; + long input_mode; + unsigned long status; + } hints; + memset(&hints, 0, sizeof(hints)); + hints.flags = (1L << 1); /* MWM_HINTS_DECORATIONS */ + hints.decorations = decorated ? 1 : 0; + XChangeProperty(_sapp_tc.display, w->xwin, _sapp_tc.MOTIF_WM_HINTS, _sapp_tc.MOTIF_WM_HINTS, + 32, PropModeReplace, (unsigned char*)&hints, 5); +} + +/*-- dimensions (polled per tick; ConfigureNotify is deliberately unused) -----*/ +static bool _sapp_tc_x11_update_dimensions(_sapp_tc_window_t* w) { + XWindowAttributes attribs; + memset(&attribs, 0, sizeof(attribs)); + XGetWindowAttributes(_sapp_tc.display, w->xwin, &attribs); + if ((attribs.width <= 0) || (attribs.height <= 0)) { + return false; + } + /* X11 has no separate backing scale: framebuffer == window pixels */ + const bool changed = (attribs.width != w->fb_width) || (attribs.height != w->fb_height); + w->fb_width = attribs.width; + w->fb_height = attribs.height; + const float scale = (w->dpi_scale > 0.0f) ? w->dpi_scale : 1.0f; + w->win_width = (int)((float)attribs.width / scale + 0.5f); + w->win_height = (int)((float)attribs.height / scale + 0.5f); + return changed; +} + +/*-- fullscreen (EWMH _NET_WM_STATE; must be called after XMapWindow) ---------*/ +static void _sapp_tc_x11_set_fullscreen(bool enable) { + _sapp_tc_window_t* w = _sapp_tc.app.main; + if (!w) return; + if (_sapp_tc.NET_WM_STATE && _sapp_tc.NET_WM_STATE_FULLSCREEN) { + const long op = enable ? 1 : 0; /* _NET_WM_STATE_ADD : _NET_WM_STATE_REMOVE */ + _sapp_tc_x11_send_event(w->xwin, _sapp_tc.NET_WM_STATE, + op, (long)_sapp_tc.NET_WM_STATE_FULLSCREEN, 0, 1, 0); + } + _sapp_tc.app.fullscreen = enable; + XFlush(_sapp_tc.display); +} + +/*-- synchronous event wait (poll on the connection fd with a deadline) ------*/ +static bool _sapp_tc_x11_wait_for_event(Window xwin, int type, double timeout_sec, XEvent* out_event) { + const double deadline = _sapp_tc_now() + timeout_sec; + for (;;) { + if (XCheckTypedWindowEvent(_sapp_tc.display, xwin, type, out_event)) { + return true; + } + const double remaining = deadline - _sapp_tc_now(); + if (remaining <= 0.0) { + return false; + } + struct pollfd pfd; + pfd.fd = ConnectionNumber(_sapp_tc.display); + pfd.events = POLLIN; + pfd.revents = 0; + poll(&pfd, 1, (int)(remaining * 1000.0) + 1); + } +} + +/*-- clipboard (CLIPBOARD selection, UTF8_STRING only, no INCR) ---------------*/ +static void _sapp_tc_x11_serve_selection_request(XEvent* event) { + XSelectionRequestEvent* req = &event->xselectionrequest; + if (req->selection != _sapp_tc.CLIPBOARD) return; + if (!_sapp_tc.app.clipboard_enabled || !_sapp_tc.app.clipboard) return; + XSelectionEvent reply; + memset(&reply, 0, sizeof(reply)); + reply.type = SelectionNotify; + reply.display = req->display; + reply.requestor = req->requestor; + reply.selection = req->selection; + reply.target = req->target; + reply.property = req->property; + reply.time = req->time; + if (req->target == _sapp_tc.UTF8_STRING) { + XChangeProperty(_sapp_tc.display, req->requestor, req->property, + _sapp_tc.UTF8_STRING, 8, PropModeReplace, + (unsigned char*)_sapp_tc.app.clipboard, + (int)strlen(_sapp_tc.app.clipboard)); + } else if (req->target == _sapp_tc.TARGETS) { + XChangeProperty(_sapp_tc.display, req->requestor, req->property, + XA_ATOM, 32, PropModeReplace, + (unsigned char*)&_sapp_tc.UTF8_STRING, 1); + } else { + reply.property = None; + } + XSendEvent(_sapp_tc.display, req->requestor, False, 0, (XEvent*)&reply); + XFlush(_sapp_tc.display); +} + +/*-- drag & drop (XDND v5, text/uri-list) -------------------------------------*/ +static bool _sapp_tc_x11_parse_dropped_files_list(const char* src) { + SOKOL_ASSERT(src); + SOKOL_ASSERT(_sapp_tc.app.drop_buffer); + + memset(_sapp_tc.app.drop_buffer, 0, (size_t)_sapp_tc.app.drop_max_files * (size_t)_sapp_tc.app.drop_max_path_length); + _sapp_tc.app.drop_num_files = 0; + + /* + src is (potentially percent-encoded) string made of one or multiple paths + separated by \r\n, each path starting with 'file://' + */ + bool err = false; + int src_count = 0; + char src_chr = 0; + char* dst_ptr = _sapp_tc.app.drop_buffer; + const char* dst_end_ptr = dst_ptr + (_sapp_tc.app.drop_max_path_length - 1); // room for terminating 0 + while (0 != (src_chr = *src++)) { + src_count++; + char dst_chr = 0; + /* check leading 'file://' */ + if (src_count <= 7) { + if (((src_count == 1) && (src_chr != 'f')) || + ((src_count == 2) && (src_chr != 'i')) || + ((src_count == 3) && (src_chr != 'l')) || + ((src_count == 4) && (src_chr != 'e')) || + ((src_count == 5) && (src_chr != ':')) || + ((src_count == 6) && (src_chr != '/')) || + ((src_count == 7) && (src_chr != '/'))) + { + /* non-file: URI in drop list: ignore entry */ + err = true; + break; + } + } else if (src_chr == '\r') { + // skip + } else if (src_chr == '\n') { + src_count = 0; + _sapp_tc.app.drop_num_files++; + // too many files is not an error + if (_sapp_tc.app.drop_num_files >= _sapp_tc.app.drop_max_files) { + break; + } + dst_ptr = _sapp_tc.app.drop_buffer + _sapp_tc.app.drop_num_files * _sapp_tc.app.drop_max_path_length; + dst_end_ptr = dst_ptr + (_sapp_tc.app.drop_max_path_length - 1); + } else if ((src_chr == '%') && src[0] && src[1]) { + // a percent-encoded byte (most likely UTF-8 multibyte sequence) + const char digits[3] = { src[0], src[1], 0 }; + src += 2; + dst_chr = (char) strtol(digits, 0, 16); + } else { + dst_chr = src_chr; + } + if (dst_chr) { + // dst_end_ptr already has adjustment for terminating zero + if (dst_ptr < dst_end_ptr) { + *dst_ptr++ = dst_chr; + } else { + /* dropped file path too long: abort parse */ + err = true; + break; + } + } + } + if (err) { + memset(_sapp_tc.app.drop_buffer, 0, (size_t)_sapp_tc.app.drop_max_files * (size_t)_sapp_tc.app.drop_max_path_length); + _sapp_tc.app.drop_num_files = 0; + return false; + } else { + return true; + } +} + +/*-- per-window event senders --------------------------------------------------*/ +static uint32_t _sapp_tc_x11_mods(uint32_t x11_state) { + uint32_t m = 0; + if (x11_state & ShiftMask) m |= SAPP_MODIFIER_SHIFT; + if (x11_state & ControlMask) m |= SAPP_MODIFIER_CTRL; + if (x11_state & Mod1Mask) m |= SAPP_MODIFIER_ALT; + if (x11_state & Mod4Mask) m |= SAPP_MODIFIER_SUPER; + if (x11_state & Button1Mask) m |= SAPP_MODIFIER_LMB; + if (x11_state & Button2Mask) m |= SAPP_MODIFIER_MMB; + if (x11_state & Button3Mask) m |= SAPP_MODIFIER_RMB; + return m; +} + +/* X11 doesn't set a modifier's own bit on the event that presses/releases it; + emulate (key-down ORs the bit in, key-up masks it out; same for buttons) */ +static uint32_t _sapp_tc_x11_key_modifier_bit(sapp_keycode key) { + switch (key) { + case SAPP_KEYCODE_LEFT_SHIFT: + case SAPP_KEYCODE_RIGHT_SHIFT: return SAPP_MODIFIER_SHIFT; + case SAPP_KEYCODE_LEFT_CONTROL: + case SAPP_KEYCODE_RIGHT_CONTROL: return SAPP_MODIFIER_CTRL; + case SAPP_KEYCODE_LEFT_ALT: + case SAPP_KEYCODE_RIGHT_ALT: return SAPP_MODIFIER_ALT; + case SAPP_KEYCODE_LEFT_SUPER: + case SAPP_KEYCODE_RIGHT_SUPER: return SAPP_MODIFIER_SUPER; + default: return 0; + } +} + +static uint32_t _sapp_tc_x11_button_modifier_bit(sapp_mousebutton btn) { + switch (btn) { + case SAPP_MOUSEBUTTON_LEFT: return SAPP_MODIFIER_LMB; + case SAPP_MOUSEBUTTON_RIGHT: return SAPP_MODIFIER_RMB; + case SAPP_MOUSEBUTTON_MIDDLE: return SAPP_MODIFIER_MMB; + default: return 0; + } +} + +static sapp_mousebutton _sapp_tc_x11_translate_button(unsigned int button) { + switch (button) { + case Button1: return SAPP_MOUSEBUTTON_LEFT; + case Button2: return SAPP_MOUSEBUTTON_MIDDLE; + case Button3: return SAPP_MOUSEBUTTON_RIGHT; + default: return SAPP_MOUSEBUTTON_INVALID; + } +} + +static void _sapp_tc_x11_mouse_update(_sapp_tc_window_t* w, int x, int y, bool clear_dxdy) { + /* event coordinates are raw window pixels == framebuffer pixels on X11 */ + const float fx = (float)x; + const float fy = (float)y; + if (clear_dxdy || !w->mouse_pos_valid) { + w->mouse_dx = 0.0f; + w->mouse_dy = 0.0f; + w->mouse_pos_valid = true; + } else { + w->mouse_dx = fx - w->mouse_x; + w->mouse_dy = fy - w->mouse_y; + } + w->mouse_x = fx; + w->mouse_y = fy; +} + +static void _sapp_tc_x11_mouse_event(_sapp_tc_window_t* w, sapp_event_type type, sapp_mousebutton btn, uint32_t mods) { + sapp_event e; + memset(&e, 0, sizeof(e)); + e.type = type; + e.mouse_button = btn; + e.mouse_x = w->mouse_x; + e.mouse_y = w->mouse_y; + e.mouse_dx = w->mouse_dx; + e.mouse_dy = w->mouse_dy; + e.modifiers = mods; + _sapp_tc_send(w, &e); +} + +static void _sapp_tc_x11_scroll_event(_sapp_tc_window_t* w, float x, float y, uint32_t mods) { + sapp_event e; + memset(&e, 0, sizeof(e)); + e.type = SAPP_EVENTTYPE_MOUSE_SCROLL; + e.mouse_x = w->mouse_x; + e.mouse_y = w->mouse_y; + e.scroll_x = x; + e.scroll_y = y; + e.modifiers = mods; + _sapp_tc_send(w, &e); +} + +static void _sapp_tc_x11_key_event(_sapp_tc_window_t* w, sapp_event_type type, sapp_keycode key, bool repeat, uint32_t mods) { + sapp_event e; + memset(&e, 0, sizeof(e)); + e.type = type; + e.key_code = key; + e.key_repeat = repeat; + e.modifiers = mods; + _sapp_tc_send(w, &e); + /* Ctrl+V paste notification (exact-CTRL match, sokol_app.h parity); the + app reads the clipboard in its handler */ + if ((type == SAPP_EVENTTYPE_KEY_DOWN) && _sapp_tc.app.clipboard_enabled && + (mods == SAPP_MODIFIER_CTRL) && (key == SAPP_KEYCODE_V)) { + sapp_event pe; + memset(&pe, 0, sizeof(pe)); + pe.type = SAPP_EVENTTYPE_CLIPBOARD_PASTED; + pe.modifiers = SAPP_MODIFIER_CTRL; + _sapp_tc_send(w, &pe); + } +} + +static void _sapp_tc_x11_char_event(_sapp_tc_window_t* w, uint32_t c, bool repeat, uint32_t mods) { + if ((c < 32) || (c == 127)) return; /* control characters */ + sapp_event e; + memset(&e, 0, sizeof(e)); + e.type = SAPP_EVENTTYPE_CHAR; + e.char_code = c; + e.key_repeat = repeat; + e.modifiers = mods; + _sapp_tc_send(w, &e); +} + +static void _sapp_tc_x11_app_event(_sapp_tc_window_t* w, sapp_event_type type) { + sapp_event e; + memset(&e, 0, sizeof(e)); + e.type = type; + _sapp_tc_send(w, &e); +} + +/*-- XDND handlers -------------------------------------------------------------*/ +static void _sapp_tc_x11_on_selectionnotify(XEvent* event) { + /* XDND drop data arrival (clipboard replies are consumed synchronously by + _sapp_tc_x11_wait_for_event inside sapp_get_clipboard_string) */ + if (!_sapp_tc.app.drop_enabled) return; + if (event->xselection.property != _sapp_tc.xdnd.XdndSelection) return; + char* data = 0; + const uint32_t result = (uint32_t)_sapp_tc_x11_get_window_property( + event->xselection.requestor, + event->xselection.property, + event->xselection.target, + (unsigned char**)&data); + if (result && data) { + if (_sapp_tc_x11_parse_dropped_files_list(data)) { + _sapp_tc_window_t* w = _sapp_tc_lookup_xwin(event->xselection.requestor); + if (!w) w = _sapp_tc.xdnd.over; + if (!w) w = _sapp_tc.app.main; + if (w) { + /* FIXME (upstream parity): no modifier state available here */ + _sapp_tc_x11_app_event(w, SAPP_EVENTTYPE_FILES_DROPPED); + } + } + } + if (_sapp_tc.xdnd.version >= 2) { + XEvent reply; + memset(&reply, 0, sizeof(reply)); + reply.type = ClientMessage; + reply.xclient.window = _sapp_tc.xdnd.source; + reply.xclient.message_type = _sapp_tc.xdnd.XdndFinished; + reply.xclient.format = 32; + reply.xclient.data.l[0] = (long)event->xselection.requestor; + reply.xclient.data.l[1] = (long)result; + reply.xclient.data.l[2] = (long)_sapp_tc.xdnd.XdndActionCopy; + XSendEvent(_sapp_tc.display, _sapp_tc.xdnd.source, False, NoEventMask, &reply); + XFlush(_sapp_tc.display); + } + if (data) { + XFree(data); + } +} + +static void _sapp_tc_x11_on_clientmessage(XEvent* event) { + if (XFilterEvent(event, None)) { + return; + } + _sapp_tc_window_t* w = _sapp_tc_lookup_xwin(event->xclient.window); + if (event->xclient.message_type == _sapp_tc.WM_PROTOCOLS) { + const Atom protocol = (Atom)event->xclient.data.l[0]; + if ((protocol == _sapp_tc.WM_DELETE_WINDOW) && w) { + if (w->is_main) { + /* the cancellable quit dance runs at the loop tail (upstream + parity): QUIT_REQUESTED is delivered there and a handler + may sapp_cancel_quit() */ + _sapp_tc.app.quit_requested = true; + } else if (w->desc.close_cb) { + /* user closed a secondary window: notify; the app accepts by + calling sapp_destroy_window() (w may be freed after this) */ + sapp_window handle = { w->win_id }; + w->desc.close_cb(handle, w->desc.user_data); + } + } + } else if (!_sapp_tc.app.drop_enabled) { + return; + } else if (event->xclient.message_type == _sapp_tc.xdnd.XdndEnter) { + const bool is_list = 0 != (event->xclient.data.l[1] & 1); + _sapp_tc.xdnd.source = (Window)event->xclient.data.l[0]; + _sapp_tc.xdnd.version = (int)(event->xclient.data.l[1] >> 24); + _sapp_tc.xdnd.format = None; + _sapp_tc.xdnd.over = w; + if (_sapp_tc.xdnd.version > _SAPP_TC_XDND_VERSION) { + return; + } + uint32_t count = 0; + Atom* formats = 0; + if (is_list) { + count = (uint32_t)_sapp_tc_x11_get_window_property(_sapp_tc.xdnd.source, + _sapp_tc.xdnd.XdndTypeList, XA_ATOM, (unsigned char**)&formats); + } else { + count = 3; + formats = (Atom*)event->xclient.data.l + 2; + } + for (uint32_t i = 0; i < count; i++) { + if (formats[i] == _sapp_tc.xdnd.text_uri_list) { + _sapp_tc.xdnd.format = _sapp_tc.xdnd.text_uri_list; + break; + } + } + if (is_list && formats) { + XFree(formats); + } + } else if (event->xclient.message_type == _sapp_tc.xdnd.XdndDrop) { + if (_sapp_tc.xdnd.version > _SAPP_TC_XDND_VERSION) { + return; + } + Time time = CurrentTime; + if (_sapp_tc.xdnd.format) { + if (_sapp_tc.xdnd.version >= 1) { + time = (Time)event->xclient.data.l[2]; + } + XConvertSelection(_sapp_tc.display, _sapp_tc.xdnd.XdndSelection, + _sapp_tc.xdnd.format, _sapp_tc.xdnd.XdndSelection, + event->xclient.window, time); + } else if (_sapp_tc.xdnd.version >= 2) { + XEvent reply; + memset(&reply, 0, sizeof(reply)); + reply.type = ClientMessage; + reply.xclient.window = _sapp_tc.xdnd.source; + reply.xclient.message_type = _sapp_tc.xdnd.XdndFinished; + reply.xclient.format = 32; + reply.xclient.data.l[0] = (long)event->xclient.window; + reply.xclient.data.l[1] = 0; /* drag was rejected */ + reply.xclient.data.l[2] = None; + XSendEvent(_sapp_tc.display, _sapp_tc.xdnd.source, False, NoEventMask, &reply); + XFlush(_sapp_tc.display); + } + } else if (event->xclient.message_type == _sapp_tc.xdnd.XdndPosition) { + if (_sapp_tc.xdnd.version > _SAPP_TC_XDND_VERSION) { + return; + } + _sapp_tc.xdnd.over = w; + XEvent reply; + memset(&reply, 0, sizeof(reply)); + reply.type = ClientMessage; + reply.xclient.window = _sapp_tc.xdnd.source; + reply.xclient.message_type = _sapp_tc.xdnd.XdndStatus; + reply.xclient.format = 32; + reply.xclient.data.l[0] = (long)event->xclient.window; + if (_sapp_tc.xdnd.format) { + reply.xclient.data.l[1] = 1; /* accept with no rectangle */ + if (_sapp_tc.xdnd.version >= 2) { + reply.xclient.data.l[4] = (long)_sapp_tc.xdnd.XdndActionCopy; + } + } + XSendEvent(_sapp_tc.display, _sapp_tc.xdnd.source, False, NoEventMask, &reply); + XFlush(_sapp_tc.display); + } +} + +/*-- GLX bring-up ---------------------------------------------------------------*/ +static bool _sapp_tc_glx_ext_supported(const char* ext, const char* extensions) { + if (!ext || !extensions) return false; + const char* start = extensions; + const size_t len = strlen(ext); + for (;;) { + const char* at = strstr(start, ext); + if (!at) return false; + const char* terminator = at + len; + if ((at == extensions || *(at - 1) == ' ') && + (*terminator == ' ' || *terminator == '\0')) { + return true; + } + start = terminator; + } +} + +typedef void (*_sapp_tc_glx_voidfn_t)(void); +static _sapp_tc_glx_voidfn_t _sapp_tc_glx_getprocaddr(const char* name) { + return (_sapp_tc_glx_voidfn_t)glXGetProcAddressARB((const GLubyte*)name); +} + +static bool _sapp_tc_glx_init(void) { + int error_base = 0, event_base = 0; + if (!glXQueryExtension(_sapp_tc.display, &error_base, &event_base)) { + fprintf(stderr, "sokol_app_tc.h: GLX extension not present on the X server\n"); + return false; + } + int major = 0, minor = 0; + if (!glXQueryVersion(_sapp_tc.display, &major, &minor)) { + fprintf(stderr, "sokol_app_tc.h: glXQueryVersion() failed\n"); + return false; + } + if ((major < 1) || ((major == 1) && (minor < 3))) { + fprintf(stderr, "sokol_app_tc.h: GLX version 1.3 or higher required\n"); + return false; + } + const char* exts = glXQueryExtensionsString(_sapp_tc.display, _sapp_tc.screen); + if (_sapp_tc_glx_ext_supported("GLX_EXT_swap_control", exts)) { + _sapp_tc.SwapIntervalEXT = (void (*)(Display*, GLXDrawable, int)) + _sapp_tc_glx_getprocaddr("glXSwapIntervalEXT"); + } + if (_sapp_tc_glx_ext_supported("GLX_MESA_swap_control", exts)) { + _sapp_tc.SwapIntervalMESA = (int (*)(int)) + _sapp_tc_glx_getprocaddr("glXSwapIntervalMESA"); + } + if (_sapp_tc_glx_ext_supported("GLX_ARB_create_context", exts) && + _sapp_tc_glx_ext_supported("GLX_ARB_create_context_profile", exts)) { + _sapp_tc.CreateContextAttribsARB = + (GLXContext (*)(Display*, GLXFBConfig, GLXContext, Bool, const int*)) + _sapp_tc_glx_getprocaddr("glXCreateContextAttribsARB"); + } + if (!_sapp_tc.CreateContextAttribsARB) { + fprintf(stderr, "sokol_app_tc.h: GLX_ARB_create_context(_profile) required\n"); + return false; + } + return true; +} + +static int _sapp_tc_glx_attrib(GLXFBConfig cfg, int attrib) { + int value = 0; + glXGetFBConfigAttrib(_sapp_tc.display, cfg, attrib, &value); + return value; +} + +/* pick ONE fbconfig for the whole app (shared context; every window's visual + must match it): RGBA8, >= depth24/stencil8, doublebuffered, closest MSAA */ +static bool _sapp_tc_glx_choose_fbconfig(void) { + int count = 0; + GLXFBConfig* configs = glXGetFBConfigs(_sapp_tc.display, _sapp_tc.screen, &count); + if (!configs || (count == 0)) { + if (configs) XFree(configs); + return false; + } + const int want_samples = (_sapp_tc.app.desc.sample_count > 1) ? _sapp_tc.app.desc.sample_count : 0; + int best = -1; + int best_score = INT_MAX; + for (int i = 0; i < count; i++) { + if (!(_sapp_tc_glx_attrib(configs[i], GLX_RENDER_TYPE) & GLX_RGBA_BIT)) continue; + if (!(_sapp_tc_glx_attrib(configs[i], GLX_DRAWABLE_TYPE) & GLX_WINDOW_BIT)) continue; + if (!_sapp_tc_glx_attrib(configs[i], GLX_DOUBLEBUFFER)) continue; + if (_sapp_tc_glx_attrib(configs[i], GLX_RED_SIZE) != 8) continue; + if (_sapp_tc_glx_attrib(configs[i], GLX_GREEN_SIZE) != 8) continue; + if (_sapp_tc_glx_attrib(configs[i], GLX_BLUE_SIZE) != 8) continue; + if (_sapp_tc_glx_attrib(configs[i], GLX_ALPHA_SIZE) != 8) continue; + const int depth = _sapp_tc_glx_attrib(configs[i], GLX_DEPTH_SIZE); + const int stencil = _sapp_tc_glx_attrib(configs[i], GLX_STENCIL_SIZE); + if ((depth < 24) || (stencil < 8)) continue; + const int samples = _sapp_tc_glx_attrib(configs[i], GLX_SAMPLES); + int sample_diff = samples - want_samples; + if (sample_diff < 0) sample_diff = -sample_diff; + const int score = sample_diff * 100 + (depth - 24) + (stencil - 8); + if (score < best_score) { + best_score = score; + best = i; + } + } + bool ok = false; + if (best >= 0) { + _sapp_tc.fbconfig = configs[best]; + /* report the ACTUAL sample count (a request the hardware can't satisfy + degrades to the closest supported config) */ + const int got_samples = _sapp_tc_glx_attrib(configs[best], GLX_SAMPLES); + _sapp_tc.app.desc.sample_count = (got_samples > 1) ? got_samples : 1; + ok = true; + } else { + fprintf(stderr, "sokol_app_tc.h: no suitable GLXFBConfig found\n"); + } + XFree(configs); + return ok; +} + +static bool _sapp_tc_glx_create_context(void) { + int gl_major = _sapp_tc.app.desc.gl.major_version; + int gl_minor = _sapp_tc.app.desc.gl.minor_version; + if (gl_major == 0) { + gl_major = 4; /* upstream Linux GLCORE default: 4.3 core */ + gl_minor = 3; + } + const int attribs[] = { + GLX_CONTEXT_MAJOR_VERSION_ARB, gl_major, + GLX_CONTEXT_MINOR_VERSION_ARB, gl_minor, + GLX_CONTEXT_PROFILE_MASK_ARB, GLX_CONTEXT_CORE_PROFILE_BIT_ARB, + GLX_CONTEXT_FLAGS_ARB, GLX_CONTEXT_FORWARD_COMPATIBLE_BIT_ARB, + None + }; + _sapp_tc_x11_grab_error_handler(); + _sapp_tc.ctx = _sapp_tc.CreateContextAttribsARB(_sapp_tc.display, _sapp_tc.fbconfig, + NULL, True, attribs); + _sapp_tc_x11_release_error_handler(); + if (!_sapp_tc.ctx) { + fprintf(stderr, "sokol_app_tc.h: failed to create GL %d.%d core context\n", gl_major, gl_minor); + return false; + } + return true; +} + +static void _sapp_tc_glx_set_swapinterval(_sapp_tc_window_t* w, int interval) { + /* GLX_EXT_swap_control is per-drawable: each window gets its own interval. + The MESA fallback is context-wide -- with more than one window it is set + to 0 (never block) and the main tick self-heals to timer pacing. */ + if (_sapp_tc.SwapIntervalEXT) { + _sapp_tc.SwapIntervalEXT(_sapp_tc.display, w->glx_win, interval); + } else if (_sapp_tc.SwapIntervalMESA) { + _sapp_tc.SwapIntervalMESA(interval); + } +} + +/*-- window creation / destruction ---------------------------------------------*/ +static bool _sapp_tc_x11_create_native_window(_sapp_tc_window_t* w, const char* title, + int width_pt, int height_pt, bool borderless) { + XVisualInfo* vi = glXGetVisualFromFBConfig(_sapp_tc.display, _sapp_tc.fbconfig); + if (!vi) { + return false; + } + w->colormap = XCreateColormap(_sapp_tc.display, _sapp_tc.root, vi->visual, AllocNone); + XSetWindowAttributes wa; + memset(&wa, 0, sizeof(wa)); + const unsigned long wamask = CWBorderPixel | CWColormap | CWEventMask; + wa.colormap = w->colormap; + wa.border_pixel = 0; + wa.event_mask = StructureNotifyMask | KeyPressMask | KeyReleaseMask | + PointerMotionMask | ButtonPressMask | ButtonReleaseMask | + ExposureMask | FocusChangeMask | VisibilityChangeMask | + EnterWindowMask | LeaveWindowMask | PropertyChangeMask; + /* NOTE: sizing uses the same scale that dimension reporting divides by */ + const float scale = (w->dpi_scale > 0.0f) ? w->dpi_scale : 1.0f; + int px_w = (int)((float)width_pt * scale + 0.5f); + int px_h = (int)((float)height_pt * scale + 0.5f); + if (width_pt <= 0) { + px_w = (DisplayWidth(_sapp_tc.display, _sapp_tc.screen) * 4) / 5; + } + if (height_pt <= 0) { + px_h = (DisplayHeight(_sapp_tc.display, _sapp_tc.screen) * 4) / 5; + } + _sapp_tc_x11_grab_error_handler(); + w->xwin = XCreateWindow(_sapp_tc.display, _sapp_tc.root, + 0, 0, (unsigned int)px_w, (unsigned int)px_h, + 0, /* border width */ + vi->depth, /* color depth */ + InputOutput, vi->visual, wamask, &wa); + _sapp_tc_x11_release_error_handler(); + XFree(vi); + if (!w->xwin) { + return false; + } + Atom protocols[] = { _sapp_tc.WM_DELETE_WINDOW }; + XSetWMProtocols(_sapp_tc.display, w->xwin, protocols, 1); + /* NOTE: PPosition and PSize are obsolete and ignored */ + XSizeHints* hints = XAllocSizeHints(); + if (hints) { + hints->flags = PWinGravity; + hints->win_gravity = CenterGravity; + XSetWMNormalHints(_sapp_tc.display, w->xwin, hints); + XFree(hints); + } + if (borderless) { + _sapp_tc_x11_set_decorated(w, false); + } + /* announce XDND support */ + if (_sapp_tc.app.drop_enabled) { + const Atom version = _SAPP_TC_XDND_VERSION; + XChangeProperty(_sapp_tc.display, w->xwin, _sapp_tc.xdnd.XdndAware, XA_ATOM, + 32, PropModeReplace, (unsigned char*)&version, 1); + } + _sapp_tc_x11_update_window_title(w, title ? title : "TrussC"); + /* GLX drawable wrapping the X window (the per-window object; the context + is shared) */ + _sapp_tc_x11_grab_error_handler(); + w->glx_win = glXCreateWindow(_sapp_tc.display, _sapp_tc.fbconfig, w->xwin, NULL); + _sapp_tc_x11_release_error_handler(); + if (!w->glx_win) { + return false; + } + _sapp_tc_timing_reset(&w->timing); + return true; +} + +static void _sapp_tc_x11_show_window(_sapp_tc_window_t* w) { + XMapWindow(_sapp_tc.display, w->xwin); + XEvent dummy; + _sapp_tc_x11_wait_for_event(w->xwin, VisibilityNotify, 0.1, &dummy); + XRaiseWindow(_sapp_tc.display, w->xwin); + XFlush(_sapp_tc.display); +} + +static void _sapp_tc_x11_destroy_window_resources(_sapp_tc_window_t* w) { + if (w->glx_win) { + /* never destroy the drawable that is current */ + if (glXGetCurrentDrawable() == w->glx_win) { + glXMakeCurrent(_sapp_tc.display, None, NULL); + } + glXDestroyWindow(_sapp_tc.display, w->glx_win); + w->glx_win = 0; + } + if (w->xwin) { + XUnmapWindow(_sapp_tc.display, w->xwin); + XDestroyWindow(_sapp_tc.display, w->xwin); + w->xwin = 0; + } + if (w->colormap) { + XFreeColormap(_sapp_tc.display, w->colormap); + w->colormap = 0; + } + XFlush(_sapp_tc.display); +} + +/*-- event dispatch ---------------------------------------------------------------*/ +static void _sapp_tc_x11_process_event(XEvent* ev) { + _sapp_tc_window_t* w = _sapp_tc_lookup_xwin(ev->xany.window); + switch (ev->type) { + case FocusIn: + /* ignore grab-cycle focus notifications (GLFW parity) */ + if (w && (ev->xfocus.mode != NotifyGrab) && (ev->xfocus.mode != NotifyUngrab)) { + _sapp_tc_x11_app_event(w, SAPP_EVENTTYPE_FOCUSED); + } + break; + case FocusOut: + if (w && (ev->xfocus.mode != NotifyGrab) && (ev->xfocus.mode != NotifyUngrab)) { + _sapp_tc_x11_app_event(w, SAPP_EVENTTYPE_UNFOCUSED); + } + break; + case KeyPress: { + if (!w) break; + const int keycode = (int)ev->xkey.keycode; + const sapp_keycode key = _sapp_tc_translate_key(keycode); + const bool repeat = _sapp_tc_x11_keypress_repeat(w, keycode); + uint32_t mods = _sapp_tc_x11_mods(ev->xkey.state); + mods |= _sapp_tc_x11_key_modifier_bit(key); + _sapp_tc_x11_key_event(w, SAPP_EVENTTYPE_KEY_DOWN, key, repeat, mods); + KeySym keysym = 0; + XLookupString(&ev->xkey, NULL, 0, &keysym, NULL); + const int32_t chr = _sapp_tc_x11_keysym_to_unicode(keysym); + if (chr > 0) { + _sapp_tc_x11_char_event(w, (uint32_t)chr, repeat, mods); + } + break; + } + case KeyRelease: { + if (!w) break; + const int keycode = (int)ev->xkey.keycode; + const sapp_keycode key = _sapp_tc_translate_key(keycode); + _sapp_tc_x11_keyrelease_repeat(w, keycode); + uint32_t mods = _sapp_tc_x11_mods(ev->xkey.state); + mods &= ~_sapp_tc_x11_key_modifier_bit(key); + _sapp_tc_x11_key_event(w, SAPP_EVENTTYPE_KEY_UP, key, false, mods); + break; + } + case ButtonPress: { + if (!w) break; + _sapp_tc_x11_mouse_update(w, ev->xbutton.x, ev->xbutton.y, false); + uint32_t mods = _sapp_tc_x11_mods(ev->xbutton.state); + const unsigned int xbtn = ev->xbutton.button; + if (xbtn == Button4) { _sapp_tc_x11_scroll_event(w, 0.0f, 1.0f, mods); } + else if (xbtn == Button5) { _sapp_tc_x11_scroll_event(w, 0.0f, -1.0f, mods); } + else if (xbtn == 6) { _sapp_tc_x11_scroll_event(w, 1.0f, 0.0f, mods); } + else if (xbtn == 7) { _sapp_tc_x11_scroll_event(w, -1.0f, 0.0f, mods); } + else { + const sapp_mousebutton btn = _sapp_tc_x11_translate_button(xbtn); + if (btn != SAPP_MOUSEBUTTON_INVALID) { + mods |= _sapp_tc_x11_button_modifier_bit(btn); + w->mouse_buttons |= (uint8_t)(1 << btn); + _sapp_tc_x11_mouse_event(w, SAPP_EVENTTYPE_MOUSE_DOWN, btn, mods); + } + } + break; + } + case ButtonRelease: { + if (!w) break; + _sapp_tc_x11_mouse_update(w, ev->xbutton.x, ev->xbutton.y, false); + const sapp_mousebutton btn = _sapp_tc_x11_translate_button(ev->xbutton.button); + if (btn != SAPP_MOUSEBUTTON_INVALID) { + uint32_t mods = _sapp_tc_x11_mods(ev->xbutton.state); + mods &= ~_sapp_tc_x11_button_modifier_bit(btn); + w->mouse_buttons &= (uint8_t)~(1 << btn); + _sapp_tc_x11_mouse_event(w, SAPP_EVENTTYPE_MOUSE_UP, btn, mods); + } + break; + } + case EnterNotify: + if (w) { + _sapp_tc_x11_mouse_update(w, ev->xcrossing.x, ev->xcrossing.y, true); + /* suppressed while a button is held (upstream parity) */ + if (w->mouse_buttons == 0) { + _sapp_tc_x11_mouse_event(w, SAPP_EVENTTYPE_MOUSE_ENTER, + SAPP_MOUSEBUTTON_INVALID, _sapp_tc_x11_mods(ev->xcrossing.state)); + } + } + break; + case LeaveNotify: + if (w && (w->mouse_buttons == 0)) { + _sapp_tc_x11_mouse_event(w, SAPP_EVENTTYPE_MOUSE_LEAVE, + SAPP_MOUSEBUTTON_INVALID, _sapp_tc_x11_mods(ev->xcrossing.state)); + } + break; + case MotionNotify: + if (w) { + _sapp_tc_x11_mouse_update(w, ev->xmotion.x, ev->xmotion.y, false); + _sapp_tc_x11_mouse_event(w, SAPP_EVENTTYPE_MOUSE_MOVE, + SAPP_MOUSEBUTTON_INVALID, _sapp_tc_x11_mods(ev->xmotion.state)); + } + break; + case PropertyNotify: + /* iconify/restore detection (there is no map/unmap handling) */ + if (w && (ev->xproperty.state == PropertyNewValue) && + (ev->xproperty.atom == _sapp_tc.WM_STATE)) { + const int state = _sapp_tc_x11_get_window_state(w); + if ((state == IconicState) && !w->iconified) { + w->iconified = true; + _sapp_tc_x11_app_event(w, SAPP_EVENTTYPE_ICONIFIED); + } else if ((state == NormalState) && w->iconified) { + w->iconified = false; + w->earliest_next = 0.0; /* resume promptly */ + _sapp_tc_x11_app_event(w, SAPP_EVENTTYPE_RESTORED); + } + } + break; + case VisibilityNotify: + /* occlusion gate for secondary windows: a fully covered window + stops rendering (the main window keeps upstream's behavior) */ + if (w && !w->is_main) { + const bool obscured = (ev->xvisibility.state == VisibilityFullyObscured); + if (obscured != w->occluded) { + w->occluded = obscured; + _sapp_tc_x11_app_event(w, obscured ? SAPP_EVENTTYPE_SUSPENDED + : SAPP_EVENTTYPE_RESUMED); + if (!obscured) { + w->earliest_next = 0.0; + } + } + } + break; + case SelectionNotify: + _sapp_tc_x11_on_selectionnotify(ev); + break; + case SelectionRequest: + _sapp_tc_x11_serve_selection_request(ev); + break; + case ClientMessage: + _sapp_tc_x11_on_clientmessage(ev); + break; + case DestroyNotify: + /* not a bug (upstream parity) */ + break; + default: + break; + } +} + +/*-- pacing / tick ------------------------------------------------------------------*/ +static void _sapp_tc_x11_pace(_sapp_tc_window_t* w, double seconds) { + w->earliest_next = _sapp_tc_now() + seconds; +} + +/* absolute periodic schedule (timer-paced secondary windows): pacing from the + previous deadline instead of "now" keeps the average rate at the period + despite tick jitter; resync when far off (first tick, after occlusion) */ +static void _sapp_tc_x11_pace_periodic(_sapp_tc_window_t* w, double period) { + const double now = _sapp_tc_now(); + double next = w->earliest_next + period; + if ((next <= now) || (next > now + 2.0 * period)) { + next = now + period; + } + w->earliest_next = next; +} + +static bool _sapp_tc_x11_window_due(_sapp_tc_window_t* w, double now) { + if (!w || !w->ready || !w->glx_win || w->in_tick) return false; + return w->earliest_next <= now; +} + +static void _sapp_tc_x11_tick(_sapp_tc_window_t* w) { + if (w->in_tick || !w->glx_win) return; + _sapp_tc_timing_update(&w->timing); + w->frame_duration = w->timing.smooth_dt; + const int swap_interval = (_sapp_tc.app.desc.swap_interval > 0) ? _sapp_tc.app.desc.swap_interval : 1; + + if (w->is_main) { + /* resize is polled here every frame (upstream parity: the X11 backend + has no ConfigureNotify handler; this poll is the size authority) */ + if (_sapp_tc_x11_update_dimensions(w) && _sapp_tc.app.init_called) { + _sapp_tc_x11_app_event(w, SAPP_EVENTTYPE_RESIZED); + } + glXMakeCurrent(_sapp_tc.display, w->glx_win, _sapp_tc.ctx); + w->in_tick = true; + if (!_sapp_tc.app.init_called) { + _sapp_tc.app.init_called = true; + if (_sapp_tc.app.desc.init_cb) { + _sapp_tc.app.desc.init_cb(); + } else if (_sapp_tc.app.desc.init_userdata_cb) { + _sapp_tc.app.desc.init_userdata_cb(_sapp_tc.app.desc.user_data); + } + } + if (_sapp_tc.app.desc.frame_cb) { + _sapp_tc.app.desc.frame_cb(); + } else if (_sapp_tc.app.desc.frame_userdata_cb) { + _sapp_tc.app.desc.frame_userdata_cb(_sapp_tc.app.desc.user_data); + } + _sapp_tc.app.frame_count++; + w->in_tick = false; + bool presented = false; + if (_sapp_tc.app.skip_present) { + /* one-shot event-driven present suppression (TrussC patch: keeps + the last image on screen when a frame decides not to draw) */ + _sapp_tc.app.skip_present = false; + } else if (!w->iconified) { + /* with swap interval >= 1 this call BLOCKS until vsync -- that + blocking swap is the run loop's pacing (upstream parity) */ + glXSwapBuffers(_sapp_tc.display, w->glx_win); + presented = true; + } + const double now = _sapp_tc_now(); + if (w->iconified) { + /* minimized: frame_cb keeps running, throttled (win32 parity) */ + _sapp_tc_x11_pace(w, 0.0166 * (double)swap_interval); + } else if (presented) { + const double swap_dt = now - w->last_present; + w->last_present = now; + if (swap_dt > 0.002) { + /* the swap blocked (vsync paces the loop); refine the refresh + estimate that paces the secondary windows */ + const double per_frame = swap_dt / (double)swap_interval; + if ((per_frame > (1.0 / 240.0)) && (per_frame < (1.0 / 30.0))) { + _sapp_tc.refresh_period += 0.1 * (per_frame - _sapp_tc.refresh_period); + } + w->earliest_next = 0.0; + } else { + /* the swap returned immediately (unmapped/unredirected window, + MESA context-wide interval 0, driver without vsync): fall + back to timer pacing so the loop can never busy-spin */ + _sapp_tc_x11_pace_periodic(w, _sapp_tc.refresh_period * (double)swap_interval); + } + } else { + _sapp_tc_x11_pace(w, _sapp_tc.refresh_period * (double)swap_interval); + } + } else { + /* secondary window: swap interval 0 (vsyncing N drawables would + serialize the loop to refresh/N), timer-paced to the measured + refresh estimate */ + if (w->iconified || w->occluded) { + _sapp_tc_x11_pace(w, _sapp_tc.refresh_period); + return; + } + if (_sapp_tc_x11_update_dimensions(w)) { + _sapp_tc_x11_app_event(w, SAPP_EVENTTYPE_RESIZED); + } + glXMakeCurrent(_sapp_tc.display, w->glx_win, _sapp_tc.ctx); + w->in_tick = true; + if (w->desc.tick_cb) { + sapp_window handle = { w->win_id }; + w->desc.tick_cb(handle, w->desc.user_data); + } + w->in_tick = false; + glXSwapBuffers(_sapp_tc.display, w->glx_win); + _sapp_tc_x11_pace_periodic(w, _sapp_tc.refresh_period); + } +} + +/*-- the run loop -------------------------------------------------------------------- + While the main window presents normally, its blocking vsync'd swap paces + the loop and the XPending drain runs once per frame (upstream parity). + When every window is timer-paced (minimized, skip_present), the loop + sleeps in poll() on the X connection fd until the next deadline or an + incoming event, so the process never busy-spins. */ +static void _sapp_tc_x11_run_loop(void) { + XFlush(_sapp_tc.display); + while (!_sapp_tc.app.quit_ordered) { + double now = _sapp_tc_now(); + double wake_at = now + 0.1; /* robustness cap */ + bool any_due = false; + for (int i = 0; i < _SAPP_TC_MAX_WINDOWS; i++) { + _sapp_tc_window_t* w = _sapp_tc.windows[i]; + if (!w || !w->ready || !w->glx_win || w->in_tick) continue; + if (w->earliest_next > now) { + if (w->earliest_next < wake_at) wake_at = w->earliest_next; + } else { + any_due = true; + } + } + if (!any_due && !XPending(_sapp_tc.display)) { + struct pollfd pfd; + pfd.fd = ConnectionNumber(_sapp_tc.display); + pfd.events = POLLIN; + pfd.revents = 0; + const double timeout_s = wake_at - now; + poll(&pfd, 1, (timeout_s > 0.0) ? (int)(timeout_s * 1000.0) + 1 : 0); + } + /* drain exactly the already-queued events (can't block: count bounded) */ + int count = XPending(_sapp_tc.display); + while (count--) { + XEvent event; + XNextEvent(_sapp_tc.display, &event); + _sapp_tc_x11_process_event(&event); + } + /* tick every due window (re-fetch each slot: a tick or a processed + event may have destroyed windows) */ + now = _sapp_tc_now(); + for (int i = 0; i < _SAPP_TC_MAX_WINDOWS; i++) { + _sapp_tc_window_t* w = _sapp_tc.windows[i]; + if (_sapp_tc_x11_window_due(w, now)) { + _sapp_tc_x11_tick(w); + } + } + XFlush(_sapp_tc.display); + /* the cancellable quit dance (upstream parity): WM_DELETE_WINDOW or + sapp_request_quit() land here; sapp_quit() pre-sets quit_ordered */ + if (_sapp_tc.app.quit_requested && !_sapp_tc.app.quit_ordered) { + if (_sapp_tc.app.main) { + _sapp_tc_x11_app_event(_sapp_tc.app.main, SAPP_EVENTTYPE_QUIT_REQUESTED); + } + if (_sapp_tc.app.quit_requested) { + _sapp_tc.app.quit_ordered = true; + } + } + } +} + +/*-- main window creation --------------------------------------------------------*/ +static bool _sapp_tc_x11_create_main_window(void) { + const sapp_desc* d = &_sapp_tc.app.desc; + int slot = -1; + for (int i = 0; i < _SAPP_TC_MAX_WINDOWS; i++) { + if (_sapp_tc.windows[i] == 0) { slot = i; break; } + } + if (slot < 0) return false; + + _sapp_tc_window_t* w = new _sapp_tc_window_t(); + w->win_id = ++_sapp_tc.next_id; + w->is_main = true; + w->dpi_scale = d->high_dpi ? (_sapp_tc.dpi / 96.0f) : 1.0f; + if (w->dpi_scale <= 0.0f) w->dpi_scale = 1.0f; + + /* per-window desc: the app callbacks are reached via the trampoline */ + w->desc.width = d->width; + w->desc.height = d->height; + w->desc.sample_count = d->sample_count; + w->desc.no_high_dpi = !d->high_dpi; + w->desc.event_cb = _sapp_tc_main_event_tramp; + + if (!_sapp_tc_x11_create_native_window(w, _sapp_tc.app.window_title, + d->width, d->height, false)) { + _sapp_tc_x11_destroy_window_resources(w); + delete w; + return false; + } + _sapp_tc.windows[slot] = w; + _sapp_tc.app.main = w; + w->ready = true; + _sapp_tc_x11_update_dimensions(w); /* silent startup sizing (no RESIZED) */ + glXMakeCurrent(_sapp_tc.display, w->glx_win, _sapp_tc.ctx); + { + /* capture the default-framebuffer id sapp_get_swapchain() reports */ + GLint fb = 0; + glGetIntegerv(GL_FRAMEBUFFER_BINDING, &fb); + _sapp_tc.gl_framebuffer = (uint32_t)fb; + } + _sapp_tc_glx_set_swapinterval(w, (d->swap_interval > 0) ? d->swap_interval : 1); + _sapp_tc_x11_show_window(w); + if (d->fullscreen) { + /* must be after XMapWindow */ + _sapp_tc_x11_set_fullscreen(true); + _sapp_tc_x11_update_dimensions(w); + } + _sapp_tc.app.valid = true; + return true; +} + +/*-- public API --------------------------------------------------------------------*/ +extern "C" { + +sapp_window sapp_create_window(const sapp_window_desc* desc) { + sapp_window invalid = { 0 }; + if (!desc) return invalid; + if (!_sapp_tc.app.valid || !_sapp_tc.ctx) return invalid; + + int slot = -1; + for (int i = 0; i < _SAPP_TC_MAX_WINDOWS; i++) { + if (_sapp_tc.windows[i] == 0) { slot = i; break; } + } + if (slot < 0) return invalid; + + _sapp_tc_window_t* w = new _sapp_tc_window_t(); + w->win_id = ++_sapp_tc.next_id; + w->desc = *desc; + if (w->desc.width <= 0) w->desc.width = 640; + if (w->desc.height <= 0) w->desc.height = 480; + /* all windows share ONE fbconfig/visual/context: the MSAA sample count is + the one negotiated at startup, not per-window */ + w->desc.sample_count = _sapp_tc.app.desc.sample_count; + w->dpi_scale = w->desc.no_high_dpi ? 1.0f : (_sapp_tc.dpi / 96.0f); + if (w->dpi_scale <= 0.0f) w->dpi_scale = 1.0f; + + if (!_sapp_tc_x11_create_native_window(w, w->desc.title, + w->desc.width, w->desc.height, w->desc.borderless)) { + _sapp_tc_x11_destroy_window_resources(w); + delete w; + return invalid; + } + /* never vsync a second drawable (it would serialize the run loop to + refresh/N); this window is timer-paced instead. Requires a current + context (there always is one while the app runs). With only the + context-wide MESA fallback this turns vsync off for the WHOLE context + and the main tick self-heals to timer pacing. */ + _sapp_tc_glx_set_swapinterval(w, 0); + _sapp_tc.windows[slot] = w; + w->ready = true; + _sapp_tc_x11_update_dimensions(w); + _sapp_tc_x11_show_window(w); + sapp_window handle = { w->win_id }; + return handle; +} + +void sapp_destroy_window(sapp_window win) { + _sapp_tc_window_t* w = _sapp_tc_lookup(win); + if (!w || w->is_main) return; /* the main window is owned by sapp_run */ + for (int i = 0; i < _SAPP_TC_MAX_WINDOWS; i++) { + if (_sapp_tc.windows[i] == w) { _sapp_tc.windows[i] = 0; break; } + } + if (_sapp_tc.xdnd.over == w) _sapp_tc.xdnd.over = 0; + w->desc.close_cb = 0; + _sapp_tc_x11_destroy_window_resources(w); + delete w; + /* restore the main window's drawable if the destroy released it */ + if (!glXGetCurrentContext() && _sapp_tc.app.main && _sapp_tc.app.main->glx_win) { + glXMakeCurrent(_sapp_tc.display, _sapp_tc.app.main->glx_win, _sapp_tc.ctx); + } +} + +bool sapp_window_valid(sapp_window win) { + return _sapp_tc_lookup(win) != 0; +} + +int sapp_window_width(sapp_window win) { + _sapp_tc_window_t* w = _sapp_tc_lookup(win); + return w ? w->win_width : 0; +} + +int sapp_window_height(sapp_window win) { + _sapp_tc_window_t* w = _sapp_tc_lookup(win); + return w ? w->win_height : 0; +} + +int sapp_window_framebuffer_width(sapp_window win) { + _sapp_tc_window_t* w = _sapp_tc_lookup(win); + return w ? w->fb_width : 0; +} + +int sapp_window_framebuffer_height(sapp_window win) { + _sapp_tc_window_t* w = _sapp_tc_lookup(win); + return w ? w->fb_height : 0; +} + +float sapp_window_dpi_scale(sapp_window win) { + _sapp_tc_window_t* w = _sapp_tc_lookup(win); + return w ? w->dpi_scale : 1.0f; +} + +int sapp_window_sample_count(sapp_window win) { + _sapp_tc_window_t* w = _sapp_tc_lookup(win); + return w ? w->desc.sample_count : 1; +} + +bool sapp_window_occluded(sapp_window win) { + _sapp_tc_window_t* w = _sapp_tc_lookup(win); + return w ? w->occluded : false; +} + +void sapp_window_set_title(sapp_window win, const char* title) { + _sapp_tc_window_t* w = _sapp_tc_lookup(win); + if (!w || !w->xwin || !title) return; + _sapp_tc_x11_update_window_title(w, title); +} + +double sapp_window_frame_duration(sapp_window win) { + _sapp_tc_window_t* w = _sapp_tc_lookup(win); + if (!w) return 1.0 / 60.0; + return (w->frame_duration > 0.0) ? w->frame_duration : _sapp_tc.refresh_period; +} + +/* Metal / D3D11 handles do not exist on Linux */ +const void* sapp_window_metal_current_drawable(sapp_window win) { (void)win; return 0; } +const void* sapp_window_metal_depth_stencil_texture(sapp_window win) { (void)win; return 0; } +const void* sapp_window_metal_msaa_color_texture(sapp_window win) { (void)win; return 0; } +const void* sapp_window_macos_get_window(sapp_window win) { (void)win; return 0; } +const void* sapp_window_d3d11_render_view(sapp_window win) { (void)win; return 0; } +const void* sapp_window_d3d11_resolve_view(sapp_window win) { (void)win; return 0; } +const void* sapp_window_d3d11_depth_stencil_view(sapp_window win) { (void)win; return 0; } +const void* sapp_window_win32_get_hwnd(sapp_window win) { (void)win; return 0; } + +uint32_t sapp_window_gl_framebuffer(sapp_window win) { + /* the GL "swapchain" is just the default framebuffer of whatever drawable + is current -- which is this window's during its tick_cb */ + (void)win; + return _sapp_tc.gl_framebuffer; +} + +const void* sapp_window_x11_get_window(sapp_window win) { + _sapp_tc_window_t* w = _sapp_tc_lookup(win); + return w ? (const void*)(uintptr_t)w->xwin : 0; +} + +/*-- sokol_app.h public API (Linux implementation lives here) ---------------------*/ + +void sapp_run(const sapp_desc* desc) { + if (!desc) return; + _sapp_tc.app.desc = *desc; + sapp_desc* d = &_sapp_tc.app.desc; + if (d->sample_count <= 0) d->sample_count = 1; + if (d->swap_interval <= 0) d->swap_interval = 1; + if (d->clipboard_size <= 0) d->clipboard_size = 8192; + if (d->max_dropped_files <= 0) d->max_dropped_files = 1; + if (d->max_dropped_file_path_length <= 0) d->max_dropped_file_path_length = 2048; + strncpy(_sapp_tc.app.window_title, d->window_title ? d->window_title : "sokol", + sizeof(_sapp_tc.app.window_title) - 1); + d->window_title = _sapp_tc.app.window_title; + if (d->enable_clipboard) { + _sapp_tc.app.clipboard_enabled = true; + _sapp_tc.app.clipboard_size = d->clipboard_size; + _sapp_tc.app.clipboard = (char*)calloc(1, (size_t)d->clipboard_size); + } + if (d->enable_dragndrop) { + _sapp_tc.app.drop_enabled = true; + _sapp_tc.app.drop_max_files = d->max_dropped_files; + _sapp_tc.app.drop_max_path_length = d->max_dropped_file_path_length; + _sapp_tc.app.drop_buffer = (char*)calloc( + (size_t)d->max_dropped_files, (size_t)d->max_dropped_file_path_length); + } + _sapp_tc.app.mouse_shown = true; + _sapp_tc.app.current_cursor = SAPP_MOUSECURSOR_DEFAULT; + _sapp_tc.refresh_period = 1.0 / 60.0; + + /* XInitThreads before ANY other Xlib call (TrussC runs off-thread work) */ + XInitThreads(); + XrmInitialize(); + _sapp_tc.display = XOpenDisplay(NULL); + if (!_sapp_tc.display) { + fprintf(stderr, "sokol_app_tc.h: XOpenDisplay() failed (no X server / DISPLAY not set)\n"); + abort(); + } + _sapp_tc.screen = DefaultScreen(_sapp_tc.display); + _sapp_tc.root = DefaultRootWindow(_sapp_tc.display); + _sapp_tc_x11_query_system_dpi(); + _sapp_tc_x11_init_extensions(); + _sapp_tc_x11_init_cursors(); + /* detectable auto-repeat: held keys emit repeated KeyPress WITHOUT paired + KeyRelease -- required by the software key-repeat tracking */ + XkbSetDetectableAutoRepeat(_sapp_tc.display, True, NULL); + _sapp_tc_x11_init_keytable(); + + if (_sapp_tc_glx_init() && _sapp_tc_glx_choose_fbconfig() && _sapp_tc_glx_create_context()) { + if (_sapp_tc_x11_create_main_window()) { + _sapp_tc_x11_run_loop(); + } + } + + /* user cleanup runs BEFORE any GL/window teardown (the app shuts down + sokol_gfx here, which still needs a current context) */ + if (!_sapp_tc.app.cleanup_called) { + _sapp_tc.app.cleanup_called = true; + if (_sapp_tc.app.desc.cleanup_cb) { + _sapp_tc.app.desc.cleanup_cb(); + } else if (_sapp_tc.app.desc.cleanup_userdata_cb) { + _sapp_tc.app.desc.cleanup_userdata_cb(_sapp_tc.app.desc.user_data); + } + } + /* destroy any windows the app left open (secondary first, then main) */ + for (int i = 0; i < _SAPP_TC_MAX_WINDOWS; i++) { + _sapp_tc_window_t* w = _sapp_tc.windows[i]; + if (!w || w->is_main) continue; + _sapp_tc.windows[i] = 0; + _sapp_tc_x11_destroy_window_resources(w); + delete w; + } + if (_sapp_tc.app.main) { + _sapp_tc_window_t* w = _sapp_tc.app.main; + for (int i = 0; i < _SAPP_TC_MAX_WINDOWS; i++) { + if (_sapp_tc.windows[i] == w) { _sapp_tc.windows[i] = 0; break; } + } + _sapp_tc.app.main = 0; + _sapp_tc_x11_destroy_window_resources(w); + delete w; + } + if (_sapp_tc.ctx) { + glXMakeCurrent(_sapp_tc.display, None, NULL); + glXDestroyContext(_sapp_tc.display, _sapp_tc.ctx); + _sapp_tc.ctx = 0; + } + _sapp_tc_x11_destroy_cursors(); + XCloseDisplay(_sapp_tc.display); + _sapp_tc.display = 0; + if (_sapp_tc.app.clipboard) { free(_sapp_tc.app.clipboard); _sapp_tc.app.clipboard = 0; } + if (_sapp_tc.app.drop_buffer) { free(_sapp_tc.app.drop_buffer); _sapp_tc.app.drop_buffer = 0; } + _sapp_tc.app.valid = false; +} + +bool sapp_isvalid(void) { + return _sapp_tc.app.valid; +} + +int sapp_width(void) { + _sapp_tc_window_t* w = _sapp_tc.app.main; + return (w && w->fb_width > 0) ? w->fb_width : 1; +} + +float sapp_widthf(void) { + return (float)sapp_width(); +} + +int sapp_height(void) { + _sapp_tc_window_t* w = _sapp_tc.app.main; + return (w && w->fb_height > 0) ? w->fb_height : 1; +} + +float sapp_heightf(void) { + return (float)sapp_height(); +} + +int sapp_sample_count(void) { + return _sapp_tc.app.desc.sample_count > 0 ? _sapp_tc.app.desc.sample_count : 1; +} + +bool sapp_high_dpi(void) { + _sapp_tc_window_t* w = _sapp_tc.app.main; + return _sapp_tc.app.desc.high_dpi && w && (w->dpi_scale >= 1.5f); +} + +float sapp_dpi_scale(void) { + _sapp_tc_window_t* w = _sapp_tc.app.main; + return w ? w->dpi_scale : 1.0f; +} + +uint64_t sapp_frame_count(void) { + return _sapp_tc.app.frame_count; +} + +double sapp_frame_duration(void) { + _sapp_tc_window_t* w = _sapp_tc.app.main; + if (!w) return 1.0 / 60.0; + return (w->timing.smooth_dt > 0.0) ? w->timing.smooth_dt : _sapp_tc.refresh_period; +} + +void sapp_request_quit(void) { + _sapp_tc.app.quit_requested = true; +} + +void sapp_cancel_quit(void) { + _sapp_tc.app.quit_requested = false; +} + +void sapp_quit(void) { + _sapp_tc.app.quit_ordered = true; +} + +void sapp_consume_event(void) { + _sapp_tc.app.event_consumed = true; +} + +void sapp_skip_present(void) { + /* one-shot; HONORED on glXSwapBuffers (TrussC patch: event-driven present + suppression -- keeps the last image on screen, prevents flicker) */ + _sapp_tc.app.skip_present = true; +} + +void sapp_show_keyboard(bool show) { + (void)show; /* on-screen keyboard: mobile only */ +} + +bool sapp_keyboard_shown(void) { + return false; +} + +void sapp_show_mouse(bool show) { + if (_sapp_tc.app.mouse_shown != show) { + _sapp_tc.app.mouse_shown = show; + _sapp_tc_x11_apply_cursor(); + } +} + +bool sapp_mouse_shown(void) { + return _sapp_tc.app.mouse_shown; +} + +void sapp_set_mouse_cursor(sapp_mouse_cursor cursor) { + if ((int)cursor < 0 || (int)cursor >= _SAPP_MOUSECURSOR_NUM) return; + if (cursor != _sapp_tc.app.current_cursor) { + _sapp_tc.app.current_cursor = cursor; + _sapp_tc_x11_apply_cursor(); + } +} + +sapp_mouse_cursor sapp_get_mouse_cursor(void) { + return _sapp_tc.app.current_cursor; +} + +sapp_mouse_cursor sapp_bind_mouse_cursor_image(sapp_mouse_cursor cursor, const sapp_image_desc* desc) { + if ((int)cursor < 0 || (int)cursor >= _SAPP_MOUSECURSOR_NUM) return cursor; + if (!desc || !desc->pixels.ptr || desc->width <= 0 || desc->height <= 0) return cursor; + if (desc->pixels.size < (size_t)(desc->width * desc->height * 4)) return cursor; + sapp_unbind_mouse_cursor_image(cursor); + XcursorImage* img = XcursorImageCreate(desc->width, desc->height); + if (!img) return cursor; + img->xhot = (XcursorDim)desc->cursor_hotspot_x; + img->yhot = (XcursorDim)desc->cursor_hotspot_y; + /* Xcursor wants BGRA */ + const size_t num_bytes = (size_t)(desc->width * desc->height) * 4; + for (size_t i = 0; i < num_bytes; i += 4) { + ((uint8_t*)img->pixels)[i + 0] = ((const uint8_t*)desc->pixels.ptr)[i + 2]; + ((uint8_t*)img->pixels)[i + 1] = ((const uint8_t*)desc->pixels.ptr)[i + 1]; + ((uint8_t*)img->pixels)[i + 2] = ((const uint8_t*)desc->pixels.ptr)[i + 0]; + ((uint8_t*)img->pixels)[i + 3] = ((const uint8_t*)desc->pixels.ptr)[i + 3]; + } + Cursor c = XcursorImageLoadCursor(_sapp_tc.display, img); + XcursorImageDestroy(img); + if (!c) return cursor; + _sapp_tc.custom_cursors[cursor] = c; + _sapp_tc.app.custom_cursor_bound[cursor] = true; + if (_sapp_tc.app.current_cursor == cursor) { + _sapp_tc_x11_apply_cursor(); + } + return cursor; +} + +void sapp_unbind_mouse_cursor_image(sapp_mouse_cursor cursor) { + if ((int)cursor < 0 || (int)cursor >= _SAPP_MOUSECURSOR_NUM) return; + if (!_sapp_tc.app.custom_cursor_bound[cursor]) return; + _sapp_tc.app.custom_cursor_bound[cursor] = false; + Cursor c = _sapp_tc.custom_cursors[cursor]; + _sapp_tc.custom_cursors[cursor] = 0; + if (_sapp_tc.app.current_cursor == cursor) { + _sapp_tc_x11_apply_cursor(); + } + if (c) XFreeCursor(_sapp_tc.display, c); +} + +void sapp_set_clipboard_string(const char* str) { + if (!_sapp_tc.app.clipboard_enabled || !_sapp_tc.app.clipboard || !str) return; + _sapp_tc_window_t* w = _sapp_tc.app.main; + if (!w) return; + if (strlen(str) >= (size_t)_sapp_tc.app.clipboard_size) { + return; /* string too big for the clipboard buffer */ + } + strncpy(_sapp_tc.app.clipboard, str, (size_t)_sapp_tc.app.clipboard_size - 1); + _sapp_tc.app.clipboard[_sapp_tc.app.clipboard_size - 1] = 0; + XSetSelectionOwner(_sapp_tc.display, _sapp_tc.CLIPBOARD, w->xwin, CurrentTime); + if (XGetSelectionOwner(_sapp_tc.display, _sapp_tc.CLIPBOARD) != w->xwin) { + _sapp_tc.app.clipboard[0] = 0; /* failed to become selection owner */ + } +} + +const char* sapp_get_clipboard_string(void) { + if (!_sapp_tc.app.clipboard_enabled || !_sapp_tc.app.clipboard) return ""; + _sapp_tc_window_t* w = _sapp_tc.app.main; + if (!w) return ""; + if (XGetSelectionOwner(_sapp_tc.display, _sapp_tc.CLIPBOARD) == w->xwin) { + /* we own the selection: skip the X round-trip */ + return _sapp_tc.app.clipboard; + } + _sapp_tc.app.clipboard[0] = 0; + const Atom incremental = XInternAtom(_sapp_tc.display, "INCR", False); + XConvertSelection(_sapp_tc.display, _sapp_tc.CLIPBOARD, _sapp_tc.UTF8_STRING, + _sapp_tc.SAPP_TC_SELECTION, w->xwin, CurrentTime); + XEvent event; + if (!_sapp_tc_x11_wait_for_event(w->xwin, SelectionNotify, 0.1, &event)) { + return _sapp_tc.app.clipboard; + } + if (event.xselection.property == None) { + return _sapp_tc.app.clipboard; + } + char* data = NULL; + Atom actual_type; + int actual_format; + unsigned long item_count, bytes_after; + const int ret = XGetWindowProperty(_sapp_tc.display, + event.xselection.requestor, event.xselection.property, + 0, LONG_MAX, True, _sapp_tc.UTF8_STRING, + &actual_type, &actual_format, &item_count, &bytes_after, + (unsigned char**)&data); + if ((ret != Success) || (data == NULL)) { + if (data) XFree(data); + return _sapp_tc.app.clipboard; + } + /* INCR (incremental transfer) is rejected: pastes beyond the buffer fail */ + if ((actual_type != incremental) && (item_count < (unsigned long)_sapp_tc.app.clipboard_size)) { + strncpy(_sapp_tc.app.clipboard, data, (size_t)_sapp_tc.app.clipboard_size - 1); + _sapp_tc.app.clipboard[_sapp_tc.app.clipboard_size - 1] = 0; + } + XFree(data); + return _sapp_tc.app.clipboard; +} + +void sapp_set_window_title(const char* str) { + if (!str) return; + strncpy(_sapp_tc.app.window_title, str, sizeof(_sapp_tc.app.window_title) - 1); + _sapp_tc.app.window_title[sizeof(_sapp_tc.app.window_title) - 1] = 0; + _sapp_tc_window_t* w = _sapp_tc.app.main; + if (w && w->xwin) { + _sapp_tc_x11_update_window_title(w, _sapp_tc.app.window_title); + } +} + +bool sapp_is_fullscreen(void) { + return _sapp_tc.app.fullscreen; +} + +void sapp_toggle_fullscreen(void) { + if (!_sapp_tc.app.main) return; + _sapp_tc_x11_set_fullscreen(!_sapp_tc.app.fullscreen); + _sapp_tc_x11_update_dimensions(_sapp_tc.app.main); +} + +int sapp_get_num_dropped_files(void) { + return _sapp_tc.app.drop_enabled ? _sapp_tc.app.drop_num_files : 0; +} + +const char* sapp_get_dropped_file_path(int index) { + if (!_sapp_tc.app.drop_enabled || !_sapp_tc.app.drop_buffer) return ""; + if (index < 0 || index >= _sapp_tc.app.drop_num_files) return ""; + return _sapp_tc.app.drop_buffer + (size_t)index * (size_t)_sapp_tc.app.drop_max_path_length; +} + +sapp_pixel_format sapp_color_format(void) { + return SAPP_PIXELFORMAT_RGBA8; /* plain 8-bit RGBA on GL (no 10-bit path) */ +} + +sapp_pixel_format sapp_depth_format(void) { + return SAPP_PIXELFORMAT_DEPTH_STENCIL; +} + +const void* sapp_x11_get_window(void) { + _sapp_tc_window_t* w = _sapp_tc.app.main; + return w ? (const void*)(uintptr_t)w->xwin : 0; +} + +const void* sapp_x11_get_display(void) { + return (const void*)_sapp_tc.display; +} + +/* Metal / D3D11 / win32 / macOS handles do not exist on Linux */ +const void* sapp_metal_get_device(void) { return 0; } +const void* sapp_metal_get_current_drawable(void) { return 0; } +const void* sapp_metal_get_depth_stencil_texture(void) { return 0; } +const void* sapp_metal_get_msaa_color_texture(void) { return 0; } +const void* sapp_macos_get_window(void) { return 0; } +const void* sapp_win32_get_hwnd(void) { return 0; } +const void* sapp_d3d11_get_device(void) { return 0; } +const void* sapp_d3d11_get_device_context(void) { return 0; } +const void* sapp_d3d11_get_swap_chain(void) { return 0; } + +sapp_environment sapp_get_environment(void) { + sapp_environment env; + memset(&env, 0, sizeof(env)); + env.defaults.color_format = sapp_color_format(); + env.defaults.depth_format = sapp_depth_format(); + env.defaults.sample_count = sapp_sample_count(); + /* GL has no device handle: the shared context is current on this thread */ + return env; +} + +sapp_swapchain sapp_get_swapchain(void) { + /* the GL swapchain contract is just the default-framebuffer id: rendering + targets whatever drawable is current (the main window's during its + frame_cb) -- calling this any number of times per frame is safe */ + sapp_swapchain sc; + memset(&sc, 0, sizeof(sc)); + _sapp_tc_window_t* w = _sapp_tc.app.main; + if (!w) return sc; + sc.width = sapp_width(); + sc.height = sapp_height(); + sc.sample_count = sapp_sample_count(); + sc.color_format = sapp_color_format(); + sc.depth_format = sapp_depth_format(); + sc.gl.framebuffer = _sapp_tc.gl_framebuffer; + return sc; +} + +} /* extern "C" */ #else /* other platforms */ -/*== stubs (explicit platform gap; the Linux/web/mobile ports replace these) */ +/*== stubs (explicit platform gap; the web/mobile ports replace these) ======*/ #if defined(__cplusplus) extern "C" { #endif @@ -3708,6 +6982,8 @@ const void* sapp_window_d3d11_resolve_view(sapp_window win) { (void)win; return const void* sapp_window_d3d11_depth_stencil_view(sapp_window win) { (void)win; return 0; } const void* sapp_window_macos_get_window(sapp_window win) { (void)win; return 0; } const void* sapp_window_win32_get_hwnd(sapp_window win) { (void)win; return 0; } +const void* sapp_window_x11_get_window(sapp_window win) { (void)win; return 0; } +uint32_t sapp_window_gl_framebuffer(sapp_window win) { (void)win; return 0; } #if defined(__cplusplus) } /* extern "C" */ #endif diff --git a/core/include/tc/app/tcWindow.h b/core/include/tc/app/tcWindow.h index e26f4be9..cc712a66 100644 --- a/core/include/tc/app/tcWindow.h +++ b/core/include/tc/app/tcWindow.h @@ -9,21 +9,22 @@ // and render state (WindowContext) and is driven by its display's own vsync // (macOS: one CADisplayLink per window, delivered on the main run loop; // Windows: one DXGI frame latency waitable object per swapchain, serviced by -// the message loop). Everything runs synchronously on the main thread; a -// fully occluded window simply skips rendering (fps 0), so it can never -// stall the others. +// the message loop; Linux: timer-paced ticks at the measured refresh rate, +// serviced by the X11 event loop). Everything runs synchronously on the main +// thread; a fully occluded window simply skips rendering (fps 0), so it can +// never stall the others. // // GPU resources (Texture / Fbo / Mesh / Font) live in the single shared // sokol_gfx context and can be used freely from any window. // -// Platform support: macOS and Windows. On other platforms createWindow() -// logs an error and returns nullptr. +// Platform support: macOS, Windows and Linux (X11). On other platforms +// createWindow() logs an error and returns nullptr. // // This file is included from TrussC.h (after Node / CoreEvents / WindowSettings). namespace trussc { -class TC_PLATFORMS("macos,windows") Window { +class TC_PLATFORMS("macos,windows,linux") Window { public: ~Window(); @@ -99,10 +100,10 @@ class TC_PLATFORMS("macos,windows") Window { Color clearColor_ = Color(0.05f, 0.05f, 0.08f, 1.0f); }; -// Create a secondary window. macOS and Windows (nullptr elsewhere). +// Create a secondary window. macOS, Windows and Linux (nullptr elsewhere). // The returned shared_ptr keeps the window alive; closing the window (user or // close()) destroys the native side while the handle stays valid (isOpen()). -TC_PLATFORMS("macos,windows") std::shared_ptr createWindow(const WindowSettings& settings = {}); +TC_PLATFORMS("macos,windows,linux") std::shared_ptr createWindow(const WindowSettings& settings = {}); inline Window::Window() { ctx_.isMain = false; @@ -135,9 +136,12 @@ inline void Window::setApp(std::shared_ptr app) { ctx_.rootNode = app_.get(); } -#if !defined(__APPLE__) && !defined(_WIN32) -// Stubs for platforms without window glue. The real implementations live in -// platform/mac/tcWindowMac.mm and platform/win/tcWindowWin.cpp. +#if !defined(__APPLE__) && !defined(_WIN32) && !(defined(__linux__) && defined(SOKOL_GLCORE)) +// Stubs for platforms without window glue (web / Android / Raspberry Pi). The +// real implementations live in platform/mac/tcWindowMac.mm, +// platform/win/tcWindowWin.cpp and platform/linux/tcWindowLinux.cpp (desktop +// Linux is SOKOL_GLCORE; Raspberry Pi builds the same sources with GLES3/EGL +// and keeps these stubs). // (iOS also defines __APPLE__ but has no window glue — iOS is not in the CI // build matrix; revisit if an iOS app ever links these symbols.) inline Window::~Window() {} @@ -146,7 +150,7 @@ inline void Window::setTitle(const std::string&) {} inline int Window::getWidth() const { return 0; } inline int Window::getHeight() const { return 0; } inline std::shared_ptr createWindow(const WindowSettings&) { - logError("Window") << "createWindow(): secondary windows are only supported on macOS and Windows for now"; + logError("Window") << "createWindow(): secondary windows are only supported on macOS, Windows and Linux for now"; return nullptr; } #endif diff --git a/core/platform/linux/sokol_impl.cpp b/core/platform/linux/sokol_impl.cpp index 0a76b000..ef26e143 100644 --- a/core/platform/linux/sokol_impl.cpp +++ b/core/platform/linux/sokol_impl.cpp @@ -1,8 +1,7 @@ // ============================================================================= -// sokol バックエンド実装 (Windows / Linux) +// sokol バックエンド実装 (Linux / Raspberry Pi) // ============================================================================= -#define SOKOL_IMPL #define SOKOL_NO_ENTRY // main() を自分で定義するため // Suppress warnings from third-party sokol headers. They have intentional @@ -14,8 +13,21 @@ # pragma GCC diagnostic ignored "-Wmissing-field-initializers" #endif +#if defined(SOKOL_GLCORE) +// Desktop Linux (X11 + GLX): sokol_app.h declarations only — the Linux +// implementation of the sapp_* API (main window, run loop, events) lives in +// sokol_app_tc.h below. +#include "sokol_app.h" +#define SOKOL_IMPL +#include "sokol_log.h" +#define SOKOL_APP_TC_IMPL +#include "sokol_app_tc.h" +#else +// Raspberry Pi (GLES3 / EGL): still on the upstream sokol_app.h implementation +#define SOKOL_IMPL #include "sokol_log.h" #include "sokol_app.h" +#endif #include "sokol_gfx.h" #include "sokol_glue.h" #include "util/sokol_gl_tc.h" diff --git a/core/platform/linux/tcWindowLinux.cpp b/core/platform/linux/tcWindowLinux.cpp new file mode 100644 index 00000000..778e59d2 --- /dev/null +++ b/core/platform/linux/tcWindowLinux.cpp @@ -0,0 +1,326 @@ +// ============================================================================= +// tcWindowLinux.cpp - TrussC adapter for secondary windows (Linux / X11) +// ============================================================================= +// The native windowing layer (X11 window + shared GLX context with one +// GLXWindow drawable per window, timer-paced ticks at the measured refresh +// rate, sapp_event conversion with the full sokol_app XKB keytable) lives in +// sokol/sokol_app_tc.h and knows nothing about TrussC. This file maps its C +// API onto TrussC: +// tick_cb -> WindowContext switch + per-window dt + update/draw the tree +// event_cb -> CoreEvents + App hooks + Node-tree dispatch (same keycode +// semantics as the main window: key == SAPP_KEYCODE_*) +// close_cb -> Window::close() +// All rendering shares the one sokol_gfx context; each window gets its own +// sokol_gl context (same pattern as Fbo). Mirror of platform/win/tcWindowWin.cpp. + +#include "TrussC.h" + +#if defined(__linux__) && defined(SOKOL_GLCORE) && !defined(__ANDROID__) +#include "sokol_app_tc.h" // declarations only (impl lives in sokol_impl.cpp) + +using namespace trussc; + +namespace { + +struct AdapterState { + sapp_window win{0}; +}; + +// Swapchain provider handed to WindowContext::acquireSwapchain. On GL the +// "swapchain" is just the default framebuffer of whatever drawable is +// current -- which is this window's during its tick. +sg_swapchain acquireSecondarySwapchain(void* user) { + auto* st = static_cast(user); + sg_swapchain sc = {}; + if (!st) return sc; + const int fbw = sapp_window_framebuffer_width(st->win); + const int fbh = sapp_window_framebuffer_height(st->win); + if (fbw <= 0 || fbh <= 0) return sc; + sc.width = fbw; + sc.height = fbh; + sc.sample_count = sapp_window_sample_count(st->win); + sc.color_format = SG_PIXELFORMAT_RGBA8; + sc.depth_format = SG_PIXELFORMAT_DEPTH_STENCIL; + sc.gl.framebuffer = sapp_window_gl_framebuffer(st->win); + return sc; +} + +// --- tick: drive this window's update/draw with its context active --------- +void windowTick(sapp_window swin, void* user) { + Window* win = static_cast(user); + if (!win) return; + auto& ctx = win->context(); + + const int fbw = sapp_window_framebuffer_width(swin); + const int fbh = sapp_window_framebuffer_height(swin); + if (fbw <= 0 || fbh <= 0) return; + ctx.fbWidth = fbw; + ctx.fbHeight = fbh; + ctx.dpiScale = sapp_window_dpi_scale(swin); + // Keep a RectNode root in sync with the window's logical size (same + // contract as the main App on SAPP_EVENTTYPE_RESIZED). + win->syncRootSize((float)sapp_window_width(swin), (float)sapp_window_height(swin)); + + // Per-window delta time: wall-clock between THIS window's processed + // ticks, so getDeltaTime() inside this window's update/draw is correct + // even when its display runs at a different refresh rate than the main + // window's. A long gap (occlusion) yields one large delta, exactly like + // an event-driven main window. + { + auto callNow = std::chrono::high_resolution_clock::now(); + if (!ctx.lastUpdateCallTimeInitialized) { + ctx.lastUpdateCallTimeInitialized = true; + ctx.updateDeltaTime = sapp_window_frame_duration(swin); + } else { + ctx.updateDeltaTime = std::chrono::duration(callNow - ctx.lastUpdateCallTime).count(); + } + ctx.lastUpdateCallTime = callNow; + } + + auto* prev = internal::currentWindowCtx; + internal::currentWindowCtx = &ctx; + + // Own sokol_gl context, created lazily on the first tick (sgl is set up + // by then). Same lifecycle caveats as Fbo contexts on sgl resize. + if (ctx.swapchainTarget.context.id == SG_INVALID_ID) { + sgl_context_desc_t cdesc = {}; + cdesc.max_vertices = 65536; + cdesc.max_commands = 16384; + cdesc.color_format = SG_PIXELFORMAT_RGBA8; + cdesc.depth_format = SG_PIXELFORMAT_DEPTH_STENCIL; + cdesc.sample_count = sapp_window_sample_count(swin); + ctx.swapchainTarget.context = sgl_make_context(&cdesc); + } + sgl_set_context(ctx.swapchainTarget.context); + sgl_defaults(); + + beginFrame(); + + win->events().update.notify(); + win->tickTree(); + + const Color& cc = win->clearColor_; + clear(cc.r, cc.g, cc.b, cc.a); + win->events().draw.notify(); + win->drawTreeNow(); + + present(); + win->events().afterFrame.notify(); + + sgl_set_context(sgl_default_context()); + internal::currentWindowCtx = prev; +} + +// --- events: map sapp_event onto CoreEvents / App hooks / tree dispatch ---- +void windowEvent(const sapp_event* ev, sapp_window swin, void* user) { + Window* win = static_cast(user); + if (!win) return; + auto& ctx = win->context(); + auto* prev = internal::currentWindowCtx; + internal::currentWindowCtx = &ctx; + + // Raw event pass-through (same hook addons use on the main window) + win->events().rawEvent.notify(*ev); + + // sapp coords arrive in framebuffer pixels; TrussC windows use logical + // points (secondary windows have no pixelPerfect mode for now) + const float dpi = sapp_window_dpi_scale(swin); + const float scale = (dpi > 0.0f) ? (1.0f / dpi) : 1.0f; + const bool hasShift = (ev->modifiers & SAPP_MODIFIER_SHIFT) != 0; + const bool hasCtrl = (ev->modifiers & SAPP_MODIFIER_CTRL) != 0; + const bool hasAlt = (ev->modifiers & SAPP_MODIFIER_ALT) != 0; + const bool hasSuper = (ev->modifiers & SAPP_MODIFIER_SUPER) != 0; + + switch (ev->type) { + case SAPP_EVENTTYPE_MOUSE_DOWN: { + const float x = ev->mouse_x * scale, y = ev->mouse_y * scale; + ctx.mouseX = x; ctx.mouseY = y; + ctx.mouseButton = (int)ev->mouse_button; + ctx.mousePressed = true; + MouseEventArgs e; + e.pos = e.globalPos = Vec2(x, y); + e.button = (int)ev->mouse_button; + e.shift = hasShift; e.ctrl = hasCtrl; e.alt = hasAlt; e.super = hasSuper; + e.syncLegacy(); + win->events().mousePressed.notify(e); + if (win->getApp()) win->getApp()->mousePressed(e); + win->dispatchMousePressToTree(e); + break; + } + case SAPP_EVENTTYPE_MOUSE_UP: { + const float x = ev->mouse_x * scale, y = ev->mouse_y * scale; + ctx.mouseX = x; ctx.mouseY = y; + ctx.mouseButton = -1; + ctx.mousePressed = false; + MouseEventArgs e; + e.pos = e.globalPos = Vec2(x, y); + e.button = (int)ev->mouse_button; + e.shift = hasShift; e.ctrl = hasCtrl; e.alt = hasAlt; e.super = hasSuper; + e.syncLegacy(); + win->events().mouseReleased.notify(e); + if (win->getApp()) win->getApp()->mouseReleased(e); + win->dispatchMouseReleaseToTree(e); + break; + } + case SAPP_EVENTTYPE_MOUSE_ENTER: + case SAPP_EVENTTYPE_MOUSE_MOVE: { + const float x = ev->mouse_x * scale, y = ev->mouse_y * scale; + ctx.pmouseX = ctx.mouseX; ctx.pmouseY = ctx.mouseY; + ctx.mouseX = x; ctx.mouseY = y; + internal::MouseEventRaw raw; + raw.pos = raw.globalPos = Vec2(x, y); + raw.delta = raw.globalDelta = Vec2(ev->mouse_dx * scale, ev->mouse_dy * scale); + raw.shift = hasShift; raw.ctrl = hasCtrl; raw.alt = hasAlt; raw.super = hasSuper; + if (ctx.mousePressed && ctx.mouseButton >= 0) { + raw.button = ctx.mouseButton; + MouseDragEventArgs e = internal::toDragArgs(raw); + win->events().mouseDragged.notify(e); + if (win->getApp()) win->getApp()->mouseDragged(e); + } else { + MouseMoveEventArgs e = internal::toMoveArgs(raw); + win->events().mouseMoved.notify(e); + if (win->getApp()) win->getApp()->mouseMoved(e); + } + break; + } + case SAPP_EVENTTYPE_MOUSE_LEAVE: { + // Park the cursor offscreen so the next tick's updateHoverState + // clears this window's hover (hoveredNode lives in ITS context). + ctx.pmouseX = ctx.mouseX; ctx.pmouseY = ctx.mouseY; + ctx.mouseX = -1.0f; ctx.mouseY = -1.0f; + break; + } + case SAPP_EVENTTYPE_MOUSE_SCROLL: { + ScrollEventArgs e; + e.pos = e.globalPos = Vec2(ctx.mouseX, ctx.mouseY); + e.scroll = Vec2(ev->scroll_x, ev->scroll_y); + e.shift = hasShift; e.ctrl = hasCtrl; e.alt = hasAlt; e.super = hasSuper; + e.syncLegacy(); + win->events().mouseScrolled.notify(e); + if (win->getApp()) win->getApp()->mouseScrolled(e); + break; + } + case SAPP_EVENTTYPE_KEY_DOWN: { + KeyEventArgs e; + e.key = ev->key_code; // SAPP_KEYCODE_*, identical to the main window + e.isRepeat = ev->key_repeat; + e.shift = hasShift; e.ctrl = hasCtrl; e.alt = hasAlt; e.super = hasSuper; + if (!ev->key_repeat) ctx.keysPressed.insert(e.key); + win->events().keyPressed.notify(e); + if (win->getApp()) win->getApp()->keyPressed(e); + break; + } + case SAPP_EVENTTYPE_KEY_UP: { + KeyEventArgs e; + e.key = ev->key_code; + e.isRepeat = false; + e.shift = hasShift; e.ctrl = hasCtrl; e.alt = hasAlt; e.super = hasSuper; + ctx.keysPressed.erase(e.key); + win->events().keyReleased.notify(e); + if (win->getApp()) win->getApp()->keyReleased(e); + break; + } + case SAPP_EVENTTYPE_SUSPENDED: + logNotice("Window") << "second window occluded - skipping frames (no stall)"; + break; + case SAPP_EVENTTYPE_RESUMED: + logNotice("Window") << "second window visible again - resuming ticks"; + break; + default: + // CHAR / RESIZED / FOCUSED / UNFOCUSED: rawEvent carries them; + // size sync happens per tick. + break; + } + + internal::currentWindowCtx = prev; +} + +void windowClosed(sapp_window swin, void* user) { + (void)swin; + Window* win = static_cast(user); + if (win) win->close(); // tears down native + app state +} + +} // namespace + +// --------------------------------------------------------------------------- +// Window methods (Linux implementations) +// --------------------------------------------------------------------------- +namespace trussc { + +Window::~Window() { close(); } + +void Window::close() { + auto* st = static_cast(native_); + if (!st) return; + native_ = nullptr; // re-entrancy guard (close_cb) + ctx_.acquireSwapchain = nullptr; + ctx_.acquireSwapchainUser = nullptr; + sapp_destroy_window(st->win); + if (ctx_.swapchainTarget.context.id != SG_INVALID_ID) { + sgl_destroy_context(ctx_.swapchainTarget.context); + ctx_.swapchainTarget.context.id = SG_INVALID_ID; + ctx_.swapchainTarget.cache.clear(); + } + delete st; + if (app_) { + app_->exit(); + app_->cleanup(); + internal::attachedApps.erase(app_.get()); + app_.reset(); + ctx_.rootNode = nullptr; + } +} + +void Window::setTitle(const std::string& title) { + auto* st = static_cast(native_); + if (st) sapp_window_set_title(st->win, title.c_str()); +} + +int Window::getWidth() const { + auto* st = static_cast(native_); + return st ? sapp_window_width(st->win) : 0; +} + +int Window::getHeight() const { + auto* st = static_cast(native_); + return st ? sapp_window_height(st->win) : 0; +} + +std::shared_ptr createWindow(const WindowSettings& settings) { + if (headless::isActive()) { + logError("Window") << "createWindow(): not available in headless mode"; + return nullptr; + } + auto win = std::shared_ptr(new Window()); + auto* st = new AdapterState(); + + sapp_window_desc d = {}; + d.x = -1; + d.y = -1; + d.width = settings.width; + d.height = settings.height; + d.title = settings.title.c_str(); + d.borderless = !settings.decorated; + d.no_high_dpi = !settings.highDpi; + d.sample_count = settings.sampleCount; // NOTE: forced to the shared fbconfig's count on GLX + d.tick_cb = &windowTick; + d.event_cb = &windowEvent; + d.close_cb = &windowClosed; + d.user_data = win.get(); + st->win = sapp_create_window(&d); + if (st->win.id == 0) { + delete st; + logError("Window") << "createWindow(): native window creation failed"; + return nullptr; + } + + win->native_ = st; + win->ctx_.acquireSwapchain = &acquireSecondarySwapchain; + win->ctx_.acquireSwapchainUser = st; + return win; +} + +} // namespace trussc + +#endif // __linux__ && SOKOL_GLCORE && !__ANDROID__ From 7a3e152648223184f9ac5cdd231cb0009ebcfb1a Mon Sep 17 00:00:00 2001 From: tettou771 Date: Mon, 13 Jul 2026 19:00:07 +0900 Subject: [PATCH 38/87] doc: update multiWindowExample platform note (macOS/Windows/Linux) --- examples/windowing/multiWindowExample/src/tcApp.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/windowing/multiWindowExample/src/tcApp.cpp b/examples/windowing/multiWindowExample/src/tcApp.cpp index 2c450440..87bb6f96 100644 --- a/examples/windowing/multiWindowExample/src/tcApp.cpp +++ b/examples/windowing/multiWindowExample/src/tcApp.cpp @@ -1,11 +1,11 @@ // ============================================================================= // tcApp.cpp - Multi-window example // ============================================================================= -// Press W to open a second window. It runs on its own display link (its +// Press W to open a second window. It runs on its own vsync tick (its // display's refresh rate), has its own Node tree, events and mouse, and can // draw GPU resources from the main window directly (shared sokol_gfx context). // Closing the second window leaves the main window running. -// Secondary windows are macOS-only for now; on other platforms W logs an error. +// Secondary windows: macOS / Windows / Linux; elsewhere W logs an error. #include "tcApp.h" From d6c3d0cdeacff17db0a42cc5831b9f39e05e6b1e Mon Sep 17 00:00:00 2001 From: tettou771 Date: Mon, 13 Jul 2026 19:27:31 +0900 Subject: [PATCH 39/87] fix: define GL_GLEXT_PROTOTYPES before the first GL include on Linux glext.h is include-guarded, so sokol_gfx.h's own GL_GLEXT_PROTOTYPES came too late once sokol_app_tc.h had already pulled in without it (upstream sokol_app.h's Linux impl defines it the same way). --- core/include/sokol/sokol_app_tc.h | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/core/include/sokol/sokol_app_tc.h b/core/include/sokol/sokol_app_tc.h index 6fe6e023..b3132332 100644 --- a/core/include/sokol/sokol_app_tc.h +++ b/core/include/sokol/sokol_app_tc.h @@ -3730,6 +3730,13 @@ sapp_swapchain sapp_get_swapchain(void) { #include #include #include +/* GL_GLEXT_PROTOTYPES must be defined before the FIRST include of + in the TU: glext.h is include-guarded, so sokol_gfx.h's own define comes + too late once this header has pulled the GL headers in (upstream + sokol_app.h's Linux impl defines it the same way) */ +#ifndef GL_GLEXT_PROTOTYPES +#define GL_GLEXT_PROTOTYPES +#endif #include #include #include From 5956ddfe90e363cf314aa5ae414319464c5b5bc2 Mon Sep 17 00:00:00 2001 From: tettou771 Date: Mon, 13 Jul 2026 21:01:15 +0900 Subject: [PATCH 40/87] doc: record P2 Linux driver as-built (self-healing swap pacing, EMA refresh estimate, GL_GLEXT_PROTOTYPES gotcha) --- docs/dev/sokol_app_tc-design.md | 69 ++++++++++++++++++++++----------- 1 file changed, 47 insertions(+), 22 deletions(-) diff --git a/docs/dev/sokol_app_tc-design.md b/docs/dev/sokol_app_tc-design.md index 87e2317a..446f1d2d 100644 --- a/docs/dev/sokol_app_tc-design.md +++ b/docs/dev/sokol_app_tc-design.md @@ -190,33 +190,58 @@ can feed events manually, so imgui does not chain us to sokol_app. hardware 2026-07-13: ~60fps, CPU <1%, resize/fullscreen/minimize/ second-window/clean-exit/drag-render all pass. - **P2 — Linux driver.** X11 + GLX shared context (multiwindow-glfw - pattern), timer-paced ticks. - - **KICKOFF READY — implementation contract: docs/dev/sapp-x11-impl-spec.md** - (opus-extracted). Two original design assumptions were WRONG and the - plan is corrected as follows: + pattern). + - **DONE — implemented 2026-07-13 (dd874ffa + d6c3d0cd), full CI green + (run 29242819960 incl. core tests + HotReload on Linux); real-hardware + runtime verification delegated to the linux-server agent (relay topic + multiwindow, physical X11 session, mesa/Intel + NVIDIA PRIME).** + Implementation contract: docs/dev/sapp-x11-impl-spec.md + (opus-extracted). Desktop Linux only — platform/linux/sokol_impl.cpp + switches on SOKOL_GLCORE; Raspberry Pi (SOKOL_GLES3/EGL) stays on + upstream sokol_app.h. TrussC adapter: platform/linux/tcWindowLinux.cpp + (mirror of tcWindowWin.cpp; GL swapchain = default framebuffer, RGBA8). + Two original design assumptions were WRONG; as-built model: 1. Upstream has NO timer/RandR pacing to lift — it paces purely by blocking inside a vsync'd glXSwapBuffers. And GLX DOES have per-drawable swap intervals (glXSwapIntervalEXT), just no vsync - callback. Corrected tick model: the MAIN window keeps upstream - parity (blocking vsync'd swap = the loop's pacing source); - SECONDARY windows use swap interval 0 + timer due-checks inside - the same loop (RandR-queried refresh as the period). Never vsync - more than one drawable — N blocking swaps per iteration would - serialize to refresh/N fps. Mixed-Hz fidelity is therefore coarser - than mac/win (secondary jitter ≤ one main-frame); accepted, - documented. + callback. As-built tick model: the MAIN window keeps upstream + parity (blocking vsync'd swap = the loop's pacing source), with a + self-healing guard — if the swap returns in <2ms (iconified, + skip_present, MESA context-wide interval-0 fallback, unredirected + window) the tick falls back to timer pacing, so busy-spin is + impossible; SECONDARY windows use swap interval 0 + absolute- + schedule timer due-checks inside the same loop. The period is NOT + RandR (unlinked lib) but a refresh estimate EMA-measured from the + main window's blocking presents (1/60 fallback, clamped to + [1/240, 1/30]). Never vsync more than one drawable — N blocking + swaps per iteration would serialize to refresh/N fps. When nothing + is due the loop sleeps in poll() on the X connection fd. Mixed-Hz + fidelity is therefore coarser than mac/win (secondary jitter ≤ one + main-frame); accepted, documented. 2. There is NO XIM/XIC upstream: CHAR events are XLookupString + a - static keysym→unicode table, no compose/dead keys/IME. Port as-is + static keysym→unicode table, no compose/dead keys/IME. Ported as-is (parity), IME stays tcxIME territory. - Other load-bearing facts: GL swapchain is RGBA8 (the 10-bit patch does - not apply on GLX); resize is polled per frame via XGetWindowAttributes - (no ConfigureNotify handler) — poll per window per tick; keytable uses - XKB physical key names (XkbGetNames) with software repeat tracking that - REQUIRES XkbSetDetectableAutoRepeat at startup; keep the public - sapp_x11_get_window()/sapp_x11_get_display() getters (tcPlatform_linux - applies Motif decoration hints itself); shared GLXContext + per-window - GLXWindow/Colormap, glXMakeCurrent(display, drawable, ctx) before each - window's render. + Other as-built facts: GL swapchain is RGBA8 (the 10-bit patch does + not apply on GLX); one fbconfig for the whole app — secondary windows + inherit the main's MSAA sample count (shared-context visual + compatibility); resize is polled per window per tick via + XGetWindowAttributes (no ConfigureNotify handler, upstream parity); + keytable uses XKB physical key names (XkbGetNames) with per-window + software repeat tracking + XkbSetDetectableAutoRepeat at startup; + public sapp_x11_get_window()/sapp_x11_get_display() getters kept + (tcPlatform_linux applies Motif decoration hints itself), plus new + cross-platform sapp_window_gl_framebuffer() and + sapp_window_x11_get_window(); shared GLXContext + per-window + GLXWindow/Colormap, glXMakeCurrent(display, drawable, ctx) before + each window's render; GLX entry points called directly (libGL always + linked on Linux; only extensions via glXGetProcAddressARB — no + dlopen); XDND v5 advertised on every window with the drop target + resolved per-window; quit = WM_DELETE → loop-tail cancellable dance. + Port gotcha for P3+: GL_GLEXT_PROTOTYPES must be defined before the + FIRST include in the TU — glext.h is include-guarded, so + sokol_gfx.h's own define comes too late (first CI round failed on + exactly this; caught locally beforehand via XQuartz pseudo-Linux + -fsyntax-only, which is a usable pre-CI smoke check on the mac). - **P3 — web driver.** RAF tick, HTML5 event callbacks, WebGL context; port the canvas-keyboard patch natively. - **P4 — iOS driver.** UIWindow + CAMetalLayer + CADisplayLink + touch. From b768b53c2d36db4cfeb0041fd14ef340b508b7d5 Mon Sep 17 00:00:00 2001 From: tettou771 Date: Mon, 13 Jul 2026 22:54:30 +0900 Subject: [PATCH 41/87] doc: record P2 Linux runtime verification (real hardware, all checks pass) --- docs/dev/sokol_app_tc-design.md | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/docs/dev/sokol_app_tc-design.md b/docs/dev/sokol_app_tc-design.md index 446f1d2d..8294c88e 100644 --- a/docs/dev/sokol_app_tc-design.md +++ b/docs/dev/sokol_app_tc-design.md @@ -192,9 +192,19 @@ can feed events manually, so imgui does not chain us to sokol_app. - **P2 — Linux driver.** X11 + GLX shared context (multiwindow-glfw pattern). - **DONE — implemented 2026-07-13 (dd874ffa + d6c3d0cd), full CI green - (run 29242819960 incl. core tests + HotReload on Linux); real-hardware - runtime verification delegated to the linux-server agent (relay topic - multiwindow, physical X11 session, mesa/Intel + NVIDIA PRIME).** + (run 29242819960 incl. core tests + HotReload on Linux), runtime + VERIFIED on real hardware 2026-07-13 (linux-server: Ubuntu 24.04, + Xorg+mutter, mesa Intel UHD730 + NVIDIA PRIME 580): 60.00fps stable, + CPU 4-5% of one core, second window + independent events + shared FBO, + resize both windows, WM_DELETE secondary → main survives, minimize → + swap stops but update keeps running at 1-3% CPU, clean exit 0 ×6, and + with vblank_mode=0 (vsync forced off) the self-healing timer pacing + holds 57fps at 3% CPU instead of busy-spinning. Known cosmetic quirk: + ~1s of fps overshoot (≤81fps) right after destroying a secondary + window while the refresh-estimate EMA re-settles. Keyboard keycodes + are physical-position based on ALL platforms (mac virtual-keycode / + win scancode / linux XKB name tables — sokol upstream semantics); + layout-dependent characters ride the CHAR event.** Implementation contract: docs/dev/sapp-x11-impl-spec.md (opus-extracted). Desktop Linux only — platform/linux/sokol_impl.cpp switches on SOKOL_GLCORE; Raspberry Pi (SOKOL_GLES3/EGL) stays on From 2bfaf1a53d530fb0dba060e867ea97c09db6d2db Mon Sep 17 00:00:00 2001 From: tettou771 Date: Mon, 13 Jul 2026 23:48:13 +0900 Subject: [PATCH 42/87] feat: implement web (emscripten) driver in sokol_app_tc.h (P3) The header now implements the sapp_* API on the web for both graphics backends (SOKOL_WGPU via emdawnwebgpu, SOKOL_GLES3/WebGL2), replacing upstream sokol_app.h's emscripten implementation: - rAF frame loop (sapp_run registers the callback and returns; quit tears down inside the callback), browser-clock timing - full HTML5 event set: mouse/wheel/touch/focus/fullscreen, code-string keymap, pointer-lock deferred to user gestures, clipboard (execCommand copy + paste event), drag&drop with FileReader fetch, CSS + custom BMP cursors, resize tracking (CSS size x dpr) - the TrussC keyboard-on-canvas patch is native default behavior (key handlers on the canvas selector, not the window) - WGPU async adapter/device/swapchain chain gated by init_done, wgpuInstanceProcessEvents pumped every frame; GLES3 swapchain = default framebuffer - web is single-window: sapp_create_window() returns the invalid handle and logs (explicit platform gap) - platform/web/sokol_impl.cpp switches to the declarations-only shim (same pattern as mac/win/linux) - deviation: no default-icon generator (sokol_default icon = no-op) Implementation contract: docs/dev/sapp-web-impl-spec.md. Runtime-verified in Chrome (render, input, resize, both backends). --- core/include/sokol/sokol_app_tc.h | 2908 ++++++++++++++++++++++++++++- core/platform/web/sokol_impl.cpp | 13 +- docs/dev/sapp-web-impl-spec.md | 965 ++++++++++ docs/dev/sokol_app_tc-design.md | 32 + 4 files changed, 3913 insertions(+), 5 deletions(-) create mode 100644 docs/dev/sapp-web-impl-spec.md diff --git a/core/include/sokol/sokol_app_tc.h b/core/include/sokol/sokol_app_tc.h index b3132332..9206798c 100644 --- a/core/include/sokol/sokol_app_tc.h +++ b/core/include/sokol/sokol_app_tc.h @@ -101,7 +101,10 @@ Linux: implemented (X11 + GLX, one shared GL context with a GLXWindow drawable per window; main window paced by its blocking vsync'd glXSwapBuffers, secondary windows swap interval 0 + timer-paced). - Others (web/mobile): stubs that return an invalid handle (explicit + Web: implemented, single-window (one , rAF frame loop, WebGPU or + WebGL2; sapp_create_window() returns the invalid handle -- a second + window is not representable in a browser tab). + Others (mobile): stubs that return an invalid handle (explicit platform gap). */ #define SOKOL_APP_TC_INCLUDED (1) @@ -6964,8 +6967,2909 @@ sapp_swapchain sapp_get_swapchain(void) { } } /* extern "C" */ +#elif defined(__EMSCRIPTEN__) +/*== Emscripten (web) ======================================================= + Implements the public sokol_app.h API on the web plus the multi-window + API (as stubs -- the whole backend binds to ONE element, so a + second window is not representable; sapp_create_window() returns the + invalid handle and logs). + + Unlike every native backend there is NO owned loop: sapp_run() sets + everything up, hands a per-frame callback to + emscripten_request_animation_frame_loop() (or emscripten_set_main_loop()) + and RETURNS IMMEDIATELY. Rendering, events and the quit dance all happen + later inside browser-driven callbacks; "quit" stops the rAF loop and + tears down sokol state -- the tab and the wasm module stay alive. + + Structure mirrors sokol_app.h's emscripten backend (HTML5 event + callbacks on the canvas selector, code-string keymap, deferred + pointer-lock on user gesture, execCommand copy + paste-event clipboard, + dragndrop with FileReader fetch, CSS cursors + BMP-in-JS custom cursors, + favicon icon, resize tracking via CSS size x devicePixelRatio). Both + graphics backends are supported: SOKOL_WGPU (async adapter/device chain + gated by init_done, browser auto-presents the surface at rAF boundaries) + and SOKOL_GLES3 (WebGL2 context, swapchain = default framebuffer). + + The TrussC keyboard-on-canvas patch is native here: key handlers are + registered on the canvas selector (NOT the window), so other page + elements (e.g. an editor besides the canvas) receive keyboard input + independently. The canvas element needs a tabindex attribute and focus + for keyboard events to arrive (the TrussC web shell provides both). */ +#if defined(SOKOL_WGPU) +#include /* emdawnwebgpu (modern Dawn API), NOT -sUSE_WEBGPU */ +#elif defined(SOKOL_GLES3) +#include +#else +#error "sokol_app_tc.h: web build requires SOKOL_WGPU or SOKOL_GLES3" +#endif +#include +#include +#include + +#if defined(SOKOL_APP_IMPL_INCLUDED) +#error "sokol_app_tc.h owns the web implementation of the sapp_* API; include sokol_app.h WITHOUT an implementation define in this TU" +#endif + +#ifndef SOKOL_ASSERT +#include +#define SOKOL_ASSERT(c) assert(c) +#endif +#ifndef _SOKOL_PRIVATE +#define _SOKOL_PRIVATE static +#endif +#ifndef _SOKOL_UNUSED +#define _SOKOL_UNUSED(x) (void)(x) +#endif + +/* the lifted upstream code keys web-only forks on this */ +#define _SAPP_EMSCRIPTEN (1) +#if defined(SOKOL_GLES3) +#define _SAPP_ANY_GL (1) +#endif +/* NOTE: _SAPP_WGPU_HAS_WAIT stays UNDEFINED on web -- the WGPU + adapter/device/swapchain chain is async-callback driven */ + +#define _SAPP_MAX_TITLE_LENGTH (128) +#define _SAPP_FALLBACK_DEFAULT_WINDOW_WIDTH (640) +#define _SAPP_FALLBACK_DEFAULT_WINDOW_HEIGHT (480) +#define _sapp_tc_def(val, def) (((val) == 0) ? (def) : (val)) +#if defined(__cplusplus) +#define _SAPP_STRUCT(TYPE, NAME) TYPE NAME = {} +#define _SAPP_CLEAR_ARC_STRUCT(type, item) { item = type(); } +#else +#define _SAPP_STRUCT(TYPE, NAME) TYPE NAME = {0} +#define _SAPP_CLEAR_ARC_STRUCT(type, item) { _sapp_tc_clear(&item, sizeof(item)); } +#endif + +/* controlled failure instead of sokol_app.h's log-item machinery */ +#define _SAPP_PANIC(code) do { fprintf(stderr, "sokol_app_tc.h: panic: " #code "\n"); abort(); } while (0) +#define _SAPP_ERROR(code) fprintf(stderr, "sokol_app_tc.h: error: " #code "\n") +#define _SAPP_ERROR_MSG(code, msg) fprintf(stderr, "sokol_app_tc.h: error: " #code ": %s\n", msg) +#define _SAPP_WARN_MSG(code, msg) fprintf(stderr, "sokol_app_tc.h: warn: " #code ": %s\n", msg) +#define _SAPP_INFO_MSG(code, msg) fprintf(stderr, "sokol_app_tc.h: info: " #code ": %s\n", msg) + +/* the internal monotonic clock is deliberately unused on web: all timing + comes from the timestamp the browser hands to the frame callback */ +typedef struct { int _dummy; } _sapp_tc_timestamp_t; +_SOKOL_PRIVATE void _sapp_tc_timestamp_init(_sapp_tc_timestamp_t* ts) { _SOKOL_UNUSED(ts); } +_SOKOL_PRIVATE double _sapp_tc_timestamp_now(_sapp_tc_timestamp_t* ts) { _SOKOL_UNUSED(ts); SOKOL_ASSERT(false); return 0.0; } +#ifndef SOKOL_API_IMPL +#define SOKOL_API_IMPL +#endif +#ifndef SOKOL_UNREACHABLE +#define SOKOL_UNREACHABLE SOKOL_ASSERT(false) +#endif + +typedef struct { + _sapp_tc_timestamp_t timestamp; + double dt_min; // config: min clamp value for unfiltered time delta (seconds) + double dt_max; // config: max clamp value for unfiltered time delta (seconds) + double dt_threshold; // config: threshold time delta for 'resetting' filtering (default: 0.004s, 4ms) + double alpha; // config: smoothing constant, lower values smoother, higher values faster response + double last; // last absolute time in seconds + double dt; // unfiltered frame delta in seconds, clamped to dt_min/dt_max + double ema; // intermediate ema-filter result + double smooth_dt; // smoothed frame delta in seconds +} _sapp_tc_timing_t; + +_SOKOL_PRIVATE void _sapp_tc_timing_init(_sapp_tc_timing_t* t) { + _sapp_tc_timestamp_init(&t->timestamp); + t->dt_min = 0.000001; // 1 us + t->dt_max = 0.1; // 100 ms + t->dt_threshold = 0.004; // 4ms + t->alpha = 0.025; + t->dt = 1.0 / 60.0; // a 'likely' non-null value + t->ema = t->dt; + t->smooth_dt = t->dt; +} + +_SOKOL_PRIVATE double _sapp_tc_timing_clamp(_sapp_tc_timing_t* t, double dt) { + SOKOL_ASSERT((t->dt_min > 0.0) && (t->dt_max > 0.0) && (t->dt_max >= t->dt_min)); + if (dt < t->dt_min) { + return t->dt_min; + } else if (dt > t->dt_max) { + return t->dt_max; + } else { + return dt; + } +} + +_SOKOL_PRIVATE void _sapp_tc_timing_delta(_sapp_tc_timing_t* t, double dt) { + // first clamp raw dt against min/max (min avoid division by zero, max + // may avoids glitches and 'death-spirals' during debugging + dt = _sapp_tc_timing_clamp(t, dt); + t->dt = dt; + const double error = fabs(dt - t->smooth_dt); + if (error > t->dt_threshold) { + // 'reset' filter when new delta is outside threshold + t->ema = dt; + t->smooth_dt = dt; + } else { + // simple ema-filter with fixed alpha + t->ema = t->ema + t->alpha * (dt - t->ema); + t->smooth_dt = _sapp_tc_timing_clamp(t, t->ema); + } +} + +_SOKOL_PRIVATE void _sapp_tc_timing_update(_sapp_tc_timing_t* t, double external_now) { + double now; + if (external_now == 0.0) { + now = _sapp_tc_timestamp_now(&t->timestamp); + } else { + now = external_now; + } + if (t->last > 0.0) { + double dt = now - t->last; + _sapp_tc_timing_delta(t, dt); + } + t->last = now; + +} + +_SOKOL_PRIVATE double _sapp_tc_timing_get(_sapp_tc_timing_t* t) { + return t->smooth_dt; +} + +// ███████ ████████ ██████ ██ ██ ██████ ████████ ███████ +// ██ ██ ██ ██ ██ ██ ██ ██ ██ +// ███████ ██ ██████ ██ ██ ██ ██ ███████ +// ██ ██ ██ ██ ██ ██ ██ ██ ██ +// ███████ ██ ██ ██ ██████ ██████ ██ ███████ + +#if defined(SOKOL_WGPU) +typedef struct { + WGPUInstance instance; + WGPUAdapter adapter; + WGPUDevice device; + WGPUSurface surface; + WGPUTextureFormat render_format; + WGPUTexture msaa_tex; + WGPUTextureView msaa_view; + WGPUTexture depth_stencil_tex; + WGPUTextureView depth_stencil_view; + WGPUTextureView swapchain_view; + bool init_done; +} _sapp_tc_wgpu_t; +#endif + +#if defined(_SAPP_EMSCRIPTEN) + +typedef struct { + bool mouse_lock_requested; + uint16_t mouse_buttons; +} _sapp_tc_emsc_t; +#endif // _SAPP_EMSCRIPTEN + +#if defined(_SAPP_ANY_GL) +typedef struct { + uint32_t framebuffer; +} _sapp_tc_gl_t; +#endif + +typedef struct { + bool enabled; + int buf_size; + char* buffer; +} _sapp_tc_clipboard_t; + +typedef struct { + bool enabled; + int max_files; + int max_path_length; + int num_files; + int buf_size; + char* buffer; +} _sapp_tc_drop_t; + +typedef struct { + float x, y; + float dx, dy; + bool shown; + bool locked; + bool pos_valid; + sapp_mouse_cursor current_cursor; +} _sapp_tc_mouse_t; + +/* per-app state -- a web-sized subset of sokol_app.h's _sapp_t with the same + member names, so the lifted implementation code reads unchanged. A large + part of the web backend's real state lives in JavaScript on the Module + object (Module.sapp_emsc_target = the canvas, Module.sokol_dropped_files, + Module.__sapp_tc_custom_cursors), reachable only from the EM_JS blocks. */ +typedef struct { + sapp_desc desc; + bool valid; + bool fullscreen; + bool first_frame; + bool init_called; + bool cleanup_called; + bool quit_requested; + bool quit_ordered; + bool event_consumed; + bool html5_ask_leave_site; + bool onscreen_keyboard_shown; + bool skip_present; /* stored for API parity; the browser auto-presents at rAF (ignored) */ + int window_width; + int window_height; + int framebuffer_width; + int framebuffer_height; + int sample_count; + int swap_interval; /* stored for API parity; the browser paces via rAF (ignored) */ + float dpi_scale; + uint64_t frame_count; + sapp_event event; + _sapp_tc_mouse_t mouse; + _sapp_tc_clipboard_t clipboard; + _sapp_tc_drop_t drop; + _sapp_tc_timing_t timing; + #if defined(SOKOL_WGPU) + _sapp_tc_wgpu_t wgpu; + #endif + _sapp_tc_emsc_t emsc; + #if defined(_SAPP_ANY_GL) + _sapp_tc_gl_t gl; + #endif + char html5_canvas_selector[_SAPP_MAX_TITLE_LENGTH]; + char window_title[_SAPP_MAX_TITLE_LENGTH]; + bool custom_cursor_bound[_SAPP_MOUSECURSOR_NUM]; +} _sapp_tc_t; +static _sapp_tc_t _sapp_tc; + +_SOKOL_PRIVATE void _sapp_tc_clear(void* ptr, size_t size) { + SOKOL_ASSERT(ptr && (size > 0)); + memset(ptr, 0, size); +} + +_SOKOL_PRIVATE void* _sapp_tc_malloc(size_t size) { + SOKOL_ASSERT(size > 0); + void* ptr; + if (_sapp_tc.desc.allocator.alloc_fn) { + ptr = _sapp_tc.desc.allocator.alloc_fn(size, _sapp_tc.desc.allocator.user_data); + } else { + ptr = malloc(size); + } + if (0 == ptr) { + _SAPP_PANIC(MALLOC_FAILED); + } + return ptr; +} + +_SOKOL_PRIVATE void* _sapp_tc_malloc_clear(size_t size) { + void* ptr = _sapp_tc_malloc(size); + _sapp_tc_clear(ptr, size); + return ptr; +} + +_SOKOL_PRIVATE void _sapp_tc_free(void* ptr) { + if (_sapp_tc.desc.allocator.free_fn) { + _sapp_tc.desc.allocator.free_fn(ptr, _sapp_tc.desc.allocator.user_data); + } else { + free(ptr); + } +} + +// ██ ██ ███████ ██ ██████ ███████ ██████ ███████ +// ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ +// ███████ █████ ██ ██████ █████ ██████ ███████ +// ██ ██ ██ ██ ██ ██ ██ ██ ██ +// ██ ██ ███████ ███████ ██ ███████ ██ ██ ███████ +// +// >>helpers + +// round float to int and at least 1 +_SOKOL_PRIVATE int _sapp_tc_roundf_gzero(float f) { + int val = (int)roundf(f); + if (val <= 0) { + val = 1; + } + return val; +} + +_SOKOL_PRIVATE void _sapp_tc_call_init(void) { + if (_sapp_tc.desc.init_cb) { + _sapp_tc.desc.init_cb(); + } else if (_sapp_tc.desc.init_userdata_cb) { + _sapp_tc.desc.init_userdata_cb(_sapp_tc.desc.user_data); + } + _sapp_tc.init_called = true; +} + +_SOKOL_PRIVATE void _sapp_tc_call_frame(void) { + if (_sapp_tc.init_called && !_sapp_tc.cleanup_called) { + if (_sapp_tc.desc.frame_cb) { + _sapp_tc.desc.frame_cb(); + } else if (_sapp_tc.desc.frame_userdata_cb) { + _sapp_tc.desc.frame_userdata_cb(_sapp_tc.desc.user_data); + } + } +} + +_SOKOL_PRIVATE void _sapp_tc_call_cleanup(void) { + if (!_sapp_tc.cleanup_called) { + if (_sapp_tc.desc.cleanup_cb) { + _sapp_tc.desc.cleanup_cb(); + } else if (_sapp_tc.desc.cleanup_userdata_cb) { + _sapp_tc.desc.cleanup_userdata_cb(_sapp_tc.desc.user_data); + } + _sapp_tc.cleanup_called = true; + } +} + +_SOKOL_PRIVATE bool _sapp_tc_call_event(const sapp_event* e) { + if (!_sapp_tc.cleanup_called) { + if (_sapp_tc.desc.event_cb) { + _sapp_tc.desc.event_cb(e); + } else if (_sapp_tc.desc.event_userdata_cb) { + _sapp_tc.desc.event_userdata_cb(e, _sapp_tc.desc.user_data); + } + } + if (_sapp_tc.event_consumed) { + _sapp_tc.event_consumed = false; + return true; + } else { + return false; + } +} + + +_SOKOL_PRIVATE char* _sapp_tc_dropped_file_path_ptr(int index) { + SOKOL_ASSERT(_sapp_tc.drop.buffer); + SOKOL_ASSERT((index >= 0) && (index <= _sapp_tc.drop.max_files)); + int offset = index * _sapp_tc.drop.max_path_length; + SOKOL_ASSERT(offset < _sapp_tc.drop.buf_size); + return &_sapp_tc.drop.buffer[offset]; +} + +/* Copy a string (either zero-terminated or with explicit length) + into a fixed size buffer with guaranteed zero-termination. + + Return false if the string didn't fit into the buffer and had to be clamped. + + FIXME: Currently UTF-8 strings might become invalid if the string + is clamped, because the last zero-byte might be written into + the middle of a multi-byte sequence. +*/ +_SOKOL_PRIVATE bool _sapp_tc_strcpy_range(const char* src, size_t src_len, char* dst, size_t dst_buf_len) { + SOKOL_ASSERT(src && dst && (dst_buf_len > 0)); + if (0 == src_len) { + src_len = dst_buf_len; + } + char* const end = &(dst[dst_buf_len-1]); + char c = 0; + for (size_t i = 0; i < dst_buf_len; i++) { + c = *src; + if (i >= src_len) { + c = 0; + } + if (c != 0) { + src++; + } + *dst++ = c; + } + // truncated? + if (c != 0) { + *end = 0; + return false; + } else { + return true; + } +} + +_SOKOL_PRIVATE bool _sapp_tc_strcpy(const char* src, char* dst, size_t dst_buf_len) { + return _sapp_tc_strcpy_range(src, 0, dst, dst_buf_len); +} + +_SOKOL_PRIVATE sapp_desc _sapp_tc_desc_defaults(const sapp_desc* desc) { + SOKOL_ASSERT((desc->allocator.alloc_fn && desc->allocator.free_fn) || (!desc->allocator.alloc_fn && !desc->allocator.free_fn)); + sapp_desc res = *desc; + res.sample_count = _sapp_tc_def(res.sample_count, 1); + res.swap_interval = _sapp_tc_def(res.swap_interval, 1); + if (0 == res.gl.major_version) { + #if defined(SOKOL_GLCORE) + res.gl.major_version = 4; + #if defined(_SAPP_APPLE) + res.gl.minor_version = 1; + #else + res.gl.minor_version = 3; + #endif + #elif defined(SOKOL_GLES3) + res.gl.major_version = 3; + #if defined(_SAPP_ANDROID) || defined(_SAPP_LINUX) + res.gl.minor_version = 1; + #else + res.gl.minor_version = 0; + #endif + #endif + } + res.html5.canvas_selector = _sapp_tc_def(res.html5.canvas_selector, "#canvas"); + res.clipboard_size = _sapp_tc_def(res.clipboard_size, 8192); + res.max_dropped_files = _sapp_tc_def(res.max_dropped_files, 1); + res.max_dropped_file_path_length = _sapp_tc_def(res.max_dropped_file_path_length, 2048); + res.window_title = _sapp_tc_def(res.window_title, "sokol"); + return res; +} + +_SOKOL_PRIVATE void _sapp_tc_init_state(const sapp_desc* desc) { + SOKOL_ASSERT(desc); + SOKOL_ASSERT(desc->width >= 0); + SOKOL_ASSERT(desc->height >= 0); + SOKOL_ASSERT(desc->sample_count >= 0); + SOKOL_ASSERT(desc->swap_interval >= 0); + SOKOL_ASSERT(desc->clipboard_size >= 0); + SOKOL_ASSERT(desc->max_dropped_files >= 0); + SOKOL_ASSERT(desc->max_dropped_file_path_length >= 0); + _SAPP_CLEAR_ARC_STRUCT(_sapp_tc_t, _sapp_tc); + _sapp_tc.desc = _sapp_tc_desc_defaults(desc); + _sapp_tc.first_frame = true; + // NOTE: _sapp_tc.desc.width/height may be 0! Platform backends need to deal with this + _sapp_tc.window_width = _sapp_tc.desc.width; + _sapp_tc.window_height = _sapp_tc.desc.height; + _sapp_tc.framebuffer_width = _sapp_tc.window_width; + _sapp_tc.framebuffer_height = _sapp_tc.window_height; + _sapp_tc.sample_count = _sapp_tc.desc.sample_count; + _sapp_tc.swap_interval = _sapp_tc.desc.swap_interval; + _sapp_tc_strcpy(_sapp_tc.desc.html5.canvas_selector, _sapp_tc.html5_canvas_selector, sizeof(_sapp_tc.html5_canvas_selector)); + _sapp_tc.desc.html5.canvas_selector = _sapp_tc.html5_canvas_selector; + _sapp_tc.html5_ask_leave_site = _sapp_tc.desc.html5.ask_leave_site; + _sapp_tc.clipboard.enabled = _sapp_tc.desc.enable_clipboard; + if (_sapp_tc.clipboard.enabled) { + _sapp_tc.clipboard.buf_size = _sapp_tc.desc.clipboard_size; + _sapp_tc.clipboard.buffer = (char*) _sapp_tc_malloc_clear((size_t)_sapp_tc.clipboard.buf_size); + } + _sapp_tc.drop.enabled = _sapp_tc.desc.enable_dragndrop; + if (_sapp_tc.drop.enabled) { + _sapp_tc.drop.max_files = _sapp_tc.desc.max_dropped_files; + _sapp_tc.drop.max_path_length = _sapp_tc.desc.max_dropped_file_path_length; + _sapp_tc.drop.buf_size = _sapp_tc.drop.max_files * _sapp_tc.drop.max_path_length; + _sapp_tc.drop.buffer = (char*) _sapp_tc_malloc_clear((size_t)_sapp_tc.drop.buf_size); + } + _sapp_tc_strcpy(_sapp_tc.desc.window_title, _sapp_tc.window_title, sizeof(_sapp_tc.window_title)); + _sapp_tc.desc.window_title = _sapp_tc.window_title; + _sapp_tc.dpi_scale = 1.0f; + _sapp_tc.fullscreen = _sapp_tc.desc.fullscreen; + _sapp_tc.mouse.shown = true; + _sapp_tc_timing_init(&_sapp_tc.timing); +} + +_SOKOL_PRIVATE void _sapp_tc_discard_state(void) { + if (_sapp_tc.clipboard.enabled) { + SOKOL_ASSERT(_sapp_tc.clipboard.buffer); + _sapp_tc_free((void*)_sapp_tc.clipboard.buffer); + } + if (_sapp_tc.drop.enabled) { + SOKOL_ASSERT(_sapp_tc.drop.buffer); + _sapp_tc_free((void*)_sapp_tc.drop.buffer); + } + for (int i = 0; i < _SAPP_MOUSECURSOR_NUM; i++) { + sapp_unbind_mouse_cursor_image((sapp_mouse_cursor) i); + } + _SAPP_CLEAR_ARC_STRUCT(_sapp_tc_t, _sapp_tc); +} + +_SOKOL_PRIVATE void _sapp_tc_init_event(sapp_event_type type) { + _sapp_tc_clear(&_sapp_tc.event, sizeof(_sapp_tc.event)); + _sapp_tc.event.type = type; + _sapp_tc.event.frame_count = _sapp_tc.frame_count; + _sapp_tc.event.mouse_button = SAPP_MOUSEBUTTON_INVALID; + _sapp_tc.event.window_width = _sapp_tc.window_width; + _sapp_tc.event.window_height = _sapp_tc.window_height; + _sapp_tc.event.framebuffer_width = _sapp_tc.framebuffer_width; + _sapp_tc.event.framebuffer_height = _sapp_tc.framebuffer_height; + _sapp_tc.event.mouse_x = _sapp_tc.mouse.x; + _sapp_tc.event.mouse_y = _sapp_tc.mouse.y; + _sapp_tc.event.mouse_dx = _sapp_tc.mouse.dx; + _sapp_tc.event.mouse_dy = _sapp_tc.mouse.dy; +} + +_SOKOL_PRIVATE bool _sapp_tc_events_enabled(void) { + /* only send events when an event callback is set, and the init function was called */ + return (_sapp_tc.desc.event_cb || _sapp_tc.desc.event_userdata_cb) && _sapp_tc.init_called; +} + +_SOKOL_PRIVATE void _sapp_tc_clear_drop_buffer(void) { + if (_sapp_tc.drop.enabled) { + SOKOL_ASSERT(_sapp_tc.drop.buffer); + _sapp_tc_clear(_sapp_tc.drop.buffer, (size_t)_sapp_tc.drop.buf_size); + } +} + +_SOKOL_PRIVATE void _sapp_tc_frame(void) { + if (_sapp_tc.first_frame) { + _sapp_tc.first_frame = false; + _sapp_tc_call_init(); + } + _sapp_tc_call_frame(); + _sapp_tc.frame_count++; +} + +_SOKOL_PRIVATE bool _sapp_tc_image_validate(const sapp_image_desc* desc) { + SOKOL_ASSERT(desc->width > 0); + SOKOL_ASSERT(desc->height > 0); + SOKOL_ASSERT(desc->pixels.ptr != 0); + SOKOL_ASSERT(desc->pixels.size > 0); + const size_t wh_size = (size_t)(desc->width * desc->height) * sizeof(uint32_t); + if (wh_size != desc->pixels.size) { + _SAPP_ERROR(IMAGE_DATA_SIZE_MISMATCH); + return false; + } + return true; +} + +_SOKOL_PRIVATE int _sapp_tc_image_bestmatch(const sapp_image_desc image_descs[], int num_images, int width, int height) { + int least_diff = 0x7FFFFFFF; + int least_index = 0; + for (int i = 0; i < num_images; i++) { + int diff = (image_descs[i].width * image_descs[i].height) - (width * height); + if (diff < 0) { + diff = -diff; + } + if (diff < least_diff) { + least_diff = diff; + least_index = i; + } + } + return least_index; +} + +_SOKOL_PRIVATE int _sapp_tc_icon_num_images(const sapp_icon_desc* desc) { + int index = 0; + for (; index < SAPP_MAX_ICONIMAGES; index++) { + if (0 == desc->images[index].pixels.ptr) { + break; + } + } + return index; +} + +_SOKOL_PRIVATE bool _sapp_tc_validate_icon_desc(const sapp_icon_desc* desc, int num_images) { + SOKOL_ASSERT(num_images <= SAPP_MAX_ICONIMAGES); + for (int i = 0; i < num_images; i++) { + const sapp_image_desc* img_desc = &desc->images[i]; + if (!_sapp_tc_image_validate(img_desc)) { + return false; + } + } + return true; +} + +// >>wgpu +#if defined(SOKOL_WGPU) + +_SOKOL_PRIVATE WGPUStringView _sapp_tc_wgpu_stringview(const char* str) { + WGPUStringView res; + if (str) { + res.data = str; + res.length = strlen(str); + } else { + res.data = 0; + res.length = 0; + } + return res; +} + +_SOKOL_PRIVATE WGPUCallbackMode _sapp_tc_wgpu_callbackmode(void) { + #if defined(_SAPP_WGPU_HAS_WAIT) + return WGPUCallbackMode_WaitAnyOnly; + #else + return WGPUCallbackMode_AllowProcessEvents; + #endif +} + +_SOKOL_PRIVATE void _sapp_tc_wgpu_await(WGPUFuture future) { + #if defined(_SAPP_WGPU_HAS_WAIT) + SOKOL_ASSERT(_sapp_tc.wgpu.instance); + _SAPP_STRUCT(WGPUFutureWaitInfo, wait_info); + wait_info.future = future; + WGPUWaitStatus res = wgpuInstanceWaitAny(_sapp_tc.wgpu.instance, 1, &wait_info, UINT64_MAX); + SOKOL_ASSERT(res == WGPUWaitStatus_Success); _SOKOL_UNUSED(res); + #else + // this code path should never be called + _SOKOL_UNUSED(future); + SOKOL_ASSERT(false); + #endif +} + +_SOKOL_PRIVATE WGPUTextureFormat _sapp_tc_wgpu_pick_render_format(size_t count, const WGPUTextureFormat* formats) { + // NOTE: only accept non-SRGB formats until sokol_app.h gets proper SRGB support + SOKOL_ASSERT((count > 0) && formats); + for (size_t i = 0; i < count; i++) { + const WGPUTextureFormat fmt = formats[i]; + switch (fmt) { + case WGPUTextureFormat_RGBA8Unorm: + case WGPUTextureFormat_BGRA8Unorm: + return fmt; + default: break; + } + } + // FIXME: fallback might still return an SRGB format + return formats[0]; +} + +_SOKOL_PRIVATE void _sapp_tc_wgpu_create_swapchain(bool called_from_resize) { + SOKOL_ASSERT(_sapp_tc.wgpu.instance); + SOKOL_ASSERT(_sapp_tc.wgpu.device); + SOKOL_ASSERT(0 == _sapp_tc.wgpu.msaa_tex); + SOKOL_ASSERT(0 == _sapp_tc.wgpu.msaa_view); + SOKOL_ASSERT(0 == _sapp_tc.wgpu.depth_stencil_tex); + SOKOL_ASSERT(0 == _sapp_tc.wgpu.depth_stencil_view); + + if (!called_from_resize) { + SOKOL_ASSERT(0 == _sapp_tc.wgpu.surface); + _SAPP_STRUCT(WGPUSurfaceDescriptor, surf_desc); + #if defined (_SAPP_EMSCRIPTEN) + _SAPP_STRUCT(WGPUEmscriptenSurfaceSourceCanvasHTMLSelector, html_canvas_desc); + html_canvas_desc.chain.sType = WGPUSType_EmscriptenSurfaceSourceCanvasHTMLSelector; + html_canvas_desc.selector = _sapp_tc_wgpu_stringview(_sapp_tc.html5_canvas_selector); + surf_desc.nextInChain = &html_canvas_desc.chain; + #elif defined(_SAPP_MACOS) + _SAPP_STRUCT(WGPUSurfaceSourceMetalLayer, from_metal_layer); + from_metal_layer.chain.sType = WGPUSType_SurfaceSourceMetalLayer; + from_metal_layer.layer = _sapp_tc.macos.view.layer; + surf_desc.nextInChain = &from_metal_layer.chain; + #elif defined(_SAPP_WIN32) + _SAPP_STRUCT(WGPUSurfaceSourceWindowsHWND, from_hwnd); + from_hwnd.chain.sType = WGPUSType_SurfaceSourceWindowsHWND; + from_hwnd.hinstance = GetModuleHandleW(NULL); + from_hwnd.hwnd = _sapp_tc.win32.hwnd; + surf_desc.nextInChain = &from_hwnd.chain; + #elif defined(_SAPP_LINUX) + _SAPP_STRUCT(WGPUSurfaceSourceXlibWindow, from_xlib); + from_xlib.chain.sType = WGPUSType_SurfaceSourceXlibWindow; + from_xlib.display = _sapp_tc.x11.display; + from_xlib.window = _sapp_tc.x11.window; + surf_desc.nextInChain = &from_xlib.chain; + #else + #error "sokol_app.h: unsupported WebGPU platform" + #endif + _sapp_tc.wgpu.surface = wgpuInstanceCreateSurface(_sapp_tc.wgpu.instance, &surf_desc); + if (0 == _sapp_tc.wgpu.surface) { + _SAPP_PANIC(WGPU_SWAPCHAIN_CREATE_SURFACE_FAILED); + } + _SAPP_STRUCT(WGPUSurfaceCapabilities, surf_caps); + WGPUStatus caps_status = wgpuSurfaceGetCapabilities(_sapp_tc.wgpu.surface, _sapp_tc.wgpu.adapter, &surf_caps); + if (caps_status != WGPUStatus_Success) { + _SAPP_PANIC(WGPU_SWAPCHAIN_SURFACE_GET_CAPABILITIES_FAILED); + } + _sapp_tc.wgpu.render_format = _sapp_tc_wgpu_pick_render_format(surf_caps.formatCount, surf_caps.formats); + } + + SOKOL_ASSERT(_sapp_tc.wgpu.surface); + _SAPP_STRUCT(WGPUSurfaceConfiguration, surf_conf); + surf_conf.device = _sapp_tc.wgpu.device; + surf_conf.format = _sapp_tc.wgpu.render_format; + surf_conf.usage = WGPUTextureUsage_RenderAttachment; + surf_conf.width = (uint32_t)_sapp_tc.framebuffer_width; + surf_conf.height = (uint32_t)_sapp_tc.framebuffer_height; + surf_conf.alphaMode = WGPUCompositeAlphaMode_Opaque; + #if defined(_SAPP_EMSCRIPTEN) + // FIXME: make this further configurable? + if (_sapp_tc.desc.html5.premultiplied_alpha) { + surf_conf.alphaMode = WGPUCompositeAlphaMode_Premultiplied; + } + #endif + surf_conf.presentMode = WGPUPresentMode_Fifo; + wgpuSurfaceConfigure(_sapp_tc.wgpu.surface, &surf_conf); + + _SAPP_STRUCT(WGPUTextureDescriptor, ds_desc); + ds_desc.usage = WGPUTextureUsage_RenderAttachment; + ds_desc.dimension = WGPUTextureDimension_2D; + ds_desc.size.width = (uint32_t)_sapp_tc.framebuffer_width; + ds_desc.size.height = (uint32_t)_sapp_tc.framebuffer_height; + ds_desc.size.depthOrArrayLayers = 1; + ds_desc.format = WGPUTextureFormat_Depth32FloatStencil8; + ds_desc.mipLevelCount = 1; + ds_desc.sampleCount = (uint32_t)_sapp_tc.sample_count; + _sapp_tc.wgpu.depth_stencil_tex = wgpuDeviceCreateTexture(_sapp_tc.wgpu.device, &ds_desc); + if (0 == _sapp_tc.wgpu.depth_stencil_tex) { + _SAPP_PANIC(WGPU_SWAPCHAIN_CREATE_DEPTH_STENCIL_TEXTURE_FAILED); + } + _sapp_tc.wgpu.depth_stencil_view = wgpuTextureCreateView(_sapp_tc.wgpu.depth_stencil_tex, 0); + if (0 == _sapp_tc.wgpu.depth_stencil_view) { + _SAPP_PANIC(WGPU_SWAPCHAIN_CREATE_DEPTH_STENCIL_VIEW_FAILED); + } + + if (_sapp_tc.sample_count > 1) { + _SAPP_STRUCT(WGPUTextureDescriptor, msaa_desc); + msaa_desc.usage = WGPUTextureUsage_RenderAttachment; + msaa_desc.dimension = WGPUTextureDimension_2D; + msaa_desc.size.width = (uint32_t)_sapp_tc.framebuffer_width; + msaa_desc.size.height = (uint32_t)_sapp_tc.framebuffer_height; + msaa_desc.size.depthOrArrayLayers = 1; + msaa_desc.format = _sapp_tc.wgpu.render_format; + msaa_desc.mipLevelCount = 1; + msaa_desc.sampleCount = (uint32_t)_sapp_tc.sample_count; + _sapp_tc.wgpu.msaa_tex = wgpuDeviceCreateTexture(_sapp_tc.wgpu.device, &msaa_desc); + if (0 == _sapp_tc.wgpu.msaa_tex) { + _SAPP_PANIC(WGPU_SWAPCHAIN_CREATE_MSAA_TEXTURE_FAILED); + } + _sapp_tc.wgpu.msaa_view = wgpuTextureCreateView(_sapp_tc.wgpu.msaa_tex, 0); + if (0 == _sapp_tc.wgpu.msaa_view) { + _SAPP_PANIC(WGPU_SWAPCHAIN_CREATE_MSAA_VIEW_FAILED); + } + } +} + +_SOKOL_PRIVATE void _sapp_tc_wgpu_discard_swapchain(bool called_from_resize) { + if (_sapp_tc.wgpu.msaa_view) { + wgpuTextureViewRelease(_sapp_tc.wgpu.msaa_view); + _sapp_tc.wgpu.msaa_view = 0; + } + if (_sapp_tc.wgpu.msaa_tex) { + wgpuTextureRelease(_sapp_tc.wgpu.msaa_tex); + _sapp_tc.wgpu.msaa_tex = 0; + } + if (_sapp_tc.wgpu.depth_stencil_view) { + wgpuTextureViewRelease(_sapp_tc.wgpu.depth_stencil_view); + _sapp_tc.wgpu.depth_stencil_view = 0; + } + if (_sapp_tc.wgpu.depth_stencil_tex) { + wgpuTextureRelease(_sapp_tc.wgpu.depth_stencil_tex); + _sapp_tc.wgpu.depth_stencil_tex = 0; + } + if (!called_from_resize) { + if (_sapp_tc.wgpu.surface) { + wgpuSurfaceRelease(_sapp_tc.wgpu.surface); + _sapp_tc.wgpu.surface = 0; + } + } +} + +_SOKOL_PRIVATE void _sapp_tc_wgpu_swapchain_next(void) { + SOKOL_ASSERT(0 == _sapp_tc.wgpu.swapchain_view); + _SAPP_STRUCT(WGPUSurfaceTexture, surf_tex); + wgpuSurfaceGetCurrentTexture(_sapp_tc.wgpu.surface, &surf_tex); + switch (surf_tex.status) { + case WGPUSurfaceGetCurrentTextureStatus_SuccessOptimal: + case WGPUSurfaceGetCurrentTextureStatus_SuccessSuboptimal: + // all ok + break; + case WGPUSurfaceGetCurrentTextureStatus_Timeout: + case WGPUSurfaceGetCurrentTextureStatus_Outdated: + case WGPUSurfaceGetCurrentTextureStatus_Lost: + if (surf_tex.texture) { + wgpuTextureRelease(surf_tex.texture); + } + _sapp_tc_wgpu_discard_swapchain(false); + _sapp_tc_wgpu_create_swapchain(false); + // FIXME: currently this will assert in the caller + return; + case WGPUSurfaceGetCurrentTextureStatus_Error: + default: + _SAPP_PANIC(WGPU_SWAPCHAIN_GETCURRENTTEXTURE_FAILED); + break; + } + _sapp_tc.wgpu.swapchain_view = wgpuTextureCreateView(surf_tex.texture, 0); + SOKOL_ASSERT(_sapp_tc.wgpu.swapchain_view); +} + +_SOKOL_PRIVATE void _sapp_tc_wgpu_swapchain_size_changed(void) { + if (_sapp_tc.wgpu.surface) { + _sapp_tc_wgpu_discard_swapchain(true); + _sapp_tc_wgpu_create_swapchain(true); + } +} + +_SOKOL_PRIVATE void _sapp_tc_wgpu_device_lost_cb(const WGPUDevice* dev, WGPUDeviceLostReason reason, WGPUStringView msg, void* ud1, void* ud2) { + _SOKOL_UNUSED(dev); _SOKOL_UNUSED(reason); _SOKOL_UNUSED(ud1); _SOKOL_UNUSED(ud2); + // NOTE: on wgpuInstanceRelease(), the device lost callback is always called with + // WGPUDeviceLostReason_CallbackCancelled (even though no device should exist at that point) + if (reason != WGPUDeviceLostReason_CallbackCancelled) { + SOKOL_ASSERT(msg.data && (msg.length > 0)); + char buf[1024]; + _sapp_tc_strcpy_range(msg.data, msg.length, buf, sizeof(buf)); + _SAPP_ERROR_MSG(WGPU_DEVICE_LOST, buf); + } +} + +// NOTE: emdawnwebgpu doesn't seem to have a device logging callback +#if !defined(_SAPP_EMSCRIPTEN) +_SOKOL_PRIVATE void _sapp_tc_wgpu_device_logging_cb(WGPULoggingType log_type, WGPUStringView msg, void* ud1, void* ud2) { + _SOKOL_UNUSED(log_type); _SOKOL_UNUSED(ud1); _SOKOL_UNUSED(ud2); + SOKOL_ASSERT(msg.data && (msg.length > 0)); + char buf[1024]; + _sapp_tc_strcpy_range(msg.data, msg.length, buf, sizeof(buf)); + switch (log_type) { + case WGPULoggingType_Warning: + _SAPP_WARN_MSG(WGPU_DEVICE_LOG, buf); + break; + case WGPULoggingType_Error: + _SAPP_ERROR_MSG(WGPU_DEVICE_LOG, buf); + break; + default: + _SAPP_INFO_MSG(WGPU_DEVICE_LOG, buf); + break; + } +} +#endif + +_SOKOL_PRIVATE void _sapp_tc_wgpu_uncaptured_error_cb(const WGPUDevice* dev, WGPUErrorType err_type, WGPUStringView msg, void* ud1, void* ud2) { + _SOKOL_UNUSED(dev); _SOKOL_UNUSED(ud1); _SOKOL_UNUSED(ud2); + if (err_type != WGPUErrorType_NoError) { + SOKOL_ASSERT(msg.data && (msg.length > 0)); + char buf[1024]; + _sapp_tc_strcpy_range(msg.data, msg.length, buf, sizeof(buf)); + _SAPP_ERROR_MSG(WGPU_DEVICE_UNCAPTURED_ERROR, buf); + } +} + +_SOKOL_PRIVATE void _sapp_tc_wgpu_request_device_cb(WGPURequestDeviceStatus status, WGPUDevice device, WGPUStringView msg, void* userdata1, void* userdata2) { + _SOKOL_UNUSED(msg); + _SOKOL_UNUSED(userdata1); + _SOKOL_UNUSED(userdata2); + SOKOL_ASSERT(!_sapp_tc.wgpu.init_done); + if (status != WGPURequestDeviceStatus_Success) { + if (status == WGPURequestDeviceStatus_Error) { + _SAPP_PANIC(WGPU_REQUEST_DEVICE_STATUS_ERROR); + } else { + _SAPP_PANIC(WGPU_REQUEST_DEVICE_STATUS_UNKNOWN); + } + } + SOKOL_ASSERT(device); + _sapp_tc.wgpu.device = device; + #if !defined(_SAPP_EMSCRIPTEN) + _SAPP_STRUCT(WGPULoggingCallbackInfo, cb_info); + cb_info.callback = _sapp_tc_wgpu_device_logging_cb; + wgpuDeviceSetLoggingCallback(_sapp_tc.wgpu.device, cb_info); + #endif + _sapp_tc_wgpu_create_swapchain(false); + _sapp_tc.wgpu.init_done = true; +} + +_SOKOL_PRIVATE void _sapp_tc_wgpu_create_device_and_swapchain(void) { + SOKOL_ASSERT(_sapp_tc.wgpu.adapter); + size_t cur_feature_index = 1; + #define _SAPP_WGPU_MAX_REQUESTED_FEATURES (16) + WGPUFeatureName requiredFeatures[_SAPP_WGPU_MAX_REQUESTED_FEATURES] = { + WGPUFeatureName_Depth32FloatStencil8, + }; + // check for optional features we're interested in + if (wgpuAdapterHasFeature(_sapp_tc.wgpu.adapter, WGPUFeatureName_TextureCompressionBC)) { + SOKOL_ASSERT(cur_feature_index < _SAPP_WGPU_MAX_REQUESTED_FEATURES); + requiredFeatures[cur_feature_index++] = WGPUFeatureName_TextureCompressionBC; + } + if (wgpuAdapterHasFeature(_sapp_tc.wgpu.adapter, WGPUFeatureName_TextureCompressionETC2)) { + SOKOL_ASSERT(cur_feature_index < _SAPP_WGPU_MAX_REQUESTED_FEATURES); + requiredFeatures[cur_feature_index++] = WGPUFeatureName_TextureCompressionETC2; + } + if (wgpuAdapterHasFeature(_sapp_tc.wgpu.adapter, WGPUFeatureName_TextureCompressionASTC)) { + SOKOL_ASSERT(cur_feature_index < _SAPP_WGPU_MAX_REQUESTED_FEATURES); + requiredFeatures[cur_feature_index++] = WGPUFeatureName_TextureCompressionASTC; + } + if (wgpuAdapterHasFeature(_sapp_tc.wgpu.adapter, WGPUFeatureName_DualSourceBlending)) { + SOKOL_ASSERT(cur_feature_index < _SAPP_WGPU_MAX_REQUESTED_FEATURES); + requiredFeatures[cur_feature_index++] = WGPUFeatureName_DualSourceBlending; + } + if (wgpuAdapterHasFeature(_sapp_tc.wgpu.adapter, WGPUFeatureName_ShaderF16)) { + SOKOL_ASSERT(cur_feature_index < _SAPP_WGPU_MAX_REQUESTED_FEATURES); + requiredFeatures[cur_feature_index++] = WGPUFeatureName_ShaderF16; + } + if (wgpuAdapterHasFeature(_sapp_tc.wgpu.adapter, WGPUFeatureName_Float32Filterable)) { + SOKOL_ASSERT(cur_feature_index < _SAPP_WGPU_MAX_REQUESTED_FEATURES); + requiredFeatures[cur_feature_index++] = WGPUFeatureName_Float32Filterable; + } + if (wgpuAdapterHasFeature(_sapp_tc.wgpu.adapter, WGPUFeatureName_Float32Blendable)) { + SOKOL_ASSERT(cur_feature_index < _SAPP_WGPU_MAX_REQUESTED_FEATURES); + requiredFeatures[cur_feature_index++] = WGPUFeatureName_Float32Blendable; + } + if (wgpuAdapterHasFeature(_sapp_tc.wgpu.adapter, WGPUFeatureName_TextureFormatsTier2)) { + SOKOL_ASSERT(cur_feature_index < _SAPP_WGPU_MAX_REQUESTED_FEATURES); + requiredFeatures[cur_feature_index++] = WGPUFeatureName_TextureFormatsTier2; + } + #undef _SAPP_WGPU_MAX_REQUESTED_FEATURES + + WGPULimits adapterLimits = WGPU_LIMITS_INIT; + wgpuAdapterGetLimits(_sapp_tc.wgpu.adapter, &adapterLimits); + + WGPULimits requiredLimits = WGPU_LIMITS_INIT; + requiredLimits.maxColorAttachments = adapterLimits.maxColorAttachments; + requiredLimits.maxSampledTexturesPerShaderStage = adapterLimits.maxSampledTexturesPerShaderStage; + requiredLimits.maxStorageBuffersPerShaderStage = adapterLimits.maxStorageBuffersPerShaderStage; + requiredLimits.maxStorageTexturesPerShaderStage = adapterLimits.maxStorageTexturesPerShaderStage; + + _SAPP_STRUCT(WGPURequestDeviceCallbackInfo, cb_info); + cb_info.mode = _sapp_tc_wgpu_callbackmode(); + cb_info.callback = _sapp_tc_wgpu_request_device_cb; + + _SAPP_STRUCT(WGPUDeviceDescriptor, dev_desc); + dev_desc.requiredFeatureCount = cur_feature_index; + dev_desc.requiredFeatures = requiredFeatures; + dev_desc.requiredLimits = &requiredLimits; + dev_desc.deviceLostCallbackInfo.mode = WGPUCallbackMode_AllowProcessEvents; + dev_desc.deviceLostCallbackInfo.callback = _sapp_tc_wgpu_device_lost_cb; + dev_desc.uncapturedErrorCallbackInfo.callback = _sapp_tc_wgpu_uncaptured_error_cb; + WGPUFuture future = wgpuAdapterRequestDevice(_sapp_tc.wgpu.adapter, &dev_desc, cb_info); + #if defined(_SAPP_WGPU_HAS_WAIT) + _sapp_tc_wgpu_await(future); + #else + _SOKOL_UNUSED(future); + #endif +} + +_SOKOL_PRIVATE void _sapp_tc_wgpu_request_adapter_cb(WGPURequestAdapterStatus status, WGPUAdapter adapter, WGPUStringView msg, void* userdata1, void* userdata2) { + _SOKOL_UNUSED(msg); + _SOKOL_UNUSED(userdata1); + _SOKOL_UNUSED(userdata2); + if (status != WGPURequestAdapterStatus_Success) { + switch (status) { + case WGPURequestAdapterStatus_Unavailable: _SAPP_PANIC(WGPU_REQUEST_ADAPTER_STATUS_UNAVAILABLE); break; + case WGPURequestAdapterStatus_Error: _SAPP_PANIC(WGPU_REQUEST_ADAPTER_STATUS_ERROR); break; + default: _SAPP_PANIC(WGPU_REQUEST_ADAPTER_STATUS_UNKNOWN); break; + } + } + SOKOL_ASSERT(adapter); + _sapp_tc.wgpu.adapter = adapter; + #if !defined(_SAPP_WGPU_HAS_WAIT) + // chain device creation + _sapp_tc_wgpu_create_device_and_swapchain(); + #endif +} + +_SOKOL_PRIVATE void _sapp_tc_wgpu_create_adapter(void) { + SOKOL_ASSERT(_sapp_tc.wgpu.instance); + // FIXME: power preference? + _SAPP_STRUCT(WGPURequestAdapterCallbackInfo, cb_info); + cb_info.mode = _sapp_tc_wgpu_callbackmode(); + cb_info.callback = _sapp_tc_wgpu_request_adapter_cb; + WGPUFuture future = wgpuInstanceRequestAdapter(_sapp_tc.wgpu.instance, 0, cb_info); + #if defined(_SAPP_WGPU_HAS_WAIT) + _sapp_tc_wgpu_await(future); + #else + _SOKOL_UNUSED(future); + #endif +} + +_SOKOL_PRIVATE void _sapp_tc_wgpu_init(void) { + SOKOL_ASSERT(0 == _sapp_tc.wgpu.instance); + SOKOL_ASSERT(!_sapp_tc.wgpu.init_done); + + _SAPP_STRUCT(WGPUInstanceDescriptor, desc); + #if defined(_SAPP_WGPU_HAS_WAIT) + WGPUInstanceFeatureName inst_features[1] = { + WGPUInstanceFeatureName_TimedWaitAny, + }; + desc.requiredFeatureCount = 1; + desc.requiredFeatures = inst_features; + #endif + _sapp_tc.wgpu.instance = wgpuCreateInstance(&desc); + if (0 == _sapp_tc.wgpu.instance) { + _SAPP_PANIC(WGPU_CREATE_INSTANCE_FAILED); + } + // NOTE: on Emscripten, device and swapchain creation are chained in the callacks + _sapp_tc_wgpu_create_adapter(); + #if defined(_SAPP_WGPU_HAS_WAIT) + _sapp_tc_wgpu_create_device_and_swapchain(); + SOKOL_ASSERT(_sapp_tc.wgpu.init_done); + #endif +} + +_SOKOL_PRIVATE void _sapp_tc_wgpu_discard(void) { + _sapp_tc_wgpu_discard_swapchain(false); + if (_sapp_tc.wgpu.device) { + wgpuDeviceRelease(_sapp_tc.wgpu.device); + _sapp_tc.wgpu.device = 0; + } + if (_sapp_tc.wgpu.adapter) { + wgpuAdapterRelease(_sapp_tc.wgpu.adapter); + _sapp_tc.wgpu.adapter = 0; + } + if (_sapp_tc.wgpu.instance) { + wgpuInstanceRelease(_sapp_tc.wgpu.instance); + _sapp_tc.wgpu.instance = 0; + } +} + +_SOKOL_PRIVATE void _sapp_tc_wgpu_frame(void) { + wgpuInstanceProcessEvents(_sapp_tc.wgpu.instance); + if (_sapp_tc.wgpu.init_done) { + _sapp_tc_frame(); + if (_sapp_tc.wgpu.swapchain_view) { + wgpuTextureViewRelease(_sapp_tc.wgpu.swapchain_view); + _sapp_tc.wgpu.swapchain_view = 0; + } + #if !defined(_SAPP_EMSCRIPTEN) + // Modified by tettou771 for TrussC: skip present support + if (!_sapp_tc.skip_present) { + wgpuSurfacePresent(_sapp_tc.wgpu.surface); + } else { + _sapp_tc.skip_present = false; + } + #endif + } +} +#endif // SOKOL_WGPU + +#if defined(_SAPP_EMSCRIPTEN) + +#if defined(EM_JS_DEPS) +EM_JS_DEPS(sokol_app_tc, "$withStackSave,$stringToUTF8OnStack,$findCanvasEventTarget") +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +typedef void (*_sapp_tc_html5_fetch_callback) (const sapp_html5_fetch_response*); + +EMSCRIPTEN_KEEPALIVE void _sapp_tc_emsc_onpaste(const char* str) { + if (_sapp_tc.clipboard.enabled) { + _sapp_tc_strcpy(str, _sapp_tc.clipboard.buffer, (size_t)_sapp_tc.clipboard.buf_size); + if (_sapp_tc_events_enabled()) { + _sapp_tc_init_event(SAPP_EVENTTYPE_CLIPBOARD_PASTED); + _sapp_tc_call_event(&_sapp_tc.event); + } + } +} + +/* https://developer.mozilla.org/en-US/docs/Web/API/WindowEventHandlers/onbeforeunload */ +EMSCRIPTEN_KEEPALIVE int _sapp_tc_html5_get_ask_leave_site(void) { + return _sapp_tc.html5_ask_leave_site ? 1 : 0; +} + +EMSCRIPTEN_KEEPALIVE void _sapp_tc_emsc_begin_drop(int num) { + if (!_sapp_tc.drop.enabled) { + return; + } + if (num < 0) { + num = 0; + } + if (num > _sapp_tc.drop.max_files) { + num = _sapp_tc.drop.max_files; + } + _sapp_tc.drop.num_files = num; + _sapp_tc_clear_drop_buffer(); +} + +EMSCRIPTEN_KEEPALIVE void _sapp_tc_emsc_drop(int i, const char* name) { + /* NOTE: name is only the filename part, not a path */ + if (!_sapp_tc.drop.enabled) { + return; + } + if (0 == name) { + return; + } + SOKOL_ASSERT(_sapp_tc.drop.num_files <= _sapp_tc.drop.max_files); + if ((i < 0) || (i >= _sapp_tc.drop.num_files)) { + return; + } + if (!_sapp_tc_strcpy(name, _sapp_tc_dropped_file_path_ptr(i), (size_t)_sapp_tc.drop.max_path_length)) { + _SAPP_ERROR(DROPPED_FILE_PATH_TOO_LONG); + _sapp_tc.drop.num_files = 0; + } +} + +EMSCRIPTEN_KEEPALIVE void _sapp_tc_emsc_end_drop(int x, int y, int mods) { + if (!_sapp_tc.drop.enabled) { + return; + } + if (0 == _sapp_tc.drop.num_files) { + /* there was an error copying the filenames */ + _sapp_tc_clear_drop_buffer(); + return; + + } + if (_sapp_tc_events_enabled()) { + _sapp_tc.mouse.x = (float)x * _sapp_tc.dpi_scale; + _sapp_tc.mouse.y = (float)y * _sapp_tc.dpi_scale; + _sapp_tc.mouse.dx = 0.0f; + _sapp_tc.mouse.dy = 0.0f; + _sapp_tc_init_event(SAPP_EVENTTYPE_FILES_DROPPED); + // see _sapp_tc_js_add_dragndrop_listeners for mods constants + if (mods & 1) { _sapp_tc.event.modifiers |= SAPP_MODIFIER_SHIFT; } + if (mods & 2) { _sapp_tc.event.modifiers |= SAPP_MODIFIER_CTRL; } + if (mods & 4) { _sapp_tc.event.modifiers |= SAPP_MODIFIER_ALT; } + if (mods & 8) { _sapp_tc.event.modifiers |= SAPP_MODIFIER_SUPER; } + _sapp_tc_call_event(&_sapp_tc.event); + } +} + +EMSCRIPTEN_KEEPALIVE void _sapp_tc_emsc_invoke_fetch_cb(int index, int success, int error_code, _sapp_tc_html5_fetch_callback callback, uint32_t fetched_size, void* buf_ptr, uint32_t buf_size, void* user_data) { + _SAPP_STRUCT(sapp_html5_fetch_response, response); + response.succeeded = (0 != success); + response.error_code = (sapp_html5_fetch_error) error_code; + response.file_index = index; + response.data.ptr = buf_ptr; + response.data.size = fetched_size; + response.buffer.ptr = buf_ptr; + response.buffer.size = buf_size; + response.user_data = user_data; + callback(&response); +} + +// will be called after the request/exitFullscreen promise rejects +// to restore the _sapp_tc.fullscreen flag to the actual fullscreen state +EMSCRIPTEN_KEEPALIVE void _sapp_tc_emsc_set_fullscreen_flag(int f) { + _sapp_tc.fullscreen = (bool)f; +} + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +EM_JS(void, _sapp_tc_js_add_beforeunload_listener, (void), { + Module.sokol_beforeunload = (event) => { + if (__sapp_tc_html5_get_ask_leave_site() != 0) { + event.preventDefault(); + event.returnValue = ' '; + } + }; + window.addEventListener('beforeunload', Module.sokol_beforeunload); +}) + +EM_JS(void, _sapp_tc_js_remove_beforeunload_listener, (void), { + window.removeEventListener('beforeunload', Module.sokol_beforeunload); +}) + +EM_JS(void, _sapp_tc_js_add_clipboard_listener, (void), { + Module.sokol_paste = (event) => { + const pasted_str = event.clipboardData.getData('text'); + withStackSave(() => { + const cstr = stringToUTF8OnStack(pasted_str); + __sapp_tc_emsc_onpaste(cstr); + }); + }; + window.addEventListener('paste', Module.sokol_paste); +}) + +EM_JS(void, _sapp_tc_js_remove_clipboard_listener, (void), { + window.removeEventListener('paste', Module.sokol_paste); +}) + +EM_JS(void, _sapp_tc_js_write_clipboard, (const char* c_str), { + const str = UTF8ToString(c_str); + const ta = document.createElement('textarea'); + ta.setAttribute('autocomplete', 'off'); + ta.setAttribute('autocorrect', 'off'); + ta.setAttribute('autocapitalize', 'off'); + ta.setAttribute('spellcheck', 'false'); + ta.style.left = -100 + 'px'; + ta.style.top = -100 + 'px'; + ta.style.height = 1; + ta.style.width = 1; + ta.value = str; + document.body.appendChild(ta); + ta.select(); + document.execCommand('copy'); + document.body.removeChild(ta); +}) + +_SOKOL_PRIVATE void _sapp_tc_emsc_set_clipboard_string(const char* str) { + _sapp_tc_js_write_clipboard(str); +} + +EM_JS(void, _sapp_tc_js_add_dragndrop_listeners, (void), { + Module.sokol_drop_files = []; + Module.sokol_dragenter = (event) => { + event.stopPropagation(); + event.preventDefault(); + }; + Module.sokol_dragleave = (event) => { + event.stopPropagation(); + event.preventDefault(); + }; + Module.sokol_dragover = (event) => { + event.stopPropagation(); + event.preventDefault(); + }; + Module.sokol_drop = (event) => { + event.stopPropagation(); + event.preventDefault(); + const files = event.dataTransfer.files; + Module.sokol_dropped_files = files; + __sapp_tc_emsc_begin_drop(files.length); + for (let i = 0; i < files.length; i++) { + withStackSave(() => { + const cstr = stringToUTF8OnStack(files[i].name); + __sapp_tc_emsc_drop(i, cstr); + }); + } + let mods = 0; + if (event.shiftKey) { mods |= 1; } + if (event.ctrlKey) { mods |= 2; } + if (event.altKey) { mods |= 4; } + if (event.metaKey) { mods |= 8; } + // FIXME? see computation of targetX/targetY in emscripten via getClientBoundingRect + __sapp_tc_emsc_end_drop(event.clientX, event.clientY, mods); + }; + \x2F\x2A\x2A @suppress {missingProperties} \x2A\x2F + const canvas = Module.sapp_emsc_target; + canvas.addEventListener('dragenter', Module.sokol_dragenter, false); + canvas.addEventListener('dragleave', Module.sokol_dragleave, false); + canvas.addEventListener('dragover', Module.sokol_dragover, false); + canvas.addEventListener('drop', Module.sokol_drop, false); +}) + +EM_JS(uint32_t, _sapp_tc_js_dropped_file_size, (int index), { + \x2F\x2A\x2A @suppress {missingProperties} \x2A\x2F + const files = Module.sokol_dropped_files; + if ((index < 0) || (index >= files.length)) { + return 0; + } else { + return files[index].size; + } +}) + +EM_JS(void, _sapp_tc_js_fetch_dropped_file, (int index, _sapp_tc_html5_fetch_callback callback, void* buf_ptr, uint32_t buf_size, void* user_data), { + const reader = new FileReader(); + reader.onload = (loadEvent) => { + const content = loadEvent.target.result; + if (content.byteLength > buf_size) { + // SAPP_HTML5_FETCH_ERROR_BUFFER_TOO_SMALL + __sapp_tc_emsc_invoke_fetch_cb(index, 0, 1, callback, 0, buf_ptr, buf_size, user_data); + } else { + HEAPU8.set(new Uint8Array(content), buf_ptr); + __sapp_tc_emsc_invoke_fetch_cb(index, 1, 0, callback, content.byteLength, buf_ptr, buf_size, user_data); + } + }; + reader.onerror = () => { + // SAPP_HTML5_FETCH_ERROR_OTHER + __sapp_tc_emsc_invoke_fetch_cb(index, 0, 2, callback, 0, buf_ptr, buf_size, user_data); + }; + \x2F\x2A\x2A @suppress {missingProperties} \x2A\x2F + const files = Module.sokol_dropped_files; + reader.readAsArrayBuffer(files[index]); +}) + +EM_JS(void, _sapp_tc_js_remove_dragndrop_listeners, (void), { + \x2F\x2A\x2A @suppress {missingProperties} \x2A\x2F + const canvas = Module.sapp_emsc_target; + canvas.removeEventListener('dragenter', Module.sokol_dragenter); + canvas.removeEventListener('dragleave', Module.sokol_dragleave); + canvas.removeEventListener('dragover', Module.sokol_dragover); + canvas.removeEventListener('drop', Module.sokol_drop); +}) + +EM_JS(void, _sapp_tc_js_init, (const char* c_str_target_selector, const char* c_str_document_title), { + if (c_str_document_title !== 0) { + document.title = UTF8ToString(c_str_document_title); + } + const target_selector_str = UTF8ToString(c_str_target_selector); + if (Module['canvas'] !== undefined) { + if (typeof Module['canvas'] === 'object') { + specialHTMLTargets[target_selector_str] = Module['canvas']; + } else { + console.warn("sokol_app.h: Module['canvas'] is set but is not an object"); + } + } + Module.sapp_emsc_target = findCanvasEventTarget(target_selector_str); + if (!Module.sapp_emsc_target) { + console.warn("sokol_app.h: can't find html5_canvas_selector ", target_selector_str); + } + if (!Module.sapp_emsc_target.requestPointerLock) { + console.warn("sokol_app.h: target doesn't support requestPointerLock: ", target_selector_str); + } +}) + +_SOKOL_PRIVATE EM_BOOL _sapp_tc_emsc_pointerlockchange_cb(int emsc_type, const EmscriptenPointerlockChangeEvent* emsc_event, void* user_data) { + _SOKOL_UNUSED(emsc_type); + _SOKOL_UNUSED(user_data); + _sapp_tc.mouse.locked = emsc_event->isActive; + return EM_TRUE; +} + +_SOKOL_PRIVATE EM_BOOL _sapp_tc_emsc_pointerlockerror_cb(int emsc_type, const void* reserved, void* user_data) { + _SOKOL_UNUSED(emsc_type); + _SOKOL_UNUSED(reserved); + _SOKOL_UNUSED(user_data); + _sapp_tc.mouse.locked = false; + _sapp_tc.emsc.mouse_lock_requested = false; + return true; +} + +EM_JS(void, _sapp_tc_js_request_pointerlock, (void), { + if (Module.sapp_emsc_target) { + if (Module.sapp_emsc_target.requestPointerLock) { + Module.sapp_emsc_target.requestPointerLock(); + } + } +}) + +EM_JS(void, _sapp_tc_js_exit_pointerlock, (void), { + if (document.exitPointerLock) { + document.exitPointerLock(); + } +}) + +_SOKOL_PRIVATE void _sapp_tc_emsc_lock_mouse(bool lock) { + if (lock) { + /* request mouse-lock during event handler invocation (see _sapp_tc_emsc_update_mouse_lock_state) */ + _sapp_tc.emsc.mouse_lock_requested = true; + } else { + /* NOTE: the _sapp_tc.mouse_locked state will be set in the pointerlockchange callback */ + _sapp_tc.emsc.mouse_lock_requested = false; + _sapp_tc_js_exit_pointerlock(); + } +} + +/* called from inside event handlers to check if mouse lock had been requested, + and if yes, actually enter mouse lock. +*/ +_SOKOL_PRIVATE void _sapp_tc_emsc_update_mouse_lock_state(void) { + if (_sapp_tc.emsc.mouse_lock_requested) { + _sapp_tc.emsc.mouse_lock_requested = false; + _sapp_tc_js_request_pointerlock(); + } +} + +// set mouse cursor type +EM_JS(void, _sapp_tc_js_set_cursor, (int cursor_type, int shown, int use_custom_cursor_image), { + if (Module.sapp_emsc_target) { + let cursor; + if (shown === 0) { + cursor = "none"; + } else if (use_custom_cursor_image != 0) { + cursor = Module.__sapp_tc_custom_cursors[cursor_type].css_property; + } else switch (cursor_type) { + case 0: cursor = "auto"; break; // SAPP_MOUSECURSOR_DEFAULT + case 1: cursor = "default"; break; // SAPP_MOUSECURSOR_ARROW + case 2: cursor = "text"; break; // SAPP_MOUSECURSOR_IBEAM + case 3: cursor = "crosshair"; break; // SAPP_MOUSECURSOR_CROSSHAIR + case 4: cursor = "pointer"; break; // SAPP_MOUSECURSOR_POINTING_HAND + case 5: cursor = "ew-resize"; break; // SAPP_MOUSECURSOR_RESIZE_EW + case 6: cursor = "ns-resize"; break; // SAPP_MOUSECURSOR_RESIZE_NS + case 7: cursor = "nwse-resize"; break; // SAPP_MOUSECURSOR_RESIZE_NWSE + case 8: cursor = "nesw-resize"; break; // SAPP_MOUSECURSOR_RESIZE_NESW + case 9: cursor = "all-scroll"; break; // SAPP_MOUSECURSOR_RESIZE_ALL + case 10: cursor = "not-allowed"; break; // SAPP_MOUSECURSOR_NOT_ALLOWED + default: cursor = "auto"; break; + } + Module.sapp_emsc_target.style.cursor = cursor; + } +}) + +_SOKOL_PRIVATE void _sapp_tc_emsc_update_cursor(sapp_mouse_cursor cursor, bool shown) { + SOKOL_ASSERT((cursor >= 0) && (cursor < _SAPP_MOUSECURSOR_NUM)); + bool custom_cursor = _sapp_tc.custom_cursor_bound[cursor]; + _sapp_tc_js_set_cursor((int)cursor, shown ? 1 : 0, custom_cursor ? 1 : 0); +} + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdollar-in-identifier-extension" +EM_JS(void, _sapp_tc_js_make_custom_mouse_cursor, (int cursor_slot_idx, int width, int height, const void* pixels_ptr, int hotspot_x, int hotspot_y), { + // encode the cursor pixels into a BMP which then is encoded into an 'object url' + const bmp_hdr_size = 14; + const dib_hdr_size = 124; // common values are 56, I saw 124 for the rgba32-1.bmp file of the test suite included in firefox, and 108 from wikipedia example 2 (transparent) + const pixels_size = width * height * 4; + const bmp_size = bmp_hdr_size + dib_hdr_size + pixels_size; + const bmp = new Uint8Array(bmp_size); + let idx = 0; + const w8 = (val) => { + bmp[idx++] = val & 255; + }; + const w16 = (val) => { + bmp[idx++] = val & 255; + bmp[idx++] = (val >> 8) & 255; + }; + const w32 = (val) => { + bmp[idx++] = val & 255; + bmp[idx++] = (val >> 8) & 255; + bmp[idx++] = (val >> 16) & 255; + bmp[idx++] = (val >> 24) & 255; + }; + + // bmp file header + w8(66); // 'B' + w8(77); // 'M' + w32(bmp_size); + w32(0); // reserved + w32(bmp_hdr_size + dib_hdr_size); // offset to pixel data + assert(idx == bmp_hdr_size); + + // DIB header + w32(dib_hdr_size); // header size + w32(width); + w32(height); + w16(1); // planes + w16(32); // bits per pixel + w32(3); // compression method. 3 = BI_BITFIELDS + w32(pixels_size); // image size + w32(2835); // pixel per metre horizontal + w32(2835); // pixel per metre vertical + w32(0); // colors number + w32(0); // important colors + w32(0x000000ff); // red channel bit mask (big endian) + w32(0x0000ff00); // green channel bit mask (big endian) + w32(0x00ff0000); // blue channel bit mask (big endian) + w32(0xff000000); // alpha channel bit mask (big endian) + w8(66); w8(71); w8(82); w8(115); // color space type: 'sRGB' + idx += 64; // color space stuff, unused for 'Win ' or 'sRGB' + assert(idx == bmp_hdr_size + dib_hdr_size); + const row_pitch = width * 4; + for (let y = 0; y < height; y++) { + const src_idx = pixels_ptr + y * row_pitch; + const dst_idx = idx + (height - y - 1) * row_pitch; + const row_data = HEAPU8.slice(src_idx, src_idx + row_pitch); + bmp.set(row_data, dst_idx); + } + const blob = new Blob([bmp.buffer], { type: 'image/bmp' }); + const url = URL.createObjectURL(blob); + + const cursor_slot = { + css_property: `url('${url}') ${hotspot_x} ${hotspot_y}, auto`, + blob_url: url // so we can release it later + }; + + // Store a reference to the js cursor object in a global table, indexed by its sapp_mouse_cursor + if (!Module.__sapp_tc_custom_cursors) { + Module.__sapp_tc_custom_cursors = Array().fill(null); + } + Module.__sapp_tc_custom_cursors[cursor_slot_idx] = cursor_slot; +}) + +#pragma GCC diagnostic pop +EM_JS(void, _sapp_tc_js_destroy_custom_mouse_cursor, (int cursor_slot_idx), { + if (Module.__sapp_tc_custom_cursors) { + const cursor = Module.__sapp_tc_custom_cursors[cursor_slot_idx]; + URL.revokeObjectURL(cursor.blob_url); // release the url, which should allow the blob to be garbage collected. + Module.__sapp_tc_custom_cursors[cursor_slot_idx] = null; // clear this array entry + } +}) + +_SOKOL_PRIVATE bool _sapp_tc_emsc_make_custom_mouse_cursor(sapp_mouse_cursor cursor, const sapp_image_desc* desc) { + _sapp_tc_js_make_custom_mouse_cursor((int)cursor, desc->width, desc->height, desc->pixels.ptr, desc->cursor_hotspot_x, desc->cursor_hotspot_y); + return true; +} + +_SOKOL_PRIVATE void _sapp_tc_emsc_destroy_custom_mouse_cursor(sapp_mouse_cursor cursor) { + _sapp_tc_js_destroy_custom_mouse_cursor((int) cursor); +} + +// NOTE: this callback is needed to react to the user actively leaving fullscreen mode via Esc +_SOKOL_PRIVATE EM_BOOL _sapp_tc_emsc_fullscreenchange_cb(int emsc_type, const EmscriptenFullscreenChangeEvent* emsc_event, void* user_data) { + _SOKOL_UNUSED(emsc_type); + _SOKOL_UNUSED(user_data); + _sapp_tc.fullscreen = emsc_event->isFullscreen; + return true; +} + +EM_JS(void, _sapp_tc_js_toggle_fullscreen, (void), { + const canvas = Module.sapp_emsc_target; + if (canvas) { + // NOTE: Safari had the prefix until 2023, Firefox until 2018 + const fullscreenElement = document.fullscreenElement || document.webkitFullscreenElement || document.mozFullScreenElement; + let p = undefined; + if (!fullscreenElement) { + if (canvas.requestFullscreen) { + p = canvas.requestFullscreen(); + } else if (canvas.webkitRequestFullscreen) { + p = canvas.webkitRequestFullscreen(); + } else if (canvas.mozRequestFullScreen) { + p = canvas.mozRequestFullScreen(); + } + if (p) { + p.catch((err) => { + console.warn('_sapp_tc_js_toggle_fullscreen(): failed to enter fullscreen mode with', err); + __sapp_tc_emsc_set_fullscreen_flag(0); + }); + } else { + console.warn('_sapp_tc_js_toogle_fullscreen(): browser has no [webkit|moz]requestFullscreen function'); + __sapp_tc_emsc_set_fullscreen_flag(0); + } + } else { + if (document.exitFullscreen) { + p = document.exitFullscreen(); + } else if (document.webkitExitFullscreen) { + p = document.webkitExitFullscreen(); + } else if (document.mozCancelFullScreen) { + p = document.mozCancelFullScreen(); + } + if (p) { + p.catch((err) => { + console.warn('_sapp_tc_js_toggle_fullscreen(): failed to exit fullscreen mode with', err); + __sapp_tc_emsc_set_fullscreen_flag(1); + }); + } else { + console.warn('_sapp_tc_js_toggle_fullscreen(): browser has no [wekbit|moz]exitFullscreen'); + // NOTE: don't need to explicitly set the fullscreen flag here + } + } + } +}) + +_SOKOL_PRIVATE void _sapp_tc_emsc_toggle_fullscreen(void) { + // toggle the fullscreen flag preliminary, this may be undone + // when requesting/exiting fullscreen mode actually fails + _sapp_tc.fullscreen = !_sapp_tc.fullscreen; + _sapp_tc_js_toggle_fullscreen(); +} + +/* JS helper functions to update browser tab favicon */ +EM_JS(void, _sapp_tc_js_clear_favicon, (void), { + const link = document.getElementById('sokol-app-favicon'); + if (link) { + document.head.removeChild(link); + } +}) + +EM_JS(void, _sapp_tc_js_set_favicon, (int w, int h, const uint8_t* pixels), { + const canvas = document.createElement('canvas'); + canvas.width = w; + canvas.height = h; + const ctx = canvas.getContext('2d'); + const img_data = ctx.createImageData(w, h); + img_data.data.set(HEAPU8.subarray(pixels, pixels + w*h*4)); + ctx.putImageData(img_data, 0, 0); + const new_link = document.createElement('link'); + new_link.id = 'sokol-app-favicon'; + new_link.rel = 'shortcut icon'; + new_link.href = canvas.toDataURL(); + document.head.appendChild(new_link); +}) + +_SOKOL_PRIVATE void _sapp_tc_emsc_set_icon(const sapp_icon_desc* icon_desc, int num_images) { + SOKOL_ASSERT((num_images > 0) && (num_images <= SAPP_MAX_ICONIMAGES)); + _sapp_tc_js_clear_favicon(); + // find the best matching image candidate for 16x16 pixels + int img_index = _sapp_tc_image_bestmatch(icon_desc->images, num_images, 16, 16); + const sapp_image_desc* img_desc = &icon_desc->images[img_index]; + _sapp_tc_js_set_favicon(img_desc->width, img_desc->height, (const uint8_t*) img_desc->pixels.ptr); +} + +_SOKOL_PRIVATE uint32_t _sapp_tc_emsc_mouse_button_mods(uint16_t buttons) { + uint32_t m = 0; + if (0 != (buttons & (1<<0))) { m |= SAPP_MODIFIER_LMB; } + if (0 != (buttons & (1<<1))) { m |= SAPP_MODIFIER_RMB; } // not a bug + if (0 != (buttons & (1<<2))) { m |= SAPP_MODIFIER_MMB; } // not a bug + return m; +} + +_SOKOL_PRIVATE uint32_t _sapp_tc_emsc_mouse_event_mods(const EmscriptenMouseEvent* ev) { + uint32_t m = 0; + if (ev->ctrlKey) { m |= SAPP_MODIFIER_CTRL; } + if (ev->shiftKey) { m |= SAPP_MODIFIER_SHIFT; } + if (ev->altKey) { m |= SAPP_MODIFIER_ALT; } + if (ev->metaKey) { m |= SAPP_MODIFIER_SUPER; } + m |= _sapp_tc_emsc_mouse_button_mods(_sapp_tc.emsc.mouse_buttons); + return m; +} + +_SOKOL_PRIVATE uint32_t _sapp_tc_emsc_key_event_mods(const EmscriptenKeyboardEvent* ev) { + uint32_t m = 0; + if (ev->ctrlKey) { m |= SAPP_MODIFIER_CTRL; } + if (ev->shiftKey) { m |= SAPP_MODIFIER_SHIFT; } + if (ev->altKey) { m |= SAPP_MODIFIER_ALT; } + if (ev->metaKey) { m |= SAPP_MODIFIER_SUPER; } + m |= _sapp_tc_emsc_mouse_button_mods(_sapp_tc.emsc.mouse_buttons); + return m; +} + +_SOKOL_PRIVATE uint32_t _sapp_tc_emsc_touch_event_mods(const EmscriptenTouchEvent* ev) { + uint32_t m = 0; + if (ev->ctrlKey) { m |= SAPP_MODIFIER_CTRL; } + if (ev->shiftKey) { m |= SAPP_MODIFIER_SHIFT; } + if (ev->altKey) { m |= SAPP_MODIFIER_ALT; } + if (ev->metaKey) { m |= SAPP_MODIFIER_SUPER; } + m |= _sapp_tc_emsc_mouse_button_mods(_sapp_tc.emsc.mouse_buttons); + return m; +} + +_SOKOL_PRIVATE EM_BOOL _sapp_tc_emsc_size_changed(int event_type, const EmscriptenUiEvent* ui_event, void* user_data) { + _SOKOL_UNUSED(event_type); + _SOKOL_UNUSED(user_data); + double w, h; + emscripten_get_element_css_size(_sapp_tc.html5_canvas_selector, &w, &h); + /* The above method might report zero when toggling HTML5 fullscreen, + in that case use the window's inner width reported by the + emscripten event. This works ok when toggling *into* fullscreen + but doesn't properly restore the previous canvas size when switching + back from fullscreen. + + In general, due to the HTML5's fullscreen API's flaky nature it is + recommended to use 'soft fullscreen' (stretching the WebGL canvas + over the browser windows client rect) with a CSS definition like this: + + position: absolute; + top: 0px; + left: 0px; + margin: 0px; + border: 0; + width: 100%; + height: 100%; + overflow: hidden; + display: block; + */ + if (w < 1.0) { + w = ui_event->windowInnerWidth; + } else { + _sapp_tc.window_width = _sapp_tc_roundf_gzero(w); + } + if (h < 1.0) { + h = ui_event->windowInnerHeight; + } else { + _sapp_tc.window_height = _sapp_tc_roundf_gzero(h); + } + if (_sapp_tc.desc.high_dpi) { + _sapp_tc.dpi_scale = emscripten_get_device_pixel_ratio(); + } + _sapp_tc.framebuffer_width = _sapp_tc_roundf_gzero(w * _sapp_tc.dpi_scale); + _sapp_tc.framebuffer_height = _sapp_tc_roundf_gzero(h * _sapp_tc.dpi_scale); + emscripten_set_canvas_element_size(_sapp_tc.html5_canvas_selector, _sapp_tc.framebuffer_width, _sapp_tc.framebuffer_height); + #if defined(SOKOL_WGPU) + // on WebGPU: recreate size-dependent rendering surfaces + _sapp_tc_wgpu_swapchain_size_changed(); + #endif + if (_sapp_tc_events_enabled()) { + _sapp_tc_init_event(SAPP_EVENTTYPE_RESIZED); + _sapp_tc_call_event(&_sapp_tc.event); + } + return true; +} + +_SOKOL_PRIVATE EM_BOOL _sapp_tc_emsc_mouse_cb(int emsc_type, const EmscriptenMouseEvent* emsc_event, void* user_data) { + _SOKOL_UNUSED(user_data); + bool consume_event = !_sapp_tc.desc.html5.bubble_mouse_events; + _sapp_tc.emsc.mouse_buttons = emsc_event->buttons; + if (_sapp_tc.mouse.locked) { + _sapp_tc.mouse.dx = (float) emsc_event->movementX; + _sapp_tc.mouse.dy = (float) emsc_event->movementY; + } else { + float new_x = emsc_event->targetX * _sapp_tc.dpi_scale; + float new_y = emsc_event->targetY * _sapp_tc.dpi_scale; + if (_sapp_tc.mouse.pos_valid) { + _sapp_tc.mouse.dx = new_x - _sapp_tc.mouse.x; + _sapp_tc.mouse.dy = new_y - _sapp_tc.mouse.y; + } + _sapp_tc.mouse.x = new_x; + _sapp_tc.mouse.y = new_y; + _sapp_tc.mouse.pos_valid = true; + } + if (_sapp_tc_events_enabled() && (emsc_event->button >= 0) && (emsc_event->button < SAPP_MAX_MOUSEBUTTONS)) { + sapp_event_type type; + bool is_button_event = false; + bool clear_dxdy = false; + switch (emsc_type) { + case EMSCRIPTEN_EVENT_MOUSEDOWN: + type = SAPP_EVENTTYPE_MOUSE_DOWN; + is_button_event = true; + break; + case EMSCRIPTEN_EVENT_MOUSEUP: + type = SAPP_EVENTTYPE_MOUSE_UP; + is_button_event = true; + break; + case EMSCRIPTEN_EVENT_MOUSEMOVE: + type = SAPP_EVENTTYPE_MOUSE_MOVE; + break; + case EMSCRIPTEN_EVENT_MOUSEENTER: + type = SAPP_EVENTTYPE_MOUSE_ENTER; + clear_dxdy = true; + break; + case EMSCRIPTEN_EVENT_MOUSELEAVE: + type = SAPP_EVENTTYPE_MOUSE_LEAVE; + clear_dxdy = true; + break; + default: + type = SAPP_EVENTTYPE_INVALID; + break; + } + if (clear_dxdy) { + _sapp_tc.mouse.dx = 0.0f; + _sapp_tc.mouse.dy = 0.0f; + } + if (type != SAPP_EVENTTYPE_INVALID) { + _sapp_tc_init_event(type); + _sapp_tc.event.modifiers = _sapp_tc_emsc_mouse_event_mods(emsc_event); + if (is_button_event) { + switch (emsc_event->button) { + case 0: _sapp_tc.event.mouse_button = SAPP_MOUSEBUTTON_LEFT; break; + case 1: _sapp_tc.event.mouse_button = SAPP_MOUSEBUTTON_MIDDLE; break; + case 2: _sapp_tc.event.mouse_button = SAPP_MOUSEBUTTON_RIGHT; break; + default: _sapp_tc.event.mouse_button = (sapp_mousebutton)emsc_event->button; break; + } + } else { + _sapp_tc.event.mouse_button = SAPP_MOUSEBUTTON_INVALID; + } + consume_event |= _sapp_tc_call_event(&_sapp_tc.event); + } + // mouse lock can only be activated in mouse button events (not in move, enter or leave) + if (is_button_event) { + _sapp_tc_emsc_update_mouse_lock_state(); + } + } + return consume_event; +} + +_SOKOL_PRIVATE EM_BOOL _sapp_tc_emsc_wheel_cb(int emsc_type, const EmscriptenWheelEvent* emsc_event, void* user_data) { + _SOKOL_UNUSED(emsc_type); + _SOKOL_UNUSED(user_data); + bool consume_event = !_sapp_tc.desc.html5.bubble_wheel_events; + _sapp_tc.emsc.mouse_buttons = emsc_event->mouse.buttons; + if (_sapp_tc_events_enabled()) { + _sapp_tc_init_event(SAPP_EVENTTYPE_MOUSE_SCROLL); + _sapp_tc.event.modifiers = _sapp_tc_emsc_mouse_event_mods(&emsc_event->mouse); + /* see https://github.com/floooh/sokol/issues/339 */ + float scale; + switch (emsc_event->deltaMode) { + case DOM_DELTA_PIXEL: scale = -0.01f; break; + case DOM_DELTA_LINE: scale = -1.33f; break; + case DOM_DELTA_PAGE: scale = -10.0f; break; // FIXME: this is a guess + default: scale = -0.1f; break; // shouldn't happen + } + _sapp_tc.event.scroll_x = scale * (float)emsc_event->deltaX; + _sapp_tc.event.scroll_y = scale * (float)emsc_event->deltaY; + consume_event |= _sapp_tc_call_event(&_sapp_tc.event); + } + _sapp_tc_emsc_update_mouse_lock_state(); + return consume_event; +} + +static struct { + const char* str; + sapp_keycode code; +} _sapp_tc_emsc_keymap[] = { + { "Backspace", SAPP_KEYCODE_BACKSPACE }, + { "Tab", SAPP_KEYCODE_TAB }, + { "Enter", SAPP_KEYCODE_ENTER }, + { "ShiftLeft", SAPP_KEYCODE_LEFT_SHIFT }, + { "ShiftRight", SAPP_KEYCODE_RIGHT_SHIFT }, + { "ControlLeft", SAPP_KEYCODE_LEFT_CONTROL }, + { "ControlRight", SAPP_KEYCODE_RIGHT_CONTROL }, + { "AltLeft", SAPP_KEYCODE_LEFT_ALT }, + { "AltRight", SAPP_KEYCODE_RIGHT_ALT }, + { "Pause", SAPP_KEYCODE_PAUSE }, + { "CapsLock", SAPP_KEYCODE_CAPS_LOCK }, + { "Escape", SAPP_KEYCODE_ESCAPE }, + { "Space", SAPP_KEYCODE_SPACE }, + { "PageUp", SAPP_KEYCODE_PAGE_UP }, + { "PageDown", SAPP_KEYCODE_PAGE_DOWN }, + { "End", SAPP_KEYCODE_END }, + { "Home", SAPP_KEYCODE_HOME }, + { "ArrowLeft", SAPP_KEYCODE_LEFT }, + { "ArrowUp", SAPP_KEYCODE_UP }, + { "ArrowRight", SAPP_KEYCODE_RIGHT }, + { "ArrowDown", SAPP_KEYCODE_DOWN }, + { "PrintScreen", SAPP_KEYCODE_PRINT_SCREEN }, + { "Insert", SAPP_KEYCODE_INSERT }, + { "Delete", SAPP_KEYCODE_DELETE }, + { "Digit0", SAPP_KEYCODE_0 }, + { "Digit1", SAPP_KEYCODE_1 }, + { "Digit2", SAPP_KEYCODE_2 }, + { "Digit3", SAPP_KEYCODE_3 }, + { "Digit4", SAPP_KEYCODE_4 }, + { "Digit5", SAPP_KEYCODE_5 }, + { "Digit6", SAPP_KEYCODE_6 }, + { "Digit7", SAPP_KEYCODE_7 }, + { "Digit8", SAPP_KEYCODE_8 }, + { "Digit9", SAPP_KEYCODE_9 }, + { "KeyA", SAPP_KEYCODE_A }, + { "KeyB", SAPP_KEYCODE_B }, + { "KeyC", SAPP_KEYCODE_C }, + { "KeyD", SAPP_KEYCODE_D }, + { "KeyE", SAPP_KEYCODE_E }, + { "KeyF", SAPP_KEYCODE_F }, + { "KeyG", SAPP_KEYCODE_G }, + { "KeyH", SAPP_KEYCODE_H }, + { "KeyI", SAPP_KEYCODE_I }, + { "KeyJ", SAPP_KEYCODE_J }, + { "KeyK", SAPP_KEYCODE_K }, + { "KeyL", SAPP_KEYCODE_L }, + { "KeyM", SAPP_KEYCODE_M }, + { "KeyN", SAPP_KEYCODE_N }, + { "KeyO", SAPP_KEYCODE_O }, + { "KeyP", SAPP_KEYCODE_P }, + { "KeyQ", SAPP_KEYCODE_Q }, + { "KeyR", SAPP_KEYCODE_R }, + { "KeyS", SAPP_KEYCODE_S }, + { "KeyT", SAPP_KEYCODE_T }, + { "KeyU", SAPP_KEYCODE_U }, + { "KeyV", SAPP_KEYCODE_V }, + { "KeyW", SAPP_KEYCODE_W }, + { "KeyX", SAPP_KEYCODE_X }, + { "KeyY", SAPP_KEYCODE_Y }, + { "KeyZ", SAPP_KEYCODE_Z }, + { "MetaLeft", SAPP_KEYCODE_LEFT_SUPER }, + { "MetaRight", SAPP_KEYCODE_RIGHT_SUPER }, + { "Numpad0", SAPP_KEYCODE_KP_0 }, + { "Numpad1", SAPP_KEYCODE_KP_1 }, + { "Numpad2", SAPP_KEYCODE_KP_2 }, + { "Numpad3", SAPP_KEYCODE_KP_3 }, + { "Numpad4", SAPP_KEYCODE_KP_4 }, + { "Numpad5", SAPP_KEYCODE_KP_5 }, + { "Numpad6", SAPP_KEYCODE_KP_6 }, + { "Numpad7", SAPP_KEYCODE_KP_7 }, + { "Numpad8", SAPP_KEYCODE_KP_8 }, + { "Numpad9", SAPP_KEYCODE_KP_9 }, + { "NumpadMultiply", SAPP_KEYCODE_KP_MULTIPLY }, + { "NumpadAdd", SAPP_KEYCODE_KP_ADD }, + { "NumpadSubtract", SAPP_KEYCODE_KP_SUBTRACT }, + { "NumpadDecimal", SAPP_KEYCODE_KP_DECIMAL }, + { "NumpadDivide", SAPP_KEYCODE_KP_DIVIDE }, + { "F1", SAPP_KEYCODE_F1 }, + { "F2", SAPP_KEYCODE_F2 }, + { "F3", SAPP_KEYCODE_F3 }, + { "F4", SAPP_KEYCODE_F4 }, + { "F5", SAPP_KEYCODE_F5 }, + { "F6", SAPP_KEYCODE_F6 }, + { "F7", SAPP_KEYCODE_F7 }, + { "F8", SAPP_KEYCODE_F8 }, + { "F9", SAPP_KEYCODE_F9 }, + { "F10", SAPP_KEYCODE_F10 }, + { "F11", SAPP_KEYCODE_F11 }, + { "F12", SAPP_KEYCODE_F12 }, + { "NumLock", SAPP_KEYCODE_NUM_LOCK }, + { "ScrollLock", SAPP_KEYCODE_SCROLL_LOCK }, + { "Semicolon", SAPP_KEYCODE_SEMICOLON }, + { "Equal", SAPP_KEYCODE_EQUAL }, + { "Comma", SAPP_KEYCODE_COMMA }, + { "Minus", SAPP_KEYCODE_MINUS }, + { "Period", SAPP_KEYCODE_PERIOD }, + { "Slash", SAPP_KEYCODE_SLASH }, + { "Backquote", SAPP_KEYCODE_GRAVE_ACCENT }, + { "BracketLeft", SAPP_KEYCODE_LEFT_BRACKET }, + { "Backslash", SAPP_KEYCODE_BACKSLASH }, + { "BracketRight", SAPP_KEYCODE_RIGHT_BRACKET }, + { "Quote", SAPP_KEYCODE_GRAVE_ACCENT }, // FIXME: ??? + { 0, SAPP_KEYCODE_INVALID }, +}; + +_SOKOL_PRIVATE sapp_keycode _sapp_tc_emsc_translate_key(const char* str) { + int i = 0; + const char* keystr; + while (( keystr = _sapp_tc_emsc_keymap[i].str )) { + if (0 == strcmp(str, keystr)) { + return _sapp_tc_emsc_keymap[i].code; + } + i += 1; + } + return SAPP_KEYCODE_INVALID; +} + +// returns true if the key code is a 'character key', this is used to decide +// if a key event needs to bubble up to create a char event +_SOKOL_PRIVATE bool _sapp_tc_emsc_is_char_key(sapp_keycode key_code) { + return key_code < SAPP_KEYCODE_WORLD_1; +} + +_SOKOL_PRIVATE EM_BOOL _sapp_tc_emsc_key_cb(int emsc_type, const EmscriptenKeyboardEvent* emsc_event, void* user_data) { + _SOKOL_UNUSED(user_data); + bool consume_event = false; + if (_sapp_tc_events_enabled()) { + sapp_event_type type; + switch (emsc_type) { + case EMSCRIPTEN_EVENT_KEYDOWN: + type = SAPP_EVENTTYPE_KEY_DOWN; + break; + case EMSCRIPTEN_EVENT_KEYUP: + type = SAPP_EVENTTYPE_KEY_UP; + break; + case EMSCRIPTEN_EVENT_KEYPRESS: + type = SAPP_EVENTTYPE_CHAR; + break; + default: + type = SAPP_EVENTTYPE_INVALID; + break; + } + if (type != SAPP_EVENTTYPE_INVALID) { + bool send_keyup_followup = false; + _sapp_tc_init_event(type); + _sapp_tc.event.key_repeat = emsc_event->repeat; + _sapp_tc.event.modifiers = _sapp_tc_emsc_key_event_mods(emsc_event); + if (type == SAPP_EVENTTYPE_CHAR) { + // NOTE: charCode doesn't appear to be supported on Android Chrome + _sapp_tc.event.char_code = emsc_event->charCode; + consume_event |= !_sapp_tc.desc.html5.bubble_char_events; + } else { + if (0 != emsc_event->code[0]) { + // This code path is for desktop browsers which send untranslated 'physical' key code strings + // (which is what we actually want for key events) + _sapp_tc.event.key_code = _sapp_tc_emsc_translate_key(emsc_event->code); + } else { + // This code path is for mobile browsers which only send localized key code + // strings. Note that the translation will only work for a small subset + // of localization-agnostic keys (like Enter, arrow keys, etc...), but + // regular alpha-numeric keys will all result in an SAPP_KEYCODE_INVALID) + _sapp_tc.event.key_code = _sapp_tc_emsc_translate_key(emsc_event->key); + } + + // Special hack for macOS: if the Super key is pressed, macOS doesn't + // send keyUp events. As a workaround, to prevent keys from + // "sticking", we'll send a keyup event following a keydown + // when the SUPER key is pressed + if ((type == SAPP_EVENTTYPE_KEY_DOWN) && + (_sapp_tc.event.key_code != SAPP_KEYCODE_LEFT_SUPER) && + (_sapp_tc.event.key_code != SAPP_KEYCODE_RIGHT_SUPER) && + (_sapp_tc.event.modifiers & SAPP_MODIFIER_SUPER)) + { + send_keyup_followup = true; + } + + // 'character key events' will always need to bubble up, otherwise the browser + // wouldn't be able to generate character events. + if (!_sapp_tc_emsc_is_char_key(_sapp_tc.event.key_code)) { + consume_event |= !_sapp_tc.desc.html5.bubble_key_events; + } + } + consume_event |= _sapp_tc_call_event(&_sapp_tc.event); + if (send_keyup_followup) { + _sapp_tc.event.type = SAPP_EVENTTYPE_KEY_UP; + consume_event |= _sapp_tc_call_event(&_sapp_tc.event); + } + } + } + _sapp_tc_emsc_update_mouse_lock_state(); + return consume_event; +} + +_SOKOL_PRIVATE EM_BOOL _sapp_tc_emsc_touch_cb(int emsc_type, const EmscriptenTouchEvent* emsc_event, void* user_data) { + _SOKOL_UNUSED(user_data); + bool consume_event = !_sapp_tc.desc.html5.bubble_touch_events; + if (_sapp_tc_events_enabled()) { + sapp_event_type type; + switch (emsc_type) { + case EMSCRIPTEN_EVENT_TOUCHSTART: + type = SAPP_EVENTTYPE_TOUCHES_BEGAN; + break; + case EMSCRIPTEN_EVENT_TOUCHMOVE: + type = SAPP_EVENTTYPE_TOUCHES_MOVED; + break; + case EMSCRIPTEN_EVENT_TOUCHEND: + type = SAPP_EVENTTYPE_TOUCHES_ENDED; + break; + case EMSCRIPTEN_EVENT_TOUCHCANCEL: + type = SAPP_EVENTTYPE_TOUCHES_CANCELLED; + break; + default: + type = SAPP_EVENTTYPE_INVALID; + break; + } + if (type != SAPP_EVENTTYPE_INVALID) { + _sapp_tc_init_event(type); + _sapp_tc.event.modifiers = _sapp_tc_emsc_touch_event_mods(emsc_event); + _sapp_tc.event.num_touches = emsc_event->numTouches; + if (_sapp_tc.event.num_touches > SAPP_MAX_TOUCHPOINTS) { + _sapp_tc.event.num_touches = SAPP_MAX_TOUCHPOINTS; + } + for (int i = 0; i < _sapp_tc.event.num_touches; i++) { + const EmscriptenTouchPoint* src = &emsc_event->touches[i]; + sapp_touchpoint* dst = &_sapp_tc.event.touches[i]; + dst->identifier = (uintptr_t)src->identifier; + dst->pos_x = src->targetX * _sapp_tc.dpi_scale; + dst->pos_y = src->targetY * _sapp_tc.dpi_scale; + dst->changed = src->isChanged; + } + consume_event |= _sapp_tc_call_event(&_sapp_tc.event); + } + } + return consume_event; +} + +_SOKOL_PRIVATE EM_BOOL _sapp_tc_emsc_focus_cb(int emsc_type, const EmscriptenFocusEvent* emsc_event, void* user_data) { + _SOKOL_UNUSED(emsc_type); + _SOKOL_UNUSED(emsc_event); + _SOKOL_UNUSED(user_data); + if (_sapp_tc_events_enabled()) { + _sapp_tc_init_event(SAPP_EVENTTYPE_FOCUSED); + _sapp_tc_call_event(&_sapp_tc.event); + } + return true; +} + +_SOKOL_PRIVATE EM_BOOL _sapp_tc_emsc_blur_cb(int emsc_type, const EmscriptenFocusEvent* emsc_event, void* user_data) { + _SOKOL_UNUSED(emsc_type); + _SOKOL_UNUSED(emsc_event); + _SOKOL_UNUSED(user_data); + if (_sapp_tc_events_enabled()) { + _sapp_tc_init_event(SAPP_EVENTTYPE_UNFOCUSED); + _sapp_tc_call_event(&_sapp_tc.event); + } + return true; +} + +#if defined(SOKOL_GLES3) +_SOKOL_PRIVATE EM_BOOL _sapp_tc_emsc_webgl_context_cb(int emsc_type, const void* reserved, void* user_data) { + _SOKOL_UNUSED(reserved); + _SOKOL_UNUSED(user_data); + sapp_event_type type; + switch (emsc_type) { + case EMSCRIPTEN_EVENT_WEBGLCONTEXTLOST: type = SAPP_EVENTTYPE_SUSPENDED; break; + case EMSCRIPTEN_EVENT_WEBGLCONTEXTRESTORED: type = SAPP_EVENTTYPE_RESUMED; break; + default: type = SAPP_EVENTTYPE_INVALID; break; + } + if (_sapp_tc_events_enabled() && (SAPP_EVENTTYPE_INVALID != type)) { + _sapp_tc_init_event(type); + _sapp_tc_call_event(&_sapp_tc.event); + } + return true; +} + +_SOKOL_PRIVATE void _sapp_tc_emsc_webgl_init(void) { + EmscriptenWebGLContextAttributes attrs; + emscripten_webgl_init_context_attributes(&attrs); + attrs.alpha = _sapp_tc.desc.alpha; + attrs.depth = true; + attrs.stencil = true; + attrs.antialias = _sapp_tc.sample_count > 1; + attrs.premultipliedAlpha = _sapp_tc.desc.html5.premultiplied_alpha; + attrs.preserveDrawingBuffer = _sapp_tc.desc.html5.preserve_drawing_buffer; + attrs.enableExtensionsByDefault = true; + attrs.majorVersion = 2; + EMSCRIPTEN_WEBGL_CONTEXT_HANDLE ctx = emscripten_webgl_create_context(_sapp_tc.html5_canvas_selector, &attrs); + // FIXME: error message? + emscripten_webgl_make_context_current(ctx); + glGetIntegerv(GL_FRAMEBUFFER_BINDING, (GLint*)&_sapp_tc.gl.framebuffer); +} +#endif + +_SOKOL_PRIVATE void _sapp_tc_emsc_register_eventhandlers(void) { + // NOTE: HTML canvas doesn't receive input focus, this is why key event handlers are added + // to the window object (this could be worked around by adding a "tab index" to the + // canvas) + emscripten_set_mousedown_callback(_sapp_tc.html5_canvas_selector, 0, true, _sapp_tc_emsc_mouse_cb); + emscripten_set_mouseup_callback(_sapp_tc.html5_canvas_selector, 0, true, _sapp_tc_emsc_mouse_cb); + emscripten_set_mousemove_callback(_sapp_tc.html5_canvas_selector, 0, true, _sapp_tc_emsc_mouse_cb); + emscripten_set_mouseenter_callback(_sapp_tc.html5_canvas_selector, 0, true, _sapp_tc_emsc_mouse_cb); + emscripten_set_mouseleave_callback(_sapp_tc.html5_canvas_selector, 0, true, _sapp_tc_emsc_mouse_cb); + emscripten_set_wheel_callback(_sapp_tc.html5_canvas_selector, 0, true, _sapp_tc_emsc_wheel_cb); + // Modified by tettou771 for TrussC: register keyboard events on canvas instead of window + // This allows other page elements (like Monaco editor) to handle keyboard events + // Canvas must have tabindex attribute to receive focus + emscripten_set_keydown_callback(_sapp_tc.html5_canvas_selector, 0, true, _sapp_tc_emsc_key_cb); + emscripten_set_keyup_callback(_sapp_tc.html5_canvas_selector, 0, true, _sapp_tc_emsc_key_cb); + emscripten_set_keypress_callback(_sapp_tc.html5_canvas_selector, 0, true, _sapp_tc_emsc_key_cb); + emscripten_set_touchstart_callback(_sapp_tc.html5_canvas_selector, 0, true, _sapp_tc_emsc_touch_cb); + emscripten_set_touchmove_callback(_sapp_tc.html5_canvas_selector, 0, true, _sapp_tc_emsc_touch_cb); + emscripten_set_touchend_callback(_sapp_tc.html5_canvas_selector, 0, true, _sapp_tc_emsc_touch_cb); + emscripten_set_touchcancel_callback(_sapp_tc.html5_canvas_selector, 0, true, _sapp_tc_emsc_touch_cb); + emscripten_set_pointerlockchange_callback(EMSCRIPTEN_EVENT_TARGET_DOCUMENT, 0, true, _sapp_tc_emsc_pointerlockchange_cb); + emscripten_set_pointerlockerror_callback(EMSCRIPTEN_EVENT_TARGET_DOCUMENT, 0, true, _sapp_tc_emsc_pointerlockerror_cb); + emscripten_set_focus_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, 0, true, _sapp_tc_emsc_focus_cb); + emscripten_set_blur_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, 0, true, _sapp_tc_emsc_blur_cb); + emscripten_set_fullscreenchange_callback(_sapp_tc.html5_canvas_selector, 0, true, _sapp_tc_emsc_fullscreenchange_cb); + _sapp_tc_js_add_beforeunload_listener(); + if (_sapp_tc.clipboard.enabled) { + _sapp_tc_js_add_clipboard_listener(); + } + if (_sapp_tc.drop.enabled) { + _sapp_tc_js_add_dragndrop_listeners(); + } + #if defined(SOKOL_GLES3) + emscripten_set_webglcontextlost_callback(_sapp_tc.html5_canvas_selector, 0, true, _sapp_tc_emsc_webgl_context_cb); + emscripten_set_webglcontextrestored_callback(_sapp_tc.html5_canvas_selector, 0, true, _sapp_tc_emsc_webgl_context_cb); + #endif +} + +_SOKOL_PRIVATE void _sapp_tc_emsc_unregister_eventhandlers(void) { + emscripten_set_mousedown_callback(_sapp_tc.html5_canvas_selector, 0, true, 0); + emscripten_set_mouseup_callback(_sapp_tc.html5_canvas_selector, 0, true, 0); + emscripten_set_mousemove_callback(_sapp_tc.html5_canvas_selector, 0, true, 0); + emscripten_set_mouseenter_callback(_sapp_tc.html5_canvas_selector, 0, true, 0); + emscripten_set_mouseleave_callback(_sapp_tc.html5_canvas_selector, 0, true, 0); + emscripten_set_wheel_callback(_sapp_tc.html5_canvas_selector, 0, true, 0); + // Modified by tettou771 for TrussC: unregister from canvas (see register above) + emscripten_set_keydown_callback(_sapp_tc.html5_canvas_selector, 0, true, 0); + emscripten_set_keyup_callback(_sapp_tc.html5_canvas_selector, 0, true, 0); + emscripten_set_keypress_callback(_sapp_tc.html5_canvas_selector, 0, true, 0); + emscripten_set_touchstart_callback(_sapp_tc.html5_canvas_selector, 0, true, 0); + emscripten_set_touchmove_callback(_sapp_tc.html5_canvas_selector, 0, true, 0); + emscripten_set_touchend_callback(_sapp_tc.html5_canvas_selector, 0, true, 0); + emscripten_set_touchcancel_callback(_sapp_tc.html5_canvas_selector, 0, true, 0); + emscripten_set_pointerlockchange_callback(EMSCRIPTEN_EVENT_TARGET_DOCUMENT, 0, true, 0); + emscripten_set_pointerlockerror_callback(EMSCRIPTEN_EVENT_TARGET_DOCUMENT, 0, true, 0); + emscripten_set_focus_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, 0, true, 0); + emscripten_set_blur_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, 0, true, 0); + emscripten_set_fullscreenchange_callback(_sapp_tc.html5_canvas_selector, 0, true, 0); + if (!_sapp_tc.desc.html5.canvas_resize) { + emscripten_set_resize_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, 0, true, 0); + } + _sapp_tc_js_remove_beforeunload_listener(); + if (_sapp_tc.clipboard.enabled) { + _sapp_tc_js_remove_clipboard_listener(); + } + if (_sapp_tc.drop.enabled) { + _sapp_tc_js_remove_dragndrop_listeners(); + } + #if defined(SOKOL_GLES3) + emscripten_set_webglcontextlost_callback(_sapp_tc.html5_canvas_selector, 0, true, 0); + emscripten_set_webglcontextrestored_callback(_sapp_tc.html5_canvas_selector, 0, true, 0); + #endif +} + +_SOKOL_PRIVATE EM_BOOL _sapp_tc_emsc_frame_animation_loop(double time, void* userData) { + _SOKOL_UNUSED(userData); + _sapp_tc_timing_update(&_sapp_tc.timing, time / 1000.0); + + #if defined(SOKOL_WGPU) + _sapp_tc_wgpu_frame(); + #else + _sapp_tc_frame(); + #endif + + // quit-handling + if (_sapp_tc.quit_requested) { + _sapp_tc_init_event(SAPP_EVENTTYPE_QUIT_REQUESTED); + _sapp_tc_call_event(&_sapp_tc.event); + if (_sapp_tc.quit_requested) { + _sapp_tc.quit_ordered = true; + } + } + if (_sapp_tc.quit_ordered) { + _sapp_tc_emsc_unregister_eventhandlers(); + #if defined(SOKOL_WGPU) + _sapp_tc_wgpu_discard(); + #endif + _sapp_tc_call_cleanup(); + _sapp_tc_discard_state(); + return EM_FALSE; + } + return EM_TRUE; +} + +_SOKOL_PRIVATE void _sapp_tc_emsc_frame_main_loop(void) { + const double time = emscripten_performance_now(); + if (!_sapp_tc_emsc_frame_animation_loop(time, 0)) { + emscripten_cancel_main_loop(); + } +} + +_SOKOL_PRIVATE void _sapp_tc_emsc_run(const sapp_desc* desc) { + _sapp_tc_init_state(desc); + _sapp_tc.fullscreen = false; // override user provided fullscreen state: can't start in fullscreen on the web! + const char* document_title = desc->html5.update_document_title ? _sapp_tc.window_title : 0; + _sapp_tc_js_init(_sapp_tc.html5_canvas_selector, document_title); + double w, h; + if (_sapp_tc.desc.html5.canvas_resize) { + w = (double) _sapp_tc_def(_sapp_tc.desc.width, _SAPP_FALLBACK_DEFAULT_WINDOW_WIDTH); + h = (double) _sapp_tc_def(_sapp_tc.desc.height, _SAPP_FALLBACK_DEFAULT_WINDOW_HEIGHT); + } else { + emscripten_get_element_css_size(_sapp_tc.html5_canvas_selector, &w, &h); + emscripten_set_resize_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, 0, false, _sapp_tc_emsc_size_changed); + } + if (_sapp_tc.desc.high_dpi) { + _sapp_tc.dpi_scale = emscripten_get_device_pixel_ratio(); + } + _sapp_tc.window_width = _sapp_tc_roundf_gzero(w); + _sapp_tc.window_height = _sapp_tc_roundf_gzero(h); + _sapp_tc.framebuffer_width = _sapp_tc_roundf_gzero(w * _sapp_tc.dpi_scale); + _sapp_tc.framebuffer_height = _sapp_tc_roundf_gzero(h * _sapp_tc.dpi_scale); + emscripten_set_canvas_element_size(_sapp_tc.html5_canvas_selector, _sapp_tc.framebuffer_width, _sapp_tc.framebuffer_height); + #if defined(SOKOL_GLES3) + _sapp_tc_emsc_webgl_init(); + #elif defined(SOKOL_WGPU) + _sapp_tc_wgpu_init(); + #endif + _sapp_tc.valid = true; + _sapp_tc_emsc_register_eventhandlers(); + sapp_set_icon(&desc->icon); + + // start the frame loop + if (_sapp_tc.desc.html5.use_emsc_set_main_loop) { + emscripten_set_main_loop(_sapp_tc_emsc_frame_main_loop, 0, _sapp_tc.desc.html5.emsc_set_main_loop_simulate_infinite_loop); + } else { + emscripten_request_animation_frame_loop(_sapp_tc_emsc_frame_animation_loop, 0); + } + // NOT A BUG: do not call _sapp_tc_discard_state() here, instead this is + // called in _sapp_tc_emsc_frame() when the application is ordered to quit +} + +#if !defined(SOKOL_NO_ENTRY) +int main(int argc, char* argv[]) { + sapp_desc desc = sokol_main(argc, argv); + _sapp_tc_emsc_run(&desc); + return 0; +} +#endif /* SOKOL_NO_ENTRY */ +#endif /* _SAPP_EMSCRIPTEN */ + +/* ---- public API (lifted from sokol_app.h >>public, web-live branches) ---- */ +#if defined(SOKOL_NO_ENTRY) +SOKOL_API_IMPL void sapp_run(const sapp_desc* desc) { + SOKOL_ASSERT(desc); + #if defined(_SAPP_MACOS) + _sapp_tc_macos_run(desc); + #elif defined(_SAPP_IOS) + _sapp_tc_ios_run(desc); + #elif defined(_SAPP_EMSCRIPTEN) + _sapp_tc_emsc_run(desc); + #elif defined(_SAPP_WIN32) + _sapp_tc_win32_run(desc); + #elif defined(_SAPP_LINUX) + _sapp_tc_linux_run(desc); + #else + #error "sapp_run() not supported on this platform" + #endif +} + +/* this is just a stub so the linker doesn't complain */ +sapp_desc sokol_main(int argc, char* argv[]) { + _SOKOL_UNUSED(argc); + _SOKOL_UNUSED(argv); + _SAPP_STRUCT(sapp_desc, desc); + return desc; +} +#else +/* likewise, in normal mode, sapp_run() is just an empty stub */ +SOKOL_API_IMPL void sapp_run(const sapp_desc* desc) { + _SOKOL_UNUSED(desc); +} +#endif + +SOKOL_API_IMPL bool sapp_isvalid(void) { + return _sapp_tc.valid; +} + +SOKOL_API_IMPL void* sapp_userdata(void) { + return _sapp_tc.desc.user_data; +} + +SOKOL_API_IMPL sapp_desc sapp_query_desc(void) { + return _sapp_tc.desc; +} + +SOKOL_API_IMPL uint64_t sapp_frame_count(void) { + return _sapp_tc.frame_count; +} + +SOKOL_API_IMPL double sapp_frame_duration(void) { + #if defined(_SAPP_MACOS) && defined(SOKOL_METAL) + return _sapp_tc_macos_mtl_timing_frame_duration(); + #elif defined(_SAPP_IOS) && defined(SOKOL_METAL) + return _sapp_tc_ios_mtl_timing_frame_duration(); + #else + return _sapp_tc_timing_get(&_sapp_tc.timing); + #endif +} + +SOKOL_API_IMPL double sapp_frame_duration_unfiltered(void) { + return _sapp_tc.timing.dt; +} + +SOKOL_API_IMPL int sapp_width(void) { + return (_sapp_tc.framebuffer_width > 0) ? _sapp_tc.framebuffer_width : 1; +} + +SOKOL_API_IMPL float sapp_widthf(void) { + return (float)sapp_width(); +} + +SOKOL_API_IMPL int sapp_height(void) { + return (_sapp_tc.framebuffer_height > 0) ? _sapp_tc.framebuffer_height : 1; +} + +SOKOL_API_IMPL float sapp_heightf(void) { + return (float)sapp_height(); +} + +SOKOL_API_IMPL sapp_pixel_format sapp_color_format(void) { + #if defined(SOKOL_WGPU) + switch (_sapp_tc.wgpu.render_format) { + case WGPUTextureFormat_RGBA8Unorm: + return SAPP_PIXELFORMAT_RGBA8; + case WGPUTextureFormat_BGRA8Unorm: + return SAPP_PIXELFORMAT_BGRA8; + default: + SOKOL_UNREACHABLE; + return SAPP_PIXELFORMAT_NONE; + } + #elif defined(SOKOL_VULKAN) + switch (_sapp_tc.vk.surface_format.format) { + case VK_FORMAT_R8G8B8A8_UNORM: + return SAPP_PIXELFORMAT_RGBA8; + case VK_FORMAT_B8G8R8A8_UNORM: + return SAPP_PIXELFORMAT_BGRA8; + default: + // FIXME! + SOKOL_UNREACHABLE; + return SAPP_PIXELFORMAT_NONE; + } + #elif defined(SOKOL_METAL) || defined(SOKOL_D3D11) + // Modified by tettou771 for TrussC: report 10-bit color format (RGB10A2) + return SAPP_PIXELFORMAT_RGB10A2; + #else + return SAPP_PIXELFORMAT_RGBA8; + #endif +} + +SOKOL_API_IMPL sapp_pixel_format sapp_depth_format(void) { + return SAPP_PIXELFORMAT_DEPTH_STENCIL; +} + +SOKOL_API_IMPL int sapp_sample_count(void) { + return _sapp_tc.sample_count; +} + +SOKOL_API_IMPL bool sapp_high_dpi(void) { + return _sapp_tc.desc.high_dpi && (_sapp_tc.dpi_scale >= 1.5f); +} + +SOKOL_API_IMPL float sapp_dpi_scale(void) { + return _sapp_tc.dpi_scale; +} + +SOKOL_API_IMPL const void* sapp_egl_get_display(void) { + SOKOL_ASSERT(_sapp_tc.valid); + #if defined(_SAPP_ANDROID) + return _sapp_tc.android.display; + #elif defined(_SAPP_LINUX) && defined(_SAPP_EGL) + return _sapp_tc.egl.display; + #else + return 0; + #endif +} + +SOKOL_API_IMPL const void* sapp_egl_get_context(void) { + SOKOL_ASSERT(_sapp_tc.valid); + #if defined(_SAPP_ANDROID) + return _sapp_tc.android.context; + #elif defined(_SAPP_LINUX) && defined(_SAPP_EGL) + return _sapp_tc.egl.context; + #else + return 0; + #endif +} + +SOKOL_API_IMPL void sapp_show_keyboard(bool show) { + #if defined(_SAPP_IOS) + _sapp_tc_ios_show_keyboard(show); + #elif defined(_SAPP_ANDROID) + _sapp_tc_android_show_keyboard(show); + #else + _SOKOL_UNUSED(show); + #endif +} + +SOKOL_API_IMPL bool sapp_keyboard_shown(void) { + return _sapp_tc.onscreen_keyboard_shown; +} + +SOKOL_API_IMPL bool sapp_is_fullscreen(void) { + return _sapp_tc.fullscreen; +} + +SOKOL_API_IMPL void sapp_toggle_fullscreen(void) { + #if defined(_SAPP_MACOS) + _sapp_tc_macos_toggle_fullscreen(); + #elif defined(_SAPP_WIN32) + _sapp_tc_win32_toggle_fullscreen(); + #elif defined(_SAPP_LINUX) + _sapp_tc_x11_toggle_fullscreen(); + #elif defined(_SAPP_EMSCRIPTEN) + _sapp_tc_emsc_toggle_fullscreen(); + #endif +} + +_SOKOL_PRIVATE void _sapp_tc_update_cursor(sapp_mouse_cursor cursor, bool shown) { + #if defined(_SAPP_MACOS) + _sapp_tc_macos_update_cursor(cursor, shown); + #elif defined(_SAPP_WIN32) + _sapp_tc_win32_update_cursor(cursor, shown, false); + #elif defined(_SAPP_LINUX) + _sapp_tc_x11_update_cursor(cursor, shown); + #elif defined(_SAPP_EMSCRIPTEN) + _sapp_tc_emsc_update_cursor(cursor, shown); + #endif + _sapp_tc.mouse.current_cursor = cursor; + _sapp_tc.mouse.shown = shown; +} + +/* NOTE that sapp_show_mouse() does not "stack" like the Win32 or macOS API functions! */ +SOKOL_API_IMPL void sapp_show_mouse(bool show) { + if (_sapp_tc.mouse.shown != show) { + _sapp_tc_update_cursor(_sapp_tc.mouse.current_cursor, show); + } +} + +SOKOL_API_IMPL bool sapp_mouse_shown(void) { + return _sapp_tc.mouse.shown; +} + +SOKOL_API_IMPL void sapp_lock_mouse(bool lock) { + #if defined(_SAPP_MACOS) + _sapp_tc_macos_lock_mouse(lock); + #elif defined(_SAPP_EMSCRIPTEN) + _sapp_tc_emsc_lock_mouse(lock); + #elif defined(_SAPP_WIN32) + _sapp_tc_win32_lock_mouse(lock); + #elif defined(_SAPP_LINUX) + _sapp_tc_x11_lock_mouse(lock); + #else + _sapp_tc.mouse.locked = lock; + #endif +} + +SOKOL_API_IMPL bool sapp_mouse_locked(void) { + return _sapp_tc.mouse.locked; +} + +SOKOL_API_IMPL void sapp_set_mouse_cursor(sapp_mouse_cursor cursor) { + SOKOL_ASSERT((cursor >= 0) && (cursor < _SAPP_MOUSECURSOR_NUM)); + if (_sapp_tc.mouse.current_cursor != cursor) { + _sapp_tc_update_cursor(cursor, _sapp_tc.mouse.shown); + } +} + +SOKOL_API_IMPL sapp_mouse_cursor sapp_get_mouse_cursor(void) { + return _sapp_tc.mouse.current_cursor; +} + +SOKOL_API_IMPL sapp_mouse_cursor sapp_bind_mouse_cursor_image(sapp_mouse_cursor cursor, const sapp_image_desc* desc) { + SOKOL_ASSERT((cursor >= 0) && (cursor < _SAPP_MOUSECURSOR_NUM)); + // NOTE: It seems that for some reason, the hotspot doesn't work if it is one less + // than the dimension of the cursor image (or more), on windows. So for a cursor + // that is 32 by 32 px, a hotspot of x = 30 works, but not x = 31. + // The cursor simply dissapears in such cases. Asserting for all platforms to make + // the behaviour consistent. + SOKOL_ASSERT(desc->cursor_hotspot_x < desc->width - 1 && desc->cursor_hotspot_y < desc->height - 1); + SOKOL_ASSERT(desc->width * desc->height * 4 == (int) desc->pixels.size); + + sapp_unbind_mouse_cursor_image(cursor); + + bool res = false; + #if defined(_SAPP_MACOS) + res = _sapp_tc_macos_make_custom_mouse_cursor(cursor, desc); + #elif defined(_SAPP_EMSCRIPTEN) + res = _sapp_tc_emsc_make_custom_mouse_cursor(cursor, desc); + #elif defined(_SAPP_WIN32) + res = _sapp_tc_win32_make_custom_mouse_cursor(cursor, desc); + #elif defined(_SAPP_LINUX) + res = _sapp_tc_x11_make_custom_mouse_cursor(cursor, desc); + #else + _SOKOL_UNUSED(desc); + #endif + _sapp_tc.custom_cursor_bound[(int)cursor] = res; + + // Update the displayed cursor in case the current cursor is the one we just bound. + if (_sapp_tc.mouse.current_cursor == cursor) { + _sapp_tc_update_cursor(cursor, _sapp_tc.mouse.shown); + } + return cursor; // returning the passed-in cursor puerly for convenience, in case you want to asign the value to a variable. +} + +SOKOL_API_IMPL void sapp_unbind_mouse_cursor_image(sapp_mouse_cursor cursor) { + SOKOL_ASSERT((cursor >= 0) && (cursor < _SAPP_MOUSECURSOR_NUM)); + if (_sapp_tc.custom_cursor_bound[(int)cursor]) { + // if this is the active cursor, first restore it to its default image, + // this must be done before attempting to destroy any cursor image + // resources which at least on win32 would fail if the cursor is still in use + _sapp_tc.custom_cursor_bound[(int)cursor] = false; + if (_sapp_tc.mouse.current_cursor == cursor) { + _sapp_tc_update_cursor(cursor, _sapp_tc.mouse.shown); + } + #if defined(_SAPP_MACOS) + _sapp_tc_macos_destroy_custom_mouse_cursor(cursor); + #elif defined(_SAPP_EMSCRIPTEN) + _sapp_tc_emsc_destroy_custom_mouse_cursor(cursor); + #elif defined(_SAPP_WIN32) + _sapp_tc_win32_destroy_custom_mouse_cursor(cursor); + #elif defined(_SAPP_LINUX) + _sapp_tc_x11_destroy_custom_mouse_cursor(cursor); + #endif + } +} + +SOKOL_API_IMPL void sapp_request_quit(void) { + _sapp_tc.quit_requested = true; +} + +SOKOL_API_IMPL void sapp_cancel_quit(void) { + _sapp_tc.quit_requested = false; +} + +SOKOL_API_IMPL void sapp_quit(void) { + _sapp_tc.quit_ordered = true; +} + +SOKOL_API_IMPL void sapp_consume_event(void) { + _sapp_tc.event_consumed = true; +} + +/* NOTE: on HTML5, sapp_set_clipboard_string() must be called from within event handler! */ +SOKOL_API_IMPL void sapp_set_clipboard_string(const char* str) { + if (!_sapp_tc.clipboard.enabled) { + return; + } + SOKOL_ASSERT(str); + #if defined(_SAPP_MACOS) + _sapp_tc_macos_set_clipboard_string(str); + #elif defined(_SAPP_EMSCRIPTEN) + _sapp_tc_emsc_set_clipboard_string(str); + #elif defined(_SAPP_WIN32) + _sapp_tc_win32_set_clipboard_string(str); + #elif defined(_SAPP_LINUX) + _sapp_tc_x11_set_clipboard_string(str); + #else + /* not implemented */ + #endif + _sapp_tc_strcpy(str, _sapp_tc.clipboard.buffer, (size_t)_sapp_tc.clipboard.buf_size); +} + +SOKOL_API_IMPL const char* sapp_get_clipboard_string(void) { + if (!_sapp_tc.clipboard.enabled) { + return ""; + } + #if defined(_SAPP_MACOS) + return _sapp_tc_macos_get_clipboard_string(); + #elif defined(_SAPP_EMSCRIPTEN) + return _sapp_tc.clipboard.buffer; + #elif defined(_SAPP_WIN32) + return _sapp_tc_win32_get_clipboard_string(); + #elif defined(_SAPP_LINUX) + return _sapp_tc_x11_get_clipboard_string(); + #else + /* not implemented */ + return _sapp_tc.clipboard.buffer; + #endif +} + +SOKOL_API_IMPL void sapp_set_window_title(const char* title) { + SOKOL_ASSERT(title); + _sapp_tc_strcpy(title, _sapp_tc.window_title, sizeof(_sapp_tc.window_title)); + #if defined(_SAPP_MACOS) + _sapp_tc_macos_update_window_title(); + #elif defined(_SAPP_WIN32) + _sapp_tc_win32_update_window_title(); + #elif defined(_SAPP_LINUX) + _sapp_tc_x11_update_window_title(); + #endif +} + +SOKOL_API_IMPL void sapp_set_icon(const sapp_icon_desc* desc) { + SOKOL_ASSERT(desc); + if (desc->sokol_default) { + /* the sokol default-icon generator is not ported: the favicon is the + page's business unless the app provides real icon images */ + return; + } + const int num_images = _sapp_tc_icon_num_images(desc); + if (num_images == 0) { + return; + } + SOKOL_ASSERT((num_images > 0) && (num_images <= SAPP_MAX_ICONIMAGES)); + if (!_sapp_tc_validate_icon_desc(desc, num_images)) { + return; + } + #if defined(_SAPP_MACOS) + _sapp_tc_macos_set_icon(desc, num_images); + #elif defined(_SAPP_WIN32) + _sapp_tc_win32_set_icon(desc, num_images); + #elif defined(_SAPP_LINUX) + _sapp_tc_x11_set_icon(desc, num_images); + #elif defined(_SAPP_EMSCRIPTEN) + _sapp_tc_emsc_set_icon(desc, num_images); + #endif +} + +SOKOL_API_IMPL int sapp_get_num_dropped_files(void) { + if (!_sapp_tc.drop.enabled) { + return 0; + } + return _sapp_tc.drop.num_files; +} + +SOKOL_API_IMPL const char* sapp_get_dropped_file_path(int index) { + SOKOL_ASSERT((index >= 0) && (index < _sapp_tc.drop.num_files)); + if (!_sapp_tc.drop.enabled) { + return ""; + } + SOKOL_ASSERT(_sapp_tc.drop.buffer); + if ((index < 0) || (index >= _sapp_tc.drop.max_files)) { + return ""; + } + return (const char*) _sapp_tc_dropped_file_path_ptr(index); +} + +SOKOL_API_IMPL uint32_t sapp_html5_get_dropped_file_size(int index) { + SOKOL_ASSERT((index >= 0) && (index < _sapp_tc.drop.num_files)); + #if defined(_SAPP_EMSCRIPTEN) + if (!_sapp_tc.drop.enabled) { + return 0; + } + return _sapp_tc_js_dropped_file_size(index); + #else + (void)index; + return 0; + #endif +} + +SOKOL_API_IMPL void sapp_html5_fetch_dropped_file(const sapp_html5_fetch_request* request) { + SOKOL_ASSERT(_sapp_tc.drop.enabled); + SOKOL_ASSERT(request); + SOKOL_ASSERT(request->callback); + SOKOL_ASSERT(request->buffer.ptr); + SOKOL_ASSERT(request->buffer.size > 0); + #if defined(_SAPP_EMSCRIPTEN) + const int index = request->dropped_file_index; + sapp_html5_fetch_error error_code = SAPP_HTML5_FETCH_ERROR_NO_ERROR; + if ((index < 0) || (index >= _sapp_tc.drop.num_files)) { + error_code = SAPP_HTML5_FETCH_ERROR_OTHER; + } + if (sapp_html5_get_dropped_file_size(index) > request->buffer.size) { + error_code = SAPP_HTML5_FETCH_ERROR_BUFFER_TOO_SMALL; + } + if (SAPP_HTML5_FETCH_ERROR_NO_ERROR != error_code) { + _sapp_tc_emsc_invoke_fetch_cb(index, + false, // success + (int)error_code, + request->callback, + 0, // fetched_size + (void*)request->buffer.ptr, + request->buffer.size, + request->user_data); + } else { + _sapp_tc_js_fetch_dropped_file(index, + request->callback, + (void*)request->buffer.ptr, + request->buffer.size, + request->user_data); + } + #else + (void)request; + #endif +} + +SOKOL_API_IMPL sapp_environment sapp_get_environment(void) { + SOKOL_ASSERT(_sapp_tc.valid); + _SAPP_STRUCT(sapp_environment, res); + res.defaults.color_format = sapp_color_format(); + res.defaults.depth_format = sapp_depth_format(); + res.defaults.sample_count = sapp_sample_count(); + #if defined(SOKOL_METAL) + #if defined(_SAPP_MACOS) + res.metal.device = (__bridge const void*) _sapp_tc.macos.mtl.device; + #else + res.metal.device = (__bridge const void*) _sapp_tc.ios.mtl.device; + #endif + #endif + #if defined(SOKOL_D3D11) + res.d3d11.device = (const void*) _sapp_tc.d3d11.device; + res.d3d11.device_context = (const void*) _sapp_tc.d3d11.device_context; + #endif + #if defined(SOKOL_WGPU) + res.wgpu.device = (const void*) _sapp_tc.wgpu.device; + #endif + #if defined(SOKOL_VULKAN) + res.vulkan.instance = (const void*) _sapp_tc.vk.instance; + res.vulkan.physical_device = (const void*) _sapp_tc.vk.physical_device; + res.vulkan.device = (const void*) _sapp_tc.vk.device; + res.vulkan.queue = (const void*) _sapp_tc.vk.queue; + res.vulkan.queue_family_index = _sapp_tc.vk.queue_family_index; + #endif + return res; +} + +SOKOL_API_IMPL sapp_swapchain sapp_get_swapchain(void) { + SOKOL_ASSERT(_sapp_tc.valid); + _SAPP_STRUCT(sapp_swapchain, res); + #if defined(SOKOL_METAL) + #if defined(_SAPP_MACOS) + res.metal.current_drawable = (__bridge const void*) _sapp_tc_macos_mtl_swapchain_next(); + res.metal.depth_stencil_texture = (__bridge const void*) _sapp_tc.macos.mtl.depth_tex; + res.metal.msaa_color_texture = (__bridge const void*) _sapp_tc.macos.mtl.msaa_tex; + #else + res.metal.current_drawable = (__bridge const void*) _sapp_tc_ios_mtl_swapchain_next(); + res.metal.depth_stencil_texture = (__bridge const void*) _sapp_tc.ios.mtl.depth_tex; + res.metal.msaa_color_texture = (__bridge const void*) _sapp_tc.ios.mtl.msaa_tex; + #endif + #endif + #if defined(SOKOL_D3D11) + SOKOL_ASSERT(_sapp_tc.d3d11.rtv); + if (_sapp_tc.sample_count > 1) { + SOKOL_ASSERT(_sapp_tc.d3d11.msaa_rtv); + res.d3d11.render_view = (const void*) _sapp_tc.d3d11.msaa_rtv; + res.d3d11.resolve_view = (const void*) _sapp_tc.d3d11.rtv; + } else { + res.d3d11.render_view = (const void*) _sapp_tc.d3d11.rtv; + } + res.d3d11.depth_stencil_view = (const void*) _sapp_tc.d3d11.dsv; + #endif + #if defined(SOKOL_WGPU) + SOKOL_ASSERT(0 == _sapp_tc.wgpu.swapchain_view); + _sapp_tc_wgpu_swapchain_next(); + // FIXME: swapchain_view being null must be allowed and should skip the frame + SOKOL_ASSERT(_sapp_tc.wgpu.swapchain_view); + if (_sapp_tc.sample_count > 1) { + SOKOL_ASSERT(_sapp_tc.wgpu.msaa_view); + res.wgpu.render_view = (const void*) _sapp_tc.wgpu.msaa_view; + res.wgpu.resolve_view = (const void*) _sapp_tc.wgpu.swapchain_view; + } else { + res.wgpu.render_view = (const void*) _sapp_tc.wgpu.swapchain_view; + } + res.wgpu.depth_stencil_view = (const void*) _sapp_tc.wgpu.depth_stencil_view; + #endif + #if defined(SOKOL_VULKAN) + _sapp_tc_vk_swapchain_next(); + // FIXME: swapchain_view being null must be allowed and should skip the frame + uint32_t img_idx = _sapp_tc.vk.cur_swapchain_image_index; + if (_sapp_tc.sample_count > 1) { + SOKOL_ASSERT(_sapp_tc.vk.msaa.img && _sapp_tc.vk.msaa.view); + res.vulkan.render_image = (const void*) _sapp_tc.vk.msaa.img; + res.vulkan.render_view = (const void*) _sapp_tc.vk.msaa.view; + res.vulkan.resolve_image = (const void*) _sapp_tc.vk.swapchain_images[img_idx]; + res.vulkan.resolve_view = (const void*) _sapp_tc.vk.swapchain_views[img_idx]; + } else { + res.vulkan.render_image = (const void*) _sapp_tc.vk.swapchain_images[img_idx]; + res.vulkan.render_view = (const void*) _sapp_tc.vk.swapchain_views[img_idx]; + } + res.vulkan.depth_stencil_image = (const void*) _sapp_tc.vk.depth.img; + res.vulkan.depth_stencil_view = (const void*) _sapp_tc.vk.depth.view; + // NOTE: using the current swapchain image index here is *NOT* a bug! The render_finished_semaphore *must* + // be associated with its swapchain image in case the swapchain implementation doesn't return swapchain images in order + res.vulkan.render_finished_semaphore = _sapp_tc.vk.sync[img_idx].render_finished_sem; + res.vulkan.present_complete_semaphore = _sapp_tc.vk.sync[_sapp_tc.vk.sync_slot].present_complete_sem; + #endif + #if defined(_SAPP_ANY_GL) + res.gl.framebuffer = _sapp_tc.gl.framebuffer; + #endif + res.width = sapp_width(); + res.height = sapp_height(); + res.color_format = sapp_color_format(); + res.depth_format = sapp_depth_format(); + res.sample_count = sapp_sample_count(); + return res; +} + +SOKOL_API_IMPL const void* sapp_macos_get_window(void) { + #if defined(_SAPP_MACOS) + const void* obj = (__bridge const void*) _sapp_tc.macos.window; + SOKOL_ASSERT(obj); + return obj; + #else + return 0; + #endif +} + +SOKOL_API_IMPL const void* sapp_ios_get_window(void) { + #if defined(_SAPP_IOS) + const void* obj = (__bridge const void*) _sapp_tc.ios.window; + SOKOL_ASSERT(obj); + return obj; + #else + return 0; + #endif +} + +// Modified by tettou771 for TrussC: runtime orientation control +SOKOL_API_IMPL void sapp_ios_set_supported_orientations(uint32_t mask) { + #if defined(_SAPP_IOS) + _sapp_tc.ios.supported_orientations = (NSUInteger)mask; + #if __IPHONE_OS_VERSION_MAX_ALLOWED >= 160000 + if (@available(iOS 16.0, *)) { + [_sapp_tc.ios.view_ctrl setNeedsUpdateOfSupportedInterfaceOrientations]; + } + #endif + #else + (void)mask; + #endif +} + +SOKOL_API_IMPL const void* sapp_d3d11_get_swap_chain(void) { + SOKOL_ASSERT(_sapp_tc.valid); +#if defined(SOKOL_D3D11) + return _sapp_tc.d3d11.swap_chain; +#else + return 0; +#endif +} + +SOKOL_API_IMPL const void* sapp_win32_get_hwnd(void) { + SOKOL_ASSERT(_sapp_tc.valid); + #if defined(_SAPP_WIN32) + return _sapp_tc.win32.hwnd; + #else + return 0; + #endif +} + +SOKOL_API_IMPL int sapp_gl_get_major_version(void) { + SOKOL_ASSERT(_sapp_tc.valid); + #if defined(_SAPP_ANY_GL) + return _sapp_tc.desc.gl.major_version; + #else + return 0; + #endif +} + +SOKOL_API_IMPL int sapp_gl_get_minor_version(void) { + SOKOL_ASSERT(_sapp_tc.valid); + #if defined(_SAPP_ANY_GL) + return _sapp_tc.desc.gl.minor_version; + #else + return 0; + #endif +} + +SOKOL_API_IMPL bool sapp_gl_is_gles(void) { + #if defined(SOKOL_GLES3) + return true; + #else + return false; + #endif +} + +SOKOL_API_IMPL const void* sapp_x11_get_window(void) { + #if defined(_SAPP_LINUX) + return (void*)_sapp_tc.x11.window; + #else + return 0; + #endif +} + +SOKOL_API_IMPL const void* sapp_x11_get_display(void) { + #if defined(_SAPP_LINUX) + return (void*)_sapp_tc.x11.display; + #else + return 0; + #endif +} + +SOKOL_API_IMPL const void* sapp_android_get_native_activity(void) { + // NOTE: _sapp_tc.valid is not asserted here because sapp_android_get_native_activity() + // needs to be callable from within sokol_main() (see: https://github.com/floooh/sokol/issues/708) + #if defined(_SAPP_ANDROID) + return (void*)_sapp_tc.android.activity; + #else + return 0; + #endif +} + +SOKOL_API_IMPL void sapp_html5_ask_leave_site(bool ask) { + _sapp_tc.html5_ask_leave_site = ask; +} + +// Modified by tettou771 for TrussC: skip the next present call (fix D3D11 flickering) +SOKOL_API_IMPL void sapp_skip_present(void) { + _sapp_tc.skip_present = true; +} +// end of modification + + +/* ---- multi-window API: explicit platform gap on the web ------------------ + the whole backend binds to ONE element (Module.sapp_emsc_target); + a second window is not representable in a browser tab */ +sapp_window sapp_create_window(const sapp_window_desc* desc) { + _SOKOL_UNUSED(desc); + fprintf(stderr, "sokol_app_tc.h: sapp_create_window() is not supported on the web (single-canvas platform)\n"); + sapp_window w = {0}; + return w; +} +void sapp_destroy_window(sapp_window win) { _SOKOL_UNUSED(win); } +bool sapp_window_valid(sapp_window win) { _SOKOL_UNUSED(win); return false; } +int sapp_window_width(sapp_window win) { _SOKOL_UNUSED(win); return 0; } +int sapp_window_height(sapp_window win) { _SOKOL_UNUSED(win); return 0; } +int sapp_window_framebuffer_width(sapp_window win) { _SOKOL_UNUSED(win); return 0; } +int sapp_window_framebuffer_height(sapp_window win) { _SOKOL_UNUSED(win); return 0; } +float sapp_window_dpi_scale(sapp_window win) { _SOKOL_UNUSED(win); return 1.0f; } +int sapp_window_sample_count(sapp_window win) { _SOKOL_UNUSED(win); return 1; } +bool sapp_window_occluded(sapp_window win) { _SOKOL_UNUSED(win); return false; } +void sapp_window_set_title(sapp_window win, const char* title) { _SOKOL_UNUSED(win); _SOKOL_UNUSED(title); } +double sapp_window_frame_duration(sapp_window win) { _SOKOL_UNUSED(win); return 1.0 / 60.0; } +const void* sapp_window_metal_current_drawable(sapp_window win) { _SOKOL_UNUSED(win); return 0; } +const void* sapp_window_metal_depth_stencil_texture(sapp_window win) { _SOKOL_UNUSED(win); return 0; } +const void* sapp_window_metal_msaa_color_texture(sapp_window win) { _SOKOL_UNUSED(win); return 0; } +const void* sapp_window_d3d11_render_view(sapp_window win) { _SOKOL_UNUSED(win); return 0; } +const void* sapp_window_d3d11_resolve_view(sapp_window win) { _SOKOL_UNUSED(win); return 0; } +const void* sapp_window_d3d11_depth_stencil_view(sapp_window win) { _SOKOL_UNUSED(win); return 0; } +const void* sapp_window_macos_get_window(sapp_window win) { _SOKOL_UNUSED(win); return 0; } +const void* sapp_window_win32_get_hwnd(sapp_window win) { _SOKOL_UNUSED(win); return 0; } +const void* sapp_window_x11_get_window(sapp_window win) { _SOKOL_UNUSED(win); return 0; } +uint32_t sapp_window_gl_framebuffer(sapp_window win) { _SOKOL_UNUSED(win); return 0; } + +/* ---- other-backend getters (zero stubs, same set as the native sections) - */ +const void* sapp_metal_get_device(void) { return 0; } +const void* sapp_metal_get_current_drawable(void) { return 0; } +const void* sapp_metal_get_depth_stencil_texture(void) { return 0; } +const void* sapp_metal_get_msaa_color_texture(void) { return 0; } +const void* sapp_d3d11_get_device(void) { return 0; } +const void* sapp_d3d11_get_device_context(void) { return 0; } + #else /* other platforms */ -/*== stubs (explicit platform gap; the web/mobile ports replace these) ======*/ +/*== stubs (explicit platform gap; the mobile ports replace these) ==========*/ #if defined(__cplusplus) extern "C" { #endif diff --git a/core/platform/web/sokol_impl.cpp b/core/platform/web/sokol_impl.cpp index e3b956db..da373432 100644 --- a/core/platform/web/sokol_impl.cpp +++ b/core/platform/web/sokol_impl.cpp @@ -1,10 +1,11 @@ // ============================================================================= -// sokol バックエンド実装 (Emscripten / WebGL2) +// sokol バックエンド実装 (Emscripten / WebGPU or WebGL2) // ============================================================================= -#define SOKOL_IMPL #define SOKOL_NO_ENTRY // main() を自分で定義するため +// Suppress warnings from third-party sokol headers. They have intentional +// dead-code paths and unused helpers that trip -Wunused-* under -Wall/-Wextra. #if defined(__GNUC__) || defined(__clang__) # pragma GCC diagnostic push # pragma GCC diagnostic ignored "-Wunused-variable" @@ -12,8 +13,14 @@ # pragma GCC diagnostic ignored "-Wmissing-field-initializers" #endif -#include "sokol_log.h" +// sokol_app.h declarations only — the web implementation of the sapp_* API +// (canvas, rAF frame loop, HTML5 events, WGPU/WebGL2 swapchain) lives in +// sokol_app_tc.h below. #include "sokol_app.h" +#define SOKOL_IMPL +#include "sokol_log.h" +#define SOKOL_APP_TC_IMPL +#include "sokol_app_tc.h" #include "sokol_gfx.h" #include "sokol_glue.h" #include "util/sokol_gl_tc.h" diff --git a/docs/dev/sapp-web-impl-spec.md b/docs/dev/sapp-web-impl-spec.md new file mode 100644 index 00000000..c5647b33 --- /dev/null +++ b/docs/dev/sapp-web-impl-spec.md @@ -0,0 +1,965 @@ +# sokol_app.h Emscripten (Web) Backend — Behavioral Specification + +Implementation contract for replacing sokol_app.h's Emscripten/Web main-window / +app-lifecycle with `sokol_app_tc.h` (project "P3 Web driver"). All line references +are into `core/include/sokol/sokol_app.h` (14602 lines, this worktree). Focus: +`_SAPP_EMSCRIPTEN` with BOTH graphics backends — `SOKOL_WGPU` (TrussC web default) +and `SOKOL_GLES3` (WebGL2, opt-in). + +This is the fourth sibling of `sapp-mac-impl-spec.md` (event-driven `[NSApp run]`), +`sapp-win32-impl-spec.md` (owned PeekMessage loop) and `sapp-x11-impl-spec.md` +(owned XPending loop). **The web backend is unlike all three: there is NO owned +loop.** The browser owns the loop. `_sapp_emsc_run()` sets everything up, hands a +per-frame callback to `emscripten_request_animation_frame_loop()` (or +`emscripten_set_main_loop()`), and **returns immediately**. All rendering and +event dispatch happen later, re-entrant, from browser-driven callbacks. There is +no blocking swap, no timer, no message pump, no `sleep`. The whole backend is a +collection of C callbacks + `EM_JS` JavaScript shims registered against the +canvas / window / document. + +**Which #ifdef path is live in TrussC.** `core/CMakeLists.txt` compiles the web +target with **`SOKOL_WGPU` by default** and `SOKOL_GLES3` as an option (see §13). +sokol_app resolves the API at 2330–2335: `__EMSCRIPTEN__` → `_SAPP_EMSCRIPTEN`, +then requires `SOKOL_GLES3 || SOKOL_WGPU`. So on TrussC/web the live default path +is **`SOKOL_WGPU`**; **`SOKOL_GLES3`/WebGL2** is the fallback. This document +documents BOTH because the port must keep both working. Vulkan/Metal/D3D11 forks +inside shared code are dead on web and marked inline. + +**Single-window is baked in harder than any native backend.** The web "window" is +one HTML `` element, addressed by a CSS selector string +`_sapp.html5_canvas_selector` (default `"#canvas"`, 3324). There is no window +object, no NSWindow/HWND/Xwindow XID, no window list. **A second window is not +representable** in this backend at all (see §12, §14). The multiwindow port must +treat web as strictly single-window (window #0) and stub/no-op the plural-window +APIs. + +--- + +## 0. Global state touched (the shared contract) + +Single global `static _sapp_t _sapp;` embeds `_sapp_emsc_t emsc;` (3302–3303, +only under `_SAPP_EMSCRIPTEN`) and, under `SOKOL_WGPU`, `_sapp_wgpu_t wgpu;` +(3292–3294, compiled on ALL platforms that select WGPU, not web-specific). + +### `_sapp_emsc_t` (2881–2887) — the ENTIRE web-specific per-app state +```c +typedef struct { + bool mouse_lock_requested; // deferred pointer-lock (§8): request must fire from inside a user-gesture event + uint16_t mouse_buttons; // last-seen browser button bitmask (bit0=L, bit1=R, bit2=M) — see §4 mods +} _sapp_emsc_t; +``` +That is the whole struct. **Everything else the web backend needs lives in +JavaScript-side globals** (`Module.sapp_emsc_target`, `Module.sokol_*`, +`Module.__sapp_custom_cursors`, `specialHTMLTargets`) reachable only from `EM_JS` +blocks, NOT in C. This is the biggest structural surprise vs native: a large part +of the "state" is in JS closures on `Module`, not in `_sapp`. + +### `_sapp_wgpu_t` (2720–2733, `SOKOL_WGPU`) — shared with native WGPU +```c +typedef struct { + WGPUInstance instance; + WGPUAdapter adapter; + WGPUDevice device; + WGPUSurface surface; + WGPUTextureFormat render_format; // picked from surface caps (RGBA8Unorm / BGRA8Unorm) + WGPUTexture msaa_tex; // only if sample_count > 1 + WGPUTextureView msaa_view; + WGPUTexture depth_stencil_tex; // Depth32FloatStencil8 + WGPUTextureView depth_stencil_view; + WGPUTextureView swapchain_view; // per-frame; acquired in sapp_get_swapchain, released in _sapp_wgpu_frame + bool init_done; // GATE: frame callbacks no-op until async device+swapchain ready (§3) +} _sapp_wgpu_t; +``` + +### `_sapp.gl.framebuffer` (`_sapp_gl_t`, `_SAPP_ANY_GL` only) +The default framebuffer object id captured after +`emscripten_webgl_make_context_current` (§2). On WebGL2 this is **0** (the default +framebuffer). `sapp_get_swapchain()` reports it as `res.gl.framebuffer` (§11). + +Cross-platform `_sapp` fields the web path reads/writes: `desc`, `valid`, +`first_frame`, `quit_requested`, `quit_ordered`, `fullscreen`, +`window_width/height`, `framebuffer_width/height`, `dpi_scale`, `sample_count`, +`timing`, `mouse.*` (x/y/dx/dy/locked/shown/pos_valid/current_cursor), +`clipboard.*`, `drop.*`, `event`, `custom_cursor_bound[]`, `window_title`, +`html5_ask_leave_site`, `onscreen_keyboard_shown` (always false on web), +`skip_present` (TrussC patch — but **NOT USED on web WGPU**, see §3/§14), and +`html5_canvas_selector[_SAPP_MAX_TITLE_LENGTH]` (3324, the canvas CSS selector, +filled from `desc.html5.canvas_selector`). + +### `_sapp_init_state(desc)` / `_sapp_desc_defaults` (relevant subset, ~3520–3574) +`_sapp_def(val,def)` = "use def if val==0" (2291). Web-relevant defaults: +`sample_count→1` (3527), `swap_interval→1` (3528, **ignored on web** — the browser +paces via rAF), `html5.canvas_selector→"#canvas"` (3546), `clipboard_size→8192` +(3547), `max_dropped_files→1` (3548), `max_dropped_file_path_length→2048` (3549), +`window_title→"sokol"` (3550). The canvas selector string is copied into +`_sapp.html5_canvas_selector` and the desc pointer re-pointed at that stable +buffer (3573–3574). `_SAPP_FALLBACK_DEFAULT_WINDOW_WIDTH/HEIGHT = 640/480` +(2301–2302), used only in the `canvas_resize` path (§9). + +--- + +## 1. Entry / run flow — there is NO loop + +### The include + API-select block (2330–2335, 2412–2417, 2448–2453) +- **API select (2330–2335):** `__EMSCRIPTEN__` → `#define _SAPP_EMSCRIPTEN (1)`, + then `#error` unless `SOKOL_GLES3 || SOKOL_WGPU`. +- **`_SAPP_ANY_GL` (2381–2383):** defined for `SOKOL_GLCORE || SOKOL_GLES3`. On web + it's set only in the GLES3 build; **NOT set for WGPU**. Gates `_sapp.gl.*` and + the `res.gl.framebuffer` swapchain field. +- **WGPU header (2412–2417):** `#if defined(SOKOL_WGPU)` → `#include `, + and `#if !defined(__EMSCRIPTEN__)` → `#define _SAPP_WGPU_HAS_WAIT (1)`. **On web + `_SAPP_WGPU_HAS_WAIT` is NOT defined** → the whole WGPU init is async-callback + driven (§3). This single macro forks nearly all WGPU control flow between + native (synchronous `wgpuInstanceWaitAny`) and web (chained callbacks). +- **Emscripten includes (2448–2453):** `#if defined(SOKOL_GLES3) #include ` + then unconditionally `#include ` and + `#include `. **Note: no `` for the WGPU build.** + +### `_sapp_emsc_run(const sapp_desc* desc)` (8064–8102) — setup then return +1. `_sapp_init_state(desc)`. +2. **`_sapp.fullscreen = false`** (8066) — hard override of user's `desc.fullscreen` + with comment "can't start in fullscreen on the web!" (browsers only allow + fullscreen from a user gesture). This is web-specific and must be kept. +3. `document_title = desc->html5.update_document_title ? _sapp.window_title : 0` + (8067) → `sapp_js_init(_sapp.html5_canvas_selector, document_title)` (8068, + §1-JSinit) — resolves the canvas element in JS and optionally sets + `document.title`. +4. **Canvas size determination (8069–8083):** + - If `desc.html5.canvas_resize` (8070–8072): the app OWNS the canvas size — + `w/h = _sapp_def(desc.width, 640) / _sapp_def(desc.height, 480)`. No resize + listener registered. + - Else (8073–8076): the canvas size is TRACKED — + `emscripten_get_element_css_size(selector, &w, &h)` reads the CSS size, and + `emscripten_set_resize_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, ..., + _sapp_emsc_size_changed)` registers the window-resize handler (§10). + - `if (desc.high_dpi) dpi_scale = emscripten_get_device_pixel_ratio()` (8077–8079) + — else `dpi_scale` stays 1.0. **`high_dpi=false` genuinely disables DPR + scaling on web** (unlike X11 where it's unconditional). + - `window_width/height = round(w/h)`; `framebuffer_width/height = round(w/h * dpi_scale)` + (8080–8083). `_sapp_roundf_gzero` = round, clamped to ≥ 0. + - `emscripten_set_canvas_element_size(selector, framebuffer_width, framebuffer_height)` + (8084) — sets the canvas **backing-store (drawing-buffer) pixel size** to the + framebuffer size. CSS size stays whatever the page set. +5. **Renderer bring-up (8085–8089):** `#if SOKOL_GLES3 _sapp_emsc_webgl_init()` + (§2) `#elif SOKOL_WGPU _sapp_wgpu_init()` (§3). NOTE: on WGPU this only KICKS OFF + async adapter/device request; the device is not ready yet when run() returns. +6. `_sapp.valid = true` (8090). +7. `_sapp_emsc_register_eventhandlers()` (8091, §4/§5/§8) — registers every DOM + callback. **This is where the TrussC canvas-keyboard patch lives (§6).** +8. `sapp_set_icon(&desc->icon)` (8092) → `_sapp_emsc_set_icon` sets a favicon (§10). +9. **Start the frame loop (8094–8099):** + - If `desc.html5.use_emsc_set_main_loop`: + `emscripten_set_main_loop(_sapp_emsc_frame_main_loop, 0, desc.html5.emsc_set_main_loop_simulate_infinite_loop)`. + - Else (default): `emscripten_request_animation_frame_loop(_sapp_emsc_frame_animation_loop, 0)`. +10. **Returns.** Comment 8100–8101: "NOT A BUG: do not call `_sapp_discard_state()` + here" — cleanup happens later inside the frame callback on quit (§12). + +### Entry point (8104–8110) and the SOKOL_NO_ENTRY dispatch +- Default: `#if !defined(SOKOL_NO_ENTRY)` → `int main(int argc,char**){ desc = sokol_main(...); _sapp_emsc_run(&desc); return 0; }` (8105–8108). +- **TrussC compiles `SOKOL_NO_ENTRY`** so this `main` is compiled out. The live + path is `sapp_run(desc)` (13941, `#if defined(SOKOL_NO_ENTRY)`) → + `_sapp_emsc_run(desc)` (13947–13948). TrussC's `runApp()` calls `sapp_run()`. + On web `sapp_run()` **returns immediately** (docs 1174–1190) — so no user code + may run after it; the app lives entirely in callbacks. + +### The frame callbacks +- **`_sapp_emsc_frame_animation_loop(double time, void* ud)` (8027–8055)** — the + rAF callback. `time` is the rAF DOMHighResTimeStamp in **milliseconds**. + 1. `_sapp_timing_update(&_sapp.timing, time / 1000.0)` (8029) — feed the EXTERNAL + browser clock (converted ms→s) into the EMA timer. **This is the web timing + source; there is no `emscripten_get_now`/`performance.now` inside the tick + itself for the rAF path — the browser hands the timestamp.** + 2. `#if SOKOL_WGPU _sapp_wgpu_frame()` (8031–8032, §3) `#else _sapp_frame()` + (8034). `_sapp_frame` runs `init_cb` on first frame then `frame_cb`. + 3. **Quit handling (8037–8053):** if `quit_requested` → fire + `SAPP_EVENTTYPE_QUIT_REQUESTED`; if still requested → `quit_ordered=true`. + If `quit_ordered` → `_sapp_emsc_unregister_eventhandlers()` (§4), + `#if SOKOL_WGPU _sapp_wgpu_discard()`, `_sapp_call_cleanup()`, + `_sapp_discard_state()`, `return EM_FALSE` (stops the rAF loop). Else `EM_TRUE`. +- **`_sapp_emsc_frame_main_loop(void)` (8057–8062)** — the `set_main_loop` variant. + `time = emscripten_performance_now()` (8058, **THIS is the `performance.now()` + path**), then calls the animation-loop body; if it returns false, + `emscripten_cancel_main_loop()`. So the two loop modes differ only in the clock + source (rAF timestamp vs `performance.now()`) and the stop mechanism. + +### Frame timing plumbing (2579–2580, 2609–2610, 2626–2629, 2881) +- `_sapp_timestamp_t` on emscripten is `{ int _dummy; }` (2579–2580); + `_sapp_timestamp_init` is `(void)ts` (2609–2610); **`_sapp_timestamp_now` + asserts `false` and returns 0.0 (2626–2629)** — the internal monotonic clock is + deliberately UNUSED on web. All timing comes from the `time` argument passed into + `_sapp_timing_update` by the frame callbacks. The EMA filter itself + (`_sapp_timing_*`, dt_min/dt_max/threshold/alpha, seed 1/60) is identical to the + other backends. `sapp_frame_duration()` → smoothed; `_unfiltered()` → raw dt. + +--- + +## 2. GLES3 / WebGL2 renderer bring-up (`SOKOL_GLES3` only) + +### Context creation (`_sapp_emsc_webgl_init`, 7935–7950) +```c +EmscriptenWebGLContextAttributes attrs; +emscripten_webgl_init_context_attributes(&attrs); // fills defaults +attrs.alpha = _sapp.desc.alpha; +attrs.depth = true; +attrs.stencil = true; +attrs.antialias = _sapp.sample_count > 1; // MSAA on iff sample_count>1 +attrs.premultipliedAlpha = _sapp.desc.html5.premultiplied_alpha; +attrs.preserveDrawingBuffer= _sapp.desc.html5.preserve_drawing_buffer; +attrs.enableExtensionsByDefault = true; +attrs.majorVersion = 2; // WebGL2 (== GLES3) +EMSCRIPTEN_WEBGL_CONTEXT_HANDLE ctx = emscripten_webgl_create_context(_sapp.html5_canvas_selector, &attrs); +// FIXME: error message? (no error check on ctx==0) +emscripten_webgl_make_context_current(ctx); +glGetIntegerv(GL_FRAMEBUFFER_BINDING, (GLint*)&_sapp.gl.framebuffer); // capture default FBO (== 0) +``` +- **`attrs.powerPreference` / `attrs.majorVersion` note:** upstream sets NO + `powerPreference` here (defaults to browser default). `majorVersion=2` is the + WebGL2/GLES3 selector; there is no `minorVersion` for WebGL. The context handle + `ctx` is discarded after make-current (**not stored in `_sapp`** — the current + context is thread-implicit like GLX). No error handling on failure (FIXME). +- **Swapchain semantics:** the "swapchain" is just the default framebuffer id + (0). `sapp_get_swapchain()` returns `res.gl.framebuffer = _sapp.gl.framebuffer` + (14478) + width/height/color_format/depth_format/sample_count. **No + render_view / textures** — WebGL resolves MSAA implicitly via the context's + `antialias` attribute, so there's no explicit MSAA texture like WGPU/Metal. +- `sample_count` meaning on WebGL: it only toggles `antialias` on/off; the actual + sample count is browser-chosen. `sapp_sample_count()` still returns the + requested `_sapp.sample_count` (14052) — a mild lie the port should preserve for + API consistency (sokol_gfx swapchain expects the requested count). + +### Context loss (`_sapp_emsc_webgl_context_cb`, 7918–7933, `SOKOL_GLES3` only) +Maps `EMSCRIPTEN_EVENT_WEBGLCONTEXTLOST → SAPP_EVENTTYPE_SUSPENDED` and +`WEBGLCONTEXTRESTORED → SAPP_EVENTTYPE_RESUMED`, fires the sapp event. Registered +in `register_eventhandlers` (7985–7988) / unregistered (8021–8024), both guarded +by `#if defined(SOKOL_GLES3)`. **Note: the lost/restored handlers do NOT actually +recreate the GL context** — they only surface SUSPENDED/RESUMED to the app. Real +context recovery is not implemented (app is expected to rebuild GPU resources on +RESUMED). WGPU has no analogous webgl-context-loss path (it has device-lost, §3). + +--- + +## 3. WGPU renderer bring-up (`SOKOL_WGPU`, TrussC web DEFAULT) + +**Header / API flavor — CRITICAL (2412–2413, 3891–3895):** this vendored sokol +uses the **modern Dawn-style `webgpu.h`** (`WGPUStringView`, `WGPUFuture`, +`wgpuInstanceWaitAny`, `WGPURequestAdapterCallbackInfo`, `WGPUSurfaceConfiguration`, +`wgpuSurfaceGetCurrentTexture`, `wgpuSurfacePresent`), NOT the old Emscripten +`emscripten/html5_webgpu.h` / `wgpuInstanceCreateSurface`-with- +`WGPUSurfaceDescriptorFromCanvasHTMLSelector`. The surface source struct is +`WGPUEmscriptenSurfaceSourceCanvasHTMLSelector` with +`sType = WGPUSType_EmscriptenSurfaceSourceCanvasHTMLSelector` (3892–3894) — these +symbols come from **emdawnwebgpu** (Emscripten's Dawn port). **The port's web WGPU +build MUST link `--use-port=emdawnwebgpu`** (or equivalent), NOT the legacy +`-sUSE_WEBGPU=1` bindings. This is a hard ABI dependency — the whole `_sapp_wgpu_*` +code will not compile against the old bindings. (Verify against §13 CMake flags.) + +### The async device dance (web = `!_SAPP_WGPU_HAS_WAIT`) +On web, adapter→device→swapchain are chained through callbacks because there is no +synchronous wait. Call order and the `init_done` gate: + +1. **`_sapp_wgpu_init()` (4213–4235):** create `WGPUInstance` via + `wgpuCreateInstance(&desc)` (on web, `desc.requiredFeatures` is empty because + `_SAPP_WGPU_HAS_WAIT` is off — the `TimedWaitAny` feature block 4218–4224 is + compiled out). PANIC `WGPU_CREATE_INSTANCE_FAILED` on null. Then + `_sapp_wgpu_create_adapter()`. The `#if _SAPP_WGPU_HAS_WAIT` synchronous + `_sapp_wgpu_create_device_and_swapchain()` (4231–4233) is **compiled out on web** + — comment 4229: "on Emscripten, device and swapchain creation are chained in + the callbacks." +2. **`_sapp_wgpu_create_adapter()` (4199–4210):** `callbackmode = AllowProcessEvents` + (web, 3846) — NOT `WaitAnyOnly`. `wgpuInstanceRequestAdapter(instance, 0, cb_info)`. + On web the returned `WGPUFuture` is ignored (no await); the callback fires later. +3. **`_sapp_wgpu_request_adapter_cb` (4180–4197):** stores `_sapp.wgpu.adapter`; + on web (`#if !_SAPP_WGPU_HAS_WAIT`, 4193–4195) chains + `_sapp_wgpu_create_device_and_swapchain()`. +4. **`_sapp_wgpu_create_device_and_swapchain()` (4110–4177):** queries adapter + features (BC/ETC2/ASTC compression, DualSourceBlending, ShaderF16, + Float32Filterable, Float32Blendable, TextureFormatsTier2 — added iff present), + always requires `Depth32FloatStencil8` (4114–4116). Sets device-lost + + uncaptured-error callbacks. `callbackmode = AllowProcessEvents` on web. + `wgpuAdapterRequestDevice(...)`; web ignores the future. +5. **`_sapp_wgpu_request_device_cb` (4087–4108):** stores `_sapp.wgpu.device`; + `#if !_SAPP_EMSCRIPTEN` sets a logging callback (4101–4105) — **skipped on web** + (emdawnwebgpu has no `wgpuDeviceSetLoggingCallback`). Then + `_sapp_wgpu_create_swapchain(false)` and **`_sapp.wgpu.init_done = true`** + (4107) — this flips the gate that lets frames render. +6. **`_sapp_wgpu_device_lost_cb` (4044–4055):** logs unless reason is + `CallbackCancelled` (fired on instance release). `_sapp_wgpu_device_logging_cb` + (4058–4074) and `_sapp_wgpu_uncaptured_error_cb` (4077–4085) exist; the logging + one is `#if !_SAPP_EMSCRIPTEN` (4057), so not used on web. + +### Swapchain creation (`_sapp_wgpu_create_swapchain`, 3880–3982) +- **Surface (first call only, 3888–3926):** builds a `WGPUSurfaceDescriptor` whose + `nextInChain` is the emscripten canvas-selector struct (3891–3895): + `html_canvas_desc.selector = _sapp_wgpu_stringview(_sapp.html5_canvas_selector)`. + So the WGPU surface is bound to the SAME canvas selector as everything else. + `wgpuInstanceCreateSurface` → PANIC `WGPU_SWAPCHAIN_CREATE_SURFACE_FAILED` on null. + `wgpuSurfaceGetCapabilities(surface, adapter, &surf_caps)` → PANIC on non-Success. + `render_format = _sapp_wgpu_pick_render_format(...)` (3864–3878): picks the first + of `RGBA8Unorm`/`BGRA8Unorm` (non-SRGB only). This drives `sapp_color_format()` + (14019–14028: RGBA8Unorm→RGBA8, BGRA8Unorm→BGRA8). +- **Surface config (3928–3943):** `WGPUSurfaceConfiguration` with device, + `render_format`, `usage = RenderAttachment`, size = framebuffer w/h, + `alphaMode = Opaque` **but on emscripten (3936–3940) if + `desc.html5.premultiplied_alpha` → `alphaMode = Premultiplied`**, + `presentMode = Fifo` (vsync). `wgpuSurfaceConfigure(...)`. +- **Depth-stencil texture (3945–3961):** always created, `Depth32FloatStencil8`, + size = framebuffer w/h, sample_count = `_sapp.sample_count`. PANIC on failure. +- **MSAA texture (3963–3980):** only if `sample_count > 1`; `render_format`, + sample_count. PANIC on failure. + +### Per-frame (`_sapp_wgpu_frame`, 4253–4270) +```c +wgpuInstanceProcessEvents(_sapp.wgpu.instance); // pump async callbacks (adapter/device ready) — ESSENTIAL on web +if (_sapp.wgpu.init_done) { // GATE: no rendering until device+swapchain ready + _sapp_frame(); // init_cb (first frame) + frame_cb + if (_sapp.wgpu.swapchain_view) { // released the view acquired in sapp_get_swapchain + wgpuTextureViewRelease(_sapp.wgpu.swapchain_view); + _sapp.wgpu.swapchain_view = 0; + } + #if !defined(_SAPP_EMSCRIPTEN) // <-- present is a NATIVE-ONLY call + if (!_sapp.skip_present) { wgpuSurfacePresent(_sapp.wgpu.surface); } + else { _sapp.skip_present = false; } + #endif +} +``` +- **`wgpuInstanceProcessEvents` every frame is load-bearing on web** — it's how the + async adapter/device-request callbacks actually get delivered; without it + `init_done` never flips and nothing ever renders. +- **`init_done` gate:** for the first N rAF ticks (until the async device arrives) + `_sapp_wgpu_frame` does nothing but pump events. The app's `init_cb`/`frame_cb` + do not run until the device is ready. **The port must replicate this gate** — + callers of `_sapp_frame` on web-WGPU must wait for the device. +- **No explicit present on web (4261 `#if !_SAPP_EMSCRIPTEN`)** — the browser + auto-presents the configured surface at rAF boundaries. Consequently + **`skip_present` (the TrussC D3D11-flicker patch) is a no-op on web** — the whole + present block is compiled out. TrussC's event-driven-render "skip present" trick + cannot suppress a web frame; don't rely on it there (§14). + +### Swapchain acquire / resize / discard +- **`_sapp_wgpu_swapchain_next()` (4009–4035):** called from `sapp_get_swapchain()` + (14442–14444). `wgpuSurfaceGetCurrentTexture`; on `SuccessOptimal/Suboptimal` + create the view; on `Timeout/Outdated/Lost` release + **discard & recreate the + swapchain** (4024–4025) then... (falls through — note: does not re-acquire in the + same call, so `swapchain_view` may be left 0; there's a FIXME at 14445 that null + view "should skip the frame" but currently asserts). On `Error` → PANIC. +- **`_sapp_wgpu_swapchain_size_changed()` (4037–4042):** discard(true) + + create(true) — keeps the surface, recreates depth/MSAA + reconfigures. Called + from `_sapp_emsc_size_changed` (7548–7551, `#if SOKOL_WGPU`) on every canvas + resize. +- **`_sapp_wgpu_discard_swapchain(bool from_resize)` (3984–4006):** releases + msaa/depth views+textures; releases the surface only when `!from_resize` (4001). +- **`_sapp_wgpu_discard()` (4237–4251):** swapchain + device + adapter + instance + release. Called from the quit path (8047–8048). + +### WGPU environment / swapchain getters mapping (14404–14455) +- `sapp_get_environment().wgpu.device = _sapp.wgpu.device` (14405). +- `sapp_get_swapchain()` (14442–14454): asserts `swapchain_view==0`, calls + `_sapp_wgpu_swapchain_next()`, then (sample_count>1) `render_view = msaa_view`, + `resolve_view = swapchain_view`; else `render_view = swapchain_view`; + `depth_stencil_view = depth_stencil_view`. These map straight to + `sglue_swapchain()` (sokol_glue.h 193–195). **NOTE: the doc-comment public + getters `sapp_wgpu_get_device/render_view/resolve_view/depth_stencil_view` + (declared in the header comment at 361–364) are NOT implemented in this version** + — the live contract is `sapp_get_environment()`/`sapp_get_swapchain()` consumed + by `sglue_environment()`/`sglue_swapchain()`. Don't resurrect the old getters; + keep the environment/swapchain path. + +--- + +## 4. Event registration and dispatch model + +### Registration (`_sapp_emsc_register_eventhandlers`, 7953–7989) +All handlers are registered with `useCapture = true` (the `true` third arg) and +`0` userData. Targets: + +| DOM event | emscripten setter | target | callback | +|---|---|---|---| +| mousedown/up/move/enter/leave | `emscripten_set_mouse*_callback` | **canvas selector** | `_sapp_emsc_mouse_cb` | +| wheel | `emscripten_set_wheel_callback` | **canvas selector** | `_sapp_emsc_wheel_cb` | +| keydown/keyup/keypress | `emscripten_set_key*_callback` | **canvas selector** ⟵ TrussC PATCH (§6) | `_sapp_emsc_key_cb` | +| touchstart/move/end/cancel | `emscripten_set_touch*_callback` | **canvas selector** | `_sapp_emsc_touch_cb` | +| pointerlockchange/error | `emscripten_set_pointerlock*_callback` | `EMSCRIPTEN_EVENT_TARGET_DOCUMENT` | `_sapp_emsc_pointerlock*_cb` | +| focus/blur | `emscripten_set_focus/blur_callback` | `EMSCRIPTEN_EVENT_TARGET_WINDOW` | `_sapp_emsc_focus_cb`/`_blur_cb` | +| fullscreenchange | `emscripten_set_fullscreenchange_callback` | **canvas selector** | `_sapp_emsc_fullscreenchange_cb` | +| beforeunload | `sapp_js_add_beforeunload_listener` (EM_JS) | `window` | JS closure | +| paste (if clipboard) | `sapp_js_add_clipboard_listener` (EM_JS) | `window` | JS → `_sapp_emsc_onpaste` | +| dragenter/leave/over/drop (if drop) | `sapp_js_add_dragndrop_listeners` (EM_JS) | canvas | JS → begin/drop/end | +| webglcontextlost/restored (GLES3 only) | `emscripten_set_webglcontext*_callback` | canvas | `_sapp_emsc_webgl_context_cb` | + +**Upstream comment 7954–7956** (now stale after the patch): "HTML canvas doesn't +receive input focus, this is why key event handlers are added to the window object +(this could be worked around by adding a 'tab index' to the canvas)". The TrussC +patch (§6) does exactly that workaround. Note the resize handler is registered +separately in `_sapp_emsc_run` (8075), not here. + +### Unregistration (`_sapp_emsc_unregister_eventhandlers`, 7991–8025) +Mirror image — all set to callback `0`. **Asymmetry to preserve (8011–8013):** the +resize callback is only unregistered here `if (!_sapp.desc.html5.canvas_resize)` +(because it was only registered when tracking). Keyboard callbacks are also +unregistered from the canvas selector (patched, comment 7998). + +### The `_sapp_events_enabled()` gate + consume/bubble model +Every callback body wraps event delivery in `if (_sapp_events_enabled())` +(event_cb set AND init_called) — no events before the first frame, identical to +native. Callbacks return an `EM_BOOL` **"consume" value**: returning true tells +the browser this handler consumed the event (preventDefault). The `bubble_*` +desc flags invert this: +- `consume_event = !_sapp.desc.html5.bubble_mouse_events` (7561) — default bubble + off ⇒ consume on. Then `consume_event |= _sapp_call_event(&_sapp.event)` (7622): + the app's `sapp_consume_event()` also forces consume. Same shape for wheel + (`bubble_wheel_events`, 7635), touch (`bubble_touch_events`, 7855), keys (§5). +- **`sapp_consume_event()` (14237–14239)** just sets `_sapp.event_consumed`; on web + the return of `_sapp_call_event` reflects that and becomes the DOM + preventDefault. So consuming a web event = preventDefault on the DOM event. + +--- + +## 5. Per-event-type semantics + +### Mouse (`_sapp_emsc_mouse_cb`, 7559–7630) +- Stores `_sapp.emsc.mouse_buttons = emsc_event->buttons` (browser bitmask) every + call. **Position:** if `mouse.locked` → `dx/dy = movementX/movementY` (raw + pointer-lock deltas, 7563–7565); else `x/y = targetX/targetY * dpi_scale`, and + `dx/dy` computed vs previous when `pos_valid` (7566–7576). Note **targetX/Y are + CSS pixels, scaled by dpi_scale to framebuffer pixels** — the app sees + framebuffer-space coords. +- Type map (7581–7604): MOUSEDOWN→MOUSE_DOWN (button event), MOUSEUP→MOUSE_UP + (button event), MOUSEMOVE→MOUSE_MOVE, MOUSEENTER→MOUSE_ENTER (clear dx/dy), + MOUSELEAVE→MOUSE_LEAVE (clear dx/dy). Guarded by + `emsc_event->button in [0, SAPP_MAX_MOUSEBUTTONS)` (7577). +- **Button map (7613–7618):** 0→LEFT, 1→MIDDLE, 2→RIGHT (browser order differs from + sokol's L/M/R — note this is NOT a straight cast for 1/2). Non-button events set + `mouse_button = SAPP_MOUSEBUTTON_INVALID`. +- Modifiers (`_sapp_emsc_mouse_event_mods`, 7477–7485): ctrl/shift/alt/meta → + CTRL/SHIFT/ALT/SUPER, plus held-button mods from `mouse_buttons` via + `_sapp_emsc_mouse_button_mods` (7469–7475: bit0→LMB, **bit1→RMB, bit2→MMB** — + "not a bug" comments; browser button bitmask order R/M swapped vs sokol enum). +- **Pointer-lock activation (7625–7627):** `_sapp_emsc_update_mouse_lock_state()` + is called only on button events (mouse-lock can only be requested from a user + gesture; see §8). + +### Wheel (`_sapp_emsc_wheel_cb`, 7632–7654) +- MOUSE_SCROLL. **Delta scaling by `deltaMode` (7641–7647):** `DOM_DELTA_PIXEL → + -0.01`, `DOM_DELTA_LINE → -1.33`, `DOM_DELTA_PAGE → -10.0` (guessed), + default `-0.1`. `scroll_x/y = scale * deltaX/deltaY`. **Sign is negated** (natural + scroll direction). This exact table (github issue #339) is load-bearing — + lift verbatim. Also updates mouse-lock state (7652). + +### Keyboard (`_sapp_emsc_key_cb`, 7783–7851) + keymap (7656–7775) +- Type map: KEYDOWN→KEY_DOWN, KEYUP→KEY_UP, KEYPRESS→CHAR (7788–7801). +- `key_repeat = emsc_event->repeat` (7805). Modifiers via + `_sapp_emsc_key_event_mods` (7487–7495). +- **CHAR path (7807–7810):** `char_code = emsc_event->charCode`; consume unless + `bubble_char_events`. (Comment: charCode unsupported on Android Chrome.) +- **KEY path (7811–7840):** + - **Keycode table = HTML5 `code` strings** (7812–7815): if `emsc_event->code[0]` + non-empty (desktop browsers send physical `code` like "KeyA", "Digit1", + "ArrowLeft") → `_sapp_emsc_translate_key(code)`. Else (mobile browsers send + only localized `key`) → `_sapp_emsc_translate_key(key)` — works only for a small + localization-agnostic subset (Enter/arrows/etc.), alpha-numerics become + INVALID. **This is a `str→sapp_keycode` linear-search table + `_sapp_emsc_keymap[]` (7656–7763)** keyed on `code` strings ("Backspace", + "Tab", "Enter", "ShiftLeft"…"KeyZ", "Digit0"…"Numpad0"…"F1"…, punctuation). + ~115 entries. **Lift the entire table verbatim** — it's the authoritative + web keycode map. Note the "Quote" → `SAPP_KEYCODE_GRAVE_ACCENT` "FIXME: ???" + quirk (7761) is a known upstream bug; keep as-is for parity. + - **macOS Super-key keyup hack (7824–7834):** on KEY_DOWN, if a non-Super key is + pressed while SUPER modifier is held, set `send_keyup_followup = true` — because + macOS browsers don't deliver keyUp while Cmd is held, so keys would "stick". A + synthetic KEY_UP is fired right after the KEY_DOWN (7843–7846). Load-bearing; + lift. + - **Forward-special-keys / char-key logic (7836–7840):** `_sapp_emsc_is_char_key` + (7777–7781) = `key_code < SAPP_KEYCODE_WORLD_1`. **Only NON-char keys are + consumed** (`consume_event |= !bubble_key_events` at 7838–7840). Char keys are + deliberately left to bubble so the browser can generate the follow-up + `keypress`/CHAR event. This is the "forward special keys" rule: prevent-default + F-keys/Tab/Backspace/arrows/etc. (non-char) to stop browser default actions, + but let character keys through. **This interacts with the TrussC canvas patch + (§6):** with keys on the canvas + tabindex, preventDefault on the canvas stops + the page from scrolling on space/arrows while the canvas is focused. +- `_sapp_emsc_update_mouse_lock_state()` at 7849 (keys are user gestures too). + +### Touch (`_sapp_emsc_touch_cb`, 7853–7894) +- **Real multi-touch, NOT mouse emulation.** TOUCHSTART→TOUCHES_BEGAN, + MOVE→TOUCHES_MOVED, END→TOUCHES_ENDED, CANCEL→TOUCHES_CANCELLED (7858–7873). + `num_touches = min(emsc numTouches, SAPP_MAX_TOUCHPOINTS)` (7878–7881). Per point: + `identifier`, `pos_x/y = targetX/Y * dpi_scale`, `changed = isChanged` + (7882–7889). Consume unless `bubble_touch_events`. No synthetic mouse events are + generated from touch — the app gets native touch events. + +### Focus / blur (7896–7916) +`_sapp_emsc_focus_cb` → FOCUSED, `_sapp_emsc_blur_cb` → UNFOCUSED. Registered on +`EMSCRIPTEN_EVENT_TARGET_WINDOW` (not canvas). Always return true. + +### Fullscreen change (`_sapp_emsc_fullscreenchange_cb`, 7378–7384) +Syncs `_sapp.fullscreen = emsc_event->isFullscreen` — needed to catch the user +pressing Esc to exit fullscreen. No sapp event is fired. + +### Resize (`_sapp_emsc_size_changed`, 7507–7557) — see §10. + +--- + +## 6. THE TRUSSC CANVAS-KEYBOARD PATCH (native feature of the port) + +**TRUSSC_MODIFICATIONS.md patch #2 "Keyboard Events on Canvas (Emscripten)".** The +patched lines are in `_sapp_emsc_register_eventhandlers` (7963–7968) and +`_sapp_emsc_unregister_eventhandlers` (7998–8001): + +```c +// Modified by tettou771 for TrussC: register keyboard events on canvas instead of window +// This allows other page elements (like Monaco editor) to handle keyboard events +// Canvas must have tabindex attribute to receive focus +emscripten_set_keydown_callback(_sapp.html5_canvas_selector, 0, true, _sapp_emsc_key_cb); +emscripten_set_keyup_callback(_sapp.html5_canvas_selector, 0, true, _sapp_emsc_key_cb); +emscripten_set_keypress_callback(_sapp.html5_canvas_selector, 0, true, _sapp_emsc_key_cb); +``` + +**What differs from upstream:** upstream sokol registers keydown/keyup/keypress on +`EMSCRIPTEN_EVENT_TARGET_WINDOW` (so the app grabs ALL page keystrokes globally). +TrussC registers them on `_sapp.html5_canvas_selector` instead, so keyboard events +only reach the sokol app when the **canvas element itself has focus**. This lets +other DOM elements on the same page (the Monaco code editor in TrussSketch, form +inputs, etc.) receive keyboard input independently — critical for the web IDE +coexistence. + +**Requirement this imposes:** an HTML canvas element cannot receive keyboard focus +unless it has a `tabindex` attribute. **The shell/host page MUST set `tabindex` +(e.g. `tabindex="0"` or `-1`) on the canvas**, and something must give the canvas +focus (click-to-focus works by default once tabindex is present; the page may also +call `canvas.focus()`). Without tabindex, keyboard events never fire → the app +appears to ignore the keyboard. The stale upstream comment at 7954–7956 documents +exactly this workaround. + +**Port directive:** this is a NATIVE feature of `sokol_app_tc.h`, not an optional +patch. **P3 must register keyboard handlers on the canvas selector by default** +(matching this behavior), and the TrussC web shell.html must keep `tabindex` on the +canvas (verify against §13). Do NOT revert to window-target keyboard. + +**Other emscripten-relevant patches in TRUSSC_MODIFICATIONS.md:** patch #1 +"Skip Present" adds `skip_present` to the WGPU present path — but that path is +`#if !_SAPP_EMSCRIPTEN` (4261), so **skip_present is a no-op on web** (§3/§14). +Patch #3 (RGB10A2) is Metal/D3D11 only; comment explicitly says "WebGL unchanged" +— web color format is RGBA8/BGRA8 (§11). No other patches touch the emscripten +path. + +--- + +## 7. Clipboard, drag&drop, fetch (EM_JS-heavy) + +### Clipboard +- **Set (`_sapp_emsc_set_clipboard_string` → `sapp_js_write_clipboard`, 7079–7099):** + creates a hidden off-screen `