Summary
On 2026-08-15 around 23:18–23:35 local time, NegPy (packaged .app, v0.51.0) was open and being used to compare two loaded negatives. The whole machine — not just NegPy — stopped responding to any input and had to be recovered with a forced hardware restart (power button held). Confirmed by a macOS kernel panic log with panic string btn_rst (the panic macOS logs specifically for a user-forced restart), timestamped 9 seconds before the next NegPy session booted.
This issue is the evidence trail from reconstructing what happened, for whoever picks this up — I'm not equipped to dig further into the WebGPU/Metal side myself.
Environment
- NegPy 0.51.0, packaged
/Applications/NegPy.app (ad-hoc signed), not a dev run
- macOS 15.7.9 (24G830)
- MacBook Pro 18,1 (M1 Pro, 16 GB unified memory)
- GPU backend: Apple M1 Pro (IntegratedGPU) via Metal (
rendering.backend = "metal" in override.toml)
override.toml had [performance] cpu_parallel = true — see "Ruled out" below
Timeline (from ~/Documents/NegPy/negpy.log and macOS's unified log, cross-referenced by PID)
| Time |
Event |
| 23:18:23 |
New NegPy instance launches (app<...>:PID 8155). CPU parallel kernels: enabled (override=True, platform=darwin) |
| 23:18:32–23:18:36 |
Loads/renders two files fine: c2025-04-13-0002.tif and Kodak Gold 200_Frame028_R_linear.tiff (last successful first_render logged at 23:18:36) |
| 23:18:36 |
Last line NegPy's Python logger ever writes for this session. No exception, no traceback, nothing. |
| 23:18:36 – 23:33:55 |
negpy.log: total silence. macOS's runningboardd, however, keeps reporting PID 8155 as running-active the whole time — the process is alive at the OS level, just producing no application-level activity. Last such state update: 23:33:55.610. |
| 23:25:00 – 23:34:59 |
System-wide unified log stays busy the entire time — thousands of lines/minute from kernel, WiFi, Bluetooth, mDNSResponder, etc. (checked explicitly to rule out a full kernel lockup — see "Ruled out" below). |
| 23:35:14 |
Kernel panic, panic(cpu 1 caller ...): btn_rst — logged to /Library/Logs/DiagnosticReports/forceReset-full-2026-08-15-233514.0002.diag. This is macOS's panic string for a user-forced hardware restart (power button held), not an automatic fault/watchdog panic. |
| 23:35:18 |
New NegPy launch request (likely auto-relaunch via macOS's "reopen windows on login" / PersistentAppsSupport, seen in loginwindow logs) |
| 23:35:23 |
Fresh NegPy session boots, negpy.log resumes |
| 23:35:57 |
WARNING desktop.main: CPU parallel kernels disabled after an unclean shutdown — the app's own self-check (main.py:_offer_to_disable_cpu_parallel) correctly detected the prior session never called clean_shutdown, and the user accepted the dialog offering to turn cpu_parallel off |
No .ips/.crash report for NegPy itself exists anywhere in ~/Library/Logs/DiagnosticReports or /Library/Logs/DiagnosticReports for this window — only the system-level forceReset-full / ResetCounter pair from the forced restart.
What was loaded
The file most likely in view when things went silent: Kodak Gold 200_Frame028_R_linear.tiff, 216 MB, 7968×5320, 16-bit RGB, Adobe-deflate compressed, unusually small strips (RowsPerStrip=5, 1064 strips), source SONY ILCE-7RM2. It had loaded successfully many times before and after this incident (both in this same session and the recovery session right after), so the file by itself isn't reliably reproducing a hang — something about the sequence of actions around 23:18:36 is implicated, not just "open this file."
Ruled out: Numba CPU-parallel abort
negpy/kernel/system/parallel.py documents a known failure mode: Numba's workqueue threading layer hard-aborts the process with no Python exception and nothing written to the log if two threads enter a parallel=True kernel concurrently. override.toml had cpu_parallel = true forced on macOS, despite the code's own comment that this is unverified there ("Defaults to true except on macOS, where it defaults to false while crash reports are investigated"). The silent-log symptom initially looked like a strong match.
Ruled out because:
- That abort kills a single process almost immediately — it would not explain 15 minutes of the process still showing as
running-active to runningboardd, and would not require a hardware forced restart to recover from.
- The system-wide unified log (kernel, WiFi, Bluetooth, daemons) kept logging normally through the entire silent window, which is inconsistent with a process-fatal abort but consistent with "one process's screen output froze while the rest of the OS kept running."
I still turned cpu_parallel off in override.toml (real, separate, documented risk per the module's own comments), but it's very unlikely to be the cause of this incident.
Leading theory: GPU/display-pipeline wedge (unconfirmed)
The pattern — one process's window stops updating/responding while background kernel and daemon activity continues normally, ending in a state only a forced hardware restart recovers from, with no automatic GPU-watchdog panic ever firing on its own — matches a wedged Metal/AGX GPU command queue. macOS's WindowServer shares the GPU with every app's Metal/WebGPU submissions; if one app's GPU work jams that shared queue, the whole screen can lock solid (no cursor movement, Force Quit unreachable) while unrelated kernel subsystems (which don't touch the GPU) keep running fine underneath — exactly what the per-minute unified-log activity shows.
NegPy's GPUEngine (negpy/services/rendering/gpu_engine.py, WGSL via wgpu) was the last thing doing GPU work before the silence. Candidate contributing factors, none confirmed:
- The image is ~127 megapixels; the pipeline runs a dozen-plus GPU compute stages per render, each potentially allocating a full-resolution intermediate buffer.
- The machine has 16 GB unified memory, shared between CPU and GPU — a large enough working set across pipeline stages could hit real memory pressure, which is one known trigger class for Apple Silicon GPU driver wedges.
- The file's unusual strip layout (
RowsPerStrip=5, 1064 strips) is not obviously GPU-relevant (that's a CPU-side TIFF decode detail), but is flagged here in case it correlates with something on the decode → upload path.
No automatic GPU-watchdog panic preceded the forced one, so macOS's own driver-level recovery never engaged — meaning either the wedge was too deep for that mechanism, or the freeze wasn't purely a self-contained GPU-driver bug at all. I don't have a way to distinguish those from the outside.
Open questions for whoever investigates next
- Can this be reproduced deliberately with a large (>100 MP) source and heavy GPU-stage usage (e.g. defect repair / textural analysis, which the log shows was active —
textural=True — on this file shortly before other loads that session)?
- Does
GPUEngine's per-stage buffer lifecycle release intermediate textures between stages, or could VRAM/unified-memory usage accumulate across repeated loads of a huge file within one session?
- Is there any bound today on
max_texture_size for a file this large, or does "auto" let a 7968×5320 image go through uncapped?
- Worth adding a client-side watchdog (e.g. a heartbeat timer that detects the GPU submission queue not returning within N seconds) so a wedge surfaces as a recoverable error dialog instead of a silent full-system freeze?
Evidence
Everything above was reconstructed from:
~/Documents/NegPy/negpy.log (session logging, self-diagnostic messages)
~/Documents/NegPy/override.toml (the cpu_parallel = true setting)
- macOS unified log (
log show), filtered by PID and by system-wide per-minute activity
/Library/Logs/DiagnosticReports/forceReset-full-2026-08-15-233514.0002.diag and the paired ResetCounter-2026-08-15-233521.diag
tifffile inspection of the source TIFF
Summary
On 2026-08-15 around 23:18–23:35 local time, NegPy (packaged
.app, v0.51.0) was open and being used to compare two loaded negatives. The whole machine — not just NegPy — stopped responding to any input and had to be recovered with a forced hardware restart (power button held). Confirmed by a macOS kernel panic log with panic stringbtn_rst(the panic macOS logs specifically for a user-forced restart), timestamped 9 seconds before the next NegPy session booted.This issue is the evidence trail from reconstructing what happened, for whoever picks this up — I'm not equipped to dig further into the WebGPU/Metal side myself.
Environment
/Applications/NegPy.app(ad-hoc signed), not a dev runrendering.backend = "metal"in override.toml)override.tomlhad[performance] cpu_parallel = true— see "Ruled out" belowTimeline (from
~/Documents/NegPy/negpy.logand macOS's unified log, cross-referenced by PID)CPU parallel kernels: enabled (override=True, platform=darwin)c2025-04-13-0002.tifandKodak Gold 200_Frame028_R_linear.tiff(last successfulfirst_renderlogged at 23:18:36)negpy.log: total silence. macOS'srunningboardd, however, keeps reporting PID 8155 asrunning-activethe whole time — the process is alive at the OS level, just producing no application-level activity. Last such state update: 23:33:55.610.panic(cpu 1 caller ...): btn_rst— logged to/Library/Logs/DiagnosticReports/forceReset-full-2026-08-15-233514.0002.diag. This is macOS's panic string for a user-forced hardware restart (power button held), not an automatic fault/watchdog panic.PersistentAppsSupport, seen inloginwindowlogs)negpy.logresumesWARNING desktop.main: CPU parallel kernels disabled after an unclean shutdown— the app's own self-check (main.py:_offer_to_disable_cpu_parallel) correctly detected the prior session never calledclean_shutdown, and the user accepted the dialog offering to turncpu_paralleloffNo
.ips/.crashreport for NegPy itself exists anywhere in~/Library/Logs/DiagnosticReportsor/Library/Logs/DiagnosticReportsfor this window — only the system-levelforceReset-full/ResetCounterpair from the forced restart.What was loaded
The file most likely in view when things went silent:
Kodak Gold 200_Frame028_R_linear.tiff, 216 MB, 7968×5320, 16-bit RGB, Adobe-deflate compressed, unusually small strips (RowsPerStrip=5, 1064 strips), source SONY ILCE-7RM2. It had loaded successfully many times before and after this incident (both in this same session and the recovery session right after), so the file by itself isn't reliably reproducing a hang — something about the sequence of actions around 23:18:36 is implicated, not just "open this file."Ruled out: Numba CPU-parallel abort
negpy/kernel/system/parallel.pydocuments a known failure mode: Numba'sworkqueuethreading layer hard-aborts the process with no Python exception and nothing written to the log if two threads enter aparallel=Truekernel concurrently.override.tomlhadcpu_parallel = trueforced on macOS, despite the code's own comment that this is unverified there ("Defaults to true except on macOS, where it defaults to false while crash reports are investigated"). The silent-log symptom initially looked like a strong match.Ruled out because:
running-activetorunningboardd, and would not require a hardware forced restart to recover from.I still turned
cpu_paralleloff inoverride.toml(real, separate, documented risk per the module's own comments), but it's very unlikely to be the cause of this incident.Leading theory: GPU/display-pipeline wedge (unconfirmed)
The pattern — one process's window stops updating/responding while background kernel and daemon activity continues normally, ending in a state only a forced hardware restart recovers from, with no automatic GPU-watchdog panic ever firing on its own — matches a wedged Metal/AGX GPU command queue. macOS's WindowServer shares the GPU with every app's Metal/WebGPU submissions; if one app's GPU work jams that shared queue, the whole screen can lock solid (no cursor movement, Force Quit unreachable) while unrelated kernel subsystems (which don't touch the GPU) keep running fine underneath — exactly what the per-minute unified-log activity shows.
NegPy's
GPUEngine(negpy/services/rendering/gpu_engine.py, WGSL viawgpu) was the last thing doing GPU work before the silence. Candidate contributing factors, none confirmed:RowsPerStrip=5, 1064 strips) is not obviously GPU-relevant (that's a CPU-side TIFF decode detail), but is flagged here in case it correlates with something on the decode → upload path.No automatic GPU-watchdog panic preceded the forced one, so macOS's own driver-level recovery never engaged — meaning either the wedge was too deep for that mechanism, or the freeze wasn't purely a self-contained GPU-driver bug at all. I don't have a way to distinguish those from the outside.
Open questions for whoever investigates next
textural=True— on this file shortly before other loads that session)?GPUEngine's per-stage buffer lifecycle release intermediate textures between stages, or could VRAM/unified-memory usage accumulate across repeated loads of a huge file within one session?max_texture_sizefor a file this large, or does"auto"let a 7968×5320 image go through uncapped?Evidence
Everything above was reconstructed from:
~/Documents/NegPy/negpy.log(session logging, self-diagnostic messages)~/Documents/NegPy/override.toml(thecpu_parallel = truesetting)log show), filtered by PID and by system-wide per-minute activity/Library/Logs/DiagnosticReports/forceReset-full-2026-08-15-233514.0002.diagand the pairedResetCounter-2026-08-15-233521.diagtifffileinspection of the source TIFF