diff --git a/.gitignore b/.gitignore index 3979c50..602cb95 100644 --- a/.gitignore +++ b/.gitignore @@ -47,3 +47,10 @@ native_bridge/sfizz-src/ install/daw_engine/ app/static/js/_diag.txt src-tauri/resources/daw_engine" && cp -r* + +# debug/ run artifacts (screenshots, output captures, binaries) — scripts are committed +native_bridge/debug/*.png +native_bridge/debug/*.out +native_bridge/debug/pidtest.txt +native_bridge/debug/shm_selfcheck.exe +*.obj diff --git a/install/daw_vst_bridge.exe b/install/daw_vst_bridge.exe index cfce577..b769b6a 100644 Binary files a/install/daw_vst_bridge.exe and b/install/daw_vst_bridge.exe differ diff --git a/install/sonicforge-daw.exe b/install/sonicforge-daw.exe index a25266d..4791407 100644 Binary files a/install/sonicforge-daw.exe and b/install/sonicforge-daw.exe differ diff --git a/native_bridge/debug/driver_cdb.py b/native_bridge/debug/driver_cdb.py new file mode 100644 index 0000000..11171a8 --- /dev/null +++ b/native_bridge/debug/driver_cdb.py @@ -0,0 +1,175 @@ +# Driver verify: SHM + spawn real bridge, reproduce user flow: +# load 6x Nexus -> open GUI ch=5 -> play notes -> switch instrument ch=5 (crash point) +# -> reopen GUI -> verify alive + writeIndex advancing + no crash. +import ctypes, ctypes.wintypes as w, struct, subprocess, sys, os, time + +sys.stdout.reconfigure(encoding='utf-8', errors='replace') + +SHM_NAME = "SonicForge_DAW_IPC_VERIFY" +SIZE = 32768 # bigger than struct, safe +BRIDGE = r"C:\Users\locpham\SonicForgeStudio\native_bridge\build\Release\daw_vst_bridge.exe" +CDB = r"C:\Users\locpham\AppData\Local\Microsoft\WindowsApps\cdbX64.exe" +LOG = os.path.join(os.path.dirname(os.path.abspath(__file__)), "driver_cdb.log") +NEXUS = r"C:\Program Files\Common Files\VST3\Nexus.vst3" +AMPLE = r"C:\Program Files\Common Files\VST3\Ample Sound\ACDD.vst3" + +kernel32 = ctypes.windll.kernel32 +kernel32.CreateFileMappingA.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.wintypes.DWORD, ctypes.wintypes.DWORD, ctypes.wintypes.DWORD, ctypes.c_char_p] +kernel32.CreateFileMappingA.restype = ctypes.c_void_p +kernel32.MapViewOfFile.argtypes = [ctypes.c_void_p, ctypes.wintypes.DWORD, ctypes.wintypes.DWORD, ctypes.wintypes.DWORD, ctypes.c_size_t] +kernel32.MapViewOfFile.restype = ctypes.c_void_p +INVALID_HANDLE_VALUE = ctypes.c_void_p(-1).value +PAGE_READWRITE = 0x04 +FILE_MAP_ALL_ACCESS = 0xF001F +FILE_MAP_WRITE = 0x0002 +FILE_MAP_READ = 0x0004 + +hMap = kernel32.CreateFileMappingA(INVALID_HANDLE_VALUE, None, PAGE_READWRITE, 0, SIZE, SHM_NAME.encode()) +if not hMap: + print("CreateFileMapping failed", ctypes.get_last_error()); sys.exit(1) +ptr = kernel32.MapViewOfFile(hMap, FILE_MAP_ALL_ACCESS, 0, 0, 0) +if not ptr: + print("MapViewOfFile failed", ctypes.get_last_error()); sys.exit(1) + +def w32(off, fmt, *args): + ctypes.memmove(ptr + off, struct.pack(fmt, *args), struct.calcsize(fmt)) + +def write_control(type_, arg0, arg1, ch, path): + # find first free slot + for i in range(8): + off = 2836 + i * 1040 + t = ctypes.c_uint32.from_address(ptr + off).value + if t == 0: + w32(off, 'IIII', type_, arg0, arg1, ch) + if path: + b = path.encode('utf-8')[:1023] + b'\x00' + ctypes.memmove(ptr + off + 16, b, len(b)) + return i + raise RuntimeError("control queue full") + +def submit_controls(): + w32(11156, 'I', 0) # will set below + # reset type of all slots first + for i in range(8): + w32(2836 + i * 1040, 'I', 0) + +def push_controls(events): + submit_controls() + n = 0 + for (type_, arg0, arg1, ch, path) in events: + write_control(type_, arg0, arg1, ch, path) + n += 1 + w32(11156, 'I', n) + +def midi(cmd, ch, pitch, vel, data2=0, data3=0): + # bridge does not zero midiQueue after drain - always write slot 0, count 1 + w32(2064, 'BBBBBB', cmd, ch, pitch, vel, data2, data3) + w32(2064 + 8, 'I', 0) + w32(2832, 'I', 1) + return True + +def midi_count(): + return ctypes.c_uint32.from_address(ptr + 2832).value + +def write_index(): + return ctypes.c_uint32.from_address(ptr + 4).value + +def wait_log(pattern, timeout, since=0): + end = time.time() + timeout + lines = [] + try: + with open(LOG, 'r', encoding='utf-8', errors='replace') as f: + lines = f.readlines() + except FileNotFoundError: + pass + last = max(0, len(lines) - since) + while time.time() < end: + try: + with open(LOG, 'r', encoding='utf-8', errors='replace') as f: + lines = f.readlines() + except FileNotFoundError: + lines = [] + for i in range(last, len(lines)): + if pattern in lines[i]: + return lines[i].strip() + last = max(0, len(lines)) + time.sleep(0.2) + return None + +proc = subprocess.Popen( + [CDB, "-g", "-G", "-o", "-c", ".ecxr; .dump /ma C:\\Users\\locpham\\AppData\\Local\\Temp\\procdump\\dumps\\cdb_crash.dmp; !analyze -v; k L40; q", BRIDGE, "--shm", SHM_NAME], + env={**os.environ, "SF_SHM_NAME": SHM_NAME, "SF_PARENT_PID": str(os.getpid()), + "SF_SAMPLE_RATE": "48000", "SF_BLOCK_SIZE": "256"}, + stdout=open(LOG, 'wb'), stderr=subprocess.STDOUT, +) +time.sleep(1.5) +print("bridge pid", proc.pid, "poll", proc.poll()) + +# 1. Load 6 channels Nexus +print("[1] load 6x Nexus...") +push_controls([(2, 0, 0, ch, NEXUS) for ch in range(6)]) +print(" wait ch=5 loaded...") +r = wait_log("instrument loaded ch=5", 90) +print(" ", r) + +# 2. Open GUI ch=5 +print("[2] open GUI ch=5...") +push_controls([(4, 0, 0, 5, "nexus")]) +r = wait_log("GUI attached", 30) +print(" ", r) + +# 3. Play notes on ch=5 and ch=2 while editor open +print("[3] play notes...") +for k in range(40): + midi(0x9, 5, 60 + (k % 12), 100) + midi(0x9, 2, 48 + (k % 7), 90) + if k % 2 == 0: + midi(0x8, 5, 60 + ((k - 1) % 12), 0) + time.sleep(0.05) + +# 4. Switch instrument ch=5 while GUI open (OLD CRASH POINT) +print("[4] switch instrument ch=5 -> Ample ACDD while GUI open...") +t0 = time.time() +push_controls([(2, 0, 0, 5, AMPLE)]) +r = wait_log("instrument loaded ch=5", 40) +print(" ", r, "in", round(time.time() - t0, 1), "s") +alive = proc.poll() is None +wi = write_index() +time.sleep(2) +wi2 = write_index() +print(" alive:", alive, "writeIndex", wi, "->", wi2, "advancing:", wi2 > wi) + +# 5. Reopen GUI ch=5 (after switch) - attach path again +print("[5] reopen GUI ch=5...") +push_controls([(4, 0, 0, 5, "ample")]) +r = wait_log("GUI attached", 40) +print(" ", r) + +# 6. Final liveness check + crash check +time.sleep(3) +alive = proc.poll() is None +wi3 = write_index() +time.sleep(2) +wi4 = write_index() +print("FINAL alive:", alive, "writeIndex", wi3, "->", wi4, "advancing:", wi4 > wi3) +if not alive: + print("!!! BRIDGE CRASHED exit", proc.returncode) + +print("--- last 30 log lines ---") +try: + with open(LOG, 'r', encoding='utf-8', errors='replace') as f: + lines = f.readlines() + for l in lines[-30:]: + print(l.rstrip()) +except FileNotFoundError: + pass + +# cleanup +kernel32.UnmapViewOfFile(ptr) +kernel32.CloseHandle(hMap) +try: + proc.terminate() +except Exception: + pass +time.sleep(0.5) +print("done (cdb)") diff --git a/native_bridge/debug/driver_cdb_old.py b/native_bridge/debug/driver_cdb_old.py new file mode 100644 index 0000000..a918bdc --- /dev/null +++ b/native_bridge/debug/driver_cdb_old.py @@ -0,0 +1,175 @@ +# Driver verify: SHM + spawn real bridge, reproduce user flow: +# load 6x Nexus -> open GUI ch=5 -> play notes -> switch instrument ch=5 (crash point) +# -> reopen GUI -> verify alive + writeIndex advancing + no crash. +import ctypes, ctypes.wintypes as w, struct, subprocess, sys, os, time + +sys.stdout.reconfigure(encoding='utf-8', errors='replace') + +SHM_NAME = "SonicForge_DAW_IPC_VERIFY" +SIZE = 32768 # bigger than struct, safe +BRIDGE = r"C:\Users\locpham\SonicForgeStudio\src-tauri\binaries\daw_vst_bridge.exe" +CDB = r"C:\Users\locpham\AppData\Local\Microsoft\WindowsApps\cdbX64.exe" +LOG = os.path.join(os.path.dirname(os.path.abspath(__file__)), "driver_cdb_old.log") +NEXUS = r"C:\Program Files\Common Files\VST3\Nexus.vst3" +AMPLE = r"C:\Program Files\Common Files\VST3\Ample Sound\ACDD.vst3" + +kernel32 = ctypes.windll.kernel32 +kernel32.CreateFileMappingA.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.wintypes.DWORD, ctypes.wintypes.DWORD, ctypes.wintypes.DWORD, ctypes.c_char_p] +kernel32.CreateFileMappingA.restype = ctypes.c_void_p +kernel32.MapViewOfFile.argtypes = [ctypes.c_void_p, ctypes.wintypes.DWORD, ctypes.wintypes.DWORD, ctypes.wintypes.DWORD, ctypes.c_size_t] +kernel32.MapViewOfFile.restype = ctypes.c_void_p +INVALID_HANDLE_VALUE = ctypes.c_void_p(-1).value +PAGE_READWRITE = 0x04 +FILE_MAP_ALL_ACCESS = 0xF001F +FILE_MAP_WRITE = 0x0002 +FILE_MAP_READ = 0x0004 + +hMap = kernel32.CreateFileMappingA(INVALID_HANDLE_VALUE, None, PAGE_READWRITE, 0, SIZE, SHM_NAME.encode()) +if not hMap: + print("CreateFileMapping failed", ctypes.get_last_error()); sys.exit(1) +ptr = kernel32.MapViewOfFile(hMap, FILE_MAP_ALL_ACCESS, 0, 0, 0) +if not ptr: + print("MapViewOfFile failed", ctypes.get_last_error()); sys.exit(1) + +def w32(off, fmt, *args): + ctypes.memmove(ptr + off, struct.pack(fmt, *args), struct.calcsize(fmt)) + +def write_control(type_, arg0, arg1, ch, path): + # find first free slot + for i in range(8): + off = 2836 + i * 1040 + t = ctypes.c_uint32.from_address(ptr + off).value + if t == 0: + w32(off, 'IIII', type_, arg0, arg1, ch) + if path: + b = path.encode('utf-8')[:1023] + b'\x00' + ctypes.memmove(ptr + off + 16, b, len(b)) + return i + raise RuntimeError("control queue full") + +def submit_controls(): + w32(11156, 'I', 0) # will set below + # reset type of all slots first + for i in range(8): + w32(2836 + i * 1040, 'I', 0) + +def push_controls(events): + submit_controls() + n = 0 + for (type_, arg0, arg1, ch, path) in events: + write_control(type_, arg0, arg1, ch, path) + n += 1 + w32(11156, 'I', n) + +def midi(cmd, ch, pitch, vel, data2=0, data3=0): + # bridge does not zero midiQueue after drain - always write slot 0, count 1 + w32(2064, 'BBBBBB', cmd, ch, pitch, vel, data2, data3) + w32(2064 + 8, 'I', 0) + w32(2832, 'I', 1) + return True + +def midi_count(): + return ctypes.c_uint32.from_address(ptr + 2832).value + +def write_index(): + return ctypes.c_uint32.from_address(ptr + 4).value + +def wait_log(pattern, timeout, since=0): + end = time.time() + timeout + lines = [] + try: + with open(LOG, 'r', encoding='utf-8', errors='replace') as f: + lines = f.readlines() + except FileNotFoundError: + pass + last = max(0, len(lines) - since) + while time.time() < end: + try: + with open(LOG, 'r', encoding='utf-8', errors='replace') as f: + lines = f.readlines() + except FileNotFoundError: + lines = [] + for i in range(last, len(lines)): + if pattern in lines[i]: + return lines[i].strip() + last = max(0, len(lines)) + time.sleep(0.2) + return None + +proc = subprocess.Popen( + [CDB, "-g", "-G", "-o", "-c", ".ecxr; .dump /ma C:\\Users\\locpham\\AppData\\Local\\Temp\\procdump\\dumps\\cdb_crash.dmp; !analyze -v; k L40; q", BRIDGE, "--shm", SHM_NAME], + env={**os.environ, "SF_SHM_NAME": SHM_NAME, "SF_PARENT_PID": str(os.getpid()), + "SF_SAMPLE_RATE": "48000", "SF_BLOCK_SIZE": "256"}, + stdout=open(LOG, 'wb'), stderr=subprocess.STDOUT, +) +time.sleep(1.5) +print("bridge pid", proc.pid, "poll", proc.poll()) + +# 1. Load 6 channels Nexus +print("[1] load 6x Nexus...") +push_controls([(2, 0, 0, ch, NEXUS) for ch in range(6)]) +print(" wait ch=5 loaded...") +r = wait_log("instrument loaded ch=5", 90) +print(" ", r) + +# 2. Open GUI ch=5 +print("[2] open GUI ch=5...") +push_controls([(4, 0, 0, 5, "nexus")]) +r = wait_log("GUI attached", 30) +print(" ", r) + +# 3. Play notes on ch=5 and ch=2 while editor open +print("[3] play notes...") +for k in range(40): + midi(0x9, 5, 60 + (k % 12), 100) + midi(0x9, 2, 48 + (k % 7), 90) + if k % 2 == 0: + midi(0x8, 5, 60 + ((k - 1) % 12), 0) + time.sleep(0.05) + +# 4. Switch instrument ch=5 while GUI open (OLD CRASH POINT) +print("[4] switch instrument ch=5 -> Ample ACDD while GUI open...") +t0 = time.time() +push_controls([(2, 0, 0, 5, AMPLE)]) +r = wait_log("instrument loaded ch=5", 40) +print(" ", r, "in", round(time.time() - t0, 1), "s") +alive = proc.poll() is None +wi = write_index() +time.sleep(2) +wi2 = write_index() +print(" alive:", alive, "writeIndex", wi, "->", wi2, "advancing:", wi2 > wi) + +# 5. Reopen GUI ch=5 (after switch) - attach path again +print("[5] reopen GUI ch=5...") +push_controls([(4, 0, 0, 5, "ample")]) +r = wait_log("GUI attached", 40) +print(" ", r) + +# 6. Final liveness check + crash check +time.sleep(3) +alive = proc.poll() is None +wi3 = write_index() +time.sleep(2) +wi4 = write_index() +print("FINAL alive:", alive, "writeIndex", wi3, "->", wi4, "advancing:", wi4 > wi3) +if not alive: + print("!!! BRIDGE CRASHED exit", proc.returncode) + +print("--- last 30 log lines ---") +try: + with open(LOG, 'r', encoding='utf-8', errors='replace') as f: + lines = f.readlines() + for l in lines[-30:]: + print(l.rstrip()) +except FileNotFoundError: + pass + +# cleanup +kernel32.UnmapViewOfFile(ptr) +kernel32.CloseHandle(hMap) +try: + proc.terminate() +except Exception: + pass +time.sleep(0.5) +print("done (cdb)") diff --git a/native_bridge/debug/driver_procdump.py b/native_bridge/debug/driver_procdump.py new file mode 100644 index 0000000..b15ff44 --- /dev/null +++ b/native_bridge/debug/driver_procdump.py @@ -0,0 +1,176 @@ +# Driver verify: SHM + spawn real bridge, reproduce user flow: +# load 6x Nexus -> open GUI ch=5 -> play notes -> switch instrument ch=5 (crash point) +# -> reopen GUI -> verify alive + writeIndex advancing + no crash. +import ctypes, ctypes.wintypes as w, struct, subprocess, sys, os, time + +sys.stdout.reconfigure(encoding='utf-8', errors='replace') + +SHM_NAME = "SonicForge_DAW_IPC_VERIFY" +SIZE = 32768 # bigger than struct, safe +BRIDGE = r"C:\Users\locpham\SonicForgeStudio\native_bridge\build\Release\daw_vst_bridge.exe" +PROCDUMP = r"C:\Users\locpham\AppData\Local\Temp\procdump\procdump64.exe" +DUMPDIR = r"C:\Users\locpham\AppData\Local\Temp\procdump\dumps" +LOG = os.path.join(os.path.dirname(os.path.abspath(__file__)), "driver_procdump.log") +NEXUS = r"C:\Program Files\Common Files\VST3\Nexus.vst3" +AMPLE = r"C:\Program Files\Common Files\VST3\Ample Sound\ACDD.vst3" + +kernel32 = ctypes.windll.kernel32 +kernel32.CreateFileMappingA.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.wintypes.DWORD, ctypes.wintypes.DWORD, ctypes.wintypes.DWORD, ctypes.c_char_p] +kernel32.CreateFileMappingA.restype = ctypes.c_void_p +kernel32.MapViewOfFile.argtypes = [ctypes.c_void_p, ctypes.wintypes.DWORD, ctypes.wintypes.DWORD, ctypes.wintypes.DWORD, ctypes.c_size_t] +kernel32.MapViewOfFile.restype = ctypes.c_void_p +INVALID_HANDLE_VALUE = ctypes.c_void_p(-1).value +PAGE_READWRITE = 0x04 +FILE_MAP_ALL_ACCESS = 0xF001F +FILE_MAP_WRITE = 0x0002 +FILE_MAP_READ = 0x0004 + +hMap = kernel32.CreateFileMappingA(INVALID_HANDLE_VALUE, None, PAGE_READWRITE, 0, SIZE, SHM_NAME.encode()) +if not hMap: + print("CreateFileMapping failed", ctypes.get_last_error()); sys.exit(1) +ptr = kernel32.MapViewOfFile(hMap, FILE_MAP_ALL_ACCESS, 0, 0, 0) +if not ptr: + print("MapViewOfFile failed", ctypes.get_last_error()); sys.exit(1) + +def w32(off, fmt, *args): + ctypes.memmove(ptr + off, struct.pack(fmt, *args), struct.calcsize(fmt)) + +def write_control(type_, arg0, arg1, ch, path): + # find first free slot + for i in range(8): + off = 2836 + i * 1040 + t = ctypes.c_uint32.from_address(ptr + off).value + if t == 0: + w32(off, 'IIII', type_, arg0, arg1, ch) + if path: + b = path.encode('utf-8')[:1023] + b'\x00' + ctypes.memmove(ptr + off + 16, b, len(b)) + return i + raise RuntimeError("control queue full") + +def submit_controls(): + w32(11156, 'I', 0) # will set below + # reset type of all slots first + for i in range(8): + w32(2836 + i * 1040, 'I', 0) + +def push_controls(events): + submit_controls() + n = 0 + for (type_, arg0, arg1, ch, path) in events: + write_control(type_, arg0, arg1, ch, path) + n += 1 + w32(11156, 'I', n) + +def midi(cmd, ch, pitch, vel, data2=0, data3=0): + # bridge does not zero midiQueue after drain - always write slot 0, count 1 + w32(2064, 'BBBBBB', cmd, ch, pitch, vel, data2, data3) + w32(2064 + 8, 'I', 0) + w32(2832, 'I', 1) + return True + +def midi_count(): + return ctypes.c_uint32.from_address(ptr + 2832).value + +def write_index(): + return ctypes.c_uint32.from_address(ptr + 4).value + +def wait_log(pattern, timeout, since=0): + end = time.time() + timeout + lines = [] + try: + with open(LOG, 'r', encoding='utf-8', errors='replace') as f: + lines = f.readlines() + except FileNotFoundError: + pass + last = max(0, len(lines) - since) + while time.time() < end: + try: + with open(LOG, 'r', encoding='utf-8', errors='replace') as f: + lines = f.readlines() + except FileNotFoundError: + lines = [] + for i in range(last, len(lines)): + if pattern in lines[i]: + return lines[i].strip() + last = max(0, len(lines)) + time.sleep(0.2) + return None + +proc = subprocess.Popen( + [PROCDUMP, "-accepteula", "-x", DUMPDIR, BRIDGE, "--shm", SHM_NAME], # second chance = real crash (first-chance caught a benign Nexus json throw) + env={**os.environ, "SF_SHM_NAME": SHM_NAME, "SF_PARENT_PID": str(os.getpid()), + "SF_SAMPLE_RATE": "48000", "SF_BLOCK_SIZE": "256"}, + stdout=open(LOG, 'wb'), stderr=subprocess.STDOUT, +) +time.sleep(1.5) +print("bridge pid", proc.pid, "poll", proc.poll()) + +# 1. Load 6 channels Nexus +print("[1] load 6x Nexus...") +push_controls([(2, 0, 0, ch, NEXUS) for ch in range(6)]) +print(" wait ch=5 loaded...") +r = wait_log("instrument loaded ch=5", 90) +print(" ", r) + +# 2. Open GUI ch=5 +print("[2] open GUI ch=5...") +push_controls([(4, 0, 0, 5, "nexus")]) +r = wait_log("GUI attached", 30) +print(" ", r) + +# 3. Play notes on ch=5 and ch=2 while editor open +print("[3] play notes...") +for k in range(40): + midi(0x9, 5, 60 + (k % 12), 100) + midi(0x9, 2, 48 + (k % 7), 90) + if k % 2 == 0: + midi(0x8, 5, 60 + ((k - 1) % 12), 0) + time.sleep(0.05) + +# 4. Switch instrument ch=5 while GUI open (OLD CRASH POINT) +print("[4] switch instrument ch=5 -> Ample ACDD while GUI open...") +t0 = time.time() +push_controls([(2, 0, 0, 5, AMPLE)]) +r = wait_log("instrument loaded ch=5", 40) +print(" ", r, "in", round(time.time() - t0, 1), "s") +alive = proc.poll() is None +wi = write_index() +time.sleep(2) +wi2 = write_index() +print(" alive:", alive, "writeIndex", wi, "->", wi2, "advancing:", wi2 > wi) + +# 5. Reopen GUI ch=5 (after switch) - attach path again +print("[5] reopen GUI ch=5...") +push_controls([(4, 0, 0, 5, "ample")]) +r = wait_log("GUI attached", 40) +print(" ", r) + +# 6. Final liveness check + crash check +time.sleep(3) +alive = proc.poll() is None +wi3 = write_index() +time.sleep(2) +wi4 = write_index() +print("FINAL alive:", alive, "writeIndex", wi3, "->", wi4, "advancing:", wi4 > wi3) +if not alive: + print("!!! BRIDGE CRASHED exit", proc.returncode) + +print("--- last 30 log lines ---") +try: + with open(LOG, 'r', encoding='utf-8', errors='replace') as f: + lines = f.readlines() + for l in lines[-30:]: + print(l.rstrip()) +except FileNotFoundError: + pass + +# cleanup +kernel32.UnmapViewOfFile(ptr) +kernel32.CloseHandle(hMap) +try: + proc.terminate() +except Exception: + pass +time.sleep(0.5) +print("done (procdump)") diff --git a/native_bridge/debug/fix_bisect_queryinterface.py b/native_bridge/debug/fix_bisect_queryinterface.py new file mode 100644 index 0000000..10eac63 --- /dev/null +++ b/native_bridge/debug/fix_bisect_queryinterface.py @@ -0,0 +1,47 @@ +p = r"C:/Users/locpham/SonicForgeStudio/native_bridge/src/Vst3Instrument.cpp" +data = open(p, "rb").read() +n = b"\r\n" + +old1 = n.join([ + b" tresult queryInterface(const char* iid, void** v) override {", + b" if (std::memcmp(iid, Steinberg::Vst::IComponentHandler::iid, 16) == 0 ||", + b" std::memcmp(iid, Steinberg::FUnknown::iid, 16) == 0) {", + b" *v = static_cast(this);", + b" addRef();", + b" return kResultOk;", + b" }", + b" *v = nullptr;", + b" return kNoInterface;", + b" }", +]) +new1 = n.join([ + b" tresult queryInterface(const char*, void** v) override {", + b" *v = nullptr;", + b" return kNoInterface;", + b" }", +]) + +old2 = n.join([ + b" tresult queryInterface(const char* iid, void** v) override {", + b" if (std::memcmp(iid, Steinberg::IPlugFrame::iid, 16) == 0 ||", + b" std::memcmp(iid, Steinberg::FUnknown::iid, 16) == 0) {", + b" *v = static_cast(this);", + b" addRef();", + b" return kResultOk;", + b" }", + b" *v = nullptr;", + b" return kNoInterface;", + b" }", +]) +new2 = n.join([ + b" tresult queryInterface(const char*, void** v) override {", + b" *v = nullptr;", + b" return kNoInterface;", + b" }", +]) + +c1 = data.count(old1) +c2 = data.count(old2) +data = data.replace(old1, new1).replace(old2, new2) +open(p, "wb").write(data) +print(f"edit1={c1} edit2={c2}") diff --git a/native_bridge/debug/fix_revert_w2.py b/native_bridge/debug/fix_revert_w2.py new file mode 100644 index 0000000..026e87e --- /dev/null +++ b/native_bridge/debug/fix_revert_w2.py @@ -0,0 +1,194 @@ +import subprocess + +def load(p): + return open(p, encoding='utf-8', newline='').read() + +def save(p, s): + open(p, 'w', encoding='utf-8', newline='').write(s) + +def rep(s, needle, repl, tag): + n = s.count(needle) + if n != 1: + raise SystemExit("FAIL %s: needle count=%d" % (tag, n)) + return s.replace(needle, repl) + +cr = '\r\n' + +# ---- main.cpp ---- +mp = load('native_bridge/src/main.cpp') + +mp = rep(mp, + "int main(int argc, char* argv[]) {" + cr + " std::cout << \"[NativeBridge] Starting DAW Host Bridge Engine...\" << std::endl;" + cr + "#ifdef _WIN32" + cr + " CoInitializeEx(nullptr, COINIT_MULTITHREADED);" + cr + cr + " // Disable Power Throttling (Efficiency Mode) for this background process", + "int main(int argc, char* argv[]) {" + cr + " std::cout << \"[NativeBridge] Starting DAW Host Bridge Engine...\" << std::endl;" + cr + "#ifdef _WIN32" + cr + " // Disable Power Throttling (Efficiency Mode) for this background process", + "coinit") + +mp = rep(mp, + " SetProcessInformation(GetCurrentProcess(), ProcessPowerThrottling, &powerState, sizeof(powerState));" + cr + cr + " // Elevate main audio processing thread to time-critical priority" + cr + " SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_TIME_CRITICAL);", + " SetProcessInformation(GetCurrentProcess(), ProcessPowerThrottling, &powerState, sizeof(powerState));", + "timecritical") + +mp = rep(mp, + " instruments.setEditorOpenPredicate([](const std::string& /*lp*/) {" + cr + " return false;" + cr + " });", + " instruments.setEditorOpenPredicate([](const std::string& lp) {" + cr + " std::lock_guard lk(g_editorMutex);" + cr + " auto it = g_editorPathCount.find(lp);" + cr + " return it != g_editorPathCount.end() && it->second > 0;" + cr + " });", + "predicate") + +blob = subprocess.check_output(['git', 'show', '0cec3d3:native_bridge/src/main.cpp']).decode('utf-8') +if '\r\n' not in blob and '\n' in blob: + blob = blob.replace('\n', '\r\n') + +def fn_start(s): + i = s.find('static void post_close_gui(uint32_t ch, HWND hwnd) {' + cr + ' ChannelWorker* w = nullptr;') + if i < 0: + i = s.find('static void post_close_gui(uint32_t ch, HWND hwnd) {\n ChannelWorker* w = nullptr;') + if i < 0: + raise SystemExit("fn_start not found") + return i + +cur_start = fn_start(mp) +cur_end = mp.find('int main(int argc, char* argv[]) {', cur_start) +old_start = fn_start(blob) +old_end = blob.find('int main(int argc, char* argv[]) {', old_start) +mp = rep(mp, mp[cur_start:cur_end], blob[old_start:old_end], "post_close_gui") + +needle5 = ( + " workers[guiCh]->post([&instruments, guiCh, arg1, arg2 = pluginId]() {" + cr + + " if (!instruments.get(guiCh)) {" + cr + + " std::cerr << \"[NativeBridge] GUI attach FAILED hwnd=\" << (void*)arg1" + cr + + " << \" plugin=\" << arg2 << \" ch=\" << guiCh << \" (no instrument loaded)\" << std::endl;" + cr + + " return;" + cr + + " }" + cr + + " if (auto* inst0 = instruments.get(guiCh)) {" + cr + + " if (inst0->hasAttachedView()) {" + cr + + " return;" + cr + + " }" + cr + + " }" + cr + + " void* hwnd = (void*)arg1;" + cr + + "#ifdef _WIN32" + cr + + " HWND nativeHwnd = nullptr;" + cr + + " {" + cr + + " std::lock_guard lock(g_guiMutex);" + cr + + " auto it = g_guiWindows.find(guiCh);" + cr + + " if (it != g_guiWindows.end()) nativeHwnd = (HWND)it->second;" + cr + + " }" + cr + + " if (hwnd == 0) {" + cr + + " if (nativeHwnd && IsWindow(nativeHwnd)) {" + cr + + " hwnd = (void*)nativeHwnd;" + cr + + " SetWindowTextA(nativeHwnd, arg2.c_str());" + cr + + " ShowWindow(nativeHwnd, SW_SHOW);" + cr + + " SetForegroundWindow(nativeHwnd);" + cr + + " // Reuse: cap nhat USERDATA (channel+1) - inst CU da bi thay" + cr + + " // the boi assign() -> WM_DESTROY sau nay lookup inst MOI." + cr + + " SetWindowLongPtrA(nativeHwnd, GWLP_USERDATA, (LONG_PTR)(guiCh + 1));" + cr + + " } else {" + cr + + " nativeHwnd = (HWND)create_native_vst_window(arg2.c_str());" + cr + + " if (!nativeHwnd) {" + cr + + " std::cerr << \"[NativeBridge] GUI create window FAILED plugin=\" << arg2 << std::endl;" + cr + + " return;" + cr + + " }" + cr + + " {" + cr + + " std::lock_guard lock(g_guiMutex);" + cr + + " g_guiWindows[guiCh] = nativeHwnd; // keep window alive" + cr + + " g_hwndToCh[nativeHwnd] = guiCh; // WM_DESTROY cleanup" + cr + + " }" + cr + + " SetWindowLongPtrA(nativeHwnd, GWLP_USERDATA, (LONG_PTR)(guiCh + 1));" + cr + + " hwnd = (void*)nativeHwnd;" + cr + + " }" + cr + + " }" + cr + + "#endif" + cr + + " if (auto* inst0 = instruments.get(guiCh)) {" + cr + + " if (inst0->hasAttachedView()) {" + cr + + " return;" + cr + + " }" + cr + + " }" + cr + + "#ifdef _WIN32" + cr + + " // Option B: chi 1 editor VST mo tai 1 thoi diem toan" +) +repl5 = ( + " void* hwnd = (void*)arg1;" + cr + + "#ifdef _WIN32" + cr + + " // Window PHAI thuoc MAIN thread (audio loop pump nay dispatch" + cr + + " // messages cua no moi vong lap). Tao/cap nhat window ngay tai day." + cr + + " HWND nativeHwnd = nullptr;" + cr + + " {" + cr + + " std::lock_guard lock(g_guiMutex);" + cr + + " auto it = g_guiWindows.find(guiCh);" + cr + + " if (it != g_guiWindows.end()) nativeHwnd = (HWND)it->second;" + cr + + " }" + cr + + " if (hwnd == 0) {" + cr + + " if (nativeHwnd && IsWindow(nativeHwnd)) {" + cr + + " hwnd = (void*)nativeHwnd;" + cr + + " SetWindowTextA(nativeHwnd, pluginId.c_str());" + cr + + " ShowWindow(nativeHwnd, SW_SHOW);" + cr + + " SetForegroundWindow(nativeHwnd);" + cr + + " // Reuse: cap nhat USERDATA (channel+1) - inst CU da bi thay" + cr + + " // the boi assign() -> WM_DESTROY sau nay lookup inst MOI." + cr + + " SetWindowLongPtrA(nativeHwnd, GWLP_USERDATA, (LONG_PTR)(guiCh + 1));" + cr + + " } else {" + cr + + " nativeHwnd = (HWND)create_native_vst_window(pluginId.c_str());" + cr + + " if (!nativeHwnd) {" + cr + + " std::cerr << \"[NativeBridge] GUI create window FAILED plugin=\" << pluginId << std::endl;" + cr + + " return;" + cr + + " }" + cr + + " {" + cr + + " std::lock_guard lock(g_guiMutex);" + cr + + " g_guiWindows[guiCh] = nativeHwnd; // keep window alive" + cr + + " g_hwndToCh[nativeHwnd] = guiCh; // WM_DESTROY cleanup" + cr + + " }" + cr + + " SetWindowLongPtrA(nativeHwnd, GWLP_USERDATA, (LONG_PTR)(guiCh + 1));" + cr + + " hwnd = (void*)nativeHwnd;" + cr + + " }" + cr + + " }" + cr + + "#endif" + cr + + " workers[guiCh]->post([&instruments, guiCh, hwnd, arg2 = pluginId]() {" + cr + + " if (!instruments.get(guiCh)) {" + cr + + " std::cerr << \"[NativeBridge] GUI attach FAILED hwnd=\" << hwnd" + cr + + " << \" plugin=\" << arg2 << \" ch=\" << guiCh << \" (no instrument loaded)\" << std::endl;" + cr + + " return;" + cr + + " }" + cr + + " if (auto* inst0 = instruments.get(guiCh)) {" + cr + + " if (inst0->hasAttachedView()) {" + cr + + " return;" + cr + + " }" + cr + + " }" + cr + + "#ifdef _WIN32" + cr + + " // Option B: chi 1 editor VST mo tai 1 thoi diem toan" +) +mp = rep(mp, needle5, repl5, "handleOpenGui-window") + +mp = rep(mp, + " try {" + cr + " instruments.setBypassed(true);" + cr + " sleep_ms(10);" + cr + " ok = inst->reloadForGUI();" + cr + " instruments.setBypassed(false);" + cr + " // IME recursion fix: strip the editor's IMC before", + " try {" + cr + " ok = inst->reloadForGUI();" + cr + " // IME recursion fix: strip the editor's IMC before", + "bypass-open1") + +mp = rep(mp, + " } catch (...) {" + cr + " instruments.setBypassed(false);" + cr + " std::cerr << \"[NativeBridge] GUI attach EXCEPTION ch=\" << guiCh", + " } catch (...) {" + cr + " std::cerr << \"[NativeBridge] GUI attach EXCEPTION ch=\" << guiCh", + "bypass-open2") + +mp = rep(mp, + ' << " (channel active with editor open)" << std::endl;', + ' << " (channel muted while editor open)" << std::endl;', + "logmsg") + +mp = rep(mp, + " if (ch != UINT32_MAX && g_engine) {" + cr + " auto* inst = g_engine->get(ch);" + cr + " if (inst) {" + cr + " inst->resizeView(w, h);" + cr + " }" + cr + " }" + cr + " return 0;", + " if (ch != UINT32_MAX && g_workers) {" + cr + " auto wit = g_workers->find(ch);" + cr + " if (wit != g_workers->end()) {" + cr + " // onSize() must run on the channel worker (the view was attached" + cr + " // in its COM STA apartment), not on the main pump thread." + cr + " wit->second->post([ch, w, h]() {" + cr + " if (!g_engine) return;" + cr + " auto* i = g_engine->get(ch);" + cr + " if (i) i->resizeView(w, h);" + cr + " });" + cr + " }" + cr + " }" + cr + " return 0;", + "wmsize") + +save('native_bridge/src/main.cpp', mp) + +# ---- Vst3Instrument.cpp ---- +vp = load('native_bridge/src/Vst3Instrument.cpp') + +vp = rep(vp, + "bool Vst3Instrument::reloadForGUI() {" + cr + "#ifndef HAVE_VST3SDK" + cr + " return false;" + cr + "#else" + cr + " auto* s = static_cast(state_);" + cr + " if (!s || !s->controller) return false;" + cr + " return true;" + cr + "#endif" + cr + "}", + "bool Vst3Instrument::reloadForGUI() {" + cr + "#ifndef HAVE_VST3SDK" + cr + " return false;" + cr + "#else" + cr + " auto* s = static_cast(state_);" + cr + " if (!s || !s->controller) return false;" + cr + " if (s->view && guiAttached_) return true;" + cr + " if (hasAttachedOnce_ && !guiAttached_) {" + cr + " // Reopen GUI sau khi dong: mot so plugin (Nexus...) hang o" + cr + " // view->attached() LAN 2 tren cung component instance. Tao lai inst" + cr + " // moi tren CUNG worker thread (COM STA con song) roi attach lai." + cr + " // PHAN NAY PHAI chay tren worker thread — apartment cua channel song" + cr + " // o day; chay tren thread tam thi apartment moi chet ngay sau do" + cr + " // (reopen lan sau -> treo nhu bridge_like)." + cr + " if (!reload()) return false;" + cr + " s = static_cast(state_);" + cr + " if (!s || !s->controller) return false;" + cr + " }" + cr + " return true;" + cr + "#endif" + cr + "}", + "reloadForGUI") + +vp = rep(vp, + "void Vst3Instrument::closeGUI() {" + cr + "#ifndef HAVE_VST3SDK" + cr + " return;" + cr + "#else" + cr + " auto* s = static_cast(state_);" + cr + " if (s && s->view) {" + cr + " if (guiAttached_) {" + cr + " try {" + cr + " s->view->removed();" + cr + " } catch (...) {" + cr + " std::cerr << \"[Vst3Instrument] Exception inside view->removed()\" << std::endl;" + cr + " }" + cr + " }" + cr + " s->view = nullptr;" + cr + " }" + cr + " guiAttached_ = false;" + cr + "#endif" + cr + "}", + "void Vst3Instrument::closeGUI() {" + cr + "#ifndef HAVE_VST3SDK" + cr + " return;" + cr + "#else" + cr + " auto* s = static_cast(state_);" + cr + " // Do NOT call s->view->removed(): Nexus's removed() enters an internal" + cr + " // modal wait for a message only this thread's message pump can dispatch" + cr + " // — but this thread is inside removed(), so it deadlocks forever" + cr + " // (observed: closeGUI job never returns; wdisp flood from the editor)." + cr + " // Nulling the view and letting the next terminate() (reload / destructor)" + cr + " // tear the editor windows down on this same thread is the deterministic" + cr + " // path out. Callers (post_close_gui / load job) additionally destroy the" + cr + " // editor's child windows right here on this thread so the plugin stops" + cr + " // pumping; the leaked editor object is inert once its windows are gone." + cr + " if (s) s->view = nullptr;" + cr + " guiAttached_ = false;" + cr + "#endif" + cr + "}", + "closeGUI") + +save('native_bridge/src/Vst3Instrument.cpp', vp) +print("ALL EDITS OK") diff --git a/native_bridge/debug/fix_wmsize_helper.py b/native_bridge/debug/fix_wmsize_helper.py new file mode 100644 index 0000000..52addf6 --- /dev/null +++ b/native_bridge/debug/fix_wmsize_helper.py @@ -0,0 +1,50 @@ +import subprocess + +def load(p): + return open(p, encoding='utf-8', newline='').read() + +def save(p, s): + open(p, 'w', encoding='utf-8', newline='').write(s) + +def rep(s, needle, repl, tag): + n = s.count(needle) + if n != 1: + raise SystemExit("FAIL %s: needle count=%d" % (tag, n)) + return s.replace(needle, repl) + +cr = '\r\n' +mp = load('native_bridge/src/main.cpp') + +# 1. forward-declare helper near post_close_gui fwd decl +mp = rep(mp, + "static void post_close_gui(uint32_t ch, HWND hwnd); // defined after ChannelWorker", + "static void post_close_gui(uint32_t ch, HWND hwnd); // defined after ChannelWorker" + cr + "static void post_resize_view(uint32_t ch, int w, int h); // defined after ChannelWorker", + "fwd") + +# 2. define helper after ChannelWorker class, before same-plugin helpers +anchor = "// --- same-plugin-DLL silence helpers" +helper = ( + "// resizeView() must run on the channel worker (the view was attached in its" + cr + + "// COM STA apartment), never on the main pump thread." + cr + + "static void post_resize_view(uint32_t ch, int w, int h) {" + cr + + " if (g_workers) {" + cr + + " auto wit = g_workers->find(ch);" + cr + + " if (wit != g_workers->end()) {" + cr + + " wit->second->post([ch, w, h]() {" + cr + + " if (!g_engine) return;" + cr + + " auto* i = g_engine->get(ch);" + cr + + " if (i) i->resizeView(w, h);" + cr + + " });" + cr + + " }" + cr + + " }" + cr + + "}" + cr + cr + anchor) +mp = rep(mp, anchor, helper, "helper") + +# 3. WM_SIZE: call helper instead of inline g_workers block +mp = rep(mp, + " if (ch != UINT32_MAX && g_workers) {" + cr + " auto wit = g_workers->find(ch);" + cr + " if (wit != g_workers->end()) {" + cr + " // onSize() must run on the channel worker (the view was attached" + cr + " // in its COM STA apartment), not on the main pump thread." + cr + " wit->second->post([ch, w, h]() {" + cr + " if (!g_engine) return;" + cr + " auto* i = g_engine->get(ch);" + cr + " if (i) i->resizeView(w, h);" + cr + " });" + cr + " }" + cr + " }" + cr + " return 0;", + " if (ch != UINT32_MAX) {" + cr + " post_resize_view(ch, w, h);" + cr + " }" + cr + " return 0;", + "wmsize") + +save('native_bridge/src/main.cpp', mp) +print("OK") diff --git a/native_bridge/debug/patch_fix.py b/native_bridge/debug/patch_fix.py new file mode 100644 index 0000000..b658541 --- /dev/null +++ b/native_bridge/debug/patch_fix.py @@ -0,0 +1,306 @@ +#!/usr/bin/env python3 +"""Apply owner-thread-destroy + pump-gate crash fix to main.cpp (LF file).""" +import sys, io + +p = r"C:/Users/locpham/SonicForgeStudio/native_bridge/src/main.cpp" +s = open(p, "rb").read().decode("utf-8") +orig = s + +def rep(old, new, tag): + global s + n = s.count(old) + assert n == 1, f"[{tag}] count={n}" + s = s.replace(old, new, 1) + print(f"ok: {tag}") + +# 1. globals +rep( +"""static std::map>* g_workers = nullptr;""", +"""static std::map>* g_workers = nullptr; +static std::mutex g_tidMutex; +static std::map g_tidToWorker; // worker tid -> worker (owner-thread destroy)""", +"globals") + +# 2. fwd decl +rep( +"""static void post_close_gui(uint32_t ch, HWND hwnd); // defined after ChannelWorker""", +"""static void post_close_gui(uint32_t ch, HWND hwnd); // defined after ChannelWorker +static bool is_teardown_window(HWND hwnd); // fwd — defined after ChannelWorker""", +"fwd") + +# 3. constructor register tid +rep( +""" th_ = std::thread([this] { +#ifdef _WIN32 + OleInitialize(nullptr);""", +""" th_ = std::thread([this] { +#ifdef _WIN32 + { + std::lock_guard lk(g_tidMutex); + g_tidToWorker[GetCurrentThreadId()] = this; + } + OleInitialize(nullptr);""", +"register tid") + +# 4. pump gate +rep( +""" for (int pumped = 0; pumped < 16; ++pumped) { + if (!PeekMessageW(&msg, nullptr, 0, 0, PM_REMOVE)) break; + TranslateMessage(&msg);""", +""" for (int pumped = 0; pumped < 16; ++pumped) { + if (!PeekMessageW(&msg, nullptr, 0, 0, PM_REMOVE)) break; + // CRASH FIX (0xc000041d): JUCE editor child windows of a + // plugin DLL are owned by the FIRST worker that ran the + // DLL's global JUCE MessageManager - not by the channel + // worker doing the teardown. While another worker's LOAD / + // close job tears an instance down (reloading / close in + // flight), dispatching a message (e.g. WM_PAINT) into the + // plugin wndproc reads instance state being destroyed -> + // AV in USER32 (observed: crash on the owner worker's + // pump, right after the teardown job closed the view). + // Drop the message instead (PeekMessageW already removed + // it): the editor may glitch, the bridge survives. + if (is_teardown_window(msg.hwnd)) continue; + TranslateMessage(&msg);""", +"pump gate") + +# 5. unregister tid +rep( +"""#ifdef _WIN32 + OleUninitialize(); +#endif + });""", +"""#ifdef _WIN32 + { + std::lock_guard lk(g_tidMutex); + g_tidToWorker.erase(GetCurrentThreadId()); + } + OleUninitialize(); +#endif + });""", +"unregister tid") + +# 6. post_and_wait +rep( +""" void post(std::function job) { + { + std::lock_guard lk(mu_); + jobs_.push_back(std::move(job)); + } + cv_.notify_all(); + } + +private:""", +""" void post(std::function job) { + { + std::lock_guard lk(mu_); + jobs_.push_back(std::move(job)); + } + cv_.notify_all(); + } + // Run job on THIS worker and wait (5s cap) until it finished. Used to + // serialize a window destroy with the owner worker's pump. MUST NOT be + // called from a job running on this same worker (deadlock) - callers + // destroy directly when owner tid == current tid. + bool post_and_wait(std::function job) { + std::mutex doneMx; + std::condition_variable doneCv; + bool done = false; + { + std::lock_guard lk(mu_); + jobs_.push_back([&]() { + job(); + { + std::lock_guard dk(doneMx); + done = true; + } + doneCv.notify_all(); + }); + } + cv_.notify_all(); + std::unique_lock dk(doneMx); + return doneCv.wait_for(dk, std::chrono::seconds(5), [&] { return done; }); + } + +private:""", +"post_and_wait") + +# 7. helpers after close_in_flight +rep( +"""static bool close_in_flight(uint32_t ch) { + std::lock_guard lk(g_attachMutex); + return g_closeInFlight[ch]; +}""", +"""static bool close_in_flight(uint32_t ch) { + std::lock_guard lk(g_attachMutex); + return g_closeInFlight[ch]; +} + +// True if hwnd (or any ancestor) is a plugin editor window whose channel is +// mid-teardown (reloading or close job in flight). Editor child windows +// (JUCE) have the native VST window as parent; the native window is in +// g_hwndToCh. JUCE's global message window has no such parent - walk stops +// -> false (never dropped). +static bool is_teardown_window(HWND hwnd) { + for (HWND h = hwnd; h; h = GetParent(h)) { + uint32_t ch = UINT32_MAX; + { + std::lock_guard lock(g_guiMutex); + auto it = g_hwndToCh.find(h); + if (it != g_hwndToCh.end()) ch = it->second; + } + if (ch != UINT32_MAX) { + if (close_in_flight(ch)) return true; + return g_engine ? g_engine->isReloading(ch) : false; + } + } + return false; +} + +// CRASH FIX (0xc000041d): plugin editor child windows are owned by the +// thread that first ran the plugin's JUCE MessageManager (a single worker), +// NOT by the channel worker doing the teardown. Cross-thread DestroyWindow +// races the owner's message pump. Post the destroy to the OWNER worker +// (serialized with its pump - no wndproc entry can race) and wait. Fall +// back to direct destroy when the owner is the current thread or unknown +// (leak > crash). Non-trivial: destroy jobs left queued on timeout are +// harmless - IsWindow guards them, and the pump gate already stopped +// dispatching to teardown windows. +static void destroy_window_on_owner(HWND hwnd) { + if (!hwnd || !IsWindow(hwnd)) return; + DWORD ownerTid = GetWindowThreadProcessId(hwnd, nullptr); + DWORD curTid = GetCurrentThreadId(); + if (ownerTid == 0 || ownerTid == curTid) { + if (IsWindow(hwnd)) DestroyWindow(hwnd); + return; + } + ChannelWorker* owner = nullptr; + { + std::lock_guard lk(g_tidMutex); + auto it = g_tidToWorker.find(ownerTid); + if (it != g_tidToWorker.end()) owner = it->second; + } + if (!owner) { + // Window belongs to a thread we don't control (e.g. main thread). + // Cross-thread DestroyWindow is unsafe -> leave it (leak > crash). + std::cerr << "[NativeBridge] destroy_window_on_owner: owner tid=" << ownerTid + << " not a bridge worker - leaving window " << hwnd << std::endl; + return; + } + if (!owner->post_and_wait([hwnd]() { + if (IsWindow(hwnd)) DestroyWindow(hwnd); + })) + std::cerr << "[NativeBridge] destroy_window_on_owner: timeout waiting owner tid=" + << ownerTid << " to destroy " << hwnd << std::endl; +}""", +"helpers") + +# 8. post_close_gui: destroy outside g_loadMutex, via owner +rep( +""" bool reloadOk = false; + { + // destroy children + reload in ONE lock: destroying the + // editor windows runs the plugin's window proc (DLL + // entry) on this worker; it must not race another + // thread's createInstance/createView of the same DLL. + // Same-thread destroy is valid (attachView created them + // on this worker); without destroy the editor keeps + // pumping (0x47b flood) while the audio loop processes + // the instance (2 threads in one DLL) -> Nexus USER32 crash. + std::lock_guard lg(g_loadMutex); + // CRASH FIX: reload() = terminate + loadPlugin + // (createInstance) — Nexus may throw a C++ exception + // here too; catch it, treat as failed reload, and let + // the restore below unmute the channel (instance may be + // broken; next load/assign rebuilds it). + try { +#ifdef _WIN32 + disable_ime_contexts(hwnd); + if (hwnd && IsWindow(hwnd)) { + while (HWND c = FindWindowExA(hwnd, nullptr, nullptr, nullptr)) { + std::cerr << "[dbg] closeGUI destroy child=" << c + << " owner_tid=" << GetWindowThreadProcessId(c, nullptr) + << " cur_tid=" << GetCurrentThreadId() << std::endl; + DestroyWindow(c); + } + } +#endif + reloadOk = i->reload(); + } catch (...) { + std::cerr << "[NativeBridge] closeGUI reload EXCEPTION ch=" << ch + << " — plugin threw (createInstance)" << std::endl; + reloadOk = false; + } + }""", +""" bool reloadOk = false; +#ifdef _WIN32 + // CRASH FIX (0xc000041d): destroy the editor's child windows + // on the thread that OWNS them (the plugin's JUCE + // MessageManager thread - a single worker), never + // cross-thread. Wait for each destroy to finish (owner worker + // serializes it with its pump) BEFORE reload() - otherwise + // reload's createInstance enters the DLL while the owner + // thread is still inside the plugin wndproc (2 threads in one + // DLL -> Nexus crash). Kept OUTSIDE g_loadMutex: the wait + // must not block other loads (the owner worker may itself be + // waiting on that lock). + disable_ime_contexts(hwnd); + if (hwnd && IsWindow(hwnd)) { + while (HWND c = FindWindowExA(hwnd, nullptr, nullptr, nullptr)) { + std::cerr << "[dbg] closeGUI destroy child=" << c + << " owner_tid=" << GetWindowThreadProcessId(c, nullptr) + << " cur_tid=" << GetCurrentThreadId() << std::endl; + destroy_window_on_owner(c); + } + } +#endif + { + // reload() = terminate + loadPlugin (createInstance) — + // serialized with all other plugin-DLL entry points. + // Nexus may throw a C++ exception here too; catch it, + // treat as failed reload, and let the restore below + // unmute the channel (instance may be broken; next + // load/assign rebuilds it). + std::lock_guard lg(g_loadMutex); + try { + reloadOk = i->reload(); + } catch (...) { + std::cerr << "[NativeBridge] closeGUI reload EXCEPTION ch=" << ch + << " — plugin threw (createInstance)" << std::endl; + reloadOk = false; + } + }""", +"post_close_gui destroy") + +# 9. LOAD close-editor destroy loop via owner +rep( +""" // Destroy the old editor's child windows (worker-owned, + // created by attachView here) so they stop pumping; + // the parent window is hidden and kept for reuse. + // See post_close_gui for the same pattern. + while (HWND c = FindWindowExA(hToHide, nullptr, nullptr, nullptr)) { + std::cerr << "[dbg] load close-editor destroy child=" << c + << " owner_tid=" << GetWindowThreadProcessId(c, nullptr) + << " cur_tid=" << GetCurrentThreadId() << std::endl; + DestroyWindow(c); + }""", +""" // CRASH FIX (0xc000041d): destroy the old + // editor's child windows on the thread that OWNS + // them (JUCE MessageManager worker), never + // cross-thread - DestroyWindow from this LOAD + // worker raced the owner's pump dispatching + // WM_PAINT into the plugin wndproc -> AV. Wait + // for each destroy before assign() below. The + // parent window is hidden and kept for reuse. + // See post_close_gui for the same pattern. + while (HWND c = FindWindowExA(hToHide, nullptr, nullptr, nullptr)) { + std::cerr << "[dbg] load close-editor destroy child=" << c + << " owner_tid=" << GetWindowThreadProcessId(c, nullptr) + << " cur_tid=" << GetCurrentThreadId() << std::endl; + destroy_window_on_owner(c); + }""", +"load destroy loop") + +assert s != orig +open(p, "wb").write(s.encode("utf-8")) +print("written") diff --git a/native_bridge/debug/probe_app.py b/native_bridge/debug/probe_app.py new file mode 100644 index 0000000..b2494f3 --- /dev/null +++ b/native_bridge/debug/probe_app.py @@ -0,0 +1,108 @@ +# Drive the REAL app's bridge SHM: open GUI ch=0, screenshot, pixel analysis. +import ctypes, ctypes.wintypes as w, struct, subprocess, sys, os, time, re + +sys.stdout.reconfigure(encoding='utf-8', errors='replace') + +SHM_NAME = "SonicForge_DAW_IPC" +SIZE = 32768 +LOG = os.path.join(os.path.dirname(os.path.abspath(__file__)), "probe_app.log") + +user32 = ctypes.windll.user32 +kernel32 = ctypes.windll.kernel32 +kernel32.CreateFileMappingA.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.wintypes.DWORD, ctypes.wintypes.DWORD, ctypes.wintypes.DWORD, ctypes.c_char_p] +kernel32.CreateFileMappingA.restype = ctypes.c_void_p +kernel32.MapViewOfFile.argtypes = [ctypes.c_void_p, ctypes.wintypes.DWORD, ctypes.wintypes.DWORD, ctypes.wintypes.DWORD, ctypes.c_size_t] +kernel32.MapViewOfFile.restype = ctypes.c_void_p +INVALID_HANDLE_VALUE = ctypes.c_void_p(-1).value +PAGE_READWRITE = 0x04 +FILE_MAP_ALL_ACCESS = 0xF001F + +hMap = kernel32.CreateFileMappingA(INVALID_HANDLE_VALUE, None, PAGE_READWRITE, 0, SIZE, SHM_NAME.encode()) +ptr = kernel32.MapViewOfFile(hMap, FILE_MAP_ALL_ACCESS, 0, 0, 0) + +def w32(off, fmt, *args): + ctypes.memmove(ptr + off, struct.pack(fmt, *args), struct.calcsize(fmt)) + +def write_control(type_, arg0, arg1, ch, path): + for i in range(8): + off = 2836 + i * 1040 + t = ctypes.c_uint32.from_address(ptr + off).value + if t == 0: + w32(off, 'IIII', type_, arg0, arg1, ch) + if path: + b = path.encode('utf-8')[:1023] + b'\x00' + ctypes.memmove(ptr + off + 16, b, len(b)) + return i + raise RuntimeError("control queue full") + +def push_controls(events): + for i in range(8): + w32(2836 + i * 1040, 'I', 0) + for (type_, arg0, arg1, ch, path) in events: + write_control(type_, arg0, arg1, ch, path) + w32(11156, 'I', len(events)) + +def wait_log(pattern, timeout): + t0 = time.time() + while time.time() - t0 < timeout: + try: + with open(LOG, 'r', encoding='utf-8', errors='replace') as f: + lines = f.readlines() + for i in range(len(lines)): + if pattern in lines[i]: + return lines[i].strip() + except FileNotFoundError: + pass + time.sleep(0.2) + return None + +# copy app bridge.log to local log for wait_log +applog = os.path.join(os.environ.get('APPDATA', '.'), 'SonicForgeDAW', 'logs', 'bridge.log') +open(LOG, 'w', encoding='utf-8').write(open(applog, encoding='utf-8', errors='replace').read()) + +print("push OPEN_GUI ch=0...") +push_controls([(4, 0, 0, 0, "nexus")]) +r = wait_log("GUI attached", 40) +print("[open]", r) +hwnd = None +if r: + m = re.search(r'hwnd=([0-9A-Fa-f]+)', r) + if m: + hwnd = int(m.group(1), 16) +print("hwnd =", hex(hwnd) if hwnd else None) +time.sleep(6) + +if hwnd: + rw = w.RECT() + user32.GetWindowRect(hwnd, ctypes.byref(rw)) + print("window rect", rw.left, rw.top, rw.right, rw.bottom, "vis", user32.IsWindowVisible(hwnd)) + wdt, hgt = rw.right - rw.left, rw.bottom - rw.top + ps = f""" +Add-Type -AssemblyName System.Drawing +$bmp = New-Object System.Drawing.Bitmap({wdt}, {hgt}) +$g = [System.Drawing.Graphics]::FromImage($bmp) +$g.CopyFromScreen({rw.left}, {rw.top}, 0, 0, $bmp.Size) +$bmp.Save('C:/Users/locpham/SonicForgeStudio/native_bridge/debug/probe_app.png') +$g.Dispose(); $bmp.Dispose() +""" + subprocess.run(["powershell", "-NoProfile", "-Command", ps], check=False) + print("screenshot saved probe_app.png") +else: + # dump all windows with class SonicForge_Native_VST3_Class + import ctypes + found = [] + @ctypes.WINFUNCTYPE(w.BOOL, w.HWND, w.LPARAM) + def cb(h, lp): + buf = ctypes.create_unicode_buffer(64) + user32.GetClassNameW(h, buf, 64) + if 'SonicForge' in buf.value: + found.append((h, buf.value)) + return True + user32.EnumWindows(cb, 0) + print("native vst windows:", [(hex(h), c) for h, c in found]) + +kernel32.UnmapViewOfFile.argtypes = [ctypes.c_void_p] +kernel32.CloseHandle.argtypes = [ctypes.c_void_p] +kernel32.UnmapViewOfFile(ptr) +kernel32.CloseHandle(hMap) +print("done") diff --git a/native_bridge/debug/probe_app2.py b/native_bridge/debug/probe_app2.py new file mode 100644 index 0000000..21e8274 --- /dev/null +++ b/native_bridge/debug/probe_app2.py @@ -0,0 +1,130 @@ +# Drive REAL app bridge SHM (SonicForge_DAW_IPC_): load Nexus ch=0, open GUI, screenshot, brightness. +import ctypes, ctypes.wintypes as w, struct, subprocess, sys, os, time, re +from PIL import Image + +sys.stdout.reconfigure(encoding='utf-8', errors='replace') + +DAW_PID = sys.argv[1] if len(sys.argv) > 1 else "21148" +SHM_NAME = b"SonicForge_DAW_IPC" +SIZE = 32768 +HERE = os.path.dirname(os.path.abspath(__file__)) +LOG = os.path.join(HERE, "probe_app2.log") +APP_LOG = os.path.join(os.environ.get('APPDATA', '.'), 'SonicForgeDAW', 'logs', 'bridge.log') +NEXUS = r"C:\Program Files\Common Files\VST3\Nexus.vst3" +PNG = os.path.join(HERE, "probe_app2.png") + +user32 = ctypes.windll.user32 +kernel32 = ctypes.windll.kernel32 +kernel32.OpenFileMappingA.argtypes = [ctypes.wintypes.DWORD, w.BOOL, ctypes.c_char_p] +kernel32.OpenFileMappingA.restype = ctypes.c_void_p +kernel32.MapViewOfFile.argtypes = [ctypes.c_void_p, ctypes.wintypes.DWORD, ctypes.wintypes.DWORD, ctypes.wintypes.DWORD, ctypes.c_size_t] +kernel32.MapViewOfFile.restype = ctypes.c_void_p +FILE_MAP_ALL_ACCESS = 0xF001F + +hMap = kernel32.OpenFileMappingA(FILE_MAP_ALL_ACCESS, False, SHM_NAME) +if not hMap: + print("OpenFileMapping failed", ctypes.get_last_error()); sys.exit(1) +ptr = kernel32.MapViewOfFile(hMap, FILE_MAP_ALL_ACCESS, 0, 0, 0) +if not ptr: + print("MapViewOfFile failed", ctypes.get_last_error()); sys.exit(1) + +def w32(off, fmt, *args): + ctypes.memmove(ptr + off, struct.pack(fmt, *args), struct.calcsize(fmt)) + +def write_control(type_, arg0, arg1, ch, path): + for i in range(8): + off = 2836 + i * 1040 + t = ctypes.c_uint32.from_address(ptr + off).value + if t == 0: + w32(off, 'IIII', type_, arg0, arg1, ch) + if path: + b = path.encode('utf-8')[:1023] + b'\x00' + ctypes.memmove(ptr + off + 16, b, len(b)) + return i + raise RuntimeError("control queue full") + +def push_controls(events): + for i in range(8): + w32(2836 + i * 1040, 'I', 0) + for (type_, arg0, arg1, ch, path) in events: + write_control(type_, arg0, arg1, ch, path) + w32(11156, 'I', len(events)) + +def log_lines(): + try: + with open(APP_LOG, 'r', encoding='utf-8', errors='replace') as f: + return f.readlines() + except FileNotFoundError: + return [] + +def wait_log(pattern, timeout, since): + end = time.time() + timeout + while time.time() < end: + lines = log_lines() + for i in range(since, len(lines)): + if pattern in lines[i]: + return lines[i].strip() + time.sleep(0.2) + return None + +since = len(log_lines()) +print("shm", SHM_NAME, "log lines", since) +print("[1] load Nexus ch=0...") +push_controls([(2, 0, 0, 0, NEXUS)]) +r = wait_log("instrument loaded ch=0", 60, since) +print(" ", r) +since = len(log_lines()) +print("[2] open GUI ch=0...") +push_controls([(4, 0, 0, 0, "nexus")]) +r = wait_log("GUI attached", 40, since) +print("[open]", r) +hwnd = None +if r: + m = re.search(r'hwnd=([0-9A-Fa-f]+)', r) + if m: + hwnd = int(m.group(1), 16) +print("hwnd =", hex(hwnd) if hwnd else None) +time.sleep(8) + +if hwnd: + rw = w.RECT() + user32.GetWindowRect(hwnd, ctypes.byref(rw)) + print("window rect", rw.left, rw.top, rw.right, rw.bottom, "vis", user32.IsWindowVisible(hwnd)) + wdt, hgt = rw.right - rw.left, rw.bottom - rw.top + if wdt <= 0 or hgt <= 0: + print("bad rect"); sys.exit(1) + ps = f""" +Add-Type -AssemblyName System.Drawing +$bmp = New-Object System.Drawing.Bitmap({wdt}, {hgt}) +$g = [System.Drawing.Graphics]::FromImage($bmp) +$g.CopyFromScreen({rw.left}, {rw.top}, 0, 0, $bmp.Size) +$bmp.Save('{PNG}') +$g.Dispose(); $bmp.Dispose() +""" + subprocess.run(["powershell", "-NoProfile", "-Command", ps], check=False) + img = Image.open(PNG).convert('L') + px = list(img.getdata()) + n = len(px) + avg = sum(px) / n + buckets = [0, 0, 0, 0, 0, 0, 0, 0] + for v in px: + buckets[v // 32] += 1 + pct = [round(100 * b / n) for b in buckets] + print("screenshot", PNG, img.size, "avgLum", round(avg, 1), "buckets[0-31..224-255]", pct) +else: + found = [] + @ctypes.WINFUNCTYPE(w.BOOL, w.HWND, w.LPARAM) + def cb(h, lp): + buf = ctypes.create_unicode_buffer(64) + user32.GetClassNameW(h, buf, 64) + if 'SonicForge' in buf.value: + found.append((h, buf.value)) + return True + user32.EnumWindows(cb, 0) + print("native vst windows:", [(hex(h), c) for h, c in found]) + +kernel32.UnmapViewOfFile.argtypes = [ctypes.c_void_p] +kernel32.CloseHandle.argtypes = [ctypes.c_void_p] +kernel32.UnmapViewOfFile(ptr) +kernel32.CloseHandle(hMap) +print("done") diff --git a/native_bridge/debug/probe_app3.py b/native_bridge/debug/probe_app3.py new file mode 100644 index 0000000..3848db1 --- /dev/null +++ b/native_bridge/debug/probe_app3.py @@ -0,0 +1,121 @@ +# Drive REAL app bridge SHM: cycle several VST3 plugins on ch=0, screenshot each, brightness. +# Usage: python probe_app3.py +import ctypes, ctypes.wintypes as w, struct, subprocess, sys, os, time, re +from PIL import Image + +sys.stdout.reconfigure(encoding='utf-8', errors='replace') + +SHM_NAME = b"SonicForge_DAW_IPC" +SIZE = 32768 +HERE = os.path.dirname(os.path.abspath(__file__)) +APP_LOG = os.path.join(os.environ.get('APPDATA', '.'), 'SonicForgeDAW', 'logs', 'bridge.log') +VST = r"C:\Program Files\Common Files\VST3" +PLUGINS = [ + ("nexus", os.path.join(VST, "Nexus.vst3")), + ("kontakt", os.path.join(VST, "Kontakt.vst3")), + ("scaler2", os.path.join(VST, "Scaler2.vst3")), + ("ample", os.path.join(VST, "Ample Sound", "ACDD.vst3")), +] + +user32 = ctypes.windll.user32 +kernel32 = ctypes.windll.kernel32 +kernel32.OpenFileMappingA.argtypes = [ctypes.wintypes.DWORD, w.BOOL, ctypes.c_char_p] +kernel32.OpenFileMappingA.restype = ctypes.c_void_p +kernel32.MapViewOfFile.argtypes = [ctypes.c_void_p, ctypes.wintypes.DWORD, ctypes.wintypes.DWORD, ctypes.wintypes.DWORD, ctypes.c_size_t] +kernel32.MapViewOfFile.restype = ctypes.c_void_p +FILE_MAP_ALL_ACCESS = 0xF001F + +hMap = kernel32.OpenFileMappingA(FILE_MAP_ALL_ACCESS, False, SHM_NAME) +if not hMap: + print("OpenFileMapping failed", ctypes.get_last_error()); sys.exit(1) +ptr = kernel32.MapViewOfFile(hMap, FILE_MAP_ALL_ACCESS, 0, 0, 0) +if not ptr: + print("MapViewOfFile failed", ctypes.get_last_error()); sys.exit(1) + +def w32(off, fmt, *args): + ctypes.memmove(ptr + off, struct.pack(fmt, *args), struct.calcsize(fmt)) + +def write_control(type_, arg0, arg1, ch, path): + for i in range(8): + off = 2836 + i * 1040 + t = ctypes.c_uint32.from_address(ptr + off).value + if t == 0: + w32(off, 'IIII', type_, arg0, arg1, ch) + if path: + b = path.encode('utf-8')[:1023] + b'\x00' + ctypes.memmove(ptr + off + 16, b, len(b)) + return i + raise RuntimeError("control queue full") + +def push_controls(events): + for i in range(8): + w32(2836 + i * 1040, 'I', 0) + for (type_, arg0, arg1, ch, path) in events: + write_control(type_, arg0, arg1, ch, path) + w32(11156, 'I', len(events)) + +def log_lines(): + try: + with open(APP_LOG, 'r', encoding='utf-8', errors='replace') as f: + return f.readlines() + except FileNotFoundError: + return [] + +def wait_log(pattern, timeout, since): + end = time.time() + timeout + while time.time() < end: + lines = log_lines() + for i in range(since, len(lines)): + if pattern in lines[i]: + return lines[i].strip() + time.sleep(0.2) + return None + +def shot(hwnd, tag): + rw = w.RECT() + user32.GetWindowRect(hwnd, ctypes.byref(rw)) + wdt, hgt = rw.right - rw.left, rw.bottom - rw.top + if wdt <= 0 or hgt <= 0: + return None + png = os.path.join(HERE, f"probe_app3_{tag}.png") + ps = f""" +Add-Type -AssemblyName System.Drawing +$bmp = New-Object System.Drawing.Bitmap({wdt}, {hgt}) +$g = [System.Drawing.Graphics]::FromImage($bmp) +$g.CopyFromScreen({rw.left}, {rw.top}, 0, 0, $bmp.Size) +$bmp.Save('{png}') +$g.Dispose(); $bmp.Dispose() +""" + subprocess.run(["powershell", "-NoProfile", "-Command", ps], check=False) + img = Image.open(png).convert('L') + px = list(img.getdata()) + n = len(px) + avg = sum(px) / n + buckets = [0, 0, 0, 0, 0, 0, 0, 0] + for v in px: + buckets[v // 32] += 1 + pct = [round(100 * b / n) for b in buckets] + return f"avgLum {round(avg,1)} buckets[0-31..224-255] {pct} size {img.size}" + +for tag, path in PLUGINS: + since = len(log_lines()) + print(f"--- {tag} ---") + push_controls([(2, 0, 0, 0, path)]) + r = wait_log("instrument loaded ch=0", 90, since) + print(" load:", (r or "TIMEOUT").split(" type=")[0]) + since = len(log_lines()) + push_controls([(4, 0, 0, 0, tag)]) + r = wait_log("GUI attached", 40, since) + print(" open:", (r or "TIMEOUT")[:100]) + m = re.search(r'hwnd=([0-9A-Fa-f]+)', r or "") + if not m: + print(" NO HWND"); continue + hwnd = int(m.group(1), 16) + time.sleep(8) + res = shot(hwnd, tag) + print(" shot:", res) + # close GUI via WM_CLOSE (VstWindowProc hides + detaches view) + user32.SendMessageW(hwnd, 0x0010, 0, 0) # WM_CLOSE + time.sleep(3) + +print("done") diff --git a/native_bridge/debug/probe_fix_flow.py b/native_bridge/debug/probe_fix_flow.py new file mode 100644 index 0000000..77f9bd9 --- /dev/null +++ b/native_bridge/debug/probe_fix_flow.py @@ -0,0 +1,180 @@ +# Verify fix flow user: create SHM plain -> spawn bridge (no debugger) -> load Nexus ch=0 +# -> open GUI -> screenshot -> close -> reopen -> verify unmute + other plugin loads. +import ctypes, ctypes.wintypes as w, struct, subprocess, sys, os, time, re +from PIL import Image + +sys.stdout.reconfigure(encoding='utf-8', errors='replace') + +SHM_NAME = "SonicForge_DAW_IPC" +SIZE = 32768 +HERE = os.path.dirname(os.path.abspath(__file__)) +BRIDGE = r"C:\Users\locpham\SonicForgeStudio\native_bridge\build\Release\daw_vst_bridge.exe" +LOG = os.path.join(HERE, "probe_fix_flow.log") +NEXUS = r"C:\Program Files\Common Files\VST3\Nexus.vst3" +AMPLE = r"C:\Program Files\Common Files\VST3\Ample Sound\ACDD.vst3" + +kernel32 = ctypes.windll.kernel32 +kernel32.CreateFileMappingA.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.wintypes.DWORD, ctypes.wintypes.DWORD, ctypes.wintypes.DWORD, ctypes.c_char_p] +kernel32.CreateFileMappingA.restype = ctypes.c_void_p +kernel32.MapViewOfFile.argtypes = [ctypes.c_void_p, ctypes.wintypes.DWORD, ctypes.wintypes.DWORD, ctypes.wintypes.DWORD, ctypes.c_size_t] +kernel32.MapViewOfFile.restype = ctypes.c_void_p +INVALID_HANDLE_VALUE = ctypes.c_void_p(-1).value +PAGE_READWRITE = 0x04 +FILE_MAP_ALL_ACCESS = 0xF001F + +hMap = kernel32.CreateFileMappingA(INVALID_HANDLE_VALUE, None, PAGE_READWRITE, 0, SIZE, SHM_NAME.encode()) +if not hMap: + print("CreateFileMapping failed", ctypes.get_last_error()); sys.exit(1) +ptr = kernel32.MapViewOfFile(hMap, FILE_MAP_ALL_ACCESS, 0, 0, 0) +if not ptr: + print("MapViewOfFile failed", ctypes.get_last_error()); sys.exit(1) + +def w32(off, fmt, *args): + ctypes.memmove(ptr + off, struct.pack(fmt, *args), struct.calcsize(fmt)) + +def write_control(type_, arg0, arg1, ch, path): + for i in range(8): + off = 2836 + i * 1040 + t = ctypes.c_uint32.from_address(ptr + off).value + if t == 0: + w32(off, 'IIII', type_, arg0, arg1, ch) + if path: + b = path.encode('utf-8')[:1023] + b'\x00' + ctypes.memmove(ptr + off + 16, b, len(b)) + return i + raise RuntimeError("control queue full") + +def push_controls(events): + for i in range(8): + w32(2836 + i * 1040, 'I', 0) + for (type_, arg0, arg1, ch, path) in events: + write_control(type_, arg0, arg1, ch, path) + w32(11156, 'I', len(events)) + +def write_index(): + return ctypes.c_uint32.from_address(ptr + 4).value + +def wait_log(pattern, timeout, since=0): + end = time.time() + timeout + while time.time() < end: + try: + with open(LOG, 'r', encoding='utf-8', errors='replace') as f: + lines = f.readlines() + except FileNotFoundError: + lines = [] + for i in range(max(0, len(lines) - 1 - since), len(lines)): + if pattern in lines[i]: + return lines[i].strip() + time.sleep(0.2) + return None + +open(LOG, 'w', encoding='utf-8').close() +proc = subprocess.Popen( + [BRIDGE, "--shm", SHM_NAME], + env={**os.environ, "SF_SHM_NAME": SHM_NAME, "SF_PARENT_PID": str(os.getpid()), + "SF_SAMPLE_RATE": "48000", "SF_BLOCK_SIZE": "256"}, + stdout=open(LOG, 'ab'), stderr=subprocess.STDOUT, +) +time.sleep(1.5) +print("bridge pid", proc.pid, "poll", proc.poll()) +if proc.poll() is not None: + print("!!! BRIDGE EXITED EARLY"); sys.exit(1) + +def shot(hwnd, tag): + user32 = ctypes.windll.user32 + rw = w.RECT() + user32.GetWindowRect(hwnd, ctypes.byref(rw)) + wdt, hgt = rw.right - rw.left, rw.bottom - rw.top + if wdt <= 0 or hgt <= 0: + return f"bad rect {rw.left},{rw.top},{rw.right},{rw.bottom}" + png = os.path.join(HERE, f"probe_fix_{tag}.png") + ps = f""" +Add-Type -AssemblyName System.Drawing +$bmp = New-Object System.Drawing.Bitmap({wdt}, {hgt}) +$g = [System.Drawing.Graphics]::FromImage($bmp) +$g.CopyFromScreen({rw.left}, {rw.top}, 0, 0, $bmp.Size) +$bmp.Save('{png}') +$g.Dispose(); $bmp.Dispose() +""" + subprocess.run(["powershell", "-NoProfile", "-Command", ps], check=False) + img = Image.open(png).convert('L') + px = list(img.getdata()) + n = len(px) + avg = sum(px) / n + buckets = [0, 0, 0, 0, 0, 0, 0, 0] + for v in px: + buckets[v // 32] += 1 + pct = [round(100 * b / n) for b in buckets] + return f"avgLum {round(avg,1)} buckets[0-31..224-255] {pct} size {img.size}" + +def muted_now(): + # bridge log "channel muted while editor open" appears while editor open; + # after close, next load/open should NOT show mute. We instead check writeIndex. + return None + +# [1] load Nexus ch=0 +since = len(open(LOG, 'rb').read().splitlines()) +print("[1] load Nexus ch=0...") +push_controls([(2, 0, 0, 0, NEXUS)]) +r = wait_log("instrument loaded ch=0", 90) +print(" ", r) + +# [2] open GUI +since = len(open(LOG, 'rb').read().splitlines()) +print("[2] open GUI ch=0...") +push_controls([(4, 0, 0, 0, "nexus")]) +r = wait_log("GUI attached", 40) +print(" ", r) +m = re.search(r'hwnd=([0-9A-Fa-f]+)', r or "") +hwnd = int(m.group(1), 16) if m else None +time.sleep(8) +if hwnd: + print(" shot:", shot(hwnd, "nexus_open")) +alive = proc.poll() is None +print(" alive:", alive) +wi = write_index(); time.sleep(2); wi2 = write_index() +print(" writeIndex", wi, "->", wi2, "advancing:", wi2 > wi) + +# [3] close GUI via WM_CLOSE +if hwnd: + ctypes.windll.user32.SendMessageW(hwnd, 0x0010, 0, 0) + time.sleep(4) +alive = proc.poll() is None +print("[3] after close alive:", alive) + +# [4] reopen GUI (user symptom: reopen -> mute) +since = len(open(LOG, 'rb').read().splitlines()) +print("[4] reopen GUI ch=0...") +push_controls([(4, 0, 0, 0, "nexus")]) +r = wait_log("GUI attached", 40) +print(" ", r) +m = re.search(r'hwnd=([0-9A-Fa-f]+)', r or "") +hwnd = int(m.group(1), 16) if m else None +time.sleep(8) +if hwnd: + print(" shot:", shot(hwnd, "nexus_reopen")) +alive = proc.poll() is None +wi = write_index(); time.sleep(2); wi2 = write_index() +print(" alive:", alive, "writeIndex", wi, "->", wi2, "advancing:", wi2 > wi) + +# [5] load OTHER plugin while GUI open (user symptom: not load) +since = len(open(LOG, 'rb').read().splitlines()) +print("[5] load Ample while Nexus GUI open...") +push_controls([(2, 0, 0, 0, AMPLE)]) +r = wait_log("instrument loaded ch=0", 60) +print(" ", r) +alive = proc.poll() is None +wi = write_index(); time.sleep(2); wi2 = write_index() +print(" alive:", alive, "writeIndex", wi, "->", wi2, "advancing:", wi2 > wi) + +print("FINAL alive:", proc.poll() is None) +print("--- last 25 log lines ---") +for l in open(LOG, 'r', encoding='utf-8', errors='replace').readlines()[-25:]: + print(l.rstrip()) + +kernel32.UnmapViewOfFile.argtypes = [ctypes.c_void_p] +kernel32.CloseHandle.argtypes = [ctypes.c_void_p] +kernel32.UnmapViewOfFile(ptr) +kernel32.CloseHandle(hMap) +proc.terminate() +print("done") diff --git a/native_bridge/debug/probe_gui.py b/native_bridge/debug/probe_gui.py new file mode 100644 index 0000000..a0586d4 --- /dev/null +++ b/native_bridge/debug/probe_gui.py @@ -0,0 +1,138 @@ +# Probe GUI render: spawn bridge, load Nexus, open GUI ch=5, dump window tree + screenshot. +# Usage: python probe_gui.py [bridge_exe] +import ctypes, ctypes.wintypes as w, struct, subprocess, sys, os, time, re + +sys.stdout.reconfigure(encoding='utf-8', errors='replace') + +SHM_NAME = "SonicForge_DAW_IPC_GUIPROBE" +SIZE = 32768 +BRIDGE = sys.argv[1] if len(sys.argv) > 1 else r"C:\Users\locpham\SonicForgeStudio\native_bridge\build\Release\daw_vst_bridge.exe" +LOG = os.path.join(os.path.dirname(os.path.abspath(__file__)), "probe_gui.log") +NEXUS = r"C:\Program Files\Common Files\VST3\Nexus.vst3" +AMPLE = r"C:\Program Files\Common Files\VST3\Ample Sound\ACDD.vst3" + +user32 = ctypes.windll.user32 +kernel32 = ctypes.windll.kernel32 +kernel32.CreateFileMappingA.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.wintypes.DWORD, ctypes.wintypes.DWORD, ctypes.wintypes.DWORD, ctypes.c_char_p] +kernel32.CreateFileMappingA.restype = ctypes.c_void_p +kernel32.MapViewOfFile.argtypes = [ctypes.c_void_p, ctypes.wintypes.DWORD, ctypes.wintypes.DWORD, ctypes.wintypes.DWORD, ctypes.c_size_t] +kernel32.MapViewOfFile.restype = ctypes.c_void_p +INVALID_HANDLE_VALUE = ctypes.c_void_p(-1).value +PAGE_READWRITE = 0x04 +FILE_MAP_ALL_ACCESS = 0xF001F + +hMap = kernel32.CreateFileMappingA(INVALID_HANDLE_VALUE, None, PAGE_READWRITE, 0, SIZE, SHM_NAME.encode()) +ptr = kernel32.MapViewOfFile(hMap, FILE_MAP_ALL_ACCESS, 0, 0, 0) + +def w32(off, fmt, *args): + ctypes.memmove(ptr + off, struct.pack(fmt, *args), struct.calcsize(fmt)) + +def write_control(type_, arg0, arg1, ch, path): + for i in range(8): + off = 2836 + i * 1040 + t = ctypes.c_uint32.from_address(ptr + off).value + if t == 0: + w32(off, 'IIII', type_, arg0, arg1, ch) + if path: + b = path.encode('utf-8')[:1023] + b'\x00' + ctypes.memmove(ptr + off + 16, b, len(b)) + return i + raise RuntimeError("control queue full") + +def push_controls(events): + for i in range(8): + w32(2836 + i * 1040, 'I', 0) + for (type_, arg0, arg1, ch, path) in events: + write_control(type_, arg0, arg1, ch, path) + w32(11156, 'I', len(events)) # controlQueueCount — bridge drains only count entries + +proc = subprocess.Popen([BRIDGE, "--shm", SHM_NAME], + env={**os.environ, "SF_SHM_NAME": SHM_NAME, "SF_PARENT_PID": str(os.getpid()), + "SF_SAMPLE_RATE": "48000", "SF_BLOCK_SIZE": "256"}, + stdout=open(LOG, 'wb'), stderr=subprocess.STDOUT) +time.sleep(1.5) + +def wait_log(pattern, timeout, since=0): + t0 = time.time() + while time.time() - t0 < timeout: + try: + with open(LOG, 'r', encoding='utf-8', errors='replace') as f: + lines = f.readlines() + for i in range(since, len(lines)): + if pattern in lines[i]: + return lines[i].strip(), i + 1 + except FileNotFoundError: + pass + time.sleep(0.2) + return None, 0 + +push_controls([(2, 0, 0, 5, NEXUS)]) +r, pos = wait_log("instrument loaded ch=5", 90) +print("[load]", r) + +push_controls([(4, 0, 0, 5, "nexus")]) +r, pos = wait_log("GUI attached", 30) +print("[open]", r) +hwnd = None +if r: + m = re.search(r'hwnd=([0-9A-Fa-f]+)', r) + if m: + hwnd = int(m.group(1), 16) +print("hwnd =", hex(hwnd) if hwnd else None) + +time.sleep(5) # let the editor paint + +def dump_tree(hwnd, depth=0): + out = [] + buf = ctypes.create_unicode_buffer(256) + user32.GetClassNameW(hwnd, buf, 256) + user32.GetWindowTextW(hwnd, buf, 256) + title = buf.value + user32.GetClassNameW(hwnd, buf, 256) + cls = buf.value + r = w.RECT() + user32.GetWindowRect(hwnd, ctypes.byref(r)) + vis = user32.IsWindowVisible(hwnd) + out.append(f"{' '*depth}h={hwnd:08X} cls='{cls}' title='{title}' rect=({r.left},{r.top},{r.right},{r.bottom}) vis={vis}") + children = [] + @ctypes.WINFUNCTYPE(w.BOOL, w.HWND, w.LPARAM) + def cb(child, lp): + children.append(child) + return True + user32.EnumChildWindows(hwnd, cb, 0) + for c in children: + out.extend(dump_tree(c, depth + 1)) + return out + +print("--- window tree ---") +if hwnd: + for ln in dump_tree(hwnd): + print(ln) + +# screenshot +if hwnd: + r = w.RECT() + user32.GetWindowRect(hwnd, ctypes.byref(r)) + wdt, hgt = r.right - r.left, r.bottom - r.top + ps = f""" +Add-Type -AssemblyName System.Drawing +$bmp = New-Object System.Drawing.Bitmap({wdt}, {hgt}) +$g = [System.Drawing.Graphics]::FromImage($bmp) +$g.CopyFromScreen({r.left}, {r.top}, 0, 0, $bmp.Size) +$bmp.Save('C:/Users/locpham/SonicForgeStudio/native_bridge/debug/probe_gui.png') +$g.Dispose(); $bmp.Dispose() +""" + subprocess.run(["powershell", "-NoProfile", "-Command", ps], check=False) + print("screenshot saved probe_gui.png") + +# cleanup +kernel32.UnmapViewOfFile.argtypes = [ctypes.c_void_p] +kernel32.CloseHandle.argtypes = [ctypes.c_void_p] +kernel32.UnmapViewOfFile(ptr) +kernel32.CloseHandle(hMap) +try: + proc.terminate() +except Exception: + pass +time.sleep(0.5) +print("done") diff --git a/native_bridge/debug/probe_gui2.py b/native_bridge/debug/probe_gui2.py new file mode 100644 index 0000000..316dfca --- /dev/null +++ b/native_bridge/debug/probe_gui2.py @@ -0,0 +1,135 @@ +# Probe Ample GUI render with current bridge exe. +import ctypes, ctypes.wintypes as w, struct, subprocess, sys, os, time, re + +sys.stdout.reconfigure(encoding='utf-8', errors='replace') + +SHM_NAME = "SonicForge_DAW_IPC_GUIPROBE2" +SIZE = 32768 +BRIDGE = sys.argv[1] if len(sys.argv) > 1 else r"C:\Users\locpham\SonicForgeStudio\native_bridge\build\Release\daw_vst_bridge.exe" +LOG = os.path.join(os.path.dirname(os.path.abspath(__file__)), "probe_gui2.log") +PLUGIN = sys.argv[2] if len(sys.argv) > 2 else r"C:\Program Files\Common Files\VST3\Ample Sound\ACDD.vst3" +PID_TAG = sys.argv[3] if len(sys.argv) > 3 else "ample" +SHOT = os.path.join(os.path.dirname(os.path.abspath(__file__)), f"probe_gui2_{PID_TAG}.png") + +user32 = ctypes.windll.user32 +kernel32 = ctypes.windll.kernel32 +kernel32.CreateFileMappingA.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.wintypes.DWORD, ctypes.wintypes.DWORD, ctypes.wintypes.DWORD, ctypes.c_char_p] +kernel32.CreateFileMappingA.restype = ctypes.c_void_p +kernel32.MapViewOfFile.argtypes = [ctypes.c_void_p, ctypes.wintypes.DWORD, ctypes.wintypes.DWORD, ctypes.wintypes.DWORD, ctypes.c_size_t] +kernel32.MapViewOfFile.restype = ctypes.c_void_p +INVALID_HANDLE_VALUE = ctypes.c_void_p(-1).value +PAGE_READWRITE = 0x04 +FILE_MAP_ALL_ACCESS = 0xF001F + +hMap = kernel32.CreateFileMappingA(INVALID_HANDLE_VALUE, None, PAGE_READWRITE, 0, SIZE, SHM_NAME.encode()) +ptr = kernel32.MapViewOfFile(hMap, FILE_MAP_ALL_ACCESS, 0, 0, 0) + +def w32(off, fmt, *args): + ctypes.memmove(ptr + off, struct.pack(fmt, *args), struct.calcsize(fmt)) + +def write_control(type_, arg0, arg1, ch, path): + for i in range(8): + off = 2836 + i * 1040 + t = ctypes.c_uint32.from_address(ptr + off).value + if t == 0: + w32(off, 'IIII', type_, arg0, arg1, ch) + if path: + b = path.encode('utf-8')[:1023] + b'\x00' + ctypes.memmove(ptr + off + 16, b, len(b)) + return i + raise RuntimeError("control queue full") + +def push_controls(events): + for i in range(8): + w32(2836 + i * 1040, 'I', 0) + for (type_, arg0, arg1, ch, path) in events: + write_control(type_, arg0, arg1, ch, path) + w32(11156, 'I', len(events)) + +proc = subprocess.Popen([BRIDGE, "--shm", SHM_NAME], + env={**os.environ, "SF_SHM_NAME": SHM_NAME, "SF_PARENT_PID": str(os.getpid()), + "SF_SAMPLE_RATE": "48000", "SF_BLOCK_SIZE": "256"}, + stdout=open(LOG, 'wb'), stderr=subprocess.STDOUT) +time.sleep(1.5) + +def wait_log(pattern, timeout, since=0): + t0 = time.time() + while time.time() - t0 < timeout: + try: + with open(LOG, 'r', encoding='utf-8', errors='replace') as f: + lines = f.readlines() + for i in range(since, len(lines)): + if pattern in lines[i]: + return lines[i].strip(), i + 1 + except FileNotFoundError: + pass + time.sleep(0.2) + return None, 0 + +push_controls([(2, 0, 0, 5, PLUGIN)]) +r, pos = wait_log("instrument loaded ch=5", 90) +print("[load]", r) + +push_controls([(4, 0, 0, 5, PID_TAG)]) +r, pos = wait_log("GUI attached", 40) +print("[open]", r) +hwnd = None +if r: + m = re.search(r'hwnd=([0-9A-Fa-f]+)', r) + if m: + hwnd = int(m.group(1), 16) +print("hwnd =", hex(hwnd) if hwnd else None) + +time.sleep(6) # let the editor paint + +def dump_tree(hwnd, depth=0): + out = [] + buf = ctypes.create_unicode_buffer(256) + user32.GetClassNameW(hwnd, buf, 256) + cls = buf.value + user32.GetWindowTextW(hwnd, buf, 256) + title = buf.value + r = w.RECT() + user32.GetWindowRect(hwnd, ctypes.byref(r)) + vis = user32.IsWindowVisible(hwnd) + out.append(f"{' '*depth}h={hwnd:08X} cls='{cls}' title='{title}' rect=({r.left},{r.top},{r.right},{r.bottom}) vis={vis}") + children = [] + @ctypes.WINFUNCTYPE(w.BOOL, w.HWND, w.LPARAM) + def cb(child, lp): + children.append(child) + return True + user32.EnumChildWindows(hwnd, cb, 0) + for c in children: + out.extend(dump_tree(c, depth + 1)) + return out + +print("--- window tree ---") +if hwnd: + for ln in dump_tree(hwnd): + print(ln) + +if hwnd: + r = w.RECT() + user32.GetWindowRect(hwnd, ctypes.byref(r)) + wdt, hgt = r.right - r.left, r.bottom - r.top + ps = f""" +Add-Type -AssemblyName System.Drawing +$bmp = New-Object System.Drawing.Bitmap({wdt}, {hgt}) +$g = [System.Drawing.Graphics]::FromImage($bmp) +$g.CopyFromScreen({r.left}, {r.top}, 0, 0, $bmp.Size) +$bmp.Save('{SHOT}') +$g.Dispose(); $bmp.Dispose() +""" + subprocess.run(["powershell", "-NoProfile", "-Command", ps], check=False) + print("screenshot saved", SHOT) + +kernel32.UnmapViewOfFile.argtypes = [ctypes.c_void_p] +kernel32.CloseHandle.argtypes = [ctypes.c_void_p] +kernel32.UnmapViewOfFile(ptr) +kernel32.CloseHandle(hMap) +try: + proc.terminate() +except Exception: + pass +time.sleep(0.5) +print("done") diff --git a/native_bridge/debug/probe_gui2_long.py b/native_bridge/debug/probe_gui2_long.py new file mode 100644 index 0000000..03b1291 --- /dev/null +++ b/native_bridge/debug/probe_gui2_long.py @@ -0,0 +1,135 @@ +# Probe Ample GUI render with current bridge exe. +import ctypes, ctypes.wintypes as w, struct, subprocess, sys, os, time, re + +sys.stdout.reconfigure(encoding='utf-8', errors='replace') + +SHM_NAME = "SonicForge_DAW_IPC_GUIPROBE2" +SIZE = 32768 +BRIDGE = sys.argv[1] if len(sys.argv) > 1 else r"C:\Users\locpham\SonicForgeStudio\native_bridge\build\Release\daw_vst_bridge.exe" +LOG = os.path.join(os.path.dirname(os.path.abspath(__file__)), "probe_gui2.log") +PLUGIN = sys.argv[2] if len(sys.argv) > 2 else r"C:\Program Files\Common Files\VST3\Ample Sound\ACDD.vst3" +PID_TAG = sys.argv[3] if len(sys.argv) > 3 else "ample" +SHOT = os.path.join(os.path.dirname(os.path.abspath(__file__)), f"probe_gui2_{PID_TAG}.png") + +user32 = ctypes.windll.user32 +kernel32 = ctypes.windll.kernel32 +kernel32.CreateFileMappingA.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.wintypes.DWORD, ctypes.wintypes.DWORD, ctypes.wintypes.DWORD, ctypes.c_char_p] +kernel32.CreateFileMappingA.restype = ctypes.c_void_p +kernel32.MapViewOfFile.argtypes = [ctypes.c_void_p, ctypes.wintypes.DWORD, ctypes.wintypes.DWORD, ctypes.wintypes.DWORD, ctypes.c_size_t] +kernel32.MapViewOfFile.restype = ctypes.c_void_p +INVALID_HANDLE_VALUE = ctypes.c_void_p(-1).value +PAGE_READWRITE = 0x04 +FILE_MAP_ALL_ACCESS = 0xF001F + +hMap = kernel32.CreateFileMappingA(INVALID_HANDLE_VALUE, None, PAGE_READWRITE, 0, SIZE, SHM_NAME.encode()) +ptr = kernel32.MapViewOfFile(hMap, FILE_MAP_ALL_ACCESS, 0, 0, 0) + +def w32(off, fmt, *args): + ctypes.memmove(ptr + off, struct.pack(fmt, *args), struct.calcsize(fmt)) + +def write_control(type_, arg0, arg1, ch, path): + for i in range(8): + off = 2836 + i * 1040 + t = ctypes.c_uint32.from_address(ptr + off).value + if t == 0: + w32(off, 'IIII', type_, arg0, arg1, ch) + if path: + b = path.encode('utf-8')[:1023] + b'\x00' + ctypes.memmove(ptr + off + 16, b, len(b)) + return i + raise RuntimeError("control queue full") + +def push_controls(events): + for i in range(8): + w32(2836 + i * 1040, 'I', 0) + for (type_, arg0, arg1, ch, path) in events: + write_control(type_, arg0, arg1, ch, path) + w32(11156, 'I', len(events)) + +proc = subprocess.Popen([BRIDGE, "--shm", SHM_NAME], + env={**os.environ, "SF_SHM_NAME": SHM_NAME, "SF_PARENT_PID": str(os.getpid()), + "SF_SAMPLE_RATE": "48000", "SF_BLOCK_SIZE": "256"}, + stdout=open(LOG, 'wb'), stderr=subprocess.STDOUT) +time.sleep(1.5) + +def wait_log(pattern, timeout, since=0): + t0 = time.time() + while time.time() - t0 < timeout: + try: + with open(LOG, 'r', encoding='utf-8', errors='replace') as f: + lines = f.readlines() + for i in range(since, len(lines)): + if pattern in lines[i]: + return lines[i].strip(), i + 1 + except FileNotFoundError: + pass + time.sleep(0.2) + return None, 0 + +push_controls([(2, 0, 0, 5, PLUGIN)]) +r, pos = wait_log("instrument loaded ch=5", 90) +print("[load]", r) + +push_controls([(4, 0, 0, 5, PID_TAG)]) +r, pos = wait_log("GUI attached", 40) +print("[open]", r) +hwnd = None +if r: + m = re.search(r'hwnd=([0-9A-Fa-f]+)', r) + if m: + hwnd = int(m.group(1), 16) +print("hwnd =", hex(hwnd) if hwnd else None) + +time.sleep(25) # let the editor paint + +def dump_tree(hwnd, depth=0): + out = [] + buf = ctypes.create_unicode_buffer(256) + user32.GetClassNameW(hwnd, buf, 256) + cls = buf.value + user32.GetWindowTextW(hwnd, buf, 256) + title = buf.value + r = w.RECT() + user32.GetWindowRect(hwnd, ctypes.byref(r)) + vis = user32.IsWindowVisible(hwnd) + out.append(f"{' '*depth}h={hwnd:08X} cls='{cls}' title='{title}' rect=({r.left},{r.top},{r.right},{r.bottom}) vis={vis}") + children = [] + @ctypes.WINFUNCTYPE(w.BOOL, w.HWND, w.LPARAM) + def cb(child, lp): + children.append(child) + return True + user32.EnumChildWindows(hwnd, cb, 0) + for c in children: + out.extend(dump_tree(c, depth + 1)) + return out + +print("--- window tree ---") +if hwnd: + for ln in dump_tree(hwnd): + print(ln) + +if hwnd: + r = w.RECT() + user32.GetWindowRect(hwnd, ctypes.byref(r)) + wdt, hgt = r.right - r.left, r.bottom - r.top + ps = f""" +Add-Type -AssemblyName System.Drawing +$bmp = New-Object System.Drawing.Bitmap({wdt}, {hgt}) +$g = [System.Drawing.Graphics]::FromImage($bmp) +$g.CopyFromScreen({r.left}, {r.top}, 0, 0, $bmp.Size) +$bmp.Save('{SHOT}') +$g.Dispose(); $bmp.Dispose() +""" + subprocess.run(["powershell", "-NoProfile", "-Command", ps], check=False) + print("screenshot saved", SHOT) + +kernel32.UnmapViewOfFile.argtypes = [ctypes.c_void_p] +kernel32.CloseHandle.argtypes = [ctypes.c_void_p] +kernel32.UnmapViewOfFile(ptr) +kernel32.CloseHandle(hMap) +try: + proc.terminate() +except Exception: + pass +time.sleep(0.5) +print("done") diff --git a/native_bridge/debug/shot_win.py b/native_bridge/debug/shot_win.py new file mode 100644 index 0000000..6b395b2 --- /dev/null +++ b/native_bridge/debug/shot_win.py @@ -0,0 +1,46 @@ +# Screenshot an existing window by handle (decimal), dump tree, pixel stats. +import ctypes, ctypes.wintypes as w, subprocess, sys, os +sys.stdout.reconfigure(encoding='utf-8', errors='replace') +user32 = ctypes.windll.user32 +hwnd = int(sys.argv[1], 16) if len(sys.argv) > 1 else 0 +out = sys.argv[2] if len(sys.argv) > 2 else "probe_win.png" + +def dump_tree(hwnd, depth=0): + out = [] + buf = ctypes.create_unicode_buffer(256) + user32.GetClassNameW(hwnd, buf, 256) + cls = buf.value + user32.GetWindowTextW(hwnd, buf, 256) + title = buf.value + r = w.RECT() + user32.GetWindowRect(hwnd, ctypes.byref(r)) + vis = user32.IsWindowVisible(hwnd) + out.append(f"{' '*depth}h={hwnd:08X} cls='{cls}' title='{title}' rect=({r.left},{r.top},{r.right},{r.bottom}) vis={vis}") + children = [] + @ctypes.WINFUNCTYPE(w.BOOL, w.HWND, w.LPARAM) + def cb(child, lp): + children.append(child) + return True + user32.EnumChildWindows(hwnd, cb, 0) + for c in children: + out.extend(dump_tree(c, depth + 1)) + return out + +print("--- window tree ---") +for ln in dump_tree(hwnd): + print(ln) + +r = w.RECT() +user32.GetWindowRect(hwnd, ctypes.byref(r)) +wdt, hgt = r.right - r.left, r.bottom - r.top +print("size", wdt, hgt) +ps = f""" +Add-Type -AssemblyName System.Drawing +$bmp = New-Object System.Drawing.Bitmap({wdt}, {hgt}) +$g = [System.Drawing.Graphics]::FromImage($bmp) +$g.CopyFromScreen({r.left}, {r.top}, 0, 0, $bmp.Size) +$bmp.Save('C:/Users/locpham/SonicForgeStudio/native_bridge/debug/{out}') +$g.Dispose(); $bmp.Dispose() +""" +subprocess.run(["powershell", "-NoProfile", "-Command", ps], check=False) +print("saved", out) diff --git a/native_bridge/debug/waitscan_probe.py b/native_bridge/debug/waitscan_probe.py new file mode 100644 index 0000000..729a37f --- /dev/null +++ b/native_bridge/debug/waitscan_probe.py @@ -0,0 +1,107 @@ +# waitscan for gui_probe bridge_exact11 (PASS case): dump threads while the +# probe is attached, to compare against the hung bridge. +import ctypes, ctypes.wintypes as w, struct, subprocess, sys, os, time +sys.stdout.reconfigure(encoding='utf-8', errors='replace') +LOG = os.path.join(os.path.dirname(os.path.abspath(__file__)), "waitscan_probe.log") +PROBE = r"C:\Users\locpham\SonicForgeStudio\native_bridge\build\Release\gui_probe.exe" +PATH = r"C:\Program Files\Common Files\VST3\Nexus.vst3" +def wait_log(pattern, timeout): + end = time.time() + timeout + while time.time() < end: + try: + with open(LOG, 'r', encoding='utf-8', errors='replace') as f: + if pattern in f.read(): + return True + except FileNotFoundError: + pass + time.sleep(0.2) + return False +proc = subprocess.Popen([PROBE, PATH, "bridge_exact11", "60"], + stdout=open(LOG, 'wb'), stderr=subprocess.STDOUT) +pid = proc.pid +print("pid", pid) +if not wait_log("isPlatformTypeSupported=0", 120): + print("TIMEOUT"); sys.exit(1) +time.sleep(3) +k32 = ctypes.windll.kernel32 +PROCESS_QUERY_INFORMATION = 0x0400 +PROCESS_VM_READ = 0x0010 +PROCESS_DUP_HANDLE = 0x0040 +TH32CS_SNAPTHREAD = 0x4 +THREAD_GET_CONTEXT = 0x0008 +THREAD_QUERY_INFORMATION = 0x0040 +THREAD_SUSPEND_RESUME = 0x0002 +k32.OpenProcess.argtypes = [w.DWORD, w.BOOL, w.DWORD] +k32.OpenProcess.restype = ctypes.c_void_p +hProc = k32.OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ | PROCESS_DUP_HANDLE, False, pid) +if not hProc: + print("OpenProcess failed", ctypes.get_last_error()); sys.exit(1) +psapi = ctypes.windll.psapi +class MODULEINFO(ctypes.Structure): + _fields_ = [("lpBaseOfDll", ctypes.c_void_p), ("SizeOfImage", w.DWORD), + ("pad", w.DWORD), ("EntryPoint", ctypes.c_void_p)] +psapi.EnumProcessModulesEx.argtypes = [ctypes.c_void_p, ctypes.POINTER(ctypes.c_void_p), w.DWORD, ctypes.POINTER(w.DWORD), w.DWORD] +psapi.GetModuleInformation.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.POINTER(MODULEINFO), w.DWORD] +psapi.GetModuleBaseNameA.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_char_p, w.DWORD] +nmods = w.DWORD(0) +psapi.EnumProcessModulesEx(hProc, None, 0, ctypes.byref(nmods), 3) +arr = (ctypes.c_void_p * (nmods.value // 8))() +psapi.EnumProcessModulesEx(hProc, arr, nmods.value, ctypes.byref(nmods), 3) +mods = [] +for m in arr: + if not m: continue + mi = MODULEINFO() + psapi.GetModuleInformation(hProc, m, ctypes.byref(mi), ctypes.sizeof(MODULEINFO)) + name = ctypes.create_string_buffer(260) + psapi.GetModuleBaseNameA(hProc, m, name, 260) + mods.append((mi.lpBaseOfDll, mi.SizeOfImage, name.value.decode('latin1'))) +def resolve(addr): + for base, size, name in mods: + if base <= addr < base + size: + return f"{name}+0x{addr-base:x}" + return f"0x{addr:x}" +class THREADENTRY32(ctypes.Structure): + _fields_ = [("dwSize", w.DWORD), ("cntUsage", w.DWORD), ("th32ThreadID", w.DWORD), + ("th32OwnerProcessID", w.DWORD), ("tpBasePri", w.LONG), + ("tpDeltaPri", w.LONG), ("dwFlags", w.DWORD)] +te = THREADENTRY32() +te.dwSize = ctypes.sizeof(THREADENTRY32) +snap = k32.CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0) +ok = k32.Thread32First(snap, ctypes.byref(te)) +threads = [] +while ok: + if te.th32OwnerProcessID == pid: + threads.append(te.th32ThreadID) + ok = k32.Thread32Next(snap, ctypes.byref(te)) +k32.CloseHandle(snap) +print(f"threads: {threads}") +class CONTEXT(ctypes.Structure): + _fields_ = [("raw", ctypes.c_ubyte * 0x4d0)] +CONTEXT_AMD64 = 0x00100000 +CONTEXT_FULL = CONTEXT_AMD64 | 0x3 +def ctx_regs(ctx): + b = bytes(ctx.raw) + u = lambda o: struct.unpack_from('