iPlug2 - C++ Audio Plug-in Framework
Loading...
Searching...
No Matches
IPlugAPP_main.cpp
1/*
2 ==============================================================================
3
4 This file is part of the iPlug 2 library. Copyright (C) the iPlug 2 developers.
5
6 See LICENSE.txt for more info.
7
8 ==============================================================================
9*/
10
11#include <memory>
12#include "wdltypes.h"
13#include "wdlstring.h"
14
15#include "IPlugPlatform.h"
16#include "IPlugAPP_host.h"
17
18#include "config.h"
19#include "resource.h"
20
21using namespace iplug;
22
23#pragma mark - WINDOWS
24#if defined OS_WIN
25#include <windows.h>
26#include <commctrl.h>
27#include <shellapi.h>
28
29// Include stb_image_write for PNG saving
30#define STB_IMAGE_WRITE_IMPLEMENTATION
31#include "../../Dependencies/IGraphics/STB/stb_image_write.h"
32
33extern WDL_DLGRET MainDlgProc(HWND hwndDlg, UINT uMsg, WPARAM wParam, LPARAM lParam);
34
35HWND gHWND;
36extern HINSTANCE gHINSTANCE;
37UINT gScrollMessage;
38
39// Save a screenshot of the given HWND to a PNG file using Win32 API
40bool SaveWindowScreenshot(HWND hwnd, const char* path)
41{
42 if (!hwnd || !path)
43 return false;
44
45 // Get client area dimensions
46 RECT clientRect;
47 if (!GetClientRect(hwnd, &clientRect))
48 return false;
49
50 int width = clientRect.right - clientRect.left;
51 int height = clientRect.bottom - clientRect.top;
52
53 if (width <= 0 || height <= 0)
54 return false;
55
56 // Create a compatible DC and bitmap
57 HDC hdcWindow = GetDC(hwnd);
58 if (!hdcWindow)
59 return false;
60
61 HDC hdcMem = CreateCompatibleDC(hdcWindow);
62 if (!hdcMem)
63 {
64 ReleaseDC(hwnd, hdcWindow);
65 return false;
66 }
67
68 // Create a 32-bit DIB section for the screenshot
69 BITMAPINFO bmi = {};
70 bmi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
71 bmi.bmiHeader.biWidth = width;
72 bmi.bmiHeader.biHeight = -height; // Top-down DIB
73 bmi.bmiHeader.biPlanes = 1;
74 bmi.bmiHeader.biBitCount = 32;
75 bmi.bmiHeader.biCompression = BI_RGB;
76
77 void* pBits = nullptr;
78 HBITMAP hBitmap = CreateDIBSection(hdcMem, &bmi, DIB_RGB_COLORS, &pBits, nullptr, 0);
79
80 if (!hBitmap || !pBits)
81 {
82 DeleteDC(hdcMem);
83 ReleaseDC(hwnd, hdcWindow);
84 return false;
85 }
86
87 HGDIOBJ hOldBitmap = SelectObject(hdcMem, hBitmap);
88
89 // Use PrintWindow to capture the window content
90 // PW_CLIENTONLY captures only the client area
91 // PW_RENDERFULLCONTENT renders the window fully, including layered content
92#ifndef PW_RENDERFULLCONTENT
93 #define PW_RENDERFULLCONTENT 0x00000002
94#endif
95 BOOL captured = PrintWindow(hwnd, hdcMem, PW_CLIENTONLY | PW_RENDERFULLCONTENT);
96
97 if (!captured)
98 {
99 // Fallback to BitBlt if PrintWindow fails
100 captured = BitBlt(hdcMem, 0, 0, width, height, hdcWindow, 0, 0, SRCCOPY);
101 }
102
103 SelectObject(hdcMem, hOldBitmap);
104 DeleteDC(hdcMem);
105 ReleaseDC(hwnd, hdcWindow);
106
107 if (!captured)
108 {
109 DeleteObject(hBitmap);
110 return false;
111 }
112
113 // Convert BGRA to RGBA for stb_image_write
114 uint8_t* pixels = static_cast<uint8_t*>(pBits);
115 for (int i = 0; i < width * height; i++)
116 {
117 // Swap B and R channels (BGRA -> RGBA)
118 uint8_t temp = pixels[i * 4 + 0];
119 pixels[i * 4 + 0] = pixels[i * 4 + 2];
120 pixels[i * 4 + 2] = temp;
121 // Set alpha to 255 (opaque) since Windows DIB may have garbage in alpha
122 pixels[i * 4 + 3] = 255;
123 }
124
125 // Use stb_image_write to save as PNG
126 int result = stbi_write_png(path, width, height, 4, pixels, width * 4);
127
128 DeleteObject(hBitmap);
129
130 return result != 0;
131}
132
133int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpszCmdParam, int nShowCmd)
134{
135 try
136 {
137#ifndef APP_ALLOW_MULTIPLE_INSTANCES
138 HANDLE hMutex = OpenMutex(MUTEX_ALL_ACCESS, 0, BUNDLE_NAME); // BUNDLE_NAME used because it won't have spaces in it
139
140 if (!hMutex)
141 hMutex = CreateMutex(0, 0, BUNDLE_NAME);
142 else
143 {
144 HWND hWnd = FindWindow(0, BUNDLE_NAME);
145 SetForegroundWindow(hWnd);
146 return 0;
147 }
148#endif
149 gHINSTANCE = hInstance;
150
151 InitCommonControls();
152 gScrollMessage = RegisterWindowMessage("MSWHEEL_ROLLMSG");
153
154 IPlugAPPHost* pAppHost = IPlugAPPHost::Create();
155
156 // Parse command line arguments
157 if (lpszCmdParam && lpszCmdParam[0])
158 {
159 char* args = _strdup(lpszCmdParam);
160 char* token = strtok(args, " ");
161 while (token)
162 {
163 if (strcmp(token, "--screenshot") == 0)
164 {
165 token = strtok(nullptr, " ");
166 if (token)
167 pAppHost->SetScreenshotPath(token);
168 }
169 else if (strcmp(token, "--no-io") == 0)
170 {
171 pAppHost->SetNoIO(true);
172 }
173 token = strtok(nullptr, " ");
174 }
175 free(args);
176 }
177
178 // Screenshot mode implies --no-io
179 if (pAppHost->IsScreenshotMode())
180 pAppHost->SetNoIO(true);
181
182 pAppHost->Init();
183 pAppHost->TryToChangeAudio();
184
185 HACCEL hAccel = LoadAccelerators(gHINSTANCE, MAKEINTRESOURCE(IDR_ACCELERATOR1));
186
187 static UINT(WINAPI *__SetProcessDpiAwarenessContext)(DPI_AWARENESS_CONTEXT);
188
189 if (!__SetProcessDpiAwarenessContext)
190 {
191 HINSTANCE h = LoadLibrary("user32.dll");
192 if (h) *(void **)&__SetProcessDpiAwarenessContext = GetProcAddress(h, "SetProcessDpiAwarenessContext");
193 if (!__SetProcessDpiAwarenessContext)
194 *(void **)&__SetProcessDpiAwarenessContext = (void*)(INT_PTR)1;
195 }
196 if ((UINT_PTR)__SetProcessDpiAwarenessContext > (UINT_PTR)1)
197 {
198 __SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2);
199 }
200
201 CreateDialog(gHINSTANCE, MAKEINTRESOURCE(IDD_DIALOG_MAIN), GetDesktopWindow(), IPlugAPPHost::MainDlgProc);
202
203#if !defined _DEBUG || defined NO_IGRAPHICS
204 HMENU menu = GetMenu(gHWND);
205 RemoveMenu(menu, 1, MF_BYPOSITION);
206 DrawMenuBar(gHWND);
207#endif
208
209 for (;;)
210 {
211 MSG msg= {0,};
212 int vvv = GetMessage(&msg, NULL, 0, 0);
213
214 if (!vvv)
215 break;
216
217 if (vvv < 0)
218 {
219 Sleep(10);
220 continue;
221 }
222
223 if (!msg.hwnd)
224 {
225 DispatchMessage(&msg);
226 continue;
227 }
228
229 if (gHWND && (TranslateAccelerator(gHWND, hAccel, &msg) || IsDialogMessage(gHWND, &msg)))
230 continue;
231
232 // default processing for other dialogs
233 HWND hWndParent = NULL;
234 HWND temphwnd = msg.hwnd;
235
236 do
237 {
238 if (GetClassLong(temphwnd, GCW_ATOM) == (INT)32770)
239 {
240 hWndParent = temphwnd;
241 if (!(GetWindowLong(temphwnd, GWL_STYLE) & WS_CHILD))
242 break; // not a child, exit
243 }
244 }
245 while (temphwnd = GetParent(temphwnd));
246
247 if (hWndParent && IsDialogMessage(hWndParent,&msg))
248 continue;
249
250 TranslateMessage(&msg);
251 DispatchMessage(&msg);
252 }
253
254 // in case gHWND didnt get destroyed -- this corresponds to SWELLAPP_DESTROY roughly
255 if (gHWND)
256 DestroyWindow(gHWND);
257
258#ifndef APP_ALLOW_MULTIPLE_INSTANCES
259 ReleaseMutex(hMutex);
260#endif
261 }
262 catch(std::exception e)
263 {
264 DBGMSG("Exception: %s", e.what());
265 return 1;
266 }
267 return 0;
268}
269#pragma mark - MAC
270#elif defined(OS_MAC)
271#import <Cocoa/Cocoa.h>
272#include <dlfcn.h>
273#include <cstring>
274#include "IPlugSWELL.h"
275#include "IPlugPaths.h"
276
277HWND gHWND;
278
279// Function pointer type for CGWindowListCreateImage
280typedef CGImageRef (*CGWindowListCreateImageFunc)(CGRect, uint32_t, uint32_t, uint32_t);
281
282// Save a screenshot of the given HWND (NSView*) to a PNG file
283extern "C" bool SaveWindowScreenshot(void* hwnd, const char* path)
284{
285 if (!hwnd || !path)
286 return false;
287
288 NSView* view = (__bridge NSView*)hwnd;
289 NSWindow* window = [view window];
290
291 if (!window)
292 return false;
293
294 // Get CGWindowListCreateImage via dlsym to bypass availability check
295 // The function still exists and works in the runtime
296 static CGWindowListCreateImageFunc pCGWindowListCreateImage = nullptr;
297 if (!pCGWindowListCreateImage)
298 {
299 void* handle = dlopen("/System/Library/Frameworks/CoreGraphics.framework/CoreGraphics", RTLD_LAZY);
300 if (handle)
301 pCGWindowListCreateImage = (CGWindowListCreateImageFunc)dlsym(handle, "CGWindowListCreateImage");
302 }
303
304 if (!pCGWindowListCreateImage)
305 return false;
306
307 // Get the window's CGWindowID
308 CGWindowID windowID = (CGWindowID)[window windowNumber];
309
310 // Capture the window content at full resolution (high DPI)
311 CGImageRef cgImage = pCGWindowListCreateImage(
312 CGRectNull, // Capture the whole window
313 kCGWindowListOptionIncludingWindow,
314 windowID,
315 kCGWindowImageBoundsIgnoreFraming // Exclude window frame, capture at screen resolution
316 );
317
318 if (!cgImage)
319 return false;
320
321 // Create NSBitmapImageRep from CGImage and save as PNG
322 NSBitmapImageRep* bitmap = [[NSBitmapImageRep alloc] initWithCGImage:cgImage];
323 CGImageRelease(cgImage);
324
325 if (!bitmap)
326 return false;
327
328 NSData* pngData = [bitmap representationUsingType:NSBitmapImageFileTypePNG properties:@{}];
329 if (!pngData)
330 return false;
331
332 NSString* filePath = [NSString stringWithUTF8String:path];
333 return [pngData writeToFile:filePath atomically:YES];
334}
335extern HMENU SWELL_app_stocksysmenu;
336
337static WDL_String gScreenshotPath;
338static bool gNoIO = false;
339
340int main(int argc, char *argv[])
341{
342#if APP_COPY_AUV3
343 //if invoked with an argument registerauv3 use plug-in kit to explicitly register auv3 app extension (doesn't happen from debugger)
344 if (argc > 2 && std::string_view(argv[2]) == "registerauv3")
345 {
346 WDL_String appexPath;
347 appexPath.SetFormatted(1024, "pluginkit -a %s%s%s.appex", argv[0], "/../../Plugins/", appexPath.get_filepart());
348 if (system(appexPath.Get()) > -1)
349 NSLog(@"Registered audiounit app extension\n");
350 else
351 NSLog(@"Failed to register audiounit app extension\n");
352 }
353#endif
354
355 // Parse command line arguments
356 for (int i = 1; i < argc; i++)
357 {
358 if (strcmp(argv[i], "--screenshot") == 0 && i + 1 < argc)
359 {
360 gScreenshotPath.Set(argv[i + 1]);
361 i++; // Skip the path argument
362 }
363 else if (strcmp(argv[i], "--no-io") == 0)
364 {
365 gNoIO = true;
366 }
367 }
368
369 if (AppIsSandboxed())
370 DBGMSG("App is sandboxed, file system access etc restricted!\n");
371
372 return NSApplicationMain(argc, (const char**) argv);
373}
374
375INT_PTR SWELLAppMain(int msg, INT_PTR parm1, INT_PTR parm2)
376{
377 IPlugAPPHost* pAppHost = nullptr;
378
379 switch (msg)
380 {
381 case SWELLAPP_ONLOAD:
382 {
383 pAppHost = IPlugAPPHost::Create();
384
385 // Set CLI options
386 if (gScreenshotPath.GetLength() > 0)
387 {
388 pAppHost->SetScreenshotPath(gScreenshotPath.Get());
389 pAppHost->SetNoIO(true); // Implicit --no-io for screenshot mode
390 }
391 else if (gNoIO)
392 {
393 pAppHost->SetNoIO(true);
394 }
395
396 pAppHost->Init();
397 pAppHost->TryToChangeAudio();
398 break;
399 }
400 case SWELLAPP_LOADED:
401 {
402 pAppHost = IPlugAPPHost::sInstance.get();
403
404 HMENU menu = SWELL_GetCurrentMenu();
405
406 if (menu)
407 {
408 // work on a new menu
409 menu = SWELL_DuplicateMenu(menu);
410 HMENU src = LoadMenu(NULL, MAKEINTRESOURCE(IDR_MENU1));
411
412 for (int x = 0; x < GetMenuItemCount(src)-1; x++)
413 {
414 HMENU sm = GetSubMenu(src,x);
415
416 if (sm)
417 {
418 char str[1024];
419 MENUITEMINFO mii = {sizeof(mii), MIIM_TYPE};
420 mii.dwTypeData = str;
421 mii.cch = sizeof(str);
422 str[0] = 0;
423 GetMenuItemInfo(src, x, TRUE, &mii);
424 MENUITEMINFO mi= {sizeof(mi), MIIM_STATE|MIIM_SUBMENU|MIIM_TYPE,MFT_STRING, 0, 0, SWELL_DuplicateMenu(sm), NULL, NULL, 0, str};
425 InsertMenuItem(menu, x+1, TRUE, &mi);
426 }
427 }
428 }
429
430 if (menu)
431 {
432 HMENU sm = GetSubMenu(menu, 1);
433 DeleteMenu(sm, ID_QUIT, MF_BYCOMMAND); // remove QUIT from our file menu, since it is in the system menu on OSX
434 DeleteMenu(sm, ID_PREFERENCES, MF_BYCOMMAND); // remove PREFERENCES from the file menu, since it is in the system menu on OSX
435
436 // remove any trailing separators
437 int a = GetMenuItemCount(sm);
438
439 while (a > 0 && GetMenuItemID(sm, a-1) == 0)
440 DeleteMenu(sm, --a, MF_BYPOSITION);
441
442 DeleteMenu(menu, 1, MF_BYPOSITION); // delete file menu
443 }
444 // Always set up screenshot shortcut
445 SetMenuItemModifier(menu, ID_SCREENSHOT, MF_BYCOMMAND, 'S', FCONTROL | FSHIFT);
446
447#if !defined _DEBUG || defined NO_IGRAPHICS
448 if (menu)
449 {
450 HMENU sm = GetSubMenu(menu, 1);
451 DeleteMenu(sm, ID_LIVE_EDIT, MF_BYCOMMAND);
452 DeleteMenu(sm, ID_SHOW_BOUNDS, MF_BYCOMMAND);
453 DeleteMenu(sm, ID_SHOW_DRAWN, MF_BYCOMMAND);
454 DeleteMenu(sm, ID_SHOW_FPS, MF_BYCOMMAND);
455
456 // remove any trailing separators
457 int a = GetMenuItemCount(sm);
458
459 while (a > 0 && GetMenuItemID(sm, a-1) == 0)
460 DeleteMenu(sm, --a, MF_BYPOSITION);
461
462 // Only delete debug menu if it's now empty (screenshot should remain)
463 if (GetMenuItemCount(sm) == 0)
464 DeleteMenu(menu, 1, MF_BYPOSITION);
465 }
466#else
467 SetMenuItemModifier(menu, ID_LIVE_EDIT, MF_BYCOMMAND, 'E', FCONTROL);
468 SetMenuItemModifier(menu, ID_SHOW_DRAWN, MF_BYCOMMAND, 'D', FCONTROL);
469 SetMenuItemModifier(menu, ID_SHOW_BOUNDS, MF_BYCOMMAND, 'B', FCONTROL);
470 SetMenuItemModifier(menu, ID_SHOW_FPS, MF_BYCOMMAND, 'F', FCONTROL);
471#endif
472
473 HWND hwnd = CreateDialog(gHINST, MAKEINTRESOURCE(IDD_DIALOG_MAIN), NULL, IPlugAPPHost::MainDlgProc);
474
475 if (menu)
476 {
477 SetMenu(hwnd, menu); // set the menu for the dialog to our menu (on Windows that menu is set from the .rc, but on SWELL
478 SWELL_SetDefaultModalWindowMenu(menu); // other windows will get the stock (bundle) menus
479 }
480
481 break;
482 }
483 case SWELLAPP_ONCOMMAND:
484 // this is to catch commands coming from the system menu etc
485 if (gHWND && (parm1&0xffff))
486 SendMessage(gHWND, WM_COMMAND, parm1 & 0xffff, 0);
487 break;
488 case SWELLAPP_DESTROY:
489 if (gHWND)
490 DestroyWindow(gHWND);
491 break;
492 case SWELLAPP_PROCESSMESSAGE:
493 MSG* pMSG = (MSG*) parm1;
494 NSView* pContentView = (NSView*) pMSG->hwnd;
495 NSEvent* pEvent = (NSEvent*) parm2;
496 int etype = (int) [pEvent type];
497
498 bool textField = [pContentView isKindOfClass:[NSText class]];
499
500 if (!textField && etype == NSKeyDown)
501 {
502 int flag, code = SWELL_MacKeyToWindowsKey(pEvent, &flag);
503
504 if (!(flag&~FVIRTKEY) && (code == VK_RETURN || code == VK_ESCAPE))
505 {
506 [pContentView keyDown: pEvent];
507 return 1;
508 }
509 }
510 break;
511 }
512 return 0;
513}
514
515#define CBS_HASSTRINGS 0
516#define SWELL_DLG_SCALE_AUTOGEN 1
517#define SET_IDD_DIALOG_PREF_SCALE 1.5
518#if PLUG_HOST_RESIZE
519#define SWELL_DLG_FLAGS_AUTOGEN SWELL_DLG_WS_FLIPPED|SWELL_DLG_WS_RESIZABLE
520#endif
521#include "swell-dlggen.h"
522#include "resources/main.rc_mac_dlg"
523#include "swell-menugen.h"
524#include "resources/main.rc_mac_menu"
525
526#pragma mark - LINUX
527#elif defined(OS_LINUX)
528//#include <IPlugSWELL.h>
529//#include "swell-internal.h" // fixes problem with HWND forward decl
530//
531//HWND gHWND;
532//UINT gScrollMessage;
533//extern HMENU SWELL_app_stocksysmenu;
534//
535//int main(int argc, char **argv)
536//{
537// SWELL_initargs(&argc, &argv);
538// SWELL_Internal_PostMessage_Init();
539// SWELL_ExtendedAPI("APPNAME", (void*) "IGraphics Test");
540//
541// HMENU menu = LoadMenu(NULL, MAKEINTRESOURCE(IDR_MENU1));
542// CreateDialog(gHINSTANCE, MAKEINTRESOURCE(IDD_DIALOG_MAIN), NULL, MainDlgProc);
543// SetMenu(gHWND, menu);
544//
545// while (!gHWND->m_hashaddestroy)
546// {
547// SWELL_RunMessageLoop();
548// Sleep(10);
549// };
550//
551// if (gHWND)
552// DestroyWindow(gHWND);
553//
554// return 0;
555//}
556//
557//INT_PTR SWELLAppMain(int msg, INT_PTR parm1, INT_PTR parm2)
558//{
559// switch (msg)
560// {
561// case SWELLAPP_ONLOAD:
562// break;
563// case SWELLAPP_LOADED:
564// {
565// HMENU menu = SWELL_GetCurrentMenu();
566//
567// if (menu)
568// {
569// // work on a new menu
570// menu = SWELL_DuplicateMenu(menu);
571// HMENU src = LoadMenu(NULL, MAKEINTRESOURCE(IDR_MENU1));
572//
573// for (auto x = 0; x < GetMenuItemCount(src)-1; x++)
574// {
575// HMENU sm = GetSubMenu(src,x);
576// if (sm)
577// {
578// char str[1024];
579// MENUITEMINFO mii = {sizeof(mii), MIIM_TYPE};
580// mii.dwTypeData = str;
581// mii.cch = sizeof(str);
582// str[0] = 0;
583// GetMenuItemInfo(src, x, TRUE, &mii);
584// MENUITEMINFO mi= {sizeof(mi), MIIM_STATE|MIIM_SUBMENU|MIIM_TYPE,MFT_STRING, 0, 0, SWELL_DuplicateMenu(sm), NULL, NULL, 0, str};
585// InsertMenuItem(menu, x+1, TRUE, &mi);
586// }
587// }
588// }
589//
590// if (menu)
591// {
592// HMENU sm = GetSubMenu(menu, 1);
593// DeleteMenu(sm, ID_QUIT, MF_BYCOMMAND); // remove QUIT from our file menu, since it is in the system menu on OSX
594// DeleteMenu(sm, ID_PREFERENCES, MF_BYCOMMAND); // remove PREFERENCES from the file menu, since it is in the system menu on OSX
595//
596// // remove any trailing separators
597// int a = GetMenuItemCount(sm);
598//
599// while (a > 0 && GetMenuItemID(sm, a-1) == 0)
600// DeleteMenu(sm, --a, MF_BYPOSITION);
601//
602// DeleteMenu(menu, 1, MF_BYPOSITION); // delete file menu
603// }
604//
605// // if we want to set any default modifiers for items in the menus, we can use:
606// // SetMenuItemModifier(menu,commandID,MF_BYCOMMAND,'A',FCONTROL) etc.
607//
608// HWND hwnd = CreateDialog(gHINST,MAKEINTRESOURCE(IDD_DIALOG_MAIN), NULL, MainDlgProc);
609//
610// if (menu)
611// {
612// SetMenu(hwnd, menu); // set the menu for the dialog to our menu (on Windows that menu is set from the .rc, but on SWELL
613// SWELL_SetDefaultModalWindowMenu(menu); // other windows will get the stock (bundle) menus
614// }
615//
616// break;
617// }
618// case SWELLAPP_ONCOMMAND:
619// // this is to catch commands coming from the system menu etc
620// if (gHWND && (parm1&0xffff))
621// SendMessage(gHWND, WM_COMMAND, parm1 & 0xffff, 0);
622// break;
623// case SWELLAPP_DESTROY:
624// if (gHWND)
625// DestroyWindow(gHWND);
626// break;
627// case SWELLAPP_PROCESSMESSAGE: // can hook keyboard input here
628// // parm1 = (MSG*), should we want it -- look in swell.h to see what the return values refer to
629// break;
630// }
631// return 0;
632//}
633//
634//#define CBS_HASSTRINGS 0
635//#define SWELL_DLG_SCALE_AUTOGEN 1
636//#define SET_IDD_DIALOG_PREF_SCALE 1.5
637//#include "swell-dlggen.h"
638//#include "resources/main.rc_mac_dlg"
639//#include "swell-menugen.h"
640//#include "resources/main.rc_mac_menu"
641#endif
Common paths useful for plug-ins.
Include to get consistently named preprocessor macros for different platforms and logging functionali...
A class that hosts an IPlug as a standalone app and provides Audio/Midi I/O.
Definition: IPlugAPP_host.h:83
void SetScreenshotPath(const char *path)
Set screenshot path for CLI screenshot mode.
void SetNoIO(bool noIO)
Set no-I/O mode (disables audio and MIDI initialization)
bool IsScreenshotMode() const
Check if in screenshot mode.