diff --git a/doc/ARCHITECTURE.md b/doc/ARCHITECTURE.md index c7226d0d7e..fac4866a2d 100644 --- a/doc/ARCHITECTURE.md +++ b/doc/ARCHITECTURE.md @@ -17,7 +17,7 @@ **Rendering layer:** - **llrender** - OpenGL abstraction: shaders (`LLGLSLShader`, `LLShaderMgr`), vertex buffers (`LLVertexBuffer`), render targets (`LLRenderTarget`), textures, compute (`ALGPUBuffer`, `ALImageSlot`), queries (`ALQueryPool`), font rendering (FreeType + HarfBuzz). The main context lays one fence a frame, straight after the swap (`ALFrameFences`, ended by `LLRender::endFrame()`), and what reuses memory the GPU reads or writes -- the query pool's batches, the uniform rings' segments, `ALGPUReadback`'s slots and `ALGPUStream`'s regions -- reuses it once the frame it last used it in is shown finished, laying no fence of its own; the vertex buffer pool hands out a store freed in a frame not yet shown finished under a new store of the same name, so that its upload waits for nothing. The main thread's texture uploads read their rows from a persistently mapped ring bound as the pixel unpack buffer (`ALStagingRing`, `LLRender::stagingRing()`), its segments reused the same way, rather than from client memory, which NVIDIA's threaded optimization copies on the application's thread and Mesa's glthread uploads from only once its own thread has caught up; an upload the ring has no room for, larger than a segment or with the next segment still the GPU's, goes from client memory as before, and so does every upload with `AlchemyRenderStagedUploads` off. A texture read back for the loaded callbacks that need its pixels is read into a buffer bound for packing and collected a few frames' ends later (`ALTextureReadback`, `LLRender::textureReadback()`, `LLImageGL::requestReadBack`), rather than into client memory, which waits for the GPU to finish everything queued ahead of the read; so is a sculpt map whose texture on screen holds more than the copy the sculpts build from (`LLViewerFetchedTexture::requestSculptReadback`), the texture kept until the read lands and the read saved as the copy, every better copy saved marking the sculpts to build again. The copy is kept within the sculpts' budget at the map's own proportions, so a long, thin map is copied whole and needs no read, and a volume builds again at the level it has from an image with more pixels than it was built from (`LLVolume::getSculptPixels`). A store the ring replaces when it changes size is deleted only once the frame it was replaced in has finished. Where the frames are stamped, the ring also lays a fence of its own as it leaves a segment or replaces a store and waits for it as well, and so does each texture readback before its pixels are taken or its buffer goes, since GL says a mapped store is done with only through a sync object and a stamp is a query. Its rows are tight, so it stages nothing on a context before `LLRender::init` has set the unpack alignment to 1 (`LLRender::unpacksTightRows`): the fonts `LLViewerWindow::initFonts` makes rasterize their ASCII glyphs before it. The fenced rings keep the frames of traffic the pacing depth (`AlchemyRenderMaxFramesAhead`, 3) needs for a segment to come round finished (`fenced_ring_slack`), and the query pool closes a frame's batch before the frame's fence and opens the next only after the fences' end, so that the pacing wait, not the pool, waits for the batch it reuses. It reads a batch no sooner than three frames after its own (`ALQueryPool::MIN_RESULT_AGE`; `AlchemyRenderQueryResultAge` sets it from 1 to 7, the pool keeping eight batches so that a batch that old is read before its names are begun again), however soon the frame finished: at the pacing depth that is the frame the pacing wait has just seen finished, so neither the CPU nor the GPU waits on a result, and an occlusion group, which issues no query while its last is pending, has one in flight however dense the scene. The fences are polled there, once a frame, on every driver, and nowhere else but a wait: a threaded driver answers a poll of a fence not yet signalled only once its own thread has caught up with it -- Mesa's glthread has at the swap, and NVIDIA's threaded optimization, whose thread a GPU-bound frame holds in the previous present, makes a poll before the cull wait for most of a frame. Measured on NVIDIA's threaded optimization in the viewer's own context, laying a fence, polling one that has signalled, a stamp and an upload from client memory or an unpack buffer queue at once; a poll of a fence not yet signalled, `glGetSynciv`, `glGetQueryObject` for a result (not for its availability), `glGen*`, any `glMapBufferRange` and every `glGet*` wait for the driver's thread. A test binary's context the driver does not thread at all, so no GL test sees it. Where the GPU writes the query results (NVIDIA's driver) the frames' ends lay no fence (`AlchemyRenderFrameStamps`): each has the GPU stamp the frame, a timestamp counted once every command before it has finished, written into a mapped store, which the poll and the pacing wait read with no GL call, since a fence's poll there, even at the swap, waits for the driver's thread to run everything before it and leaves it idle while the next frame begins. The texture uploads the LLImageGL thread hands over are polled at the frames' ends, stamped or fenced, only once that thread has seen each one's fence signalled on its own context and marked it (`LLImageGLThread::watchUpload`), which the main thread reads with no call, so that its poll answers at once; the thread waits on its fences only while nothing else is queued for it, and no context's GPU writes into a store another context maps - **llwindow** - Platform window management and input (native Win32/Cocoa, and SDL3 on Linux, where GL is EGL on Wayland and X11 alike -- the viewer includes no X11 or GLX header). Depends on llrender -- **alrender** - Render infrastructure between llrender and newview that holds no viewer state: the render resources' plan (`ALRenderResourcePlan`, made from the values in `ALRenderResourceInputs`), the pool that lends the frame's short-lived targets by description (`ALTransientTargetPool`, with its `ALTransientTargets` scopes, and `ALOnDemandTarget` for draws that take one only when one of them draws), what the programs are built from (`ALShaderInputs`, made from `ALShaderSettings`), the changes that ask for both again (`ALRenderInputRequests`) and what one application of them does (`ALRenderApplication`), the far plane (`ALFarPlane`), the water's field (`ALWaterField`), wave heights (`ALWaterHeightBake`, over pocketfft) and stencil bits (`ALWaterStencil`), the depth pyramid (`ALDepthPyramid`), the light map's blur where the session computes (`ALLightMapBlur`), the depth under the HUD (`ALHUDDepth`), snapshot cutouts (`ALCutoutImage`), whether a spot light's shadow map is drawn again (`ALSpotShadowCache`) and which automatic reflection probes a placed one swallows (`ALProbeEclipse`). Depends on llrender, llimage, llmath and llcommon; it reads no settings, and the viewer hands it the settings and the scene as values +- **alrender** - Render infrastructure between llrender and newview that holds no viewer state: the render resources' plan (`ALRenderResourcePlan`, made from the values in `ALRenderResourceInputs`), the pool that lends the frame's short-lived targets by description (`ALTransientTargetPool`, with its `ALTransientTargets` scopes, and `ALOnDemandTarget` for draws that take one only when one of them draws), what the programs are built from (`ALShaderInputs`, made from `ALShaderSettings`), the changes that ask for both again (`ALRenderInputRequests`) and what one application of them does (`ALRenderApplication`), the far plane (`ALFarPlane`), the water's field (`ALWaterField`), wave heights (`ALWaterHeightBake`, over pocketfft) and stencil bits (`ALWaterStencil`), the blended pass's stencil bit (`ALBlendStencil`), the depth pyramid (`ALDepthPyramid`), the light map's blur where the session computes (`ALLightMapBlur`), the depth under the HUD (`ALHUDDepth`), snapshot cutouts (`ALCutoutImage`), whether a spot light's shadow map is drawn again (`ALSpotShadowCache`) and which automatic reflection probes a placed one swallows (`ALProbeEclipse`). Depends on llrender, llimage, llmath and llcommon; it reads no settings, and the viewer hands it the settings and the scene as values **Content model layer:** - **llinventory** - Inventory item/folder data model @@ -132,7 +132,7 @@ Depth of field runs **before** bloom and before the tonemapper, on linear HDR. G - `mLensDirtMap`, the generated lens dirt plate, lives outside the packs too — an `LLRenderTarget` allocated the first frame the effect is enabled and released when it is switched off. **Transient targets** (`ALTransientTargetPool`, `altransienttargetpool.h`, in `LLPipeline::mTransientTargets`): what lives for one phase of a frame is not a target of its own, but lent to the pass or effect that needs it for as long as an `ALTransientTargets` scope holds it: -- each pack's light map, and SSAO's: the main view's blurred light map where the session computes, or the scratch the fragment passes blur through in the light map's description; and, once the lighting has let the light map go, the exposure meter's map of the water, in the light map's description too; +- each pack's light map, and SSAO's: the main view's blurred light map where the session computes, or the scratch the fragment passes blur through in the light map's description; and, once the lighting has let the light map go, the exposure meter's map of the water, in the light map's description too, and, under incident metering, its mask of where blended surfaces shade (R8, sharing the G-buffer's depth and stencil); - the water's copy of the screen, its colour alone (R11F_G11F_B10F under HDR, no depth: nothing reads its alpha), and the scene's depth copy, each pack's own and only where the water refracts (`ALRenderResourcePlan::refractsWater`: with `RenderTransparentWater`, in the main view and a mirror's face, never in a probe's face, which clips the scene at the water and draws it with the opaque programs), the frame's one copy of the depth: the surface writes its own over the scene's while it reads beneath it, where the haze passes, writing none, read the scene's in place past a texture barrier (`LLPipeline::hazeDepth`, `LLRender::textureBarrier`), the water and the forward pools having written it in the same binding. A context with no barrier (macOS's 4.1 may have none) plans the depth copy for them too in every pack that draws the water, refracting or not, and each haze pass copies the depth into it between letting the screen go and binding it again; - the HDR bloom pyramid (`LLPipeline::Bloom`, as deep as `bloomMipCount`), the post chain's ping and, once a pass after `colorCorrect` draws into it, its pong, SMAA's edges and blend weights (`SMAATargets`), and the legacy non-HDR glow's three; - depth of field's defocused scene and its blur, and the cross-screen filter's two scratch buffers and sum; @@ -141,7 +141,7 @@ Depth of field runs **before** bloom and before the tonemapper, on linear HDR. G A free target goes only to an acquisition of exactly its description and shared depth, so acquiring never builds storage or attaches a depth again. Targets of one description used at different times share memory without anyone saying so: the water's copy of the screen and bloom level 0 are one target. Nothing is fenced: GL runs commands in order, so a target one pass releases goes to the next. Contents are undefined on acquire, so every user clears or writes the whole target, or reads only what it wrote: the water's copy of the screen covers the field's screen rectangle (`ALWaterField::screenRect` of the surface's slab) and the ripples' reach past it, the whole screen from below the surface. The pool frees a target unused for `TRIM_FRAMES` (120) frames, or for as many as an acquisition of it asked, though none before the user is logged in, so a login keeps what the reconcile readied for the world's first frame, and everything when the main view's size changes. The previews' and bakes' targets ask 1800 (30 s at 60 fps), and so does whatever their draws take for themselves (`ALTransientTargetPool::Keep`), as a GLTF material preview takes the probe pack's post target and bloom: a preview can go seconds between draws, and building its targets again costs the frame that draws next about half a millisecond of CPU and over a millisecond before the GPU has them. The water's copies ask 1800 too, so a view that looks away from the water for a few seconds does not leave the frame that sees it again to build them. Under debug GL (`gDebugGL`, from `RenderDebugGLSession` or `DebugSession`) a release poisons the target (magenta, depth 0.5) and each acquisition names it for debuggers. Its bytes, hits and misses are plotted beside "render target MB", and the frame after each reconcile logs which resources each target served (`RenderResources`). **Post-processing chain:** -- **Auto-exposure:** Progressive histogram (`gLuminanceProgram`, `gExposureProgram`) with history fade. This frame's exposure and last frame's swap their storage, so the history is never copied. The meter weighs as sky what the world leaves at the far plane and the water drawn over it: the water writes the depth and never the G-buffer, so `generateLuminance` copies the G-buffer's normals under the water's stencil into a map the meter reads, where the G-buffer's clear marks the water over nothing +- **Auto-exposure:** Progressive histogram (`gLuminanceProgram`, `gExposureProgram`) with history fade. This frame's exposure and last frame's swap their storage, so the history is never copied. The meter weighs as sky what the world leaves at the far plane and the water drawn over it: the water writes the depth and never the G-buffer, so `generateLuminance` copies the G-buffer's normals under the water's stencil into a map the meter reads, where the G-buffer's clear marks the water over nothing. The meter measures the light that falls on surfaces, not the light they reflect (incident metering, `AlchemyExposureIncidentStrength`, default 1, in the main view only; at 0, in any other pack such as the material preview's, or where the mask could not be made, it is the reflective meter, with no albedo bound or read, and `generateExposure` takes the key from the strength the grid was metered at): `luminanceF.glsl` divides each patch's mean non-sky luminance by its mean albedo, the G-buffer's first attachment, to the power of the strength, so a white wall in dim light is not exposed as middle grey, and the key moves with it, `(1 - s) * log2(0.18)` in stops, so a scene whose surfaces average middle grey meters the same at any strength. The divisor is floored and the grid's alpha is the share of the patch's exposure its albedo has earned (`exposureHistogramV.glsl` weighs by it), so near black surfaces say nothing about the light on them; the floor and the ramp are `AlchemyExposureIncidentTuning`, passed with the strength as `meter_albedo_params`. Blended surfaces are not in the G-buffer, so the forward alpha pass (`LLDrawPoolAlpha::forwardRender`, not its depth-only pass) marks `ALBlendStencil::BLENDED_BIT` where it shades, `generateLuminance` copies that into a mask in the main view when the strength is above 0, the meter reads it as `meterBlendMap`, and a marked pixel is taken to reflect the key, which meters it as the reflective meter does. Emissive surfaces are metered as the lights they are, water over the world with the seabed's albedo, and a metal (a glTF PBR material with a metallic value, whose base colour tints its reflection and not its diffuse light) as the radiance it reflects. `scripts/content_tools/check_exposure.py` mirrors the histogram, the trim, the target and the grid's division, and is their regression test - **Lens flare sun state:** a 2x1 temporal pass (`gLensFlareStateProgram`, `lensFlareStateF.glsl`) run just before colour correction measures how much of the sun disc is unoccluded and its colour, and filters both under a rate limit; `computeLensFlare` in `postEffectUtilsF.glsl` reads one texel of it. The shader is the source of truth for the filter; `scripts/content_tools/check_lens_flare_state.py` mirrors it and is its regression test - **Bloom:** Bright-area extraction → downsample/upsample pyramid of pool targets (`LLPipeline::Bloom`) with warmth correction and optional halation carried in alpha. The additive composite is **not** a pass of its own: it is folded into `colorCorrect`'s `BLOOM_COMPOSITE` permutation. `compositeBloomHDR` is a standalone equivalent that nothing currently calls - **Cross-screen (star) filter:** `gCrossFilterProgram` streaks every thresholded highlight, seeded from the pyramid's level 0 and accumulated through two scratch targets into a sum, all three from the pool at half the pyramid base. One strictly one-sided three-pass chain per arm, at strides that tile the reachable offsets exactly once — the tiling is load-bearing and the shader explains why. Composited in `colorCorrect` alongside the pyramid, so it inherits bloom strength. Each pass reads one target and writes another, so no read waits on a barrier diff --git a/indra/alrender/CMakeLists.txt b/indra/alrender/CMakeLists.txt index 71a522c5ed..3d42703473 100644 --- a/indra/alrender/CMakeLists.txt +++ b/indra/alrender/CMakeLists.txt @@ -34,6 +34,7 @@ set( set( alrender_HEADERS + alblendstencil.h alcutoutimage.h aldepthpyramid.h alfarplane.h diff --git a/indra/alrender/alblendstencil.h b/indra/alrender/alblendstencil.h new file mode 100644 index 0000000000..18c24622bc --- /dev/null +++ b/indra/alrender/alblendstencil.h @@ -0,0 +1,55 @@ +/** + * @file alblendstencil.h + * @brief The blended pass's bit in the screen's stencil, and the stencil states that write and test it. + * + * $LicenseInfo:firstyear=2026&license=viewerlgpl$ + * Alchemy Viewer Source Code + * Copyright (C) 2026, Rye + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License only. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * $/LicenseInfo$ + */ + +#pragma once + +#include "llgl.h" +#include "llglstates.h" + +/// The screen's depth carries stencil in every render-target pack, cleared with the G-buffer each frame, and the +/// water keeps its facts in the first two bits (alwaterstencil.h). The blended pass keeps one more, in the next. +namespace ALBlendStencil +{ + +/// Where a blended surface shades: set by the forward alpha pass wherever its fragments pass the depth test. The +/// G-buffer stops at the opaque surface behind a blended one, so what it knows of those pixels (the albedo the +/// exposure meter divides by) is not what is on screen there. +constexpr GLuint BLENDED_BIT = 1u << 2; + +/// The forward alpha pass's draws: the blended bit set wherever a fragment passes the scene's depth test, and no +/// other bit touched. +class Mark : public ALGLStencil +{ +public: + Mark() : ALGLStencil(GL_ALWAYS, BLENDED_BIT, 0u, GL_KEEP, GL_KEEP, GL_REPLACE, BLENDED_BIT) {} +}; + +/// The exposure meter's mask draw: where the blended bit is set. +class OnBlended : public ALGLStencil +{ +public: + OnBlended() : ALGLStencil(GL_EQUAL, BLENDED_BIT, BLENDED_BIT, GL_KEEP, GL_KEEP, GL_KEEP, 0u) {} +}; + +} // namespace ALBlendStencil diff --git a/indra/llrender/llshadermgr.cpp b/indra/llrender/llshadermgr.cpp index b3fa45b878..ba7b1fed72 100644 --- a/indra/llrender/llshadermgr.cpp +++ b/indra/llrender/llshadermgr.cpp @@ -1581,6 +1581,8 @@ bool LLShaderMgr::saveCachedProgramBinary(LLGLSLShader* shader) } //virtual +/// Fills the reserved attribute and uniform name lists. The order of the pushes is the order of the enums in +/// llshadermgr.h, so a name added to one is added at the same place in the other. void LLShaderMgr::initAttribsAndUniforms() { //MUST match order of enum in LLVertexBuffer.h @@ -2012,6 +2014,9 @@ void LLShaderMgr::initAttribsAndUniforms() mReservedUniforms.push_back("meter_params"); mReservedUniforms.push_back("meterMap"); mReservedUniforms.push_back("meterWaterMap"); + mReservedUniforms.push_back("meterAlbedoMap"); + mReservedUniforms.push_back("meterBlendMap"); + mReservedUniforms.push_back("meter_albedo_params"); mReservedUniforms.push_back("exposureHistogram"); // Stars/Aurora/Meteors diff --git a/indra/llrender/llshadermgr.h b/indra/llrender/llshadermgr.h index 3c83e9be5e..44bddda331 100644 --- a/indra/llrender/llshadermgr.h +++ b/indra/llrender/llshadermgr.h @@ -430,6 +430,9 @@ class LLShaderMgr METER_PARAMS, // "meter_params" the exposure meter's per-pass parameters METER_MAP, // "meterMap" the exposure meter's grid METER_WATER_MAP, // "meterWaterMap" the G-buffer's normals under the water, for the meter + METER_ALBEDO_MAP, // "meterAlbedoMap" the G-buffer's albedo, which the meter divides out + METER_BLEND_MAP, // "meterBlendMap" where a blended surface shades, which the albedo cannot speak for + METER_ALBEDO_PARAMS, // "meter_albedo_params" strength (0 meters reflected light, 1 the light on surfaces), albedo floor, trusted from, to EXPOSURE_HISTOGRAM, // "exposureHistogram" 64x1, per bin (weight, weight * log2 luminance) // Alchemy-specific uniforms for Sky diff --git a/indra/newview/app_settings/settings_alchemy.xml b/indra/newview/app_settings/settings_alchemy.xml index 5075ee26bd..456207b39b 100644 --- a/indra/newview/app_settings/settings_alchemy.xml +++ b/indra/newview/app_settings/settings_alchemy.xml @@ -2707,6 +2707,32 @@ Value 0.25 + AlchemyExposureIncidentStrength + + Comment + How far automatic exposure meters the light falling on surfaces (incident metering) instead of the light they reflect (reflective metering). 0 is reflective, which exposes a white wall in dim light as middle grey; 1 is incident, so white stays white and a grey card stays grey. The incident light is estimated by dividing the reflected light by the G-buffer's albedo. 0 to 1. + Persist + 1 + Type + F32 + Value + 1.0 + + AlchemyExposureIncidentTuning + + Comment + Tuning for incident metering (AlchemyExposureIncidentStrength), for debugging. x: the albedo floor, below which a surface is read as reflecting this much so that a black one is not the brightest light in the frame (0.1). y, z: between these a patch's mean albedo earns its share of the exposure, none below y and all of it above z, so that near black surfaces say nothing about the light (0.02, 0.12). + Persist + 1 + Type + Vector3 + Value + + 0.1 + 0.02 + 0.12 + + AlchemyScopeLayout Comment diff --git a/indra/newview/app_settings/shaders/class1/deferred/exposureHistogramV.glsl b/indra/newview/app_settings/shaders/class1/deferred/exposureHistogramV.glsl index f42650db1a..3ad88befd9 100644 --- a/indra/newview/app_settings/shaders/class1/deferred/exposureHistogramV.glsl +++ b/indra/newview/app_settings/shaders/class1/deferred/exposureHistogramV.glsl @@ -25,7 +25,8 @@ // pixels (position.z 1). Each point lands in the histogram bin of its log2 luminance, and the // target's additive blend sums, per bin, the points' weights and their weighted log2 // luminances. The sums keep the mean exact; the bins only decide which points the percentile -// trim keeps. +// trim keeps. A non-sky point also counts only as much as its patch's albedo has earned (the grid's +// alpha, luminanceF.glsl), which is all of it unless the meter is reading the light on surfaces. in vec3 position; // xy: the meter texel, z: which part of it @@ -53,6 +54,8 @@ float meterWeight(vec2 uv, float mode) return 1.0 - smoothstep(0.15, 0.3, sqrt(d2)); } +/// Places one of the grid's points (each texel makes two, its ground and its sky) in the histogram bin of its log2 +/// luminance, carrying its weight and its weight times its EV, the sums that the percentile trim reads (exposureF.glsl). void main() { ivec2 texel = ivec2(position.xy); @@ -60,9 +63,10 @@ void main() bool is_sky = position.z > 0.5; float L = is_sky ? grid.g : grid.r; float share = is_sky ? grid.b : 1.0 - grid.b; + float earned = is_sky ? 1.0 : grid.a; vec2 uv = (position.xy + 0.5) / vec2(textureSize(meterMap, 0)); - float w = share * meterWeight(uv, meter_params.x) * (is_sky ? meter_params.y : 1.0); + float w = share * earned * meterWeight(uv, meter_params.x) * (is_sky ? meter_params.y : 1.0); // Held to the histogram's range, so the first and last bins cannot drag the mean with // values far outside it. diff --git a/indra/newview/app_settings/shaders/class1/deferred/luminanceF.glsl b/indra/newview/app_settings/shaders/class1/deferred/luminanceF.glsl index 9d69a7690a..7ecd6db742 100644 --- a/indra/newview/app_settings/shaders/class1/deferred/luminanceF.glsl +++ b/indra/newview/app_settings/shaders/class1/deferred/luminanceF.glsl @@ -35,6 +35,22 @@ // Every pixel of the patch counts: bilinear taps about two texels apart each average a 2x2 // block. A meter that point-samples the scene sees a sun disc or a glint appear and vanish as // the camera moves, and the exposure pumps with it. +// +// The light a surface reflects is its albedo times the light that falls on it, so a meter of the +// reflected light cannot tell a white wall in dim light from a grey one in bright light, and +// exposes both as middle grey. With meter_albedo_params.x above 0 (incident metering, at 1) the +// non-sky mean is divided by the patch's mean albedo (from the G-buffer) to that power, which +// estimates the light that falls on the patch: a white wall stays white and a grey card stays +// grey. The share of the patch's exposure that its albedo has earned goes to alpha, because a +// near black surface says nothing about the light on it; at strength 0 the grid is what it +// always was, with alpha 1. +// +// What the estimate leaves alone: an emissive surface is metered as the light it is, and may go +// hot. The water never writes the G-buffer, so over the world the albedo under it is the +// seabed's, a fair stand-in in clear water (over nothing it is sky, below). Blended surfaces are +// not in the G-buffer either, so the pass that draws them marks its pixels in the stencil, and +// meterBlendMap carries that mask here: those pixels are taken to reflect NOMINAL_ALBEDO, the +// key, which meters them exactly as reflective metering does, whatever lies behind them. out vec4 frag_color; @@ -42,6 +58,17 @@ uniform sampler2D diffuseRect; // the HDR scene, before bloom uniform sampler2D depthMap; // the scene's depth, the far plane wherever the sky is uniform sampler2D meterWaterMap; // the G-buffer's normals under the water's own pixels, 0 elsewhere uniform vec4 meter_params; // xy: a patch's size in scene texels, zw: taps across it per axis +uniform sampler2D meterAlbedoMap; // the G-buffer's albedo, decoded to linear +uniform sampler2D meterBlendMap; // r: 1 where a blended surface shades, else 0 +// x: 0 meters the light a surface reflects, 1 the light that falls on it. +// y: a surface that reflects less than this is read as if it reflected this much, since dividing by next to nothing +// would make a black surface the brightest light in the frame. +// zw: between these a patch's mean albedo earns its share of the exposure, none below z and all of it above w. +uniform vec4 meter_albedo_params; + +// What a pixel the G-buffer cannot speak for is taken to reflect: the key (EXPOSURE_KEY in pipeline.cpp). The light +// over it is then its luminance over the key, which is what reflective metering reads, at any strength. +const float NOMINAL_ALBEDO = 0.18; // The sky is what the world leaves at the far plane (deferredUtil.glsl). bool isFarDepth(float d); @@ -59,6 +86,9 @@ float lum(vec3 col) return dot(vec3(0.2126, 0.7152, 0.0722), col); } +/// One patch of the meter's grid: taps across the patch of the scene, told apart by depth into sky and ground, written as +/// the ground's mean luminance (divided by its mean albedo under incident metering), the sky's mean luminance, the sky's +/// share, and the share of the exposure the albedo has earned. void main() { vec2 scene_size = vec2(textureSize(diffuseRect, 0)); @@ -67,7 +97,9 @@ void main() vec2 tap_step = meter_params.xy / meter_params.zw; ivec2 taps = ivec2(meter_params.zw); + bool by_albedo = meter_albedo_params.x > 0.0; float ground = 0.0; + float ground_albedo = 0.0; float sky = 0.0; float sky_taps = 0.0; for (int j = 0; j < taps.y; ++j) @@ -87,14 +119,27 @@ void main() else { ground += L; + if (by_albedo) + { + bool blended = texelFetch(meterBlendMap, ivec2(px), 0).r > 0.5; + ground_albedo += blended ? NOMINAL_ALBEDO : lum(texelFetch(meterAlbedoMap, ivec2(px), 0).rgb); + } } } } float count = float(taps.x * taps.y); float ground_taps = count - sky_taps; - frag_color = vec4(ground_taps > 0.0 ? ground / ground_taps : 0.0, + float ground_mean = ground_taps > 0.0 ? ground / ground_taps : 0.0; + float earned = 1.0; + if (by_albedo && ground_taps > 0.0) + { + float albedo = ground_albedo / ground_taps; + ground_mean /= pow(max(albedo, meter_albedo_params.y), meter_albedo_params.x); + earned = mix(1.0, smoothstep(meter_albedo_params.z, meter_albedo_params.w, albedo), meter_albedo_params.x); + } + frag_color = vec4(ground_mean, sky_taps > 0.0 ? sky / sky_taps : 0.0, sky_taps / count, - 1.0); + earned); } diff --git a/indra/newview/lldrawpoolalpha.cpp b/indra/newview/lldrawpoolalpha.cpp index 888a021393..d92c7abd81 100644 --- a/indra/newview/lldrawpoolalpha.cpp +++ b/indra/newview/lldrawpoolalpha.cpp @@ -47,6 +47,7 @@ #include "llspatialpartition.h" #include "llglcommonfunc.h" #include "llvoavatar.h" +#include "alblendstencil.h" #include "llenvironment.h" @@ -248,6 +249,9 @@ void LLDrawPoolAlpha::renderPostDeferred(S32 pass) } } +/// Draws the blended surfaces over the lit scene: source-alpha blending, the destination's alpha kept as glow (or as +/// coverage in a bake), and ALBlendStencil::BLENDED_BIT marked where its colour pass shades, for the exposure meter. +/// @param rigged true to draw the rigged surfaces, with depth writes; false for the rest. void LLDrawPoolAlpha::forwardRender(bool rigged) { gPipeline.enableLightsDynamic(); @@ -290,7 +294,12 @@ void LLDrawPoolAlpha::forwardRender(bool rigged) // If the face is more than 90% transparent, then don't update the Depth buffer for Dof // We don't want the nearly invisible objects to cause of DoF effects - renderAlpha(getVertexDataMask() | LLVertexBuffer::MAP_TEXTURE_INDEX | LLVertexBuffer::MAP_TANGENT | LLVertexBuffer::MAP_TEXCOORD1 | LLVertexBuffer::MAP_TEXCOORD2, false, rigged); + { + // The albedo the exposure meter divides by stops at the opaque surface behind a blended one, so this pass marks + // where it shades (alblendstencil.h). Not the depth-only pass for depth of field, which shades nothing. + ALBlendStencil::Mark blended; + renderAlpha(getVertexDataMask() | LLVertexBuffer::MAP_TEXTURE_INDEX | LLVertexBuffer::MAP_TANGENT | LLVertexBuffer::MAP_TEXCOORD1 | LLVertexBuffer::MAP_TEXCOORD2, false, rigged); + } gGL.setColorMask(true, false); diff --git a/indra/newview/llpresetsmanager.cpp b/indra/newview/llpresetsmanager.cpp index 141490b900..12499adc82 100644 --- a/indra/newview/llpresetsmanager.cpp +++ b/indra/newview/llpresetsmanager.cpp @@ -595,6 +595,7 @@ void LLPresetsManager::getLooksControlNames(std::vector& names) "AlchemyExposureSpeedDown", "AlchemyExposureMeteringMode", "AlchemyExposureSkyWeight", + "AlchemyExposureIncidentStrength", // Color LUT "RenderColorGrade", "RenderColorGradeLUT", diff --git a/indra/newview/pipeline.cpp b/indra/newview/pipeline.cpp index 0ac7cb5d40..7fe85e7d4e 100644 --- a/indra/newview/pipeline.cpp +++ b/indra/newview/pipeline.cpp @@ -65,6 +65,7 @@ #include "alwhitebalancesolver.h" #include "alcurvemodel.h" #include "alfarplane.h" +#include "alblendstencil.h" #include "alfarplaneoverlay.h" #include "alselectionoutline.h" #include "alwaterstencil.h" @@ -8416,17 +8417,31 @@ void LLPipeline::captureScopeSample(LLRenderTarget* src) mScopeSample.flush(); } +/// Draws the exposure meter's grid, one texel of `dst` per patch of `src`: the patch's mean ground luminance (r), mean +/// sky luminance (g), sky share (b) and, under incident metering, the share of its exposure that its albedo has earned +/// (a). Sky is told by depth, and the water over nothing by its map, made here (luminanceF.glsl). +/// Incident metering, the light on the surfaces and not the light they reflect, is the main view's: the ground +/// luminance is divided by the albedo, and the pixels of blended surfaces, which the albedo cannot speak for, are read +/// as the key through a mask made from the stencil. In any other pack, or where the mask cannot be made, the meter is +/// reflective. The strength metered at is left in mMeterIncident for generateExposure. +/// @param src the HDR scene to meter, before bloom +/// @param dst the grid target void LLPipeline::generateLuminance(LLRenderTarget* src, LLRenderTarget* dst) { // The exposure meter's grid (luminanceF.glsl), from the scene before bloom: metering a // buffer the exposure already shaped would feed the exposure back into itself. LL_PROFILE_GPU_ZONE("exposure meter grid"); + LLRenderTarget& gbuffer = mRT->deferredScreen; + static LLCachedControl incident_strength(gSavedSettings, "AlchemyExposureIncidentStrength", 1.f); + // Incident metering is the main view's: only its alpha pass marks the stencil that the mask below is made from, and + // any other pack, the material preview's among them, meters the light reflected, as it did. + F32 meter_strength = renderPack() == ALRenderResourcePlan::Pack::Main ? llclamp(incident_strength(), 0.f, 1.f) : 0.f; + // Where the water lies over the sky, which the meter takes as sky (luminanceF.glsl): the G-buffer's normals copied // under the water's stencil (ALWaterStencil::SURFACE_BIT), and nothing elsewhere. The target shares the G-buffer's // depth and stencil, and is the light map's description, whose storage the lighting has let go of by now. The // water is the main view's alone, and only where it was drawn (drawsWater) does any pixel carry its stencil. - LLRenderTarget& gbuffer = mRT->deferredScreen; ALTransientTargets water_scope(mTransientTargets); LLRenderTarget* water = water_scope.acquire( ALRenderTargetDesc{ .width = gbuffer.getWidth(), .height = gbuffer.getHeight(), .color = { GL_RGBA8 } }, gbuffer, @@ -8453,9 +8468,57 @@ void LLPipeline::generateLuminance(LLRenderTarget* src, LLRenderTarget* dst) water->flush(); } + // Where a blended surface shades (ALBlendStencil::BLENDED_BIT), which the albedo cannot speak for: the G-buffer + // stops at the opaque surface behind it, so the meter reads those pixels as reflective metering would + // (luminanceF.glsl). Wanted only where the meter divides by albedo, and only the main view marks it. + ALTransientTargets blend_scope(mTransientTargets); + LLRenderTarget* blend = nullptr; + if (meter_strength > 0.f && gCopyProgram.isComplete()) + { + blend = blend_scope.acquire( + ALRenderTargetDesc{ .width = gbuffer.getWidth(), .height = gbuffer.getHeight(), .color = { GL_R8 } }, gbuffer, + "exposure.meter.blend"); + } + if (blend) + { + blend->bindTarget(); + const LLColor4 clear_color = gGL.getClearColor(); + gGL.setClearColor(LLColor4::transparent); + blend->clear(GL_COLOR_BUFFER_BIT); + gGL.setClearColor(clear_color); + { + LLGLDepthTest no_depth(GL_FALSE, GL_FALSE); + LLGLDisable no_blend(GL_BLEND); + ALBlendStencil::OnBlended on_blended; + gCopyProgram.bind(); + // White, copied where the blended bit is set: the mask is the stencil's, as a texture. + const S32 white_channel = gCopyProgram.enableTexture(LLShaderMgr::DIFFUSE_MAP); + if (white_channel > -1) + { + gGL.getTextureSlot(white_channel)->bindSampled(LLViewerFetchedTexture::sWhiteImagep, ALSamplers::PointClamp); + } + mScreenTriangleVB->setBuffer(); + mScreenTriangleVB->drawArrays(LLRender::TRIANGLES, 0, 3); + gCopyProgram.unbindTexture(LLShaderMgr::DIFFUSE_MAP); + gCopyProgram.unbind(); + } + blend->flush(); + } + + // Without the mask the meter does not divide by albedo at all: the shader reads neither the albedo nor the mask, + // where a unit left unbound holds nothing reliable (ALTextureSlot::unbind leaves a white placeholder, which would + // read as every pixel blended). generateExposure takes its key from what was metered. + if (!blend) + { + meter_strength = 0.f; + } + mMeterIncident = meter_strength; + dst->bindTarget(); LLGLDepthTest depth(GL_FALSE, GL_FALSE); + // The grid's alpha is a weight now and not an opacity: it is written, never blended. + LLGLDisable no_blend(GL_BLEND); gLuminanceProgram.bind(); @@ -8481,6 +8544,28 @@ void LLPipeline::generateLuminance(LLRenderTarget* src, LLRenderTarget* dst) } } + // Incident metering: the albedo the meter divides out, so that it measures the light falling on the surfaces and + // not the light they reflect, a white wall in dim light not being middle grey (luminanceF.glsl). The light is + // estimated from the G-buffer and not read by a sensor, so it is only as good as the albedo. The attachment is + // sRGB, which the lighting pass reads decoded to linear, so this does too. At strength 0 (reflective metering) + // nothing is bound or read. + static LLCachedControl incident_tuning(gSavedSettings, "AlchemyExposureIncidentTuning", LLVector3(0.1f, 0.02f, 0.12f)); + // Whatever the setting holds, the floor is above zero and the ramp's ends apart: the shader divides by the one and + // smoothsteps the other. + const LLVector3 tuning = incident_tuning(); + const F32 albedo_floor = llclamp(tuning.mV[VX], 0.001f, 1.f); + const F32 trusted_from = llclamp(tuning.mV[VY], 0.f, 1.f); + const F32 trusted_to = llmax(tuning.mV[VZ], trusted_from + 0.001f); + gLuminanceProgram.uniform4f(LLShaderMgr::METER_ALBEDO_PARAMS, meter_strength, albedo_floor, trusted_from, trusted_to); + const S32 albedo_channel = meter_strength > 0.f + ? gLuminanceProgram.bindTexture(LLShaderMgr::METER_ALBEDO_MAP, &gbuffer, ALSamplers::PointClamp | ALSampler::SRGBDecode, 0) + : -1; + const S32 blend_channel = blend ? gLuminanceProgram.enableTexture(LLShaderMgr::METER_BLEND_MAP) : -1; + if (blend_channel > -1) + { + blend->bindTexture(0, blend_channel, ALSamplers::PointClamp); + } + // A bilinear tap averages a 2x2 block, so taps two texels apart cover a patch. const F32 patch_w = (F32)src->getWidth() / (F32)dst->getWidth(); const F32 patch_h = (F32)src->getHeight() / (F32)dst->getHeight(); @@ -8498,10 +8583,25 @@ void LLPipeline::generateLuminance(LLRenderTarget* src, LLRenderTarget* dst) { gGL.getTextureSlot(water_channel)->unbind(); } + if (albedo_channel > -1) + { + gGL.getTextureSlot(albedo_channel)->unbind(); + } + if (blend_channel > -1) + { + gGL.getTextureSlot(blend_channel)->unbind(); + } gLuminanceProgram.unbind(); dst->flush(); } +/// Turns the meter's grid into the exposure: a histogram of its points by log2 luminance, weighted by position and by +/// sky, its trimmed mean exposed to the key, and the fade from the last exposure, written to `dst` as the scale, the EV +/// and the target (exposureHistogramV.glsl, exposureF.glsl). With the meter off, or under a classic sky, the scale is 1. +/// @param src the grid made by generateLuminance +/// @param dst receives the exposure +/// @param use_history true to fade from the last frame's exposure; false to take this grid's own at once, as a +/// one-off render such as the material preview does void LLPipeline::generateExposure(LLRenderTarget* src, LLRenderTarget* dst, bool use_history) { LL_PROFILE_GPU_ZONE("exposure"); @@ -8585,7 +8685,13 @@ void LLPipeline::generateExposure(LLRenderTarget* src, LLRenderTarget* dst, bool const F32 low = llclamp(low_percent() * 0.01f, 0.f, 0.99f); const F32 high = llclamp(high_percent() * 0.01f, low + 0.01f, 1.f); shader->uniform1f(LLShaderMgr::DT, gFrameIntervalSeconds); - shader->uniform4f(LLShaderMgr::DYNAMIC_EXPOSURE_PARAMS, log2f(EXPOSURE_KEY) + compensation(), llmin(min_ev(), max_ev()), + // The key of the grid's values, by the strength generateLuminance metered at, which is not always the setting's: the + // main view's alone, and none where no mask could be made. At strength 0 (reflective metering) they are the light + // reflected, and middle grey is their key; at 1 (incident metering) they are the light that falls on the surfaces, + // whose key is 1, so that white reads as white and a grey card as 0.18. + // For a scene whose surfaces average middle grey the two meter alike, and between them the key follows. + const F32 key_ev = (1.f - mMeterIncident) * log2f(EXPOSURE_KEY); + shader->uniform4f(LLShaderMgr::DYNAMIC_EXPOSURE_PARAMS, key_ev + compensation(), llmin(min_ev(), max_ev()), llmax(min_ev(), max_ev()), meter ? 1.f : 0.f); shader->uniform4f(LLShaderMgr::DYNAMIC_EXPOSURE_PARAMS2, low, high, llmax(speed_up(), 0.f), llmax(speed_down(), 0.f)); diff --git a/indra/newview/pipeline.h b/indra/newview/pipeline.h index e5f116ef57..e1637bf740 100644 --- a/indra/newview/pipeline.h +++ b/indra/newview/pipeline.h @@ -1151,6 +1151,9 @@ class LLPipeline LLRenderTarget mExposureHistogram; LLRenderTarget mExposureMap; LLRenderTarget mLastExposure; + // The incident strength the last generateLuminance metered at: the setting in the main view where it could make the + // blended mask, and 0 otherwise. generateExposure takes the key of the grid's values from it. + F32 mMeterIncident = 0.f; // lens flare sun state, 2x1, swapped each frame: [0] is this frame's, [1] // last frame's. Layout in lensFlareStateF.glsl. diff --git a/indra/newview/skins/default/xui/en/panel_lightbox_look.xml b/indra/newview/skins/default/xui/en/panel_lightbox_look.xml index 34bb027e68..142c386960 100644 --- a/indra/newview/skins/default/xui/en/panel_lightbox_look.xml +++ b/indra/newview/skins/default/xui/en/panel_lightbox_look.xml @@ -415,7 +415,7 @@ Row conventions: +