chore: commit G3.2+G3.3 deploy binaries + debug scripts; ignore debug run artifacts
This commit is contained in:
@@ -47,3 +47,10 @@ native_bridge/sfizz-src/
|
|||||||
install/daw_engine/
|
install/daw_engine/
|
||||||
app/static/js/_diag.txt
|
app/static/js/_diag.txt
|
||||||
src-tauri/resources/daw_engine" && cp -r*
|
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
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@@ -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)")
|
||||||
@@ -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)")
|
||||||
@@ -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)")
|
||||||
@@ -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<Steinberg::Vst::IComponentHandler*>(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<Steinberg::IPlugFrame*>(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}")
|
||||||
@@ -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<std::mutex> 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<std::mutex> 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<std::mutex> 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<std::mutex> 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<std::mutex> 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<Vst3HostState*>(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<Vst3HostState*>(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<Vst3HostState*>(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<Vst3HostState*>(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<Vst3HostState*>(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")
|
||||||
@@ -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")
|
||||||
@@ -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<uint32_t, std::unique_ptr<ChannelWorker>>* g_workers = nullptr;""",
|
||||||
|
"""static std::map<uint32_t, std::unique_ptr<ChannelWorker>>* g_workers = nullptr;
|
||||||
|
static std::mutex g_tidMutex;
|
||||||
|
static std::map<DWORD, ChannelWorker*> 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<std::mutex> 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<std::mutex> lk(g_tidMutex);
|
||||||
|
g_tidToWorker.erase(GetCurrentThreadId());
|
||||||
|
}
|
||||||
|
OleUninitialize();
|
||||||
|
#endif
|
||||||
|
});""",
|
||||||
|
"unregister tid")
|
||||||
|
|
||||||
|
# 6. post_and_wait
|
||||||
|
rep(
|
||||||
|
""" void post(std::function<void()> job) {
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lk(mu_);
|
||||||
|
jobs_.push_back(std::move(job));
|
||||||
|
}
|
||||||
|
cv_.notify_all();
|
||||||
|
}
|
||||||
|
|
||||||
|
private:""",
|
||||||
|
""" void post(std::function<void()> job) {
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> 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<void()> job) {
|
||||||
|
std::mutex doneMx;
|
||||||
|
std::condition_variable doneCv;
|
||||||
|
bool done = false;
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lk(mu_);
|
||||||
|
jobs_.push_back([&]() {
|
||||||
|
job();
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> dk(doneMx);
|
||||||
|
done = true;
|
||||||
|
}
|
||||||
|
doneCv.notify_all();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
cv_.notify_all();
|
||||||
|
std::unique_lock<std::mutex> 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<std::mutex> lk(g_attachMutex);
|
||||||
|
return g_closeInFlight[ch];
|
||||||
|
}""",
|
||||||
|
"""static bool close_in_flight(uint32_t ch) {
|
||||||
|
std::lock_guard<std::mutex> 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<std::mutex> 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<std::mutex> 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<std::mutex> 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<std::mutex> 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")
|
||||||
@@ -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")
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
# Drive REAL app bridge SHM (SonicForge_DAW_IPC_<daw_pid>): 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")
|
||||||
@@ -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")
|
||||||
@@ -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")
|
||||||
@@ -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")
|
||||||
@@ -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")
|
||||||
@@ -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")
|
||||||
@@ -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)
|
||||||
@@ -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('<Q', b, o)[0]
|
||||||
|
return dict(rip=u(0xF8), rsp=u(0x98), rcx=u(0x80), rdx=u(0x88), r8=u(0xB8), r9=u(0xC0))
|
||||||
|
k32.GetThreadContext.argtypes = [ctypes.c_void_p, ctypes.POINTER(CONTEXT)]
|
||||||
|
k32.GetThreadContext.restype = w.BOOL
|
||||||
|
k32.SuspendThread.argtypes = [ctypes.c_void_p]
|
||||||
|
k32.ResumeThread.argtypes = [ctypes.c_void_p]
|
||||||
|
k32.OpenThread.argtypes = [w.DWORD, w.BOOL, w.DWORD]
|
||||||
|
k32.OpenThread.restype = ctypes.c_void_p
|
||||||
|
k32.CloseHandle.argtypes = [ctypes.c_void_p]
|
||||||
|
for tid in threads:
|
||||||
|
ht = k32.OpenThread(THREAD_GET_CONTEXT | THREAD_QUERY_INFORMATION | THREAD_SUSPEND_RESUME, False, tid)
|
||||||
|
if not ht:
|
||||||
|
print(f"tid {tid}: open fail"); continue
|
||||||
|
k32.SuspendThread(ht)
|
||||||
|
ctx = CONTEXT()
|
||||||
|
struct.pack_into('<I', ctx.raw, 0x30, CONTEXT_FULL)
|
||||||
|
if k32.GetThreadContext(ht, ctypes.byref(ctx)):
|
||||||
|
r = ctx_regs(ctx)
|
||||||
|
print(f"tid {tid}: rip={resolve(r['rip'])} rcx=0x{r['rcx']:x} r9=0x{r['r9']:x}")
|
||||||
|
else:
|
||||||
|
print(f"tid {tid}: GetThreadContext fail")
|
||||||
|
k32.ResumeThread(ht)
|
||||||
|
k32.CloseHandle(ht)
|
||||||
|
proc.terminate()
|
||||||
Reference in New Issue
Block a user