@soundor/vst3-host 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/CMakeLists.txt +194 -0
  2. package/CMakePresets.json +115 -0
  3. package/LICENSE +21 -0
  4. package/NOTICE.md +17 -0
  5. package/README.md +40 -2
  6. package/cmake/Vst3HostDependencies.cmake +179 -0
  7. package/dist/cli.d.mts +1 -0
  8. package/dist/cli.mjs +88 -0
  9. package/dist/cli.mjs.map +1 -0
  10. package/dist/index.d.mts +586 -0
  11. package/dist/index.d.mts.map +1 -0
  12. package/dist/index.mjs +1 -0
  13. package/dist/prepare-ayjTVhfk.mjs +1149 -0
  14. package/dist/prepare-ayjTVhfk.mjs.map +1 -0
  15. package/native/src/Bundle.cpp +253 -0
  16. package/native/src/Bundle.h +32 -0
  17. package/native/src/EditorArea.h +92 -0
  18. package/native/src/EditorAreaMac.mm +52 -0
  19. package/native/src/EditorAreaWindows.cpp +66 -0
  20. package/native/src/EditorAreaX11.cpp +512 -0
  21. package/native/src/EditorWindow.h +72 -0
  22. package/native/src/EditorWindowMac.mm +134 -0
  23. package/native/src/EditorWindowWindows.cpp +148 -0
  24. package/native/src/EventLoop.h +50 -0
  25. package/native/src/EventLoopLinux.cpp +204 -0
  26. package/native/src/EventLoopMac.mm +146 -0
  27. package/native/src/EventLoopWindows.cpp +132 -0
  28. package/native/src/HostContext.cpp +217 -0
  29. package/native/src/HostContext.h +116 -0
  30. package/native/src/HostInterface.cpp +1026 -0
  31. package/native/src/HostInterface.h +138 -0
  32. package/native/src/HostWindow.cpp +901 -0
  33. package/native/src/HostWindow.h +187 -0
  34. package/native/src/Ipc.cpp +158 -0
  35. package/native/src/Ipc.h +91 -0
  36. package/native/src/Json.cpp +426 -0
  37. package/native/src/Json.h +85 -0
  38. package/native/src/LiveAudio.cpp +416 -0
  39. package/native/src/LiveAudio.h +121 -0
  40. package/native/src/Main.cpp +178 -0
  41. package/native/src/Paths.h +20 -0
  42. package/native/src/Plugin.cpp +1865 -0
  43. package/native/src/Plugin.h +321 -0
  44. package/native/src/Session.cpp +656 -0
  45. package/native/src/Session.h +82 -0
  46. package/native/src/Settings.cpp +93 -0
  47. package/native/src/Settings.h +23 -0
  48. package/native/src/Version.h.in +11 -0
  49. package/native/src/X11Library.cpp +127 -0
  50. package/native/src/X11Library.h +83 -0
  51. package/native/tools/Embed.cpp +49 -0
  52. package/package.json +58 -3
  53. package/protocol.json +7 -0
  54. package/targets.json +33 -0
@@ -0,0 +1,901 @@
1
+ #include "HostWindow.h"
2
+
3
+ #include <SDL3/SDL.h>
4
+ #include <imgui.h>
5
+ #include <imgui_impl_sdl3.h>
6
+ #include <imgui_impl_sdlrenderer3.h>
7
+
8
+ #include <algorithm>
9
+ #include <cmath>
10
+ #include <cstdio>
11
+ #include <cstdlib>
12
+ #include <cstring>
13
+ #include <utility>
14
+
15
+ namespace soundor::vst3host
16
+ {
17
+ namespace fonts
18
+ {
19
+ // Inter (OFL-1.1), embedded by the build (Vst3HostDependencies.cmake).
20
+ extern const unsigned char regular[];
21
+ extern const std::size_t regularSize;
22
+ extern const unsigned char semibold[];
23
+ extern const std::size_t semiboldSize;
24
+ } // namespace fonts
25
+
26
+ namespace
27
+ {
28
+ // Values the interface shows also change without input (the API, the
29
+ // editor, the plugin): it builds a frame this often to see, which is
30
+ // cheap, and presents it only if it differs.
31
+ constexpr auto refreshInterval = std::chrono::milliseconds(100);
32
+ // A window manager's drag has no end to see: the geometry has stopped
33
+ // changing for this long.
34
+ constexpr auto liveQuiet = std::chrono::milliseconds(150);
35
+ // A view that goes live this soon after a resize is answering it.
36
+ constexpr auto windowAnswer = std::chrono::milliseconds(200);
37
+ // How long a bare window waits for a size it asked for.
38
+ constexpr auto askedSizeTimeout = std::chrono::milliseconds(500);
39
+ // How often it asks again for the editor's size after a drag: a window
40
+ // manager may refuse while it holds the window.
41
+ constexpr int settleAsks = 8;
42
+ // How long the stage waits for the window to take the panel's width.
43
+ constexpr auto stageFollowTimeout = std::chrono::milliseconds(500);
44
+
45
+ bool initVideo(std::string& error)
46
+ {
47
+ if (SDL_WasInit(SDL_INIT_VIDEO) != 0)
48
+ return true;
49
+ #if defined(__linux__)
50
+ // VST3 editors on Linux are X11 windows.
51
+ SDL_SetHint(SDL_HINT_VIDEO_DRIVER, "x11");
52
+ #endif
53
+ SDL_SetHint(SDL_HINT_APP_NAME, "Soundor VST3 host");
54
+ SDL_SetHint(SDL_HINT_QUIT_ON_LAST_WINDOW_CLOSE, "0");
55
+ // Renderers that leave the editor's child view alone: Direct3D 9
56
+ // presents by blitting, around child windows.
57
+ const char* renderer = std::getenv("SOUNDOR_VST3_HOST_RENDERER"); // NOLINT(concurrency-mt-unsafe)
58
+ #if defined(_WIN32)
59
+ SDL_SetHint(SDL_HINT_RENDER_DRIVER, renderer != nullptr ? renderer : "direct3d,direct3d11,software");
60
+ #elif defined(__APPLE__)
61
+ SDL_SetHint(SDL_HINT_RENDER_DRIVER, renderer != nullptr ? renderer : "metal,opengl,software");
62
+ #else
63
+ SDL_SetHint(SDL_HINT_FRAMEBUFFER_ACCELERATION, "0");
64
+ // The window manager waits for the frame of each size it asks for
65
+ // (OpenGL windows only: SDL answers it when they swap).
66
+ SDL_SetHint(SDL_HINT_VIDEO_X11_ENABLE_XSYNC_EXT, "1");
67
+ SDL_SetHint(SDL_HINT_RENDER_DRIVER, renderer != nullptr ? renderer : "opengl,software");
68
+ #endif
69
+ if (! SDL_InitSubSystem(SDL_INIT_VIDEO))
70
+ {
71
+ error = std::string("no display is available for the host interface (") + SDL_GetError()
72
+ + "; is DISPLAY set? Use headless mode without one)";
73
+ return false;
74
+ }
75
+ return true;
76
+ }
77
+
78
+ void applyStyle(float scale)
79
+ {
80
+ ImGuiStyle& style = ImGui::GetStyle();
81
+ style = ImGuiStyle();
82
+ ImGui::StyleColorsDark(&style);
83
+ style.WindowRounding = 0;
84
+ style.ChildRounding = 0;
85
+ style.FrameRounding = 6;
86
+ style.PopupRounding = 6;
87
+ style.GrabRounding = 6;
88
+ style.ScrollbarRounding = 6;
89
+ style.FramePadding = ImVec2(8, 5);
90
+ style.ItemSpacing = ImVec2(8, 6);
91
+ style.FrameBorderSize = 1;
92
+ style.PopupBorderSize = 1;
93
+ style.GrabMinSize = 12;
94
+ style.ScrollbarSize = 10;
95
+ style.SeparatorTextBorderSize = 1;
96
+ auto rgb = [](int r, int g, int b, float a = 1) {
97
+ return ImVec4(static_cast<float>(r) / 255, static_cast<float>(g) / 255, static_cast<float>(b) / 255, a);
98
+ };
99
+ ImVec4* colors = style.Colors;
100
+ colors[ImGuiCol_Text] = rgb(0xe6, 0xe7, 0xea);
101
+ colors[ImGuiCol_TextDisabled] = rgb(0x8b, 0x8f, 0x98);
102
+ colors[ImGuiCol_WindowBg] = rgb(0x15, 0x16, 0x1a);
103
+ colors[ImGuiCol_ChildBg] = rgb(0x15, 0x16, 0x1a, 0);
104
+ colors[ImGuiCol_PopupBg] = rgb(0x1d, 0x1f, 0x24);
105
+ colors[ImGuiCol_Border] = rgb(0x2a, 0x2c, 0x33);
106
+ colors[ImGuiCol_FrameBg] = rgb(0x1d, 0x1f, 0x24);
107
+ colors[ImGuiCol_FrameBgHovered] = rgb(0x24, 0x26, 0x2c);
108
+ colors[ImGuiCol_FrameBgActive] = rgb(0x24, 0x26, 0x2c);
109
+ colors[ImGuiCol_CheckMark] = rgb(0x8f, 0xb3, 0xff);
110
+ colors[ImGuiCol_SliderGrab] = rgb(0x3a, 0x7b, 0xfd);
111
+ colors[ImGuiCol_SliderGrabActive] = rgb(0x8f, 0xb3, 0xff);
112
+ colors[ImGuiCol_Button] = rgb(0x1d, 0x1f, 0x24);
113
+ colors[ImGuiCol_ButtonHovered] = rgb(0x26, 0x28, 0x2f);
114
+ colors[ImGuiCol_ButtonActive] = rgb(0x3a, 0x7b, 0xfd, 0.3F);
115
+ colors[ImGuiCol_Header] = rgb(0x3a, 0x7b, 0xfd, 0.2F);
116
+ colors[ImGuiCol_HeaderHovered] = rgb(0xff, 0xff, 0xff, 0.08F);
117
+ colors[ImGuiCol_HeaderActive] = rgb(0x3a, 0x7b, 0xfd, 0.3F);
118
+ colors[ImGuiCol_ScrollbarBg] = rgb(0, 0, 0, 0);
119
+ colors[ImGuiCol_ScrollbarGrab] = rgb(0x2a, 0x2c, 0x33);
120
+ colors[ImGuiCol_ScrollbarGrabHovered] = rgb(0x3a, 0x3c, 0x44);
121
+ colors[ImGuiCol_ScrollbarGrabActive] = rgb(0x4a, 0x4c, 0x54);
122
+ colors[ImGuiCol_Separator] = rgb(0x2a, 0x2c, 0x33);
123
+ colors[ImGuiCol_NavCursor] = rgb(0x3a, 0x7b, 0xfd);
124
+ style.ScaleAllSizes(scale);
125
+ style.FontSizeBase = 13;
126
+ style.FontScaleDpi = scale;
127
+ }
128
+
129
+ /** FNV-1a over the bytes of the frame's drawing. */
130
+ uint64_t hashDrawData(const ImDrawData* data)
131
+ {
132
+ uint64_t hash = 1469598103934665603ULL;
133
+ auto mix = [&hash](const void* bytes, size_t size)
134
+ {
135
+ const auto* at = static_cast<const unsigned char*>(bytes);
136
+ for (size_t index = 0; index < size; ++index)
137
+ hash = (hash ^ at[index]) * 1099511628211ULL;
138
+ };
139
+ mix(&data->DisplaySize, sizeof data->DisplaySize);
140
+ mix(&data->FramebufferScale, sizeof data->FramebufferScale);
141
+ for (const ImDrawList* list : data->CmdLists)
142
+ {
143
+ mix(list->VtxBuffer.Data, static_cast<size_t>(list->VtxBuffer.Size) * sizeof(ImDrawVert));
144
+ mix(list->IdxBuffer.Data, static_cast<size_t>(list->IdxBuffer.Size) * sizeof(ImDrawIdx));
145
+ for (const ImDrawCmd& command : list->CmdBuffer)
146
+ {
147
+ mix(&command.ClipRect, sizeof command.ClipRect);
148
+ // The texture's identity, uploaded or not yet.
149
+ mix(&command.TexRef, sizeof command.TexRef);
150
+ mix(&command.ElemCount, sizeof command.ElemCount);
151
+ mix(&command.IdxOffset, sizeof command.IdxOffset);
152
+ mix(&command.VtxOffset, sizeof command.VtxOffset);
153
+ }
154
+ }
155
+ return hash;
156
+ }
157
+
158
+ bool texturesChanging(const ImDrawData* data)
159
+ {
160
+ if (data->Textures == nullptr)
161
+ return false;
162
+ return std::ranges::any_of(*data->Textures, [](const ImTextureData* texture)
163
+ { return texture->Status != ImTextureStatus_OK; });
164
+ }
165
+ } // namespace
166
+
167
+ std::unique_ptr<EditorWindow> HostWindow::create(EventLoop& loop, HostModel& model, const std::string& title,
168
+ int width, int height, bool bare, Callbacks callbacks,
169
+ std::string& error)
170
+ {
171
+ if (! initVideo(error))
172
+ return nullptr;
173
+ auto window = std::make_unique<HostWindow>(loop, model, bare, std::move(callbacks));
174
+ if (! window->open(title, width, height, error))
175
+ return nullptr;
176
+ return window;
177
+ }
178
+
179
+ HostWindow::HostWindow(EventLoop& loop, HostModel& model, bool bare, Callbacks callbacks)
180
+ : loop_(loop), model_(model), callbacks_(std::move(callbacks)), bare_(bare)
181
+ {
182
+ }
183
+
184
+ bool HostWindow::open(const std::string& title, int width, int height, std::string& error)
185
+ {
186
+ editorWidth_ = width;
187
+ editorHeight_ = height;
188
+ constexpr SDL_WindowFlags flags = SDL_WINDOW_HIDDEN | SDL_WINDOW_HIGH_PIXEL_DENSITY;
189
+ #if defined(__linux__)
190
+ window_ = SDL_CreateWindow(title.c_str(), width, height, flags | SDL_WINDOW_OPENGL);
191
+ if (window_ != nullptr)
192
+ {
193
+ renderer_ = SDL_CreateRenderer(window_, nullptr);
194
+ // Only an OpenGL renderer answers the window manager's requests.
195
+ if (renderer_ == nullptr || std::strcmp(SDL_GetRendererName(renderer_), "opengl") != 0)
196
+ {
197
+ if (renderer_ != nullptr)
198
+ SDL_DestroyRenderer(renderer_);
199
+ renderer_ = nullptr;
200
+ SDL_DestroyWindow(window_);
201
+ window_ = nullptr;
202
+ }
203
+ }
204
+ #endif
205
+ if (window_ == nullptr)
206
+ {
207
+ SDL_SetHint(SDL_HINT_VIDEO_X11_ENABLE_XSYNC_EXT, "0");
208
+ window_ = SDL_CreateWindow(title.c_str(), width, height, flags);
209
+ }
210
+ if (window_ == nullptr)
211
+ {
212
+ error = std::string("the host interface's window could not be created: ") + SDL_GetError();
213
+ return false;
214
+ }
215
+ if (renderer_ == nullptr)
216
+ renderer_ = SDL_CreateRenderer(window_, nullptr);
217
+ if (renderer_ == nullptr)
218
+ {
219
+ error = std::string("the host interface could not draw: ") + SDL_GetError();
220
+ return false;
221
+ }
222
+ std::fprintf(stderr, "[vst3-host] host interface renderer: %s\n", SDL_GetRendererName(renderer_));
223
+ area_ = EditorArea::create(window_, error);
224
+ if (area_ == nullptr)
225
+ return false;
226
+ if (bare_)
227
+ {
228
+ area_->allowOnlyMoveResizeClose();
229
+ area_->keepTopLeft(true);
230
+ }
231
+
232
+ IMGUI_CHECKVERSION();
233
+ context_ = ImGui::CreateContext();
234
+ ImGui::SetCurrentContext(context_);
235
+ ImGuiIO& io = ImGui::GetIO();
236
+ io.IniFilename = nullptr;
237
+ io.LogFilename = nullptr;
238
+ ImFontConfig config;
239
+ config.FontDataOwnedByAtlas = false;
240
+ // ImGui does not write to font data it does not own.
241
+ ImFont* regular = io.Fonts->AddFontFromMemoryTTF(const_cast<unsigned char*>(fonts::regular),
242
+ static_cast<int>(fonts::regularSize), 13, &config);
243
+ ImFont* semibold = io.Fonts->AddFontFromMemoryTTF(const_cast<unsigned char*>(fonts::semibold),
244
+ static_cast<int>(fonts::semiboldSize), 13, &config);
245
+ io.FontDefault = regular;
246
+ ImGui_ImplSDL3_InitForSDLRenderer(window_, renderer_);
247
+ ImGui_ImplSDLRenderer3_Init(renderer_);
248
+
249
+ interface_ = std::make_unique<HostInterface>(loop_, model_, window_, semibold, bare_);
250
+ panelOpen_ = interface_->panelOpen();
251
+ stagePanelOpen_ = panelOpen_;
252
+ applyScale();
253
+ fitWindow();
254
+ applyRefreshRate();
255
+ #if defined(__APPLE__)
256
+ // SDL reads Cocoa's events itself (its keyboard handling depends on
257
+ // it), and sends every event on to the application, the editor's
258
+ // included.
259
+ loop_.setWaiter(
260
+ [this, alive = alive_](double seconds)
261
+ {
262
+ SDL_WaitEventTimeout(nullptr, static_cast<Sint32>(seconds * 1000));
263
+ if (*alive)
264
+ pump();
265
+ });
266
+ #endif
267
+ return true;
268
+ }
269
+
270
+ HostWindow::~HostWindow()
271
+ {
272
+ closing_ = true;
273
+ *alive_ = false;
274
+ #if defined(__APPLE__)
275
+ loop_.setWaiter(nullptr);
276
+ #endif
277
+ if (timer_ != 0)
278
+ loop_.removeTimer(timer_);
279
+ interface_.reset();
280
+ if (live_)
281
+ endLive();
282
+ if (context_ != nullptr)
283
+ {
284
+ ImGui::SetCurrentContext(context_);
285
+ ImGui_ImplSDLRenderer3_Shutdown();
286
+ ImGui_ImplSDL3_Shutdown();
287
+ ImGui::DestroyContext(context_);
288
+ }
289
+ area_.reset();
290
+ if (renderer_ != nullptr)
291
+ SDL_DestroyRenderer(renderer_);
292
+ if (window_ != nullptr)
293
+ SDL_DestroyWindow(window_);
294
+ }
295
+
296
+ void HostWindow::setResizable(bool resizable)
297
+ {
298
+ resizable_ = resizable;
299
+ // Only a bare window is the editor: the host's has a stage around it,
300
+ // which the user can size whichever editor it shows.
301
+ SDL_SetWindowResizable(window_, resizable || ! bare_);
302
+ fitWindow();
303
+ }
304
+
305
+ void HostWindow::setContentSize(int width, int height)
306
+ {
307
+ if (width == editorWidth_ && height == editorHeight_)
308
+ return;
309
+ editorWidth_ = width;
310
+ editorHeight_ = height;
311
+ fitWindow();
312
+ }
313
+
314
+ std::optional<std::pair<int, int>> HostWindow::editorRoom() const
315
+ {
316
+ // A bare window grows with the editor, and so does one not yet shown,
317
+ // for the size the editor opens at (its scale).
318
+ if (bare_ || ! shown_)
319
+ return std::nullopt;
320
+ const auto [width, height] = stageEditorSize();
321
+ return std::pair { std::max(width, editorWidth_), std::max(height, editorHeight_) };
322
+ }
323
+
324
+ void HostWindow::constrainContentSize(int width, int height)
325
+ {
326
+ editorWidth_ = width;
327
+ editorHeight_ = height;
328
+ // A bare window is the editor's size, whatever its limits made of a drag.
329
+ if (bare_)
330
+ {
331
+ applyLimits();
332
+ // The window manager owns the window through a drag: it takes the
333
+ // editor's size once the drag has stopped (settleWindow()).
334
+ if (settling_)
335
+ layout();
336
+ else
337
+ fitWindow();
338
+ return;
339
+ }
340
+ // The window manager owns a border drag. Keep any extra stage space
341
+ // rather than resize the outer window back on every constraint check.
342
+ applyLimits();
343
+ const auto required = requiredSize();
344
+ const auto current = windowSize();
345
+ if (required.first > current.first || required.second > current.second)
346
+ fitWindow();
347
+ else
348
+ layout();
349
+ }
350
+
351
+ void HostWindow::show()
352
+ {
353
+ SDL_ShowWindow(window_);
354
+ SDL_RaiseWindow(window_);
355
+ SDL_SyncWindow(window_);
356
+ draggedSize_ = windowSize();
357
+ layout();
358
+ shown_ = true;
359
+ forcePresent_ = true;
360
+ invalidate();
361
+ render();
362
+ }
363
+
364
+ double HostWindow::scale() const
365
+ {
366
+ // The interface's: the editor and what is around it are the same size
367
+ // on the screen, as in the web host.
368
+ return scale_;
369
+ }
370
+
371
+ std::pair<int, int> HostWindow::windowSize() const
372
+ {
373
+ int width = 0;
374
+ int height = 0;
375
+ SDL_GetWindowSize(window_, &width, &height);
376
+ return { width, height };
377
+ }
378
+
379
+ void HostWindow::applyScale()
380
+ {
381
+ #if defined(__APPLE__)
382
+ // Points: the system scales them.
383
+ scale_ = 1;
384
+ #else
385
+ scale_ = std::max(1.0F, SDL_GetWindowDisplayScale(window_));
386
+ #endif
387
+ ImGui::SetCurrentContext(context_);
388
+ applyStyle(scale_);
389
+ }
390
+
391
+ void HostWindow::applyRefreshRate()
392
+ {
393
+ const SDL_DisplayMode* mode = SDL_GetCurrentDisplayMode(SDL_GetDisplayForWindow(window_));
394
+ const double rate = mode != nullptr && std::isfinite(mode->refresh_rate) && mode->refresh_rate > 0
395
+ ? static_cast<double>(mode->refresh_rate)
396
+ : 60.0;
397
+ frameInterval_ =
398
+ std::chrono::duration_cast<std::chrono::steady_clock::duration>(std::chrono::duration<double>(1.0 / rate));
399
+ // Wake just before a frame is due. The deadline below retains fractional
400
+ // milliseconds (59.94 Hz, 144 Hz, etc.) instead of accumulating drift.
401
+ const auto milliseconds = static_cast<unsigned>(std::max(1.0, std::floor(1000.0 / rate)));
402
+ if (timer_ != 0)
403
+ loop_.removeTimer(timer_);
404
+ timer_ = loop_.addTimer(milliseconds, [this] { pump(); });
405
+ nextFrame_ = std::chrono::steady_clock::now();
406
+ invalidate();
407
+ }
408
+
409
+ std::pair<int, int> HostWindow::naturalSize() const
410
+ {
411
+ const float padding = paddingSpace();
412
+ const float stage = std::max(static_cast<float>(editorWidth_), dockSpan()) + 2 * padding;
413
+ const float panel = panelOpen_ ? HostInterface::panelWidth * scale_ : 0;
414
+ float height = static_cast<float>(editorHeight_) + padding + dockHeight();
415
+ if (panelOpen_)
416
+ height = std::max(height, HostInterface::panelMinHeight * scale_);
417
+ return { static_cast<int>(std::ceil(stage + panel)), static_cast<int>(std::ceil(height)) };
418
+ }
419
+
420
+ std::pair<int, int> HostWindow::requiredSize() const
421
+ {
422
+ const float padding = paddingSpace();
423
+ const float panel = panelOpen_ ? HostInterface::panelWidth * scale_ : 0;
424
+ const float stage = std::max(static_cast<float>(editorWidth_), dockSpan()) + 2 * padding;
425
+ return { static_cast<int>(std::ceil(stage + panel)),
426
+ static_cast<int>(std::ceil(static_cast<float>(editorHeight_) + padding + dockHeight())) };
427
+ }
428
+
429
+ void HostWindow::setEditorMinimum(int width, int height)
430
+ {
431
+ minimumWidth_ = width;
432
+ minimumHeight_ = height;
433
+ limits_ = { 0, 0 };
434
+ applyLimits();
435
+ }
436
+
437
+ void HostWindow::setEditorMaximum(int width, int height)
438
+ {
439
+ if (! bare_)
440
+ return;
441
+ maximumWidth_ = width;
442
+ maximumHeight_ = height;
443
+ limits_ = { 0, 0 };
444
+ applyLimits();
445
+ }
446
+
447
+ void HostWindow::askWindowSize(int width, int height)
448
+ {
449
+ askedSize_ = std::pair { width, height };
450
+ askedUntil_ = std::chrono::steady_clock::now() + askedSizeTimeout;
451
+ SDL_SetWindowSize(window_, width, height);
452
+ }
453
+
454
+ void HostWindow::applyLimits()
455
+ {
456
+ // A bare window is the editor, so no smaller than the smallest editor.
457
+ // The host's window leaves the editor's size alone: no smaller than
458
+ // this editor, with its dock and its panel.
459
+ const std::pair smallest { minimumWidth_, minimumHeight_ };
460
+ const bool fixed = bare_ && ! resizable_;
461
+ const auto minimum = fixed ? naturalSize() : bare_ ? smallest : requiredSize();
462
+ if (minimum == limits_)
463
+ return;
464
+ limits_ = minimum;
465
+ SDL_SetWindowMinimumSize(window_, minimum.first, minimum.second);
466
+ if (fixed)
467
+ SDL_SetWindowMaximumSize(window_, minimum.first, minimum.second);
468
+ else
469
+ SDL_SetWindowMaximumSize(window_, bare_ ? maximumWidth_ : 0, bare_ ? maximumHeight_ : 0);
470
+ }
471
+
472
+ void HostWindow::fitWindow(int extraWidth)
473
+ {
474
+ const auto natural = naturalSize();
475
+ fitted_ = natural;
476
+ limits_ = { 0, 0 };
477
+ applyLimits();
478
+ if ((SDL_GetWindowFlags(window_) & SDL_WINDOW_MAXIMIZED) == 0)
479
+ {
480
+ // Space the user made stays. Window-manager requests are
481
+ // asynchronous: waiting here can block all plugin input and timers.
482
+ const auto [width, height] = windowSize();
483
+ const std::pair wanted =
484
+ ! bare_ ? std::pair { std::max(natural.first, width + extraWidth), std::max(natural.second, height) }
485
+ : natural;
486
+ if (wanted != std::pair { width, height })
487
+ {
488
+ area_->keepTopLeft(true);
489
+ if (bare_)
490
+ askWindowSize(wanted.first, wanted.second);
491
+ else
492
+ SDL_SetWindowSize(window_, wanted.first, wanted.second);
493
+ // The window has not changed yet, and may take a while: the
494
+ // stage stays until it has, or the editor would move away and
495
+ // back.
496
+ if (stagePanelOpen_ != panelOpen_)
497
+ {
498
+ stageFollowsAt_ = std::chrono::steady_clock::now() + stageFollowTimeout;
499
+ layout();
500
+ return;
501
+ }
502
+ }
503
+ }
504
+ stagePanelOpen_ = panelOpen_;
505
+ layout();
506
+ }
507
+
508
+ float HostWindow::paddingSpace() const
509
+ {
510
+ return bare_ ? 0 : HostInterface::padding * scale_;
511
+ }
512
+
513
+ float HostWindow::dockHeight() const
514
+ {
515
+ return bare_ ? 0 : HostInterface::dockSpace * scale_;
516
+ }
517
+
518
+ float HostWindow::dockSpan() const
519
+ {
520
+ return bare_ ? 0 : interface_->dockWidth() * scale_;
521
+ }
522
+
523
+ float HostWindow::panelSpace() const
524
+ {
525
+ return stagePanelOpen_ ? std::ceil(HostInterface::panelWidth * scale_) : 0;
526
+ }
527
+
528
+ void HostWindow::layout()
529
+ {
530
+ const auto [width, height] = windowSize();
531
+ const float padding = paddingSpace();
532
+ const float panel = panelSpace();
533
+ const float stage = static_cast<float>(width) - panel;
534
+ const float free = static_cast<float>(height) - padding - dockHeight() - static_cast<float>(editorHeight_);
535
+ // Centred on the stage, as the web host's view is.
536
+ const int x = std::max(0, static_cast<int>(std::floor((stage - static_cast<float>(editorWidth_)) / 2)));
537
+ const int y = static_cast<int>(padding + std::max(0.0F, std::floor(free / 2)));
538
+ const std::array frame { x, y, editorWidth_, editorHeight_ };
539
+ if (frame != editorFrame_ || ! area_->isAt(x, y))
540
+ {
541
+ touchLive();
542
+ editorFrame_ = frame;
543
+ area_->setFrame(x, y, editorWidth_, editorHeight_);
544
+ }
545
+ forcePresent_ = true;
546
+ invalidate();
547
+ }
548
+
549
+ void HostWindow::touchLive()
550
+ {
551
+ if (! shown_)
552
+ return;
553
+ if (! live_)
554
+ {
555
+ viewHidden_ = false;
556
+ hideViewFirst_ = false;
557
+ live_ = area_->beginLive();
558
+ // Before the editor takes a size: its image as it stands.
559
+ if (live_)
560
+ updateLiveImage();
561
+ }
562
+ liveTouched_ = std::chrono::steady_clock::now();
563
+ }
564
+
565
+ void HostWindow::endLive()
566
+ {
567
+ live_ = false;
568
+ area_->endLive();
569
+ if (liveTexture_ != nullptr)
570
+ SDL_DestroyTexture(liveTexture_);
571
+ liveTexture_ = nullptr;
572
+ }
573
+
574
+ void HostWindow::updateLiveImage()
575
+ {
576
+ const auto image = area_->takeImage();
577
+ if (! image)
578
+ return;
579
+ if (image->partial && liveTexture_ != nullptr)
580
+ {
581
+ hideViewFirst_ = true;
582
+ return;
583
+ }
584
+ if (liveTexture_ == nullptr || liveWidth_ != image->width || liveHeight_ != image->height)
585
+ {
586
+ if (liveTexture_ != nullptr)
587
+ SDL_DestroyTexture(liveTexture_);
588
+ liveTexture_ = SDL_CreateTexture(renderer_, SDL_PIXELFORMAT_XRGB8888, SDL_TEXTUREACCESS_STREAMING,
589
+ image->width, image->height);
590
+ if (liveTexture_ == nullptr)
591
+ return;
592
+ liveWidth_ = image->width;
593
+ liveHeight_ = image->height;
594
+ }
595
+ SDL_UpdateTexture(liveTexture_, nullptr, image->pixels, image->pitch);
596
+ livePartial_ = image->partial;
597
+ // The window manager has resized the window and waits for this frame,
598
+ // which it shows as the new size: the final layout, not the view's.
599
+ if (image->first && ! image->partial && std::chrono::steady_clock::now() - windowResizedAt_ < windowAnswer)
600
+ {
601
+ hideViewFirst_ = true;
602
+ livePart_.reset();
603
+ }
604
+ else if (image->first)
605
+ {
606
+ const auto width = static_cast<float>(image->width);
607
+ const auto height = static_cast<float>(image->height);
608
+ livePart_ = LivePart { static_cast<float>(image->shownX),
609
+ static_cast<float>(image->shownY),
610
+ static_cast<float>(image->validWidth),
611
+ static_cast<float>(image->validHeight),
612
+ static_cast<float>(image->validX) / width,
613
+ static_cast<float>(image->validY) / height,
614
+ static_cast<float>(image->validX + image->validWidth) / width,
615
+ static_cast<float>(image->validY + image->validHeight) / height };
616
+ }
617
+ else
618
+ {
619
+ livePart_.reset();
620
+ }
621
+ forcePresent_ = true;
622
+ }
623
+
624
+ std::pair<int, int> HostWindow::stageEditorSize() const
625
+ {
626
+ const auto [width, height] = windowSize();
627
+ const float padding = paddingSpace();
628
+ const float panel = panelSpace();
629
+ return { std::max(1, static_cast<int>(static_cast<float>(width) - panel - 2 * padding)),
630
+ std::max(1, static_cast<int>(static_cast<float>(height) - padding - dockHeight())) };
631
+ }
632
+
633
+ void HostWindow::pump()
634
+ {
635
+ if (closing_)
636
+ return;
637
+ ImGui::SetCurrentContext(context_);
638
+ const auto alive = alive_;
639
+ SDL_Event event;
640
+ #if defined(_WIN32)
641
+ // The host's message loop dispatches the window's messages; SDL's
642
+ // window procedure queues what they mean.
643
+ while (SDL_PeepEvents(&event, 1, SDL_GETEVENT, SDL_EVENT_FIRST, SDL_EVENT_LAST) > 0)
644
+ #else
645
+ while (SDL_PollEvent(&event))
646
+ #endif
647
+ {
648
+ if (! *alive)
649
+ return;
650
+ handle(event);
651
+ if (closing_)
652
+ return;
653
+ }
654
+ if (! *alive)
655
+ return;
656
+ const auto now = std::chrono::steady_clock::now();
657
+ settleWindow(now);
658
+ // A window manager that does not resize the window as asked.
659
+ if (stagePanelOpen_ != panelOpen_ && now >= stageFollowsAt_)
660
+ {
661
+ stagePanelOpen_ = panelOpen_;
662
+ layout();
663
+ }
664
+ // Only once the geometry changes: a press alone changes nothing, and
665
+ // starts nothing. Held, it goes on through a pause in the drag.
666
+ if (gripSize_ || (live_ && interface_->resizingEditor()))
667
+ touchLive();
668
+ else if (live_ && now - liveTouched_ >= liveQuiet && area_->whole())
669
+ {
670
+ endLive();
671
+ forcePresent_ = true;
672
+ invalidate();
673
+ }
674
+ if (now - lastFrame_ >= refreshInterval || interface_->animating() || live_)
675
+ invalidate(1);
676
+ if (pending_ > 0 && now >= nextFrame_)
677
+ {
678
+ // Skip missed deadlines rather than rendering catch-up frames.
679
+ nextFrame_ += frameInterval_ * ((now - nextFrame_) / frameInterval_ + 1);
680
+ render();
681
+ }
682
+ }
683
+
684
+ void HostWindow::settleWindow(std::chrono::steady_clock::time_point now)
685
+ {
686
+ if (! settling_)
687
+ return;
688
+ if (askedSize_ && now >= askedUntil_)
689
+ askedSize_.reset();
690
+ // Not before the editor has its last size from the drag.
691
+ if (askedSize_ || gripSize_ || now - windowResizedAt_ < liveQuiet)
692
+ return;
693
+ // Fullscreen, the window's size is not its own to change.
694
+ const bool matches = windowSize() == std::pair { editorWidth_, editorHeight_ };
695
+ if (matches || settlingAsks_ >= settleAsks || (SDL_GetWindowFlags(window_) & SDL_WINDOW_FULLSCREEN) != 0)
696
+ {
697
+ settling_ = false;
698
+ return;
699
+ }
700
+ ++settlingAsks_;
701
+ fitWindow();
702
+ }
703
+
704
+ void HostWindow::windowResized()
705
+ {
706
+ area_->windowChanged();
707
+ windowResizedAt_ = std::chrono::steady_clock::now();
708
+ stagePanelOpen_ = panelOpen_;
709
+ // Maximized, the next size is the restore's, which cuts the editor off
710
+ // whichever way it goes: its top left is the part that says what it is.
711
+ area_->keepTopLeft(bare_ || (SDL_GetWindowFlags(window_) & SDL_WINDOW_MAXIMIZED) != 0);
712
+ if (bare_)
713
+ bareWindowResized();
714
+ // The stage around the editor changes, and its place on it.
715
+ layout();
716
+ }
717
+
718
+ void HostWindow::bareWindowResized()
719
+ {
720
+ const auto [width, height] = windowSize();
721
+ const auto last = std::exchange(draggedSize_, std::pair { width, height });
722
+ // On its way to a size the host asked for: what comes before it is
723
+ // older than the editor's size, and must not change the editor.
724
+ if (askedSize_)
725
+ {
726
+ const bool arrived = std::abs(width - askedSize_->first) <= 2 && std::abs(height - askedSize_->second) <= 2;
727
+ if (arrived || std::chrono::steady_clock::now() >= askedUntil_)
728
+ askedSize_.reset();
729
+ if (arrived || askedSize_)
730
+ return;
731
+ }
732
+ if (! resizable_ || std::pair { width, height } == std::pair { editorWidth_, editorHeight_ })
733
+ return;
734
+ // Shown at once, and handed to the editor in render(), once it can take it.
735
+ touchLive();
736
+ settling_ = true;
737
+ settlingAsks_ = 0;
738
+ model_.plugin.previewEditorDrag(width, height, last.first, last.second);
739
+ gripSize_ = std::pair { editorWidth_, editorHeight_ };
740
+ }
741
+
742
+ void HostWindow::handle(const SDL_Event& event)
743
+ {
744
+ const SDL_WindowID id = SDL_GetWindowID(window_);
745
+ switch (event.type)
746
+ {
747
+ case SDL_EVENT_WINDOW_CLOSE_REQUESTED:
748
+ if (event.window.windowID == id && callbacks_.closed)
749
+ {
750
+ // The callback destroys this window.
751
+ closing_ = true;
752
+ loop_.post(callbacks_.closed);
753
+ }
754
+ return;
755
+ case SDL_EVENT_WINDOW_RESIZED:
756
+ if (event.window.windowID == id)
757
+ windowResized();
758
+ return;
759
+ case SDL_EVENT_WINDOW_FOCUS_GAINED:
760
+ case SDL_EVENT_WINDOW_FOCUS_LOST:
761
+ if (event.window.windowID == id && callbacks_.focusChanged)
762
+ callbacks_.focusChanged(event.type == SDL_EVENT_WINDOW_FOCUS_GAINED);
763
+ break;
764
+ case SDL_EVENT_DISPLAY_CURRENT_MODE_CHANGED:
765
+ case SDL_EVENT_DISPLAY_DESKTOP_MODE_CHANGED:
766
+ if (event.display.displayID == SDL_GetDisplayForWindow(window_))
767
+ applyRefreshRate();
768
+ return;
769
+ case SDL_EVENT_WINDOW_DISPLAY_CHANGED:
770
+ if (event.window.windowID == id)
771
+ applyRefreshRate();
772
+ return;
773
+ case SDL_EVENT_WINDOW_DISPLAY_SCALE_CHANGED:
774
+ if (event.window.windowID != id)
775
+ return;
776
+ applyScale();
777
+ fitWindow();
778
+ #if ! defined(__APPLE__)
779
+ if (callbacks_.scaleChanged)
780
+ callbacks_.scaleChanged(scale());
781
+ #endif
782
+ return;
783
+ case SDL_EVENT_WINDOW_EXPOSED:
784
+ case SDL_EVENT_WINDOW_SHOWN:
785
+ case SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED:
786
+ forcePresent_ = true;
787
+ invalidate();
788
+ return;
789
+ default:
790
+ break;
791
+ }
792
+ ImGui_ImplSDL3_ProcessEvent(&event);
793
+ // Input moves hover and focus: a few frames let ImGui settle.
794
+ invalidate(3);
795
+ }
796
+
797
+ void HostWindow::render()
798
+ {
799
+ // Apply the previous input frame's grip request before measuring and
800
+ // drawing. The native editor and the host frame then share one size.
801
+ if (const auto wanted = interface_->takeEditorSize())
802
+ {
803
+ gripSize_ = wanted;
804
+ model_.plugin.previewEditorSize(wanted->first, wanted->second);
805
+ }
806
+ if (live_)
807
+ updateLiveImage();
808
+ pending_ = std::max(0, pending_ - 1);
809
+ lastFrame_ = std::chrono::steady_clock::now();
810
+ ImGui::SetCurrentContext(context_);
811
+
812
+ const auto [width, height] = windowSize();
813
+ const float panel = panelSpace();
814
+ InterfaceLayout layout;
815
+ layout.width = static_cast<float>(width);
816
+ layout.height = static_cast<float>(height);
817
+ layout.scale = scale_;
818
+ layout.panelWidth = panel;
819
+ layout.editorX = static_cast<float>(editorFrame_[0]);
820
+ layout.editorY = static_cast<float>(editorFrame_[1]);
821
+ layout.editorWidth = static_cast<float>(editorWidth_);
822
+ layout.editorHeight = static_cast<float>(editorHeight_);
823
+ layout.resizable = resizable_;
824
+ layout.editorImage = live_ ? liveTexture_ : nullptr;
825
+ const bool part = livePart_.has_value();
826
+ layout.editorImageX = part ? livePart_->x : layout.editorX;
827
+ layout.editorImageY = part ? livePart_->y : layout.editorY;
828
+ layout.editorImageWidth = part ? livePart_->width : layout.editorWidth;
829
+ layout.editorImageHeight = part ? livePart_->height : layout.editorHeight;
830
+ layout.editorImageU0 = part ? livePart_->u0 : 0;
831
+ layout.editorImageV0 = part ? livePart_->v0 : 0;
832
+ layout.editorImageU1 = part ? livePart_->u1 : 1;
833
+ layout.editorImageV1 = part ? livePart_->v1 : 1;
834
+
835
+ ImGui_ImplSDLRenderer3_NewFrame();
836
+ ImGui_ImplSDL3_NewFrame();
837
+ ImGui::NewFrame();
838
+ interface_->draw(layout);
839
+ ImGui::Render();
840
+
841
+ ImDrawData* data = ImGui::GetDrawData();
842
+ const uint64_t hash = hashDrawData(data);
843
+ if (hash != lastHash_ || forcePresent_ || texturesChanging(data))
844
+ {
845
+ lastHash_ = hash;
846
+ forcePresent_ = false;
847
+ SDL_SetRenderScale(renderer_, data->FramebufferScale.x, data->FramebufferScale.y);
848
+ SDL_SetRenderDrawColor(renderer_, 0x0b, 0x0c, 0x0f, 0xff);
849
+ SDL_RenderClear(renderer_);
850
+ ImGui_ImplSDLRenderer3_RenderDrawData(data, renderer_);
851
+ // A development aid: SOUNDOR_VST3_HOST_CAPTURE=<file.png> keeps
852
+ // the interface's last frame (without the editor, which draws
853
+ // itself).
854
+ if (const char* capture = std::getenv("SOUNDOR_VST3_HOST_CAPTURE"); // NOLINT(concurrency-mt-unsafe)
855
+ capture != nullptr && *capture != '\0')
856
+ if (SDL_Surface* pixels = SDL_RenderReadPixels(renderer_, nullptr))
857
+ {
858
+ SDL_SavePNG(pixels, capture);
859
+ SDL_DestroySurface(pixels);
860
+ }
861
+ // The last whole image is not where the view is: the view goes first.
862
+ const bool shown = live_ && layout.editorImage != nullptr;
863
+ if (shown && hideViewFirst_)
864
+ viewHidden_ = area_->hideView();
865
+ SDL_RenderPresent(renderer_);
866
+ // The image is exactly what the view shows, so nothing changes.
867
+ if (shown && ! viewHidden_ && area_->hideView())
868
+ {
869
+ viewHidden_ = true;
870
+ if (! livePartial_)
871
+ livePart_.reset();
872
+ forcePresent_ = true;
873
+ invalidate();
874
+ }
875
+ ++presented_;
876
+ }
877
+
878
+ // After presenting: the frame at the new size shows the editor's last
879
+ // image while the editor takes its time over the size.
880
+ if (gripSize_ && area_->settled())
881
+ {
882
+ const std::pair wanted = *gripSize_;
883
+ gripSize_.reset();
884
+ model_.emit("editorResized", model_.plugin.requestEditorSize(wanted.first, wanted.second));
885
+ area_->editorGiven(editorWidth_, editorHeight_);
886
+ }
887
+ // The panel opened or closed: the window grows or shrinks beside the
888
+ // stage, which stays as it is. A dock wider than the window makes room
889
+ // for itself.
890
+ if (interface_->panelOpen() != panelOpen_)
891
+ {
892
+ panelOpen_ = interface_->panelOpen();
893
+ const int change = static_cast<int>(std::ceil(HostInterface::panelWidth * scale_));
894
+ fitWindow(panelOpen_ ? change : -change);
895
+ }
896
+ else if (const auto natural = naturalSize();
897
+ ! settling_ && (SDL_GetWindowFlags(window_) & SDL_WINDOW_MAXIMIZED) == 0 && natural != fitted_
898
+ && (width < requiredSize().first || height < requiredSize().second))
899
+ fitWindow();
900
+ }
901
+ } // namespace soundor::vst3host