{"record":{"id":"cb2e0b7c481f1f1b","repo":"d2phap/ImageGlass","slug":"ige-native-codec-codecid-was-unloaded","errorCode":null,"errorMessage":"IGE: Native codec '{CodecId}' was unloaded.","messagePattern":"IGE: Native codec '(.+?)' was unloaded\\.","errorType":"exception","errorClass":"InvalidDataException","httpStatus":null,"severity":"error","filePath":"source/ImageGlass.Lib/Plugins/NativeCodecProxy.cs","lineNumber":380,"sourceCode":"        }\n        return meta;\n    }\n\n\n    /// <summary>\n    /// Invokes the plugin decode entry point and wraps the returned pixel buffer in a codec result.\n    /// </summary>\n    private CodecDecodeResult DecodeCore(PhotoMetadata metadata, int frameIndex, CancellationToken token)\n    {\n        if (_codecApi->DecodeStaticRaster == null || _codecApi->FreePixelBuffer == null)\n        {\n            throw new NotSupportedException($\"Native codec '{CodecId}' does not support static-raster decode.\");\n        }\n\n        // hold the plugin alive (see LoadMetadataCore)\n        if (!_plugin.LiveToken.TryEnter())\n        {\n            throw new InvalidDataException($\"IGE: Native codec '{CodecId}' was unloaded.\");\n        }\n\n        // Register cancellation before entering native code and keep track of buffer ownership.\n        var cancelHandle = PluginHostApiTable.RegisterCancellation(token);\n        var filePath = metadata.FilePath;\n        IGPixelBuffer buffer = default;\n        var bufferOwned = false;\n        var ownershipTransferred = false;\n\n        try\n        {\n            IGStatus status;\n            try\n            {\n                // Ask the plugin to decode the requested frame into its ABI buffer struct.\n                fixed (char* pPath = filePath)\n                {\n                    var pathRef = new IGStringRef { Data = pPath, Length = filePath.Length };","sourceCodeStart":362,"sourceCodeEnd":398,"githubUrl":"https://github.com/d2phap/ImageGlass/blob/4a3c4feceffc5a8bb5e56ba836509634aaae47a9/source/ImageGlass.Lib/Plugins/NativeCodecProxy.cs#L362-L398","documentation":"Thrown by NativeCodecProxy.DecodeCore when _plugin.LiveToken.TryEnter() returns false. The LiveToken is the plugin-host's keepalive gate: it returns false only when the plugin is mid-teardown/unload, so a decode call would dereference memory the host is about to free. This is a defensive guard against use-after-unload, not a user error.","triggerScenarios":"Produced when a decode is requested for a codec whose plugin is being unloaded (user disabled it, the host is shutting down, or a hot-reload swapped the DLL). The TryEnter call at NativeCodecProxy.cs:380 returns false because the plugin's LiveToken has been released/invalidated.","commonSituations":"User disabled a plugin while a thumbnail/decode was in flight; the app is closing and tearing down plugins before all background work finishes; a plugin hot-reload; rapid switching between formats backed by different plugins with outstanding decode work.","solutions":["Treat this as a transient cancellation: catch InvalidDataException for this specific message and skip/retry the load when the plugin reloads.","Avoid disabling plugins while the gallery/viewer is actively decoding from them; let outstanding work drain first.","If it happens on shutdown, ensure the host drains or cancels in-flight decodes before releasing plugin LiveTokens.","Make sure your decode caller passes a CancellationToken that is linked to plugin-unload so it cancels cleanly instead of throwing."],"exampleFix":"// before\nif (!_plugin.LiveToken.TryEnter())\n    throw new InvalidDataException($\"IGE: Native codec '{CodecId}' was unloaded.\");\n\n// caller side\ncatch (InvalidDataException ex) when (ex.Message.Contains(\"was unloaded\"))\n{\n    // plugin gone mid-decode — cancel this load, do not surface as a hard failure\n    token.ThrowIfCancellationRequested();\n    return CodecDecodeResult.Failed($\"Plugin {CodecId} unloaded\");\n}","handlingStrategy":"try-catch","validationCode":"// Before decode, confirm the plugin is still alive.\nif (!proxy.Plugin.LiveToken.IsAlive) return CodecDecodeResult.Cancelled();\n// (Best-effort; the live token can still release between this check and TryEnter.)","typeGuard":"static bool IsCodecLive(NativeCodecProxy proxy) => proxy.Plugin.LiveToken.TryEnter() switch { true => ReleaseAndReturnTrue(proxy), false => false };\n// Note: TryEnter must be balanced by a release; prefer letting DecodeCore do the gate.","tryCatchPattern":"try { return proxy.DecodeStatic(metadata, frame, token); }\ncatch (InvalidDataException ex) when (ex.Message.Contains(\"was unloaded\"))\n{ token.ThrowIfCancellationRequested(); return CodecDecodeResult.Cancelled(); }","preventionTips":["Do not disable plugins while decodes are in flight; let outstanding work drain or cancel first.","Link the decode CancellationToken to plugin-unload so it cancels before reaching the unloaded check.","On shutdown, drain or cancel in-flight decodes before releasing plugin LiveTokens.","Treat the unloaded error as transient — do not surface it as a permanent file failure."],"tags":["plugin","codec","native","lifecycle","concurrency"],"backgroundTag":null,"analyzedSha":"4a3c4feceffc5a8bb5e56ba836509634aaae47a9","analyzedAt":"2026-08-13T16:58:15.523Z","schemaVersion":2},"datasetVersion":"2026-08-13T19:17:28.613Z"}