Files
filament/filament/src/details/Renderer.cpp
T

1495 lines
64 KiB
C++

/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "details/Renderer.h"
#include "Allocators.h"
#include "DebugRegistry.h"
#include "FrameHistory.h"
#include "PostProcessManager.h"
#include "RendererUtils.h"
#include "RenderPass.h"
#include "ResourceAllocator.h"
#include "details/Engine.h"
#include "details/Fence.h"
#include "details/Scene.h"
#include "details/SwapChain.h"
#include "details/Texture.h"
#include "details/View.h"
#include <filament/Camera.h>
#include <filament/Fence.h>
#include <filament/Options.h>
#include <filament/Renderer.h>
#include <backend/DriverEnums.h>
#include <backend/DriverApiForward.h>
#include <backend/Handle.h>
#include <backend/PixelBufferDescriptor.h>
#include "fg/FrameGraph.h"
#include "fg/FrameGraphId.h"
#include "fg/FrameGraphResources.h"
#include "fg/FrameGraphTexture.h"
#include <math/vec2.h>
#include <math/vec3.h>
#include <math/mat4.h>
#include <private/utils/Tracing.h>
#include <utils/compiler.h>
#include <utils/JobSystem.h>
#include <utils/Log.h>
#include <utils/ostream.h>
#include <utils/Panic.h>
#include <utils/debug.h>
#include <chrono>
#include <limits>
#include <memory>
#include <utility>
#include <stddef.h>
#include <stdint.h>
#ifdef __ANDROID__
#include <sys/system_properties.h>
#endif
// this helps visualize what dynamic-scaling is doing
#define DEBUG_DYNAMIC_SCALING false
using namespace filament::math;
using namespace utils;
namespace filament {
using namespace backend;
FRenderer::FRenderer(FEngine& engine) :
mEngine(engine),
mFrameSkipper(),
mRenderTargetHandle(engine.getDefaultRenderTarget()),
mFrameInfoManager(engine.getDriverApi()),
mHdrTranslucent(TextureFormat::RGBA16F),
mHdrQualityMedium(TextureFormat::R11F_G11F_B10F),
mHdrQualityHigh(TextureFormat::RGB16F),
mIsRGB8Supported(false),
mUserEpoch(engine.getEngineEpoch()),
mResourceAllocator(std::make_unique<ResourceAllocator>(
engine.getSharedResourceAllocatorDisposer(),
engine.getConfig(),
engine.getDriverApi()))
{
FDebugRegistry& debugRegistry = engine.getDebugRegistry();
debugRegistry.registerProperty("d.renderer.doFrameCapture",
&engine.debug.renderer.doFrameCapture);
debugRegistry.registerProperty("d.renderer.disable_buffer_padding",
&engine.debug.renderer.disable_buffer_padding);
debugRegistry.registerProperty("d.renderer.disable_subpasses",
&engine.debug.renderer.disable_subpasses);
debugRegistry.registerProperty("d.shadowmap.display_shadow_texture",
&engine.debug.shadowmap.display_shadow_texture);
debugRegistry.registerProperty("d.shadowmap.display_shadow_texture_scale",
&engine.debug.shadowmap.display_shadow_texture_scale);
debugRegistry.registerProperty("d.shadowmap.display_shadow_texture_layer",
&engine.debug.shadowmap.display_shadow_texture_layer);
debugRegistry.registerProperty("d.shadowmap.display_shadow_texture_level",
&engine.debug.shadowmap.display_shadow_texture_level);
debugRegistry.registerProperty("d.shadowmap.display_shadow_texture_channel",
&engine.debug.shadowmap.display_shadow_texture_channel);
debugRegistry.registerProperty("d.shadowmap.display_shadow_texture_layer_count",
&engine.debug.shadowmap.display_shadow_texture_layer_count);
debugRegistry.registerProperty("d.shadowmap.display_shadow_texture_level_count",
&engine.debug.shadowmap.display_shadow_texture_level_count);
debugRegistry.registerProperty("d.shadowmap.display_shadow_texture_power",
&engine.debug.shadowmap.display_shadow_texture_power);
debugRegistry.registerProperty("d.stereo.combine_multiview_images",
&engine.debug.stereo.combine_multiview_images);
DriverApi& driver = engine.getDriverApi();
mIsRGB8Supported = driver.isRenderTargetFormatSupported(TextureFormat::RGB8);
// our default HDR translucent format, fallback to LDR if not supported by the backend
if (!driver.isRenderTargetFormatSupported(TextureFormat::RGBA16F)) {
// this will clip all HDR data, but we don't have a choice
mHdrTranslucent = TextureFormat::RGBA8;
}
// our default opaque low/medium quality HDR format, fallback to LDR if not supported
if (!driver.isRenderTargetFormatSupported(mHdrQualityMedium)) {
// this will clip all HDR data, but we don't have a choice
mHdrQualityMedium = TextureFormat::RGB8;
}
// our default opaque high quality HDR format, fallback to RGBA, then medium, then LDR
// if not supported
if (!driver.isRenderTargetFormatSupported(mHdrQualityHigh)) {
mHdrQualityHigh = TextureFormat::RGBA16F;
}
if (!driver.isRenderTargetFormatSupported(mHdrQualityHigh)) {
mHdrQualityHigh = TextureFormat::R11F_G11F_B10F;
}
if (!driver.isRenderTargetFormatSupported(mHdrQualityHigh)) {
// this will clip all HDR data, but we don't have a choice
mHdrQualityHigh = TextureFormat::RGB8;
}
}
FRenderer::~FRenderer() noexcept {
// There shouldn't be any resource left when we get here, but if there is, make sure
// to free what we can (it would probably mean something when wrong).
#ifndef NDEBUG
size_t const wm = getCommandsHighWatermark();
size_t const wmpct = wm / (mEngine.getPerFrameCommandsSize() / 100);
slog.d << "Renderer: Commands High watermark "
<< wm / 1024 << " KiB (" << wmpct << "%), "
<< wm / sizeof(Command) << " commands, " << sizeof(Command) << " bytes/command"
<< io::endl;
#endif
}
void FRenderer::terminate(FEngine& engine) {
// Here we would cleanly free resources we've allocated, or we own, in particular we would
// shut down threads if we created any.
DriverApi& driver = engine.getDriverApi();
// Before we can destroy this Renderer's resources, we must make sure
// that all pending commands have been executed (as they could reference data in this
// instance, e.g. Fences, Callbacks, etc...)
if (UTILS_HAS_THREADING) {
Fence::waitAndDestroy(engine.createFence());
} else {
// In single threaded mode, allow recently-created objects (e.g. no-op fences in Skipper)
// to initialize themselves, otherwise the engine tries to destroy invalid handles.
engine.execute();
}
mFrameInfoManager.terminate(driver);
mFrameSkipper.terminate(driver);
mResourceAllocator->terminate();
}
void FRenderer::resetUserTime() {
mUserEpoch = std::chrono::steady_clock::now();
}
TextureFormat FRenderer::getHdrFormat(const FView& view, bool const translucent) const noexcept {
if (translucent) {
return mHdrTranslucent;
}
switch (view.getRenderQuality().hdrColorBuffer) {
case QualityLevel::LOW:
case QualityLevel::MEDIUM:
return mHdrQualityMedium;
case QualityLevel::HIGH:
case QualityLevel::ULTRA: {
return mHdrQualityHigh;
}
}
}
TextureFormat FRenderer::getLdrFormat(bool const translucent) const noexcept {
return (translucent || !mIsRGB8Supported) ? TextureFormat::RGBA8 : TextureFormat::RGB8;
}
std::pair<Handle<HwRenderTarget>, TargetBufferFlags>
FRenderer::getRenderTarget(FView const& view) const noexcept {
Handle<HwRenderTarget> outTarget = view.getRenderTargetHandle();
TargetBufferFlags outAttachmentMask = view.getRenderTargetAttachmentMask();
if (!outTarget) {
outTarget = mRenderTargetHandle;
outAttachmentMask = TargetBufferFlags::COLOR0 | TargetBufferFlags::DEPTH;
}
return { outTarget, outAttachmentMask };
}
TargetBufferFlags FRenderer::getClearFlags() const noexcept {
return (mClearOptions.clear ? TargetBufferFlags::COLOR : TargetBufferFlags::NONE)
| TargetBufferFlags::DEPTH_AND_STENCIL;
}
void FRenderer::initializeClearFlags() noexcept {
// We always discard and clear the depth+stencil buffers -- we don't allow sharing these
// across views (clear implies discard)
mDiscardStartFlags = ((mClearOptions.discard || mClearOptions.clear) ?
TargetBufferFlags::COLOR : TargetBufferFlags::NONE)
| TargetBufferFlags::DEPTH_AND_STENCIL;
mClearFlags = getClearFlags();
}
void FRenderer::setPresentationTime(int64_t const monotonic_clock_ns) {
FEngine::DriverApi& driver = mEngine.getDriverApi();
driver.setPresentationTime(monotonic_clock_ns);
}
void FRenderer::setVsyncTime(uint64_t const steadyClockTimeNano) noexcept {
mVsyncSteadyClockTimeNano = steadyClockTimeNano;
}
void FRenderer::skipFrame(uint64_t vsyncSteadyClockTimeNano) {
FILAMENT_TRACING_CALL(FILAMENT_TRACING_CATEGORY_FILAMENT);
FILAMENT_CHECK_PRECONDITION(!mSwapChain) <<
"skipFrame() can't be called between beginFrame() and endFrame()";
if (!vsyncSteadyClockTimeNano) {
vsyncSteadyClockTimeNano = mVsyncSteadyClockTimeNano;
mVsyncSteadyClockTimeNano = 0;
}
FEngine& engine = mEngine;
FEngine::DriverApi& driver = engine.getDriverApi();
// Gives the backend a chance to execute periodic tasks. This must be called before
// the frame skipper.
driver.tick();
// do this before engine.flush()
mResourceAllocator->gc(true);
// Run the component managers' GC in parallel
// WARNING: while doing this we can't access any component manager
auto& js = engine.getJobSystem();
auto *job = js.runAndRetain(jobs::createJob(js, nullptr, &FEngine::gc, &engine)); // gc all managers
engine.flush(); // flush command stream
// make sure we're done with the gcs
js.waitAndRelease(job);
}
bool FRenderer::beginFrame(FSwapChain* swapChain, uint64_t vsyncSteadyClockTimeNano) {
assert_invariant(swapChain);
FILAMENT_TRACING_CALL(FILAMENT_TRACING_CATEGORY_FILAMENT);
#if 0 && defined(__ANDROID__)
char scratch[PROP_VALUE_MAX + 1];
int length = __system_property_get("debug.filament.protected", scratch);
if (swapChain && length > 0) {
uint64_t flags = swapChain->getFlags();
bool value = bool(atoi(scratch));
if (value) {
flags |= SwapChain::CONFIG_PROTECTED_CONTENT;
} else {
flags &= ~SwapChain::CONFIG_PROTECTED_CONTENT;
}
swapChain->recreateWithNewFlags(mEngine, flags);
}
#endif
if (!vsyncSteadyClockTimeNano) {
vsyncSteadyClockTimeNano = mVsyncSteadyClockTimeNano;
mVsyncSteadyClockTimeNano = 0;
}
// get the timestamp as soon as possible
using namespace std::chrono;
const steady_clock::time_point now{ steady_clock::now() };
const steady_clock::time_point userVsync{ steady_clock::duration(vsyncSteadyClockTimeNano) };
const time_point<steady_clock> appVsync(vsyncSteadyClockTimeNano ? userVsync : now);
mFrameId++;
mViewRenderedCount = 0;
FILAMENT_TRACING_FRAME_ID(FILAMENT_TRACING_CATEGORY_FILAMENT, mFrameId);
FEngine& engine = mEngine;
FEngine::DriverApi& driver = engine.getDriverApi();
// start a frame capture, if requested.
if (UTILS_UNLIKELY(engine.debug.renderer.doFrameCapture)) {
driver.startCapture();
}
// latch the frame time
std::chrono::duration<double> const time(appVsync - mUserEpoch);
float const h = float(time.count());
float const l = float(time.count() - h);
mShaderUserTime = { h, l, 0, 0 };
mPreviousRenderTargets.clear();
mBeginFrameInternal = {};
mSwapChain = swapChain;
swapChain->makeCurrent(driver);
// NOTE: this makes synchronous calls to the driver
driver.updateStreams(&driver);
// Gives the backend a chance to execute periodic tasks. This must be called before
// the frame skipper.
driver.tick();
/*
* From this point, we can't do any more work in beginFrame() because the user could choose
* to ignore the return value and render the frame anyway -- which is perfectly fine.
* The remaining work will be done when the first render() call is made.
*/
auto beginFrameInternal = [this, appVsync, swapChain]() {
FEngine& engine = mEngine;
FEngine::DriverApi& driver = engine.getDriverApi();
// we need to re-set mSwapChain here because if a frame was marked as "skip" but the
// user ignored us, we'd still want mSwapChain to be set.
mSwapChain = swapChain;
driver.beginFrame(
appVsync.time_since_epoch().count(),
mDisplayInfo.refreshRate == 0.0 ? 0 : int64_t(
1'000'000'000.0 / mDisplayInfo.refreshRate),
mFrameId);
// This need to occur after the backend beginFrame() because some backends need to start
// a command buffer before creating a fence.
mFrameInfoManager.beginFrame(driver, {
.historySize = mFrameRateOptions.history
}, mFrameId);
// ask the engine to do what it needs to (e.g. updates light buffer, materials...)
engine.prepare();
};
if (mFrameSkipper.beginFrame(driver)) {
// if beginFrame() returns true, we are expecting a call to endFrame(),
// so do the beginFrame work right now, instead of requiring a call to render()
beginFrameInternal();
return true;
}
// however, if we return false, the user is allowed to ignore us and render a frame anyway,
// so we need to delay this work until that happens.
mBeginFrameInternal = beginFrameInternal;
// clear mSwapChain because the frame should be skipped (it will be re-set if the user
// ignores the skip)
mSwapChain = nullptr;
// we need to flush in this case, to make sure the tick() call is executed at some point
engine.flush();
return false;
}
void FRenderer::endFrame() {
FILAMENT_TRACING_CALL(FILAMENT_TRACING_CATEGORY_FILAMENT);
if (UTILS_UNLIKELY(mBeginFrameInternal)) {
mBeginFrameInternal();
mBeginFrameInternal = {};
}
FEngine& engine = mEngine;
FEngine::DriverApi& driver = engine.getDriverApi();
if (UTILS_HAS_THREADING) {
// on debug builds this helps to catch cases where we're writing to
// the buffer form another thread, which is currently not allowed.
driver.debugThreading();
}
FILAMENT_CHECK_PRECONDITION(engine.isValid(mSwapChain))
<< "SwapChain must remain valid until endFrame is called.";
if (mSwapChain) {
mSwapChain->commit(driver);
mSwapChain = nullptr;
}
mFrameInfoManager.endFrame(driver);
mFrameSkipper.endFrame(driver);
driver.endFrame(mFrameId);
// gives the backend a chance to execute periodic tasks
driver.tick();
// stop the frame capture, if one was requested
if (UTILS_UNLIKELY(engine.debug.renderer.doFrameCapture)) {
driver.stopCapture();
engine.debug.renderer.doFrameCapture = false;
}
// do this before engine.flush()
mResourceAllocator->gc();
// Run the component managers' GC in parallel
// WARNING: while doing this we can't access any component manager
auto& js = engine.getJobSystem();
auto *job = js.runAndRetain(jobs::createJob(js, nullptr, &FEngine::gc, &engine)); // gc all managers
engine.flush(); // flush command stream
// make sure we're done with the gcs
js.waitAndRelease(job);
}
void FRenderer::readPixels(uint32_t const xoffset, uint32_t const yoffset, uint32_t const width, uint32_t const height,
PixelBufferDescriptor&& buffer) {
const bool withinFrame = mSwapChain != nullptr;
FILAMENT_CHECK_PRECONDITION(withinFrame)
<< "readPixels() on a SwapChain must be called after "
"beginFrame() and before endFrame().";
RendererUtils::readPixels(mEngine.getDriverApi(), mRenderTargetHandle,
xoffset, yoffset, width, height, std::move(buffer));
}
void FRenderer::readPixels(FRenderTarget* renderTarget,
uint32_t const xoffset, uint32_t const yoffset, uint32_t const width, uint32_t const height,
PixelBufferDescriptor&& buffer) {
// TODO: change the following to an assert when client call sites have addressed the issue.
if (!renderTarget->supportsReadPixels()) {
slog.w << "readPixels() must be called with a renderTarget with COLOR0 created with "
"TextureUsage::BLIT_SRC. This precondition will be asserted in a later "
"release of Filament."
<< io::endl;
}
RendererUtils::readPixels(mEngine.getDriverApi(), renderTarget->getHwHandle(),
xoffset, yoffset, width, height, std::move(buffer));
}
void FRenderer::copyFrame(FSwapChain* dstSwapChain, filament::Viewport const& dstViewport,
filament::Viewport const& srcViewport, CopyFrameFlag const flags) {
FILAMENT_TRACING_CALL(FILAMENT_TRACING_CATEGORY_FILAMENT);
assert_invariant(mSwapChain);
assert_invariant(dstSwapChain);
FEngine& engine = mEngine;
FEngine::DriverApi& driver = engine.getDriverApi();
// Set the current swap chain as the read surface, and the destination
// swap chain as the draw surface so that blitting between default render
// targets results in a frame copy from the current frame to the
// destination.
driver.makeCurrent(dstSwapChain->getHwHandle(), mSwapChain->getHwHandle());
RenderPassParams params = {};
// Clear color to black if the CLEAR flag is set.
if (flags & CLEAR) {
params.clearColor = {0.f, 0.f, 0.f, 1.f};
params.flags.clear = TargetBufferFlags::COLOR;
params.flags.discardStart = TargetBufferFlags::ALL;
params.flags.discardEnd = TargetBufferFlags::NONE;
params.viewport.left = 0;
params.viewport.bottom = 0;
params.viewport.width = std::numeric_limits<uint32_t>::max();
params.viewport.height = std::numeric_limits<uint32_t>::max();
driver.beginRenderPass(mRenderTargetHandle, params);
driver.endRenderPass();
}
// Verify that the source swap chain is readable.
assert_invariant(mSwapChain->isReadable());
driver.blitDEPRECATED(TargetBufferFlags::COLOR, mRenderTargetHandle,
dstViewport, mRenderTargetHandle, srcViewport, SamplerMagFilter::LINEAR);
if (flags & SET_PRESENTATION_TIME) {
// TODO: Implement this properly, see https://github.com/google/filament/issues/633
}
if (flags & COMMIT) {
dstSwapChain->commit(driver);
}
// Reset the context and read/draw surface to the current surface so that
// frame rendering can continue or complete.
mSwapChain->makeCurrent(driver);
}
void FRenderer::renderStandaloneView(FView const* view) {
FILAMENT_TRACING_CALL(FILAMENT_TRACING_CATEGORY_FILAMENT);
using namespace std::chrono;
FILAMENT_CHECK_PRECONDITION(view->getRenderTarget())
<< "View \"" << view->getName() << "\" must have a RenderTarget associated";
FILAMENT_CHECK_PRECONDITION(!mSwapChain)
<< "renderStandaloneView() must be called outside of beginFrame() / endFrame()";
if (UTILS_LIKELY(view->getScene())) {
mPreviousRenderTargets.clear();
mFrameId++;
// ask the engine to do what it needs to (e.g. updates light buffer, materials...)
FEngine& engine = mEngine;
engine.prepare();
FEngine::DriverApi& driver = engine.getDriverApi();
driver.beginFrame(
steady_clock::now().time_since_epoch().count(),
mDisplayInfo.refreshRate == 0.0 ? 0 : int64_t(
1'000'000'000.0 / mDisplayInfo.refreshRate),
mFrameId);
renderInternal(view);
driver.endFrame(mFrameId);
// This is a workaround for internal bug b/361822355.
// TODO: properly address the bug and remove this workaround.
if (engine.getBackend() == Backend::VULKAN) {
engine.flushAndWait();
}
}
}
void FRenderer::render(FView const* view) {
FILAMENT_TRACING_CALL(FILAMENT_TRACING_CATEGORY_FILAMENT);
if (UTILS_UNLIKELY(mBeginFrameInternal)) {
// this should not happen, the user should not call render() if we returned false from
// beginFrame(). But because this is allowed, we handle it gracefully.
mBeginFrameInternal();
mBeginFrameInternal = {};
}
// after beginFrame() is called, mSwapChain should be true
assert_invariant(mSwapChain);
if (UTILS_LIKELY(view && view->getScene() && view->hasCamera())) {
if (mViewRenderedCount) {
// This is a good place to kick the GPU, since we've rendered a View before,
// and we're about to render another one.
mEngine.getDriverApi().flush();
}
renderInternal(view);
mViewRenderedCount++;
}
}
void FRenderer::renderInternal(FView const* view) {
FEngine& engine = mEngine;
FILAMENT_CHECK_PRECONDITION(!view->hasPostProcessPass() ||
engine.hasFeatureLevel(FeatureLevel::FEATURE_LEVEL_1))
<< "post-processing is not supported at FEATURE_LEVEL_0";
// per-renderpass data
RootArenaScope rootArenaScope(engine.getPerRenderPassArena());
// create a root job so no other job can escape
JobSystem& js = engine.getJobSystem();
auto *rootJob = js.setRootJob(js.createJob());
// execute the render pass
renderJob(rootArenaScope, const_cast<FView&>(*view));
// make sure to flush the command buffer
engine.flush();
// and wait for all jobs to finish as a safety (this should be a no-op)
js.runAndWait(rootJob);
}
void FRenderer::renderJob(RootArenaScope& rootArenaScope, FView& view) {
FEngine& engine = mEngine;
JobSystem& js = engine.getJobSystem();
FEngine::DriverApi& driver = engine.getDriverApi();
PostProcessManager& ppm = engine.getPostProcessManager();
ppm.setFrameUniforms(driver, view.getFrameUniforms());
// DEBUG: driver commands must all happen from the same thread. Enforce that on debug builds.
driver.debugThreading();
bool hasPostProcess = view.hasPostProcessPass();
bool hasScreenSpaceRefraction = false;
bool hasColorGrading = hasPostProcess;
bool hasDithering = view.getDithering() == Dithering::TEMPORAL;
bool hasFXAA = view.getAntiAliasing() == AntiAliasing::FXAA;
float2 scale = view.updateScale(engine, mFrameInfoManager.getLastFrameInfo(), mFrameRateOptions, mDisplayInfo);
auto msaaOptions = view.getMultiSampleAntiAliasingOptions();
auto dsrOptions = view.getDynamicResolutionOptions();
auto bloomOptions = view.getBloomOptions();
auto dofOptions = view.getDepthOfFieldOptions();
auto aoOptions = view.getAmbientOcclusionOptions();
auto taaOptions = view.getTemporalAntiAliasingOptions();
auto vignetteOptions = view.getVignetteOptions();
auto colorGrading = view.getColorGrading();
auto ssReflectionsOptions = view.getScreenSpaceReflectionsOptions();
auto guardBandOptions = view.getGuardBandOptions();
const bool isRenderingMultiview = view.hasStereo() &&
engine.getConfig().stereoscopicType == StereoscopicType::MULTIVIEW;
// FIXME: This is to override some settings that are not supported for multiview at the moment.
// Remove this when all features are supported.
if (isRenderingMultiview) {
hasPostProcess = false;
msaaOptions.enabled = false;
// Picking is not supported for multiview rendering. Clear any pending picking queries.
view.clearPickingQueries();
}
const uint8_t msaaSampleCount = msaaOptions.enabled ? msaaOptions.sampleCount : 1u;
if (!hasPostProcess) {
// disable all effects that are part of post-processing
dofOptions.enabled = false;
bloomOptions.enabled = false;
vignetteOptions.enabled = false;
taaOptions.enabled = false;
hasColorGrading = false;
hasDithering = false;
hasFXAA = false;
scale = 1.0f;
} else {
// This configures post-process materials by setting constant parameters
if (taaOptions.enabled) {
ppm.configureTemporalAntiAliasingMaterial(taaOptions);
if (taaOptions.upscaling) {
// for now TAA upscaling is incompatible with regular dsr
dsrOptions.enabled = false;
// also, upscaling doesn't work well with quater-resolution SSAO
aoOptions.resolution = 1.0;
// Currently we only support a fixed TAA upscaling ratio
scale = 0.5f;
}
}
}
const bool blendModeTranslucent = view.getBlendMode() == BlendMode::TRANSLUCENT;
// If the swap-chain is transparent or if we blend into it, we need to allocate our intermediate
// buffers with an alpha channel.
const bool needsAlphaChannel =
(mSwapChain && mSwapChain->isTransparent()) || blendModeTranslucent;
const bool isProtectedContent = mSwapChain && mSwapChain->isProtected();
// Conditions to meet to be able to use the sub-pass rendering path. This is regardless of
// whether the backend supports subpasses (or if they are disabled in the debugRegistry).
const bool isSubpassPossible =
msaaSampleCount <= 1 &&
hasColorGrading &&
!bloomOptions.enabled && !dofOptions.enabled && !taaOptions.enabled;
// whether we're scaled at all
bool scaled = any(notEqual(scale, float2(1.0f)));
// asSubpass is disabled with TAA (although it's supported) because performance was degraded
// on qualcomm hardware -- we might need a backend dependent toggle at some point
const PostProcessManager::ColorGradingConfig colorGradingConfig{
.asSubpass =
isSubpassPossible &&
driver.isFrameBufferFetchSupported() &&
!engine.debug.renderer.disable_subpasses,
.customResolve =
msaaSampleCount > 1 &&
driver.isFrameBufferFetchMultiSampleSupported() &&
msaaOptions.customResolve &&
hasColorGrading &&
!engine.debug.renderer.disable_subpasses,
.translucent = needsAlphaChannel,
.outputLuminance = hasFXAA || scaled, // ignored by translucent variants (false)
.dithering = hasDithering,
.ldrFormat = (hasColorGrading && (hasFXAA || scaled)) ?
TextureFormat::RGBA8 : getLdrFormat(needsAlphaChannel)
};
// by construction (msaaSampleCount) both asSubpass and customResolve can't be true
assert_invariant(colorGradingConfig.asSubpass + colorGradingConfig.customResolve < 2);
// vp is the user defined viewport within the View
filament::Viewport const& vp = view.getViewport();
// svp is the "rendering" viewport. That is the viewport after dynamic-resolution is applied
// as well as other adjustment, such as the guard band.
filament::Viewport svp = {
0, 0, // this is ignored
uint32_t(float(vp.width ) * scale.x),
uint32_t(float(vp.height) * scale.y)
};
if (svp.empty()) {
return;
}
// xvp is the viewport relative to svp containing the "interesting" rendering
filament::Viewport xvp = svp;
CameraInfo cameraInfo = view.computeCameraInfo(engine);
// If fxaa and scaling are not enabled, we're essentially in a very fast rendering path -- in
// this case, we would need an extra blit to "resolve" the buffer padding (because there are no
// other pass that can do it as a side effect). In this case, it is better to skip the padding,
// which won't be helping much.
const bool noBufferPadding = (isSubpassPossible &&
!hasFXAA && !scaled) || engine.debug.renderer.disable_buffer_padding;
// guardBand must be a multiple of 16 to guarantee the same exact rendering up to 4 mip levels.
float const guardBand = guardBandOptions.enabled ? 16.0f : 0.0f;
if (hasPostProcess && !noBufferPadding) {
// We always pad the rendering viewport to dimensions multiple of 16, this guarantees
// that up to 4 mipmap levels are possible with an exact 1:2 scale. This also helps
// with memory allocations and quad-shading when dynamic-resolution is enabled.
// There is a small performance cost for dimensions that are not already multiple of 16.
// But, this a no-op in common resolutions, in particular in 720p.
// The origin of rendering is not modified, the padding is added to the right/top.
//
// TODO: Should we enable when we don't have post-processing?
// Without post-processing, we usually draw directly into
// the SwapChain, and we might want to keep it this way.
auto round = [](uint32_t const x) {
constexpr uint32_t rounding = 16u;
return (x + (rounding - 1u)) & ~(rounding - 1u);
};
// compute the new rendering width and height, multiple of 16.
const float width = float(round(svp.width )) + 2.0f * guardBand;
const float height = float(round(svp.height)) + 2.0f * guardBand;
// scale the field-of-view up, so it covers exactly the extra pixels
const float3 clipSpaceScaling{
float(svp.width) / width,
float(svp.height) / height,
1.0f };
// add the extra-pixels on the right/top of the viewport
// the translation comes from (same for height): 2*((width - svp.width)/2) / width
// i.e. we offset by half the width/height delta and the 2* comes from the fact that
// clip-space has width/height of 2.
// note: this creates an asymmetric frustum -- but we eventually copy only the
// left/bottom part, which is a symmetric region.
const float2 clipSpaceTranslation{
1.0f - clipSpaceScaling.x - 2.0f * guardBand / width,
1.0f - clipSpaceScaling.y - 2.0f * guardBand / height
};
mat4f ts = mat4f::scaling(clipSpaceScaling);
ts[3].xy = -clipSpaceTranslation;
// update the camera projection
cameraInfo.projection = highPrecisionMultiply(ts, cameraInfo.projection);
// VERTEX_DOMAIN_DEVICE doesn't apply the projection, but it still needs this
// clip transform, so we apply it separately (see surface_main.vs)
cameraInfo.clipTransform = { ts[0][0], ts[1][1], ts[3].x, ts[3].y };
// adjust svp to the new, larger, rendering dimensions
svp.width = uint32_t(width);
svp.height = uint32_t(height);
xvp.left = int32_t(guardBand);
xvp.bottom = int32_t(guardBand);
}
view.prepare(engine, driver, rootArenaScope, svp, cameraInfo, getShaderUserTime(), needsAlphaChannel);
view.prepareUpscaler(scale, taaOptions, dsrOptions);
/*
* Allocate command buffer
*/
FScene& scene = *view.getScene();
// Allocate some space for our commands in the per-frame Arena, and use that space as
// an Arena for commands. All this space is released when we exit this method.
size_t const perFrameCommandsSize = engine.getPerFrameCommandsSize();
void* const arenaBegin = rootArenaScope.allocate(perFrameCommandsSize, CACHELINE_SIZE);
void* const arenaEnd = pointermath::add(arenaBegin, perFrameCommandsSize);
// This arena *must* stay valid until all commands have been processed
RenderPass::Arena commandArena("Command Arena", { arenaBegin, arenaEnd });
RenderPass::RenderFlags renderFlags = 0;
if (view.hasShadowing()) renderFlags |= RenderPass::HAS_SHADOWING;
if (view.isFrontFaceWindingInverted()) renderFlags |= RenderPass::HAS_INVERSE_FRONT_FACES;
RenderPassBuilder passBuilder(commandArena);
passBuilder.renderFlags(renderFlags);
Variant variant;
variant.setDirectionalLighting(view.hasDirectionalLighting());
variant.setDynamicLighting(view.hasDynamicLighting());
variant.setFog(view.hasFog());
// The VSM bit has a different meaning for STANDARD_VARIANT (as opposed to DEPTH_VARIANT),
// In the STANDARD_VARIANT case, we are *using* the shadow-map, and the VSM only decides which
// type of sampler is used (samplerShadow or sampler2D).
variant.setVsm(view.hasShadowing() && view.getShadowType() != ShadowType::PCF);
variant.setStereo(view.hasStereo());
/*
* Frame graph
*/
FrameGraph fg(*mResourceAllocator,
isProtectedContent ? FrameGraph::Mode::PROTECTED : FrameGraph::Mode::UNPROTECTED);
auto& blackboard = fg.getBlackboard();
/*
* Shadow pass
*/
if (view.needsShadowMap()) {
Variant shadowVariant(Variant::DEPTH_VARIANT);
// The VSM bit has a different meaning for DEPTH_VARIANT (as opposed to STANDARD_VARIANT),
// In the DEPTH_VARIANT case, we are *generating* the shadow-map, and some computations
// are handled differently. In addition, the color buffer is used.
shadowVariant.setVsm(view.getShadowType() == ShadowType::VSM);
auto shadows = view.renderShadowMaps(engine, fg, cameraInfo, mShaderUserTime,
RenderPassBuilder{ commandArena }
.renderFlags(renderFlags)
.variant(shadowVariant));
blackboard["shadows"] = shadows;
}
// When we don't have a custom RenderTarget, customRenderTarget below is nullptr and is
// recorded in the list of targets already rendered into -- this ensures that
// initializeClearFlags() is called only once for the default RenderTarget.
auto& previousRenderTargets = mPreviousRenderTargets;
FRenderTarget* const customRenderTarget = downcast(view.getRenderTarget());
if (UTILS_LIKELY(
previousRenderTargets.find(customRenderTarget) == previousRenderTargets.end())) {
previousRenderTargets.insert(customRenderTarget);
initializeClearFlags();
}
// Note: it is not well-defined what colorspace this clearColor is defined into. This leads
// to inconsistent colors depending on enabled features. When the clear is performed
// into a temporary buffer (common case), the clearColor is color-graded. A problem
// arises when transparent views are used, in this case the clear color is not
// color-graded.
const float4 clearColor = mClearOptions.clearColor;
const uint8_t clearStencil = mClearOptions.clearStencil;
const TargetBufferFlags clearFlags = mClearFlags;
const TargetBufferFlags discardStartFlags = mDiscardStartFlags;
const TargetBufferFlags keepOverrideStartFlags = TargetBufferFlags::ALL & ~discardStartFlags;
TargetBufferFlags keepOverrideEndFlags = TargetBufferFlags::NONE;
if (customRenderTarget) {
// For custom RenderTarget, we look at each attachment flag and if they have their
// SAMPLEABLE usage bit set, we assume they must not be discarded after the render pass.
keepOverrideEndFlags |= customRenderTarget->getSampleableAttachmentsMask();
}
// Renderer's ClearOptions apply once at the beginning of the frame (not for each View),
// however, it's implemented as part of executing a render pass on the current render target,
// and that happens for each View. So we need to disable clearing after the 1st View has
// been processed.
mDiscardStartFlags &= ~TargetBufferFlags::COLOR;
mClearFlags &= ~TargetBufferFlags::COLOR;
// the clearFlags and clearColor set below are "sticky" to the imported target, meaning
// they will apply anytime we render into this target, THIS INCLUDES when this target
// is "replacing" another one. E.g. typically when the color pass ends-up drawing directly
// here.
auto [viewRenderTarget, attachmentMask] = getRenderTarget(view);
FrameGraphId<FrameGraphTexture> const fgViewRenderTarget = fg.import("viewRenderTarget", {
.attachments = attachmentMask,
.viewport = DEBUG_DYNAMIC_SCALING ? svp : vp,
.clearColor = clearColor,
.samples = 0,
.clearFlags = clearFlags,
.keepOverrideStart = keepOverrideStartFlags,
.keepOverrideEnd = keepOverrideEndFlags
}, viewRenderTarget);
const TextureFormat hdrFormat = getHdrFormat(view, needsAlphaChannel);
// the clearFlags and clearColor specified below will only apply when rendering into the
// temporary color buffer. In particular, they won't apply when rendering into the main
// swapchain (imported render target above)
RendererUtils::ColorPassConfig config{
.physicalViewport = svp,
.logicalViewport = xvp,
.scale = scale,
.hdrFormat = hdrFormat,
.msaa = msaaSampleCount,
.clearFlags = getClearFlags(),
.clearColor = clearColor,
.clearStencil = clearStencil,
.ssrLodOffset = 0.0f,
.hasContactShadows = scene.hasContactShadows(),
// at this point we don't know if we have refraction, but that's handled later
.hasScreenSpaceReflectionsOrRefractions = ssReflectionsOptions.enabled,
.enabledStencilBuffer = view.isStencilBufferEnabled()
};
/*
* Depth + Color passes
*/
// updatePrimitivesLod must be run before appendCommands and once for each set
// of RenderPass::setCamera / RenderPass::setGeometry calls.
FView::updatePrimitivesLod(scene.getRenderableData(),
engine, cameraInfo, view.getVisibleRenderables());
passBuilder.camera(cameraInfo);
passBuilder.geometry(scene.getRenderableData(), view.getVisibleRenderables());
// view set-ups that need to happen before rendering
fg.addTrivialSideEffectPass("Prepare View Uniforms",
[=, &view, &engine](DriverApi& driver) {
view.prepareCamera(engine, cameraInfo);
// The code here is a little fragile. In theory, we need to call prepareViewport()
// for each render pass, because the viewport parameters depend on the resolution.
// However, in practice, we only have two resolutions: the color pass resolution,
// and the structure pass which is governed by aoOptions.resolution (this could
// change in the future).
// So here we set the parameters for the structure pass and SSAO passes which
// are always done first. The SSR pass will also use these parameters which
// is wrong if it doesn't run at the same resolution as SSAO.
// prepareViewport() is called again during the color pass, which resets the
// values correctly for the Color pass, however, this will be again wrong
// for passes that come after the Color pass, such as DoF.
//
// The solution is to call prepareViewport() for each pass, really (so we should
// move it to its own UBO).
//
// The reason why this bug is acceptable is that the viewport parameters are
// currently only used for generating noise, so it's not too bad.
// note: aoOptions.resolution is either 1.0 or 0.5, and the result is then
// guaranteed to be an integer (because xvp is a multiple of 16).
view.prepareViewport(svp,
filament::Viewport{
int32_t(float(xvp.left ) * aoOptions.resolution),
int32_t(float(xvp.bottom) * aoOptions.resolution),
uint32_t(float(xvp.width ) * aoOptions.resolution),
uint32_t(float(xvp.height) * aoOptions.resolution)});
// this needs to reset the sampler that are only set in RendererUtils::colorPass(), because
// this descriptor-set is also used for ssr/picking/structure and these could be stale
// it would be better to use a separate desriptor-set for those two cases so that we don't
// have to do this
view.unbindSamplers(engine);
view.commitUniformsAndSamplers(driver);
});
// --------------------------------------------------------------------------------------------
// structure pass -- automatically culled if not used
// Currently it consists of a simple depth pass.
// This is normally used by SSAO and contact-shadows.
// Also, picking is handled here if transparent picking is disabled.
// TODO: the scaling should depends on all passes that need the structure pass
const auto [structure, picking_] = ppm.structure(fg,
passBuilder, renderFlags, svp.width, svp.height, {
.scale = aoOptions.resolution,
.picking = view.hasPicking() && !view.isTransparentPickingEnabled()
});
auto picking = picking_;
// --------------------------------------------------------------------------------------------
// Picking pass -- automatically culled if not used
// Picking is handled here if transparent picking is enabled.
if (view.hasPicking()) {
if (view.isTransparentPickingEnabled()) {
struct PickingRenderPassData {
FrameGraphId<FrameGraphTexture> depth;
FrameGraphId<FrameGraphTexture> picking;
};
auto& pickingRenderPass = fg.addPass<PickingRenderPassData>("Picking Render Pass",
[&](FrameGraph::Builder& builder, auto& data) {
bool const isFL0 = mEngine.getDriverApi().getFeatureLevel() ==
FeatureLevel::FEATURE_LEVEL_0;
// TODO: Specify the precision for picking pass
uint32_t const width = std::max(32u,
(uint32_t)std::ceil(float(svp.width) * aoOptions.resolution));
uint32_t const height = std::max(32u,
(uint32_t)std::ceil(float(svp.height) * aoOptions.resolution));
data.depth = builder.createTexture("Depth Buffer", {
.width = width, .height = height,
.format = isFL0 ? TextureFormat::DEPTH24 : TextureFormat::DEPTH32F });
data.depth = builder.write(data.depth,
FrameGraphTexture::Usage::DEPTH_ATTACHMENT);
data.picking = builder.createTexture("Picking Buffer", {
.width = width, .height = height,
.format = isFL0 ? TextureFormat::RGBA8 : TextureFormat::RG32F });
data.picking = builder.write(data.picking,
FrameGraphTexture::Usage::COLOR_ATTACHMENT);
builder.declareRenderPass("Picking Render Target", {
.attachments = {.color = { data.picking }, .depth = data.depth },
.clearFlags = TargetBufferFlags::COLOR0 | TargetBufferFlags::DEPTH
});
},
[=, passBuilder = passBuilder](FrameGraphResources const& resources,
auto const& data, DriverApi& driver) mutable {
Variant pickingVariant(Variant::DEPTH_VARIANT);
pickingVariant.setPicking(true);
auto out = resources.getRenderPassInfo();
passBuilder.renderFlags(renderFlags);
passBuilder.variant(pickingVariant);
passBuilder.commandTypeFlags(RenderPass::CommandTypeFlags::DEPTH);
RenderPass const pass{ passBuilder.build(mEngine, driver) };
driver.beginRenderPass(out.target, out.params);
pass.getExecutor().execute(mEngine, driver);
driver.endRenderPass();
});
picking = pickingRenderPass->picking;
}
struct PickingResolvePassData {
FrameGraphId<FrameGraphTexture> picking;
};
fg.addPass<PickingResolvePassData>(
"Picking Resolve Pass",
[&](FrameGraph::Builder& builder, auto& data) {
// Note that BLIT_SRC is needed because this texture will be read later (via
// readPixels()).
data.picking =
builder.read(picking, FrameGraphTexture::Usage::COLOR_ATTACHMENT |
FrameGraphTexture::Usage::BLIT_SRC);
builder.declareRenderPass("Picking Resolve Target", {
.attachments = { .color = { data.picking }}
});
builder.sideEffect();
},
[=, &view](FrameGraphResources const& resources,
auto const&, DriverApi& driver) mutable {
auto out = resources.getRenderPassInfo();
view.executePickingQueries(driver, out.target, scale * aoOptions.resolution);
});
}
// Store this frame's camera projection in the frame history.
if (UTILS_UNLIKELY(taaOptions.enabled)) {
// Apply the TAA jitter to everything after the structure pass, starting with the color pass.
ppm.TaaJitterCamera(svp, taaOptions, view.getFrameHistory(),
&FrameHistoryEntry::taa, &cameraInfo);
fg.addTrivialSideEffectPass("Jitter Camera",
[&engine, &cameraInfo, &descriptorSet = view.getColorPassDescriptorSet()]
(DriverApi& driver) {
descriptorSet.prepareCamera(engine, cameraInfo);
descriptorSet.commit(driver);
});
}
// --------------------------------------------------------------------------------------------
// SSAO pass
if (aoOptions.enabled) {
// we could rely on FrameGraph culling, but this creates unnecessary CPU work
auto ssao = ppm.screenSpaceAmbientOcclusion(fg, svp, cameraInfo, structure, aoOptions);
blackboard["ssao"] = ssao;
}
// --------------------------------------------------------------------------------------------
// prepare screen-space reflection/refraction passes
PostProcessManager::ScreenSpaceRefConfig const ssrConfig = PostProcessManager::prepareMipmapSSR(
fg, svp.width, svp.height,
ssReflectionsOptions.enabled ? TextureFormat::RGBA16F : TextureFormat::R11F_G11F_B10F,
view.getCameraUser().getFieldOfView(Camera::Fov::VERTICAL), config.scale);
config.ssrLodOffset = ssrConfig.lodOffset;
// --------------------------------------------------------------------------------------------
// screen-space reflections pass
if (ssReflectionsOptions.enabled) {
auto reflections = ppm.ssr(fg, passBuilder,
view.getFrameHistory(), cameraInfo,
structure,
ssReflectionsOptions,
{ .width = svp.width, .height = svp.height });
if (UTILS_LIKELY(reflections)) {
// generate the mipchain
PostProcessManager::generateMipmapSSR(ppm, fg,
reflections, ssrConfig.reflection, false, ssrConfig);
}
config.screenSpaceReflectionHistoryNotReady = !reflections;
}
// --------------------------------------------------------------------------------------------
// Color passes
// this makes the viewport relative to xvp
// FIXME: we should use 'vp' when rendering directly into the swapchain, but that's hard to
// know at this point. This will usually be the case when post-process is disabled.
// FIXME: we probably should take the dynamic scaling into account too
// if MSAA is enabled, we end-up rendering in an intermediate buffer. This is the only case where
// "!hasPostProcess" doesn't guarantee rendering into the swapchain.
const bool useIntermediateBuffer = hasPostProcess || msaaOptions.enabled ||
(isRenderingMultiview && engine.debug.stereo.combine_multiview_images);
passBuilder.scissorViewport(useIntermediateBuffer ? xvp : vp);
// This one doesn't need to be a FrameGraph pass because it always happens by construction
// (i.e. it won't be culled, unless everything is culled), so no need to complexify things.
passBuilder.variant(variant);
// This is optional, if not set, the per-view descriptor-set must be set before calling execute()
passBuilder.colorPassDescriptorSet(&view.getColorPassDescriptorSet());
// color-grading as subpass is done either by the color pass or the TAA pass if any
auto colorGradingConfigForColor = colorGradingConfig;
colorGradingConfigForColor.asSubpass = colorGradingConfigForColor.asSubpass && !taaOptions.enabled;
if (colorGradingConfigForColor.asSubpass) {
// append color grading subpass after all other passes
passBuilder.customCommand(3,
RenderPass::Pass::BLENDED,
RenderPass::CustomCommand::EPILOG,
0, [&ppm, &driver, colorGradingConfigForColor]() {
ppm.colorGradingSubpass(driver, colorGradingConfigForColor);
});
} else if (colorGradingConfig.customResolve) {
// append custom resolve subpass after all other passes
passBuilder.customCommand(3,
RenderPass::Pass::BLENDED,
RenderPass::CustomCommand::EPILOG,
0, [&ppm, &driver]() {
ppm.customResolveSubpass(driver);
});
}
passBuilder.commandTypeFlags(RenderPass::CommandTypeFlags::COLOR);
// RenderPass::IS_INSTANCED_STEREOSCOPIC only applies to the color pass
if (view.hasStereo() &&
engine.getConfig().stereoscopicType == StereoscopicType::INSTANCED) {
renderFlags |= RenderPass::IS_INSTANCED_STEREOSCOPIC;
passBuilder.renderFlags(renderFlags);
}
RenderPass const pass{ passBuilder.build(engine, driver) };
FrameGraphTexture::Descriptor colorBufferDesc = {
.width = config.physicalViewport.width,
.height = config.physicalViewport.height,
.format = config.hdrFormat
};
// Set the depth to the number of layers if we're rendering multiview.
if (isRenderingMultiview) {
colorBufferDesc.depth = engine.getConfig().stereoscopicEyeCount;
colorBufferDesc.type = SamplerType::SAMPLER_2D_ARRAY;
}
// a non-drawing pass to prepare everything that need to be before the color passes execute
fg.addTrivialSideEffectPass("Prepare Color Passes",
[=, &js, &view, &ppm](DriverApi& driver) {
// prepare color grading as subpass material
if (colorGradingConfig.asSubpass) {
ppm.colorGradingPrepareSubpass(driver,
colorGrading, colorGradingConfig, vignetteOptions,
colorBufferDesc.width, colorBufferDesc.height);
} else if (colorGradingConfig.customResolve) {
ppm.customResolvePrepareSubpass(driver,
PostProcessManager::CustomResolveOp::COMPRESS);
}
// We use a framegraph pass to wait for froxelization to finish (so it can be done
// in parallel with .compile()
auto sync = view.getFroxelizerSync();
if (sync) {
js.waitAndRelease(sync);
view.commitFroxels(driver);
}
}
);
// the color pass itself + color-grading as subpass if needed
auto colorPassOutput = RendererUtils::colorPass(fg, "Color Pass", mEngine, view, {
.shadows = blackboard.get<FrameGraphTexture>("shadows"),
.ssao = blackboard.get<FrameGraphTexture>("ssao"),
.ssr = ssrConfig.ssr,
.structure = structure
},
colorBufferDesc, config, colorGradingConfigForColor, pass.getExecutor());
if (view.isScreenSpaceRefractionEnabled() && !pass.empty()) {
// This cancels the colorPass() call above if refraction is active.
// The color pass + refraction + color-grading as subpass if needed
auto const output = RendererUtils::refractionPass(fg, mEngine, view, {
.shadows = blackboard.get<FrameGraphTexture>("shadows"),
.ssao = blackboard.get<FrameGraphTexture>("ssao"),
.ssr = ssrConfig.ssr,
.structure = structure
},
config, ssrConfig, colorGradingConfigForColor, pass);
hasScreenSpaceRefraction = output.has_value();
if (hasScreenSpaceRefraction) {
colorPassOutput = output.value();
}
}
if (colorGradingConfig.customResolve) {
assert_invariant(fg.getDescriptor(colorPassOutput.linearColor).samples <= 1);
// TODO: we have to "uncompress" (i.e. detonemap) the color buffer here because it's used
// by many other passes (Bloom, TAA, DoF, etc...). We could make this more
// efficient by using ARM_shader_framebuffer_fetch. We use a load/store (i.e.
// subpass) here because it's more convenient.
colorPassOutput.linearColor =
ppm.customResolveUncompressPass(fg, colorPassOutput.linearColor);
}
// export the color buffer if screen-space reflections are enabled
if (ssReflectionsOptions.enabled) {
struct ExportSSRHistoryData {
FrameGraphId<FrameGraphTexture> history;
};
// FIXME: should we use the TAA-modified cameraInfo here or not? (we are).
mat4 const projection = cameraInfo.projection * cameraInfo.getUserViewMatrix();
fg.addPass<ExportSSRHistoryData>("Export SSR history",
[&](FrameGraph::Builder& builder, auto& data) {
// We need to use sideEffect here to ensure this pass won't be culled.
// The "output" of this pass is going to be used during the next frame as
// an "import".
builder.sideEffect();
// we can't use colorPassOutput here because it could be tonemapped
data.history = builder.sample(colorPassOutput.linearColor); // FIXME: an access must be declared for detach(), why?
}, [&view, projection](FrameGraphResources const& resources, auto const& data,
DriverApi&) {
auto& history = view.getFrameHistory();
auto& current = history.getCurrent();
current.ssr.projection = projection;
resources.detach(data.history, &current.ssr.color, &current.ssr.desc);
});
}
// this is the output of the color pass / input to post processing,
// this is only used later for comparing it with the output after post-processing
FrameGraphId<FrameGraphTexture> const postProcessInput = colorGradingConfig.asSubpass ?
colorPassOutput.tonemappedColor :
colorPassOutput.linearColor;
// input can change below
FrameGraphId<FrameGraphTexture> input = postProcessInput;
// Resolve depth -- which might be needed because of TAA or DoF. This pass will be culled
// if the depth is not used below or if the depth is not MS (e.g. it could have been
// auto-resolved).
// In practice, this is used on Vulkan and older Metal devices.
auto depth = ppm.resolve(fg, "Resolved Depth Buffer", colorPassOutput.depth, { .levels = 1 });
// Debug: CSM visualisation
if (UTILS_UNLIKELY(engine.debug.shadowmap.visualize_cascades &&
view.hasShadowing() && view.hasDirectionalLighting())) {
input = ppm.debugShadowCascades(fg, input, depth);
}
// TODO: DoF should be applied here, before TAA -- but if we do this it'll result in a lot of
// fireflies due to the instability of the highlights. This can be fixed with a
// dedicated TAA pass for the DoF, as explained in
// "Life of a Bokeh" by Guillaume Abadie, SIGGRAPH 2018
// TAA for color pass
if (taaOptions.enabled) {
input = ppm.taa(fg, input, depth, view.getFrameHistory(), &FrameHistoryEntry::taa,
taaOptions, colorGradingConfig);
if (taaOptions.upscaling) {
scale = 1.0f;
scaled = false;
UTILS_UNUSED_IN_RELEASE auto const& inputDesc = fg.getDescriptor(input);
svp.width = inputDesc.width;
svp.height = inputDesc.height;
xvp.width *= 2;
xvp.height *= 2;
}
}
// --------------------------------------------------------------------------------------------
// Post Processing...
UTILS_UNUSED_IN_RELEASE auto const& inputDesc = fg.getDescriptor(input);
assert_invariant(inputDesc.width == svp.width);
assert_invariant(inputDesc.height == svp.height);
bool mightNeedFinalBlit = true;
if (hasPostProcess) {
if (dofOptions.enabled) {
// The bokeh height is always correct regardless of the dynamic resolution scaling.
// (because the CoC is calculated w.r.t. the height), so we only need to adjust
// the width.
float const aspect = (scale.x / scale.y) * dofOptions.cocAspectRatio;
float2 const bokehScale{
aspect < 1.0f ? aspect : 1.0f,
aspect > 1.0f ? 1.0f / aspect : 1.0f
};
input = ppm.dof(fg, input, depth, cameraInfo, needsAlphaChannel,
bokehScale, dofOptions);
}
FrameGraphId<FrameGraphTexture> bloom, flare;
if (bloomOptions.enabled) {
// Generate the bloom buffer, which is stored in the blackboard as "bloom". This is
// consumed by the colorGrading pass and will be culled if colorGrading is disabled.
auto [bloom_, flare_] = ppm.bloom(fg, input, TextureFormat::R11F_G11F_B10F,
bloomOptions, taaOptions, scale);
bloom = bloom_;
flare = flare_;
}
if (hasColorGrading) {
if (!colorGradingConfig.asSubpass) {
input = ppm.colorGrading(fg, input, xvp,
bloom, flare,
colorGrading, colorGradingConfig,
bloomOptions, vignetteOptions);
// the padded buffer is resolved now
xvp.left = xvp.bottom = 0;
svp = xvp;
}
}
if (hasFXAA) {
bool const preserveAlphaChannel =
// we're transparent -- alpha channel has user data
needsAlphaChannel ||
// the color-grading pass outputted the luminance channel, and we have an upscaling pass
(hasColorGrading && colorGradingConfig.outputLuminance && scaled);
input = ppm.fxaa(fg, input, xvp, colorGradingConfig.ldrFormat, preserveAlphaChannel);
// the padded buffer is resolved now
xvp.left = xvp.bottom = 0;
svp = xvp;
}
if (scaled) {
mightNeedFinalBlit = false;
auto viewport = DEBUG_DYNAMIC_SCALING ? xvp : vp;
bool const sourceHasLuminance = !needsAlphaChannel &&
(hasColorGrading && colorGradingConfig.outputLuminance);
input = ppm.upscale(fg, needsAlphaChannel, sourceHasLuminance, dsrOptions, input, xvp, {
.width = viewport.width, .height = viewport.height,
.format = colorGradingConfig.ldrFormat }, SamplerMagFilter::LINEAR);
xvp.left = xvp.bottom = 0;
svp = xvp;
}
}
// Debug: combine the array texture for multiview into a single image.
if (UTILS_UNLIKELY(isRenderingMultiview && engine.debug.stereo.combine_multiview_images)) {
input = ppm.debugCombineArrayTexture(fg, blendModeTranslucent, input, xvp, {
.width = vp.width, .height = vp.height,
.format = colorGradingConfig.ldrFormat },
SamplerMagFilter::NEAREST, SamplerMinFilter::NEAREST);
}
if (UTILS_UNLIKELY((input == postProcessInput && viewRenderTarget == mRenderTargetHandle) &&
view.isStencilBufferEnabled())) {
// FIXME: I think this check is incomplete, if we're rendering into a custom rendertarget
// we need to check that it (not the swapchain) has a stencil buffer.
assert_invariant(mSwapChain);
FILAMENT_CHECK_PRECONDITION(mSwapChain->hasStencilBuffer())
<< "View has stencil buffer enabled, but SwapChain does not have "
"SwapChain::CONFIG_HAS_STENCIL_BUFFER flag set.";
}
/*
* Here we're ready to present the output of the framegraph, which is held in `input`.
* The presentation itself happens by forwarding `input` to the render target, which can
* either be the SwapChain's or the View's custom render target. This is done below with
* `forwardResource()`.
*
* There are however a few situations where `input` cannot be forwarded in this manner, and
* an intermediate buffer is needed instead.
*
* 1. Blending is needed (blendModeTranslucent)
* 2. Dimensions don't match, e.g.: because we have guard bands (xvp != svp)
* 3. MRT is needed (colorGradingConfig.asSubpass)
* 4. Frame history is needed (hasScreenSpaceRefraction)
* 5. Refraction is used because how clear flags work (ssReflectionsOptions.enabled)
* 6. `input` and the render target are not compatible:
* - MSAA doesn't match. Note: auto-resolve could work if the SwapChain was configured
* this way.
*
* One complication is that post-processing passes handle some of these issues automatically:
* - each post-processing pass will create an intermediate buffer, which takes care of
* - MSAA (because of auto-resolve)
* - MRT is needed
* - Frame history is needed
* - Refraction
* - the upscaling pass takes care of all the above plus blending and dimensions mismatch.
*
* TODO: we could make this work with custom render targets because we can access the texture
* for those and it could behave just like a regular resource. This would lift
* some (but not all) of those limitations. For instance we could use MRTs.
*/
if (mightNeedFinalBlit) {
assert_invariant(!scaled);
// Determine if our `input` is in fact the output of the color pass, in which case
// many of the caveat above apply.
bool const inputIsColorPass = (input == postProcessInput);
if (blendModeTranslucent ||
xvp != svp ||
(inputIsColorPass &&
(msaaSampleCount > 1 ||
colorGradingConfig.asSubpass ||
hasScreenSpaceRefraction ||
ssReflectionsOptions.enabled))) {
input = ppm.blit(fg, blendModeTranslucent, input, xvp, {
.width = vp.width, .height = vp.height,
.format = colorGradingConfig.ldrFormat },
SamplerMagFilter::NEAREST, SamplerMinFilter::NEAREST);
}
}
if (UTILS_UNLIKELY(engine.debug.shadowmap.display_shadow_texture)) {
auto shadowmap = blackboard.get<FrameGraphTexture>("shadowmap");
input = ppm.debugDisplayShadowTexture(fg, input, shadowmap,
engine.debug.shadowmap.display_shadow_texture_scale,
engine.debug.shadowmap.display_shadow_texture_layer,
engine.debug.shadowmap.display_shadow_texture_level,
engine.debug.shadowmap.display_shadow_texture_channel,
engine.debug.shadowmap.display_shadow_texture_power);
}
// auto debug = structure
// fg.forwardResource(fgViewRenderTarget, debug ? debug : input);
fg.forwardResource(fgViewRenderTarget, input);
fg.present(fgViewRenderTarget);
fg.compile();
#if FILAMENT_ENABLE_FGVIEWER
fgviewer::DebugServer* fgviewerServer = engine.debug.fgviewerServer;
if (UTILS_LIKELY(fgviewerServer)) {
fgviewerServer->update(view.getViewHandle(), fg.getFrameGraphInfo(view.getName()));
}
#endif
//fg.export_graphviz(slog.d, view.getName());
fg.execute(driver);
// save the current history entry and destroy the oldest entry
view.commitFrameHistory(engine);
recordHighWatermark(commandArena.getListener().getHighWatermark());
}
} // namespace filament