diff options
| author | Jonathan "Geenz" Goodman <geenz@geenzo.com> | 2026-05-19 23:11:16 -0400 |
|---|---|---|
| committer | Jonathan "Geenz" Goodman <geenz@geenzo.com> | 2026-05-19 23:11:16 -0400 |
| commit | 1ee317bcb7a5988658612a69a7bd52fb1a9e37c7 (patch) | |
| tree | 303722647134f138f4f5b76fba361727edc01030 | |
| parent | c4bc64c7515c19bfb1c4d619a8e5d235954ba8a4 (diff) | |
Bring back the pre-aug 2024 byte estimation for j2c streams, and add a configurable high water mark for VRAM.
| -rw-r--r-- | indra/llimage/llimagej2c.cpp | 54 | ||||
| -rw-r--r-- | indra/llimage/llimagej2c.h | 6 | ||||
| -rw-r--r-- | indra/llimagej2coj/llimagej2coj.cpp | 29 | ||||
| -rw-r--r-- | indra/llimagej2coj/llimagej2coj.h | 5 | ||||
| -rw-r--r-- | indra/newview/app_settings/settings.xml | 11 | ||||
| -rw-r--r-- | indra/newview/llviewertexture.cpp | 29 |
6 files changed, 102 insertions, 32 deletions
diff --git a/indra/llimage/llimagej2c.cpp b/indra/llimage/llimagej2c.cpp index 5a941dc958..512b370bae 100644 --- a/indra/llimage/llimagej2c.cpp +++ b/indra/llimage/llimagej2c.cpp @@ -265,40 +265,42 @@ S32 LLImageJ2C::calcHeaderSizeJ2C() return FIRST_PACKET_SIZE; // Hack. just needs to be >= actual header size... } -//static -S32 LLImageJ2C::calcDataSizeJ2C(S32 w, S32 h, S32 comp, S32 discard_level, F32 rate) +// Lean pyramid-walk byte estimator suited to packet-by-packet decoders (KDU). +// Starts at one max-block, walks resolutions by doubling area, sums each +// layer's compressed-bytes contribution. +// Reference: https://wiki.lindenlab.com/wiki/THX1138_KDU_Improvements#Byte_Range_Study +S32 LLImageJ2CImpl::estimateDataSize(S32 w, S32 h, S32 comp, S32 discard_level, F32 rate) const { - // Note: This provides an estimation for the first to last quality layer of a given discard level - // This is however an efficient approximation, as the true discard level boundary would be - // in general too big for fast fetching. - // For details about the equation used here, see https://wiki.lindenlab.com/wiki/THX1138_KDU_Improvements#Byte_Range_Study - - // Estimate the number of layers. This is consistent with what's done for j2c encoding in LLImageJ2CKDU::encodeImpl(). - constexpr S32 precision = 8; // assumed bitrate per component channel, might change in future for HDR support - constexpr S32 max_components = 4; // assumed the file has four components; three color and alpha - // Use MAX_IMAGE_SIZE_DEFAULT (currently 2048) if either dimension is unknown (zero) + constexpr S32 precision = 8; + constexpr S32 max_components = 4; S32 width = (w > 0) ? w : 2048; S32 height = (h > 0) ? h : 2048; - S32 max_dimension = llmax(width, height); // Find largest dimension - S32 block_area = MAX_BLOCK_SIZE * MAX_BLOCK_SIZE; // Calculated initial block area from established max block size (currently 64) - S32 max_layers = (S32)llmax(llround(log2f((float)max_dimension) - log2f((float)MAX_BLOCK_SIZE)), 4); // Find number of powers of two between extents and block size to a minimum of 4 - block_area *= llmax(max_layers, 1); // Adjust initial block area by max number of layers - S32 totalbytes = (S32) (MIN_LAYER_SIZE * max_components * precision); // Start estimation with a minimum reasonable size - S32 block_layers = 0; - while (block_layers <= max_layers) // Walk the layers + const S32 surface = width * height; + S32 nb_layers = 1; + S32 s = MAX_BLOCK_SIZE * MAX_BLOCK_SIZE; + S32 totalbytes = (S32)(s * max_components * precision * rate); + while (surface > s) { - if (block_layers <= (5 - discard_level)) // Walk backwards from discard 5 to required discard layer. - totalbytes += (S32) (block_area * max_components * precision * rate); // Add each block layer reduced by assumed compression rate - block_layers++; // Move to next layer - block_area *= 4; // Increase block area by power of four + if (nb_layers <= (5 - discard_level)) + totalbytes += (S32)(s * max_components * precision * rate); + nb_layers++; + s *= 4; } - - totalbytes /= 8; // to bytes - totalbytes += calcHeaderSizeJ2C(); // header - + totalbytes /= 8; + totalbytes += LLImageJ2C::calcHeaderSizeJ2C(); return totalbytes; } +//static +S32 LLImageJ2C::calcDataSizeJ2C(S32 w, S32 h, S32 comp, S32 discard_level, F32 rate) +{ + // Dispatch to the linked impl so OpenJPEG (block-aligned, needs + // over-allocation) and KDU (packet-aligned, lean) each return what + // their decoder actually needs. + static std::unique_ptr<LLImageJ2CImpl> s_estimator(fallbackCreateLLImageJ2CImpl()); + return s_estimator->estimateDataSize(w, h, comp, discard_level, rate); +} + S32 LLImageJ2C::calcHeaderSize() { return calcHeaderSizeJ2C(); diff --git a/indra/llimage/llimagej2c.h b/indra/llimage/llimagej2c.h index 19744a7f87..5173eed33b 100644 --- a/indra/llimage/llimagej2c.h +++ b/indra/llimage/llimagej2c.h @@ -106,6 +106,12 @@ class LLImageJ2CImpl { public: virtual ~LLImageJ2CImpl(); + + // Estimate the byte size of a J2C codestream sufficient to decode the + // given discard level. KDU streams packet-by-packet and uses the lean + // pyramid-walk default. OpenJPEG needs over-allocation to land on + // code-block boundaries even with strict mode off, so it overrides. + virtual S32 estimateDataSize(S32 w, S32 h, S32 comp, S32 discard_level, F32 rate) const; protected: // Find out the image size and number of channels. // Return value: diff --git a/indra/llimagej2coj/llimagej2coj.cpp b/indra/llimagej2coj/llimagej2coj.cpp index 7cfadb889d..488c07e1c9 100644 --- a/indra/llimagej2coj/llimagej2coj.cpp +++ b/indra/llimagej2coj/llimagej2coj.cpp @@ -919,3 +919,32 @@ bool LLImageJ2COJ::getMetadata(LLImageJ2C &base) base.setSize(width, height, components); return true; } + + +// OpenJPEG-tuned byte estimator. Conservative pyramid walk that accounts for +// OJ's whole-code-block decode behavior (even with strict mode off). Larger +// images get a per-resolution multiplier so the byte range lands inside the +// last needed code-block boundary. +S32 LLImageJ2COJ::estimateDataSize(S32 w, S32 h, S32 comp, S32 discard_level, F32 rate) const +{ + constexpr S32 precision = 8; + constexpr S32 max_components = 4; + S32 width = (w > 0) ? w : 2048; + S32 height = (h > 0) ? h : 2048; + S32 max_dimension = llmax(width, height); + S32 block_area = MAX_BLOCK_SIZE * MAX_BLOCK_SIZE; + S32 max_layers = (S32)llmax(llround(log2f((float)max_dimension) - log2f((float)MAX_BLOCK_SIZE)), 4); + block_area *= llmax(max_layers, 1); + S32 totalbytes = (S32)(MIN_LAYER_SIZE * max_components * precision); + S32 block_layers = 0; + while (block_layers <= max_layers) + { + if (block_layers <= (5 - discard_level)) + totalbytes += (S32)(block_area * max_components * precision * rate); + block_layers++; + block_area *= 4; + } + totalbytes /= 8; + totalbytes += LLImageJ2C::calcHeaderSizeJ2C(); + return totalbytes; +} diff --git a/indra/llimagej2coj/llimagej2coj.h b/indra/llimagej2coj/llimagej2coj.h index da49597302..6c6b379e5f 100644 --- a/indra/llimagej2coj/llimagej2coj.h +++ b/indra/llimagej2coj/llimagej2coj.h @@ -44,6 +44,11 @@ protected: virtual bool initDecode(LLImageJ2C &base, LLImageRaw &raw_image, int discard_level = -1, int* region = NULL); virtual bool initEncode(LLImageJ2C &base, LLImageRaw &raw_image, int blocks_size = -1, int precincts_size = -1, int levels = 0); virtual std::string getEngineInfo() const; +public: + // OpenJPEG decodes whole code-blocks even with strict mode off, so the + // lean packet-walk under-allocates and clips quality. Keep the older + // conservative pyramid-with-multiplier estimate here. + virtual S32 estimateDataSize(S32 w, S32 h, S32 comp, S32 discard_level, F32 rate) const override; }; #endif diff --git a/indra/newview/app_settings/settings.xml b/indra/newview/app_settings/settings.xml index d33082b449..0b700c28ae 100644 --- a/indra/newview/app_settings/settings.xml +++ b/indra/newview/app_settings/settings.xml @@ -11871,6 +11871,17 @@ <key>Value</key> <integer>0</integer> </map> + <key>TextureMemoryHighWaterMark</key> + <map> + <key>Comment</key> + <string>Fraction of budget (0..1) at which the pressure controller bypasses smoothing and slams to cap. Last-ditch min-discard also creeps without waiting for mult_progress.</string> + <key>Persist</key> + <integer>1</integer> + <key>Type</key> + <string>F32</string> + <key>Value</key> + <real>0.8</real> + </map> <key>TextureMemoryPressureBackoffStart</key> <map> <key>Comment</key> diff --git a/indra/newview/llviewertexture.cpp b/indra/newview/llviewertexture.cpp index e84481bade..82f4cc8341 100644 --- a/indra/newview/llviewertexture.cpp +++ b/indra/newview/llviewertexture.cpp @@ -602,10 +602,25 @@ void LLViewerTexture::updateClass() - (F32)pending_bytes_decrease * BYTES_TO_USED_UNITS; F32 predicted_over = predicted_used / llmax(backoff_target, 1.f); + // High water mark: when used crosses budget * high_water, skip the + // smoothed convergence and slam the controller into hard-cap state. + // Recovers the historical 90% behavior - immediate aggressive + // response instead of waiting for the lerp to chase the target. + static LLCachedControl<F32> high_water(gSavedSettings, "TextureMemoryHighWaterMark", 0.8f); + bool above_high_water = used >= budget * llclamp((F32)high_water, 0.5f, 1.f); + F32 target_mult = llclamp(powf(llmax(predicted_over, 1.f), llmax((F32)prediction_gain, 0.0001f)), 1.f, cap); - // ~63% convergence in 1/smoothing_rate seconds (default 0.25s). - F32 alpha = 1.f - expf(-llmax(dt, 0.f) * llmax((F32)smoothing_rate, 0.f)); - sMemoryPressureMultiplier += (target_mult - sMemoryPressureMultiplier) * alpha; + if (above_high_water) + { + target_mult = cap; + sMemoryPressureMultiplier = cap; + } + else + { + // ~63% convergence in 1/smoothing_rate seconds (default 0.25s). + F32 alpha = 1.f - expf(-llmax(dt, 0.f) * llmax((F32)smoothing_rate, 0.f)); + sMemoryPressureMultiplier += (target_mult - sMemoryPressureMultiplier) * alpha; + } sMemoryPressureMultiplier = llclamp(sMemoryPressureMultiplier, 1.f, cap); F32 progress = getMemoryPressureProgress(); @@ -615,12 +630,14 @@ void LLViewerTexture::updateClass() static LLCachedControl<F32> ld_ramp(gSavedSettings, "TextureLastDitchRampRate", 0.5f); static LLCachedControl<F32> ld_decay(gSavedSettings, "TextureLastDitchDecayRate", 0.5f); static LLCachedControl<F32> ld_max(gSavedSettings, "TextureLastDitchMinDiscardMax", 13.f); - bool mult_saturated = progress >= llclampf((F32)ld_engage); - if (mult_saturated && predicted_over > 1.f) + // Above the high water mark, last-ditch creeps regardless of + // mult_progress: by definition we are out of normal headroom. + bool engage = above_high_water || progress >= llclampf((F32)ld_engage); + if (engage && predicted_over > 1.f) { sLastDitchMinDiscard += llmax((F32)ld_ramp, 0.f) * dt; } - else if (predicted_over < 1.f) + else if (!above_high_water && predicted_over < 1.f) { sLastDitchMinDiscard -= llmax((F32)ld_decay, 0.f) * dt; } |
