Async image pipeline with SKBitmap filters, thread safety, and performance fixes - #36
Merged
Conversation
Replace synchronous on-demand image resolution with a background worker thread that processes a priority-sorted work queue. Images are queued automatically during pack load via ImageReference.OnImageReferenceCreated callback, and UI bindings use path-through ResolvedImage property for automatic updates when images resolve asynchronously. Key changes: - ImageReferenceService: background Thread with priority queue (SortedDictionary + ConcurrentDictionary cache), ManualResetEventSlim for sleep/wake, priority boosting for UI-requested images - ImageReference: added ResolvedImage property and static OnImageReferenceCreated callback for auto-queuing during pack load - XAML bindings: migrated from IValueConverter to path-through binding (e.g. Icon.ResolvedImage) across 5 control files (15 bindings) - UI gets 1x1 transparent placeholder immediately, replaced dynamically when background resolution completes via Dispatcher.UIThread.Post Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add --no-async-images command-line flag that disables the background image worker and forces synchronous on-demand resolution (pre-refactor behavior) for A/B comparison testing of layout issues. - ApplicationSettings: parse --no-async-images flag - ImageReferenceService: SyncMode property gates worker thread startup; sync mode hooks OnImageReferenceCreated to resolve immediately; ResolveImageReference sets ResolvedImage in sync mode for path-through bindings; QueueCount/CacheCount properties exposed - MCP: get_image_queue_status tool reports syncMode, queueCount, cacheCount for runtime diagnostics Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Sized placeholders: Read image dimensions from PNG/BMP/JPEG headers at ImageReference creation time and store as SourceWidth/SourceHeight. The image service creates correctly-sized transparent placeholder bitmaps (cached by dimension) instead of the universal 1×1 placeholder, so Avalonia's layout system measures controls at the right size before real images resolve. FilterImageReference inherits dimensions from its source; LayeredImageReference inherits from its first layer. Source image cache: ConcreteImageReferenceResolver now caches decoded base images (before filter application) in a WeakReference dictionary keyed by pack-relative path. Multiple ConcreteImageReferences pointing to the same source file with different filters share the decoded base image, avoiding redundant zip extraction and decoding. Cache is cleared on pack unload. Layout invalidation: When the background worker drains its queue, it posts a layout invalidation to the main window at Background priority so any controls that were measured with placeholder dimensions get a chance to re-measure with final image sizes. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add FallbackValue={x:Null} to all multi-segment .ResolvedImage
bindings (e.g. Icon.ResolvedImage, Image.ResolvedImage). When the
intermediate property (Icon, Image, Thumbnail, etc.) is null on some
data items, Avalonia logs a binding error because it can't traverse
the path. FallbackValue silently returns null instead, matching the
previous converter-based behavior.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ef-counting, and missing images Image pipeline overhaul: - Replace per-pixel GetPixel/SetPixel filter loops with Skia color matrix operations (grayscale, dim, brightness) for ~10-50x speedup - Eliminate PNG encode/decode round-trips between filter steps by keeping images as SKBitmap through the entire chain - Add unsafe pixel buffer access for color-key and alpha mask computation - Fix ApplyColorMatrix: clear destination bitmap and use SKBlendMode.Src to avoid corrupting semi-transparent pixels via SrcOver on uninitialized data - Fix BT601 grayscale weights: original divided by 8 (>>3), not 6 - Add strong-reference source image cache in ConcreteImageReferenceResolver Thread safety and missing images: - Add lock around ZipPackageSource.Open/Files for concurrent archive access - Convert sAlphaMasks/sPngCache to ConcurrentDictionary for background worker writes - Fix duplicate ImageReference instances: add instance tracking (mPendingInstances) so PostResolvedImage fans out ResolvedImage to ALL objects sharing an equality key - Add FromExternalURI NotifyCreated call so HTTP images enter the resolution pipeline - Fix InputMaskingImage: default to click-through when alpha mask unavailable SuspendRefresh ref-counting: - Replace boolean SuspendRefresh with int ref-count (Push/Pop pattern) - SuspendRefreshScope increments on open, decrements on close; refresh fires at zero - Add underflow detection with error logging and debug assert - Convert all 6 callsites from direct toggle to scope/Push/Pop - Fix click handler UI spike: LuaItem.OnLeftClick previously set SuspendRefresh=false inside outer LeftClickCommand scope, releasing suspend before transaction callbacks fired (~97 full RefreshAccessibility walks per click → 1 batched refresh) Package Manager game images: - Bind game banner to Game.Image.ResolvedImage instead of one-shot converter - Add Game property to PackageGroup for live binding support - Bridge HTTP image downloads with ResolvedImage in OnHttpImageLoaded Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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.
Summary
Complete overhaul of the image resolution pipeline for performance, correctness, and thread safety.
Image processing performance
GetPixel/SetPixelloops with Skia native color matrix operations (grayscale, dim, brightness, saturation) for ~10-50x speedupSKBitmapthrough the entire filter chain instead of encoding/decoding PNG between each stepuint*pointer access for color-key and alpha mask computationSKBitmaps inConcreteImageReferenceResolverto avoid redundant decodingAsync background resolution
mPendingInstances) fans outResolvedImageto allImageReferenceobjects sharing an equality keyThread safety
ZipPackageSource: Add lock aroundOpen()/Filesfor concurrent archive access from UI and background threadssAlphaMasks/sPngCache: Convert toConcurrentDictionaryfor safe background worker writesInputMaskingImage: Default to click-through when alpha mask is unavailable (prevents input blocking during async load)SuspendRefresh ref-counting
SuspendRefreshwith ref-countedPush/Poppattern viaSuspendRefreshScopeLuaItem.OnLeftClickpreviously setSuspendRefresh = falseinside the outerLeftClickCommandscope, releasing the suspend before transaction callbacks fired — causing ~97 fullRefreshAccessibilitywalks per click instead of 1 batched refreshDebug.Failfor dangling suspend bugsFilter correctness
ApplyColorMatrix: clear destination bitmap and useSKBlendMode.Srcto prevent corruption of semi-transparent pixels viaSrcOveron uninitialized buffer dataBT601grayscale weights: original code divided by 8 (>>3), not 6Package Manager game images
Game.Image.ResolvedImagefor live updates instead of one-shot converterResolvedImageproperty inOnHttpImageLoadedFiles changed
IconUtility.csImageReferenceService.csConcreteImageReferenceResolver.csFilterImageReferenceResolver.csLayeredImageReferenceResolver.csLocationDatabase.csLuaItem.csTrackableItemControl.axaml.csItemDatabase.csZipPackageSource.csInputMaskingImage.csImageReference.csApplicationModel.csPackageManagerWindow.axamlTest plan
🤖 Generated with Claude Code