fix: fall back to unconverted textures when the ktx native decoder cannot load - #9801
fix: fall back to unconverted textures when the ktx native decoder cannot load#9801alejandro-jimenez-dcl wants to merge 4 commits into
Conversation
…nnot load ## Problem `DllNotFoundException: Unable to load DLL 'ktx_unity'` from `KtxNativeInstance.CertifySupportedPlatform` whenever a texture arrives from the media-converter as `image/ktx2` on a machine where the native plugin (or a transitive DLL dependency) cannot be opened — AV quarantine, missing VC++ runtime, corrupted install. Sentry UNITY-EXPLORER-NQS: 235 events past week, ongoing. Every KTX2 texture load on that machine fails for the whole session, session after session, with no fallback. ## Root cause The KTX2-conversion pipeline is enabled based solely on the remote `ktx2-conversion` feature flag, with no verification that the native decoder is actually loadable on this machine and no fallback when it is not — a missing local capability probe for a remotely-toggled native-dependent feature. The DLL load failure itself is environmental and unfixable client-side; the permanent per-machine breakage is not. ## Fix (~60 production LOC) - New `KtxNativeSupport`: cached static capability probe (`KtxTexture.Open` on garbage bytes — returns `ErrorCode.LoadingFailed` on a healthy lib, throws on a broken one); catches `DllNotFoundException | EntryPointNotFoundException | NotSupportedException`; one-time warning when unsupported; internal probe-override test seam + `MarkUnsupported()`. - `RequestHub.SetKTXEnabled`: `enabled && KtxNativeSupport.IsSupported` — probe runs lazily, only when the flag is on. - `GetTextureWebRequest`: same gate in `Initialize`; a mid-session `DllNotFoundException` in `ExecuteKtxAsync` trips `MarkUnsupported()` and rethrows — subsequent requests self-heal onto the direct-URL path. - `NFTShapePlugin`: same gate, keeping the NFT content-info fetch consistent. - `PerformanceBenchmark` pins the probe true per-test (review finding: the real probe's debug-only error log would trip UTF's unexpected-error check in the perf lane). Result: affected machines degrade gracefully to unconverted originals (more bandwidth, zero user-visible breakage) instead of failing outright. ## Test New EditMode `KtxNativeSupportShould` (5 tests): routing to the original URL when the probe reports unavailable (the repro), routing to the converter when available (over-disable guard), probe caching, DllNotFound-in-probe handling, and the `MarkUnsupported` runtime trip. ## Validation Windows Unity 6000.4 EditMode lane at the pin: RED FAIL 1/5 as intended (request routed to the converter URL at pin despite the failing probe; 4 by-construction guards passed) / GREEN PASS 5/5. Fixes #7832 Related: #5150 (autoclosed original of this signature, still occurring), #7634 (closed duplicate), #9064 (context: registry-resolved com.unity.cloud.ktx 3.6.3) Includes inspection-warning cleanup in all touched files.
decentraland-bot
left a comment
There was a problem hiding this comment.
PR Review: fix: fall back to unconverted textures when the ktx native decoder cannot load
STEP 2 — Root-cause check
PASS. The PR correctly identifies and fixes the root cause: a missing local capability probe for a remotely-toggled native-dependent feature. The DLL load failure itself is environmental and unfixable client-side; the missing graceful degradation is the real defect. The fix does not swallow exceptions, null-check symptoms, or disable checks — it adds a proper capability probe and wires it into the three relevant decision points.
STEP 3 — Design & integration
PASS. Owner search completed.
New unit: KtxNativeSupport — a public static class with a cached bool? probe.
What it manages: Whether the current machine can load the ktx_unity native plugin — a machine-wide environmental fact, not an entity/scene/connection lifecycle.
Who already creates/destroys this capability: Nobody. There is no existing lifecycle owner for native-DLL availability. The closest pattern in the codebase is FeatureFlagsConfiguration (also static, also uses Initialize/Reset), which manages a similar global-singleton concern.
Could the logic live at an existing creation/destruction point? No. The probe is consumed from three independent call sites (RequestHub.SetKTXEnabled, GetTextureWebRequest.Initialize, NFTShapePlugin.InjectToWorld) across different assemblies. A static utility is the natural home for a cross-cutting, session-scoped, machine-level capability check. DI would add ceremony for something that has no dependencies, no lifecycle, and is genuinely process-global.
Guard layering is justified, not over-engineered:
SetKTXEnabled: prevents the flag from being true when the probe fails at boot — keeps the probe lazy (only runs when the feature flag is on).Initialize: belt-and-suspenders per-request check — necessary becauseMarkUnsupported()can flipisSupportedtofalseat runtime without updating theRequestHub.ktxEnabledfield (which was captured atSetKTXEnabledtime).ExecuteKtxAsynccatch: last-resort runtime trip for the edge case where the probe passes but a laterOpen()call hits a DLL load failure.
Each guard covers a distinct failure window. Removing the Initialize check would leave a gap between a runtime MarkUnsupported() and the session end.
Teardown/consumption trace:
NativeArray<byte>inProbe():using-scoped, released on all paths. ✅KtxTextureinProbe():Dispose()called afterOpen()returns; correctly skipped ifOpen()throws (no native state exists). ✅probeOverride+isSupportedstatic fields:Reset()clears both in[TearDown]of every test. ✅KtxTextureinExecuteKtxAsync():Dispose()in existingfinallyblock (unchanged by this PR). ✅
STEP 4 — Member audit
| Member | Visibility | Consumers | Verdict |
|---|---|---|---|
IsSupported (property) |
public | RequestHub.SetKTXEnabled, GetTextureWebRequest.Initialize, NFTShapePlugin.InjectToWorld, 6 test methods |
Multi-consumer, appropriate |
MarkUnsupported() |
internal | ExecuteKtxAsync catch block, StayUnsupported_AfterRuntimeTrip test |
2 consumers — correct scope |
Reset() |
internal | 4 test [SetUp]/[TearDown] methods |
Test-only cleanup — correct |
probeOverride |
internal field | 7 test assignments | Test seam — correct scope |
No single-use intermediates, no absent-≠-false conflation, no redundant guards.
STEP 5 — Line-level review
Findings are posted as inline comments below. Summary:
| # | Severity | File | Issue |
|---|---|---|---|
| 1 | P2 | GetTextureWebRequest.cs:111 |
catch (DllNotFoundException) misses EntryPointNotFoundException — incomplete runtime self-healing |
| 2 | P2 | NFTShapePlugin.cs:79 |
Stale isKtxEnabled snapshot — LoadNFTTypeSystem won't self-heal after runtime MarkUnsupported() |
| 3 | P2 | KtxNativeSupport.cs:65-66 |
Global unityLogger.logEnabled = false is broad — consider more surgical suppression |
STEP 6 — Complexity
COMPLEX — Introduces a new cross-cutting static utility, modifies the web request pipeline's texture routing logic, and touches feature-flag gating across multiple subsystems (RequestHub, GetTextureWebRequest, NFTShapePlugin, PerformanceBenchmark).
STEP 7 — QA assessment
QA_REQUIRED: YES — Changes affect runtime texture loading behavior. Users on machines with broken ktx_unity installations will experience different behavior (graceful fallback to unconverted originals instead of repeated failures).
STEP 8 — Non-blocking warnings
None. Main.unity is not modified.
CI Status
- ✅ PlayMode tests pass
- ❌ EditMode tests failed — infrastructure stall (watchdog triggered,
xml files: 0, total: 0, passed: 0, failed: 0). No tests ran; this is a CI environment issue, not a code regression. - ⏳ Builds (windows64, macOS) and Lint still in progress.
Security review
No security issues found. The probe uses a locally-allocated 16-byte buffer (not attacker-controlled), internal test seams are properly scoped (only DynamicProxyGenAssembly2 and DCL.Editor via InternalsVisibleTo), and the design fails closed on any error.
REVIEW_RESULT: PASS ✅
COMPLEXITY: COMPLEX
COMPLEXITY_REASON: Introduces a cross-cutting native capability probe wired into the texture request pipeline, feature-flag gating, NFT content routing, and performance test infrastructure.
QA_REQUIRED: YES
Reviewed by Jarvis 🤖 · Requested by decentraland-bot via GitHub
🚦 CI StatusBuild skipped — no changes detected under No C# files changed — lint ratchet skipped.
|
|
🔍 Jarvis reviewed this PR and found no blocking issues, but assessed it as complex — human DEV review is still required before merging. |
|
PR #9801, run #32259906606 Builds: Windows change, Windows baseline, macOS change, macOS baseline How to read this table
Intel Core i5
Exception breakdown
Apple M1
|
…ent the log-pause tradeoff
The package only logs the expected probe error under #if DEBUG (editor and development builds), so muting Unity logging for it is unnecessary; the ReportHub warning remains the only unsupported signal.
alejandro-jimenez-dcl
left a comment
There was a problem hiding this comment.
approve
Problem
DllNotFoundException: Unable to load DLL 'ktx_unity'fromKtxNativeInstance.CertifySupportedPlatformwhenever a texture arrives from themedia-converter as
image/ktx2on a machine where the native plugin (or a transitive DLLdependency) cannot be opened - AV quarantine, missing VC++ runtime, corrupted install.
Sentry UNITY-EXPLORER-NQS: 235 events past week, ongoing. Every KTX2 texture load on that
machine fails for the whole session, session after session, with no fallback.
Root cause
The KTX2-conversion pipeline is enabled based solely on the remote
ktx2-conversionfeature flag, with no verification that the native decoder is actually loadable on this
machine and no fallback when it is not - a missing local capability probe for a
remotely-toggled native-dependent feature. The DLL load failure itself is environmental and
unfixable client-side; the permanent per-machine breakage is not.
Fix (~60 production LOC)
KtxNativeSupport: cached static capability probe (KtxTexture.Openon garbagebytes - returns
ErrorCode.LoadingFailedon a healthy lib, throws on a broken one);catches
DllNotFoundException | EntryPointNotFoundException | NotSupportedException;one-time warning when unsupported; internal probe-override test seam +
MarkUnsupported().RequestHub.SetKTXEnabled:enabled && KtxNativeSupport.IsSupported- probe runs lazily,only when the flag is on.
GetTextureWebRequest: same gate inInitialize; a mid-sessionDllNotFoundExceptionin
ExecuteKtxAsynctripsMarkUnsupported()and rethrows - subsequent requestsself-heal onto the direct-URL path.
NFTShapePlugin: same gate, keeping the NFT content-info fetch consistent.PerformanceBenchmarkpins the probe true per-test (review finding: the real probe'sdebug-only error log would trip UTF's unexpected-error check in the perf lane).
Result: affected machines degrade gracefully to unconverted originals (more bandwidth, zero
user-visible breakage) instead of failing outright.
Test
New EditMode
KtxNativeSupportShould(5 tests): routing to the original URL when the probereports unavailable (the repro), routing to the converter when available (over-disable
guard), probe caching, DllNotFound-in-probe handling, and the
MarkUnsupportedruntime trip.Validation
Windows Unity 6000.4 EditMode lane at the pin: RED FAIL 1/5 as intended (request routed to
the converter URL at pin despite the failing probe; 4 by-construction guards passed) /
GREEN PASS 5/5.
Fixes #7832
Related: #5150 (autoclosed original of this signature, still occurring), #7634 (closed
duplicate), #9064 (context: registry-resolved com.unity.cloud.ktx 3.6.3)
Fixes #5150