Bugfixes and performance - #21
Merged
Merged
Conversation
UI thread wrote refPixels, refMask, required, crop as separate
fields; capture loop read them unsynchronized and could observe
a new refPixels paired with an old refMask of different size,
causing index-out-of-range or wrong similarity scores.
- Pack the four fields into an immutable CaptureState record and
swap it via Interlocked.CompareExchange so the pair is always
consistent.
- _highest max-update via CAS; resets via Interlocked.Exchange.
- _active read/written with Volatile to ensure capture loop sees
source swaps.
- Length-mismatch guard before L2NormComparer.Compare as a
belt-and-suspenders against any state slipping through.
Two splits referencing different external files with the same filename overwrote each other in the target folder, and an external copy could clobber an internal mask of the same name when processed first. Pre-pass reserves filenames already locked in by inside-folder splits; ReserveUniqueName disambiguates copies with ' (n)' suffixes.
ApplyScaledAlphaChannel looked up alphaPath in the supplied cache but never wrote back, so every call re-decoded the PNG. Cache populated on miss under a lock so concurrent Task.Run callers can't double-decode and leak. PresetEditor's _previewMaskCache started empty and was the main victim — every threshold/preview tweak hit the disk. MainWindow's pre-populated cache continues to hit immediately; on-demand misses now cache too.
WriteAllTextAsync truncates the destination before writing, so a crash mid-write left preset.json empty or partial — next load deserialized to null and the preset silently vanished from the UI. Write to a sibling .tmp and rename via File.Move(overwrite: true); NTFS makes the rename atomic on the same volume. Stale-savestate cleanup also wrapped in try/catch per file: the json is already committed at that point, and a locked leftover file shouldn't fail the whole save.
Process 8 pixels per iteration: 8 mask bytes broadcast 4× with PSHUFB, |diff| via paired SubtractSaturate+OR, PMADDWD to square and pair-sum into int32, widened to int64 each chunk to avoid overflow at the 9600 chunks of a 320×240 frame. Alpha bytes explicitly zeroed via a constant mask so the BGR-only sum matches the scalar formula even where ref/live alphas diverge in masked-out regions. Scalar fallback retained for non-AVX2 hosts and the n%8 tail.
Per-frame byte[] allocation produced ~18 MB/s of Gen0 churn at 60 fps (320×240×4). Two pre-allocated buffers cover the single- in-flight invariant enforced by _uiPostPending: capture writes to _frameBuffers[_writeIndex], flips on successful post so the UI handler keeps exclusive access until it clears _uiPostPending, and a dropped post leaves _writeIndex unchanged so the same buffer is reused next iteration.
Three SKColor[] round-trips per call (input.Pixels, scaled.Pixels, output.Pixels assignment) allocated ~24 MB of managed arrays for a 1080p mask. Replaced with an unsafe byte loop over GetPixels() IntPtrs and a native blit only if the decoder picked a non-BGRA color type. Hard alpha threshold (==255) preserved exactly so the edge behavior of the linear-resized mask matches the previous SKColor comparison.
- MainWindow: AppContext.BaseDirectory replaces Process.MainModule
(AOT-safe under self-contained NativeAOT publish), drop redundant
Dispatcher.Post wrap in CheckSavePossible (all callers UI-thread),
add OnClosed override that disposes input/thumbnail/mask caches
+ _maskedImage + _previewBitmap, narrow temp-cleanup catches to
IOException/UnauthorizedAccessException.
- TestOutputWindow: LoadPrefs/SavePrefs go async + Encoding.UTF8 +
typed catches; sync File I/O on the UI thread is gone.
- PresetEditor: rename prompt no longer overwrites an explicit
OK/Enter result with the Closed handler's null (Commit() guard).
Empty/whitespace split names rejected in ValidateSplitName and
UpdateSaveButtonState so Save can't write a malformed preset.
- ImportSplitsDialog: thumbnail load catches IOException/
UnauthorizedAccessException/ArgumentException only, logs via
Utils.LogError instead of swallowing silently.
- WebcamCapture: ErrorReported event surfaces OpenCV throws to UI
instead of breaking out of the read loop with no signal.
- PresetService: explicit Encoding.UTF8 on every File.*Async call.
- MessageBox: ShowDialog Task observed via ContinueWith; a
synchronous throw now faults tcs.Task instead of leaving callers
awaiting a never-completing TCS.
- AppJsonContext: register Split, List<Split>, List<PremadeSplit>
explicitly so trimming/AOT can't drop reflection metadata that
source-gen relies on.
- Program: AppDomain.UnhandledException + TaskScheduler
.UnobservedTaskException write a timestamped crash log to
%LOCALAPPDATA%\AutoMask\crashes (XDG equivalent on Linux). Self-
contained AOT publishes have no console and no debugger, so
fatal exceptions previously produced a silent exit.
- Cut 'what' / banner comments where the code already reads itself
(cache-dispose loop, 'Commit the switch', 'Reference inputs
captured from MainWindow', BitBltCaptureBase plumbing summary).
Keep WHY comments — race rationales, AOT contracts, SIMD
algorithm steps, Win32 quirks, Avalonia binding priority etc.
- Rename short identifiers in tight loops: _cap → _videoCapture,
dst/s/d → targetPixels/sourceRow/targetRow, sw → stopwatch, cur
→ currentSimilarity, high → highestSimilarity, src → source.
- Narrow two bare catches: WebcamCapture.StopAsync to
ObjectDisposedException; PresetEditor.UpdateOutputPreview to
IOException/UnauthorizedAccessException/ArgumentException with
Utils.LogError instead of swallowing.
- BitBltCaptureBase.TryGrabFrame releases srcDc via try/finally so
a throw between Acquire and Release (EnsureBuffers GDI exhaustion,
BitBlt fault, OOM during SKBitmap alloc) can't strand the DC.
EnsureBuffers returns bool with explicit rollback of partial GDI
allocations on each early-out path. A GetDIBits == 0 now disposes
the staging _frame so a transient failure doesn't leave a half-
populated buffer that a size-matched call would skip re-allocating.
- App.OnFrameworkInitializationCompleted registers
Dispatcher.UIThread.UnhandledException: logs to disk, shows a
MessageBox on the main window, sets Handled = true. Catches every
async-void event handler's faulted continuation since they all run
on the UI dispatcher, so an exception in a button click no longer
bubbles to AppDomain.UnhandledException and terminates the process.
- Crash-log writer hoisted from Program.cs to Utils.LogCrashToDisk
so all three unhandled-exception hooks (AppDomain, TaskScheduler,
Dispatcher) share one path.
- CaptureController.SetSourceAsync: extracted ShutdownSourceAsync
with a 5s Task.WaitAsync timeout per call so a hung StopAsync
(OpenCV native deadlock during webcam release, GDI driver stall)
cannot block the swap forever and prevent the user from switching
to a working source. Errors and timeouts surface via ErrorReported
with the source's DisplayName instead of being silently swallowed.
- CaptureController.CropAndScaleNearest cropped path: single
allocation now. SKCanvas.DrawImage with srcRect/dstRect performs
crop and scale in one operation, replacing the canvasBitmap +
Resize pair. Out-of-bounds areas stay black via pre-Clear,
matching the previous behavior.
- PresetService.LoadPresetsAsync and LoadPremadeSplitsAsync: read
every preset.json / splits.json in parallel via Task.WhenAll over
per-file Load helpers. Per-file try/catch keeps WhenAll from
fail-fasting; the failure tuple feeds the same LoadFailure list
the UI surfaces.
Windows taskbar 'Close all windows' fires Close on every top-level
window in parallel, which exposed three issues:
- TestOutputWindow.Closing set _isClosing only after the save dialog
completed, so a second Closing event raced past the gate and
spawned a duplicate dialog. _closingStarted is set synchronously
before any await, and every re-entry now sets e.Cancel = true so
Avalonia can't tear the window down while the prompt is still up.
- MainWindow.OnClosing now defers when a live-tester is still
shutting down and re-issues the parent close from the child's
Closed event. Without this, MainWindow closing immediately ended
the desktop lifetime and killed the dialog mid-flight.
- CaptureController.DisposeAsync is idempotent via Interlocked
guard. The double-close path was awaiting the disposed _swapLock
and surfacing 'Cannot access a disposed object' through the new
Dispatcher.UnhandledException hook.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.