diff --git a/android/filament-android/src/main/cpp/Renderer.cpp b/android/filament-android/src/main/cpp/Renderer.cpp index 4f7d88f319..405fddaf1c 100644 --- a/android/filament-android/src/main/cpp/Renderer.cpp +++ b/android/filament-android/src/main/cpp/Renderer.cpp @@ -153,3 +153,13 @@ Java_com_google_android_filament_Renderer_nSetDisplayInfo(JNIEnv*, jclass, jlong .presentationDeadlineNanos = (uint64_t)presentationDeadlineNanos, .vsyncOffsetNanos = (uint64_t)vsyncOffsetNanos }); } + +extern "C" JNIEXPORT void JNICALL +Java_com_google_android_filament_Renderer_nSetFrameRateOptions(JNIEnv*, jclass, + jlong nativeRenderer, jfloat interval, jfloat headRoomRatio, jfloat scaleRate, jint history) { + Renderer *renderer = (Renderer *) nativeRenderer; + renderer->setFrameRateOptions({ .headRoomRatio = headRoomRatio, + .scaleRate = scaleRate, + .history = (uint8_t)history, + .interval = (uint8_t)interval }); +} diff --git a/android/filament-android/src/main/cpp/View.cpp b/android/filament-android/src/main/cpp/View.cpp index e7638988ce..2089fe1b0d 100644 --- a/android/filament-android/src/main/cpp/View.cpp +++ b/android/filament-android/src/main/cpp/View.cpp @@ -159,20 +159,15 @@ Java_com_google_android_filament_View_nGetDithering(JNIEnv*, jclass, } extern "C" JNIEXPORT void JNICALL -Java_com_google_android_filament_View_nSetDynamicResolutionOptions(JNIEnv*, - jclass, jlong nativeView, jboolean enabled, jboolean homogeneousScaling, - jfloat targetFrameTimeMilli, jfloat headRoomRatio, jfloat scaleRate, - jfloat minScale, jfloat maxScale, jint history, jint quality) { +Java_com_google_android_filament_View_nSetDynamicResolutionOptions(JNIEnv*, jclass, jlong nativeView, + jboolean enabled, jboolean homogeneousScaling, + jfloat minScale, jfloat maxScale, jint quality) { View* view = (View*)nativeView; View::DynamicResolutionOptions options; options.enabled = enabled; options.homogeneousScaling = homogeneousScaling; - options.targetFrameTimeMilli = targetFrameTimeMilli; - options.headRoomRatio = headRoomRatio; - options.scaleRate = scaleRate; options.minScale = filament::math::float2{ minScale }; options.maxScale = filament::math::float2{ maxScale }; - options.history = (uint8_t)history; options.quality = (View::QualityLevel)quality; view->setDynamicResolutionOptions(options); } diff --git a/android/filament-android/src/main/java/com/google/android/filament/Renderer.java b/android/filament-android/src/main/java/com/google/android/filament/Renderer.java index 5479563a4a..59c93c52c9 100644 --- a/android/filament-android/src/main/java/com/google/android/filament/Renderer.java +++ b/android/filament-android/src/main/java/com/google/android/filament/Renderer.java @@ -44,6 +44,8 @@ import java.nio.ReadOnlyBufferException; public class Renderer { private final Engine mEngine; private long mNativeObject; + private DisplayInfo mDisplayInfo; + private FrameRateOptions mFrameRateOptions; /** * Information about the display this renderer is associated to @@ -68,6 +70,50 @@ public class Renderer { public long vsyncOffsetNanos = 0; }; + /** + * Use FrameRateOptions to set the desired frame rate and control how quickly the system + * reacts to GPU load changes. + * + * interval: desired frame interval in multiple of the refresh period, set in DisplayInfo + * (as 1 / DisplayInfo.refreshRate) + * + * The parameters below are relevant when some Views are using dynamic resolution scaling: + * + * headRoomRatio: additional headroom for the GPU as a ratio of the targetFrameTime. + * Useful for taking into account constant costs like post-processing or + * GPU drivers on different platforms. + * history: History size. higher values, tend to filter more (clamped to 30) + * scaleRate: rate at which the gpu load is adjusted to reach the target frame rate + * This value can be computed as 1 / N, where N is the number of frames + * needed to reach 64% of the target scale factor. + * Higher values make the dynamic resolution react faster. + * + * @see View.DynamicResolutionOptions + * @see Renderer.DisplayInfo + * + */ + public static class FrameRateOptions { + /** + * Desired frame interval in unit of 1 / DisplayInfo.refreshRate. + */ + public float interval = 1.0f / 60.0f; + + /** + * Additional headroom for the GPU as a ratio of the targetFrameTime. + */ + public float headRoomRatio = 0.0f; + + /** + * Rate at which the scale will change to reach the target frame rate. + */ + public float scaleRate = 0.125f; + + /** + * History size. higher values, tend to filter more (clamped to 30). + */ + public int history = 9; + } + /** * Indicates that the dstSwapChain passed into {@link #copyFrame} should be * committed after the frame has been copied. @@ -103,7 +149,43 @@ public class Renderer { * to accurately compute dynamic-resolution scaling and for frame-pacing. */ public void setDisplayInfo(@NonNull DisplayInfo info) { - nSetDisplayInfo(getNativeObject(), info.refreshRate, info.presentationDeadlineNanos, info.vsyncOffsetNanos); + mDisplayInfo = info; + nSetDisplayInfo(getNativeObject(), + info.refreshRate, info.presentationDeadlineNanos, info.vsyncOffsetNanos); + } + + /** + * Returns the DisplayInfo object set in {@link #setDisplayInfo} or a new instance otherwise. + * @return a DisplayInfo instance + */ + @NonNull + public DisplayInfo getDisplayInfo() { + if (mDisplayInfo == null) { + mDisplayInfo = new DisplayInfo(); + } + return mDisplayInfo; + } + + /** + * Set options controlling the desired frame-rate. + */ + public void setFrameRateOptions(@NonNull FrameRateOptions options) { + mFrameRateOptions = options; + nSetFrameRateOptions(getNativeObject(), + options.interval, options.headRoomRatio, options.scaleRate, options.history); + } + + /** + * Returns the FrameRateOptions object set in {@link #setFrameRateOptions} or a new instance + * otherwise. + * @return a FrameRateOptions instance + */ + @NonNull + public FrameRateOptions getFrameRateOptions() { + if (mFrameRateOptions == null) { + mFrameRateOptions = new FrameRateOptions(); + } + return mFrameRateOptions; } /** @@ -520,4 +602,6 @@ public class Renderer { private static native void nResetUserTime(long nativeRenderer); private static native void nSetDisplayInfo(long nativeRenderer, float refreshRate, long presentationDeadlineNanos, long vsyncOffsetNanos); + private static native void nSetFrameRateOptions(long nativeRenderer, + float interval, float headRoomRatio, float scaleRate, int history); } diff --git a/android/filament-android/src/main/java/com/google/android/filament/View.java b/android/filament-android/src/main/java/com/google/android/filament/View.java index 7c3fbd4fb9..a5de86fc9a 100644 --- a/android/filament-android/src/main/java/com/google/android/filament/View.java +++ b/android/filament-android/src/main/java/com/google/android/filament/View.java @@ -105,21 +105,6 @@ public class View { */ public boolean homogeneousScaling = false; - /** - * Desired frame time in milliseconds. - */ - public float targetFrameTimeMilli = 1000.0f / 60.0f; - - /** - * Additional headroom for the GPU as a ratio of the targetFrameTime. - */ - public float headRoomRatio = 0.0f; - - /** - * Rate at which the scale will change to reach the target frame rate. - */ - public float scaleRate = 0.125f; - /** * The minimum scale in X and Y this View should use. */ @@ -130,11 +115,6 @@ public class View { */ public float maxScale = 1.0f; - /** - * History size. higher values, tend to filter more (clamped to 30). - */ - public int history = 9; - /** * Upscaling quality. LOW: 1 bilinear taps, MEDIUM: 4 bilinear taps, HIGH: 9 bilinear taps. * If minScale needs to be very low, it might help to use MEDIUM or HIGH here. @@ -714,12 +694,8 @@ public class View { nSetDynamicResolutionOptions(getNativeObject(), options.enabled, options.homogeneousScaling, - options.targetFrameTimeMilli, - options.headRoomRatio, - options.scaleRate, options.minScale, options.maxScale, - options.history, options.quality.ordinal()); } @@ -957,10 +933,7 @@ public class View { private static native int nGetToneMapping(long nativeView); private static native void nSetDithering(long nativeView, int dithering); private static native int nGetDithering(long nativeView); - private static native void nSetDynamicResolutionOptions(long nativeView, - boolean enabled, boolean homogeneousScaling, - float targetFrameTimeMilli, float headRoomRatio, float scaleRate, - float minScale, float maxScale, int history, int quality); + private static native void nSetDynamicResolutionOptions(long nativeView, boolean enabled, boolean homogeneousScaling, float minScale, float maxScale, int quality); private static native void nSetRenderQuality(long nativeView, int hdrColorBufferQuality); private static native void nSetDynamicLightingOptions(long nativeView, float zLightNear, float zLightFar); private static native void nSetPostProcessingEnabled(long nativeView, boolean enabled); diff --git a/android/samples/sample-gltf-viewer/src/main/java/com/google/android/filament/gltf/MainActivity.kt b/android/samples/sample-gltf-viewer/src/main/java/com/google/android/filament/gltf/MainActivity.kt index 2f0c5542be..c165be69b5 100644 --- a/android/samples/sample-gltf-viewer/src/main/java/com/google/android/filament/gltf/MainActivity.kt +++ b/android/samples/sample-gltf-viewer/src/main/java/com/google/android/filament/gltf/MainActivity.kt @@ -23,6 +23,7 @@ import android.view.Choreographer import android.view.GestureDetector import android.view.MotionEvent import android.view.SurfaceView +import com.google.android.filament.View import com.google.android.filament.utils.KtxLoader import com.google.android.filament.utils.ModelViewer import com.google.android.filament.utils.Utils @@ -65,6 +66,12 @@ class MainActivity : Activity() { val options = modelViewer.view.dynamicResolutionOptions options.enabled = true; modelViewer.view.dynamicResolutionOptions = options; + + modelViewer.view.ambientOcclusion = View.AmbientOcclusion.SSAO + + val bloom = modelViewer.view.bloomOptions + bloom.enabled = true; + modelViewer.view.bloomOptions = bloom } private fun createRenderables() { diff --git a/filament/include/filament/Renderer.h b/filament/include/filament/Renderer.h index ddd77a3ed6..0d564ebcc4 100644 --- a/filament/include/filament/Renderer.h +++ b/filament/include/filament/Renderer.h @@ -80,6 +80,35 @@ public: uint64_t vsyncOffsetNanos = 0; }; + /** + * Use FrameRateOptions to set the desired frame rate and control how quickly the system + * reacts to GPU load changes. + * + * interval: desired frame interval in multiple of the refresh period, set in DisplayInfo + * (as 1 / DisplayInfo::refreshRate) + * + * The parameters below are relevant when some Views are using dynamic resolution scaling: + * + * headRoomRatio: additional headroom for the GPU as a ratio of the targetFrameTime. + * Useful for taking into account constant costs like post-processing or + * GPU drivers on different platforms. + * history: History size. higher values, tend to filter more (clamped to 30) + * scaleRate: rate at which the gpu load is adjusted to reach the target frame rate + * This value can be computed as 1 / N, where N is the number of frames + * needed to reach 64% of the target scale factor. + * Higher values make the dynamic resolution react faster. + * + * @see View::DynamicResolutionOptions + * @see Renderer::DisplayInfo + * + */ + struct FrameRateOptions { + float headRoomRatio = 0.0f; //!< additional headroom for the GPU + float scaleRate = 0.125f; //!< rate at which the system reacts to load changes + uint8_t history = 9; //!< history size + uint8_t interval = 1; //!< desired frame interval in unit of 1.0 / DisplayInfo::refreshRate + }; + /** * Information about the display this Renderer is associated to. This information is needed * to accurately compute dynamic-resolution scaling and for frame-pacing. @@ -88,6 +117,13 @@ public: */ void setDisplayInfo(const DisplayInfo& info) noexcept; + /** + * Set options controlling the desired frame-rate. + * + * @param options + */ + void setFrameRateOptions(FrameRateOptions const& options) noexcept; + /** * Get the Engine that created this Renderer. * diff --git a/filament/include/filament/View.h b/filament/include/filament/View.h index 9a2a76c3ec..3e881714e7 100644 --- a/filament/include/filament/View.h +++ b/filament/include/filament/View.h @@ -83,15 +83,6 @@ public: * enabled: enable or disables dynamic resolution on a View * homogeneousScaling: by default the system scales the major axis first. Set this to true * to force homogeneous scaling. - * scaleRate: rate at which the scale will change to reach the target frame rate - * This value can be computed as 1 / N, where N is the number of frames - * needed to reach 64% of the target scale factor. - * Higher values make the dynamic resolution react faster. - * targetFrameTimeMilli: desired frame time in milliseconds - * headRoomRatio: additional headroom for the GPU as a ratio of the targetFrameTime. - * Useful for taking into account constant costs like post-processing or - * GPU drivers on different platforms. - * history: History size. higher values, tend to filter more (clamped to 30) * minScale: the minimum scale in X and Y this View should use * maxScale: the maximum scale in X and Y this View should use * quality: upscaling quality. @@ -101,25 +92,13 @@ public: * Dynamic resolution is only supported on platforms where the time to render * a frame can be measured accurately. Dynamic resolution is currently only * supported on Android. + * + * @see Renderer::FrameRateOptions + * */ struct DynamicResolutionOptions { - DynamicResolutionOptions() = default; - - DynamicResolutionOptions(bool enabled, float scaleRate, - math::float2 minScale, math::float2 maxScale) - : minScale(minScale), maxScale(maxScale), - scaleRate(scaleRate), enabled(enabled) { - // this one exists for backward compatibility - } - - explicit DynamicResolutionOptions(bool enabled) : enabled(enabled) { } - math::float2 minScale = math::float2(0.5f); //!< minimum scale factors in x and y math::float2 maxScale = math::float2(1.0f); //!< maximum scale factors in x and y - float scaleRate = 0.125f; //!< rate at which the scale will change - float targetFrameTimeMilli = 1000.0f / 60.0f; //!< desired frame time, or budget. - float headRoomRatio = 0.0f; //!< additional headroom for the GPU - uint8_t history = 9; //!< history size bool enabled = false; //!< enable or disable dynamic resolution bool homogeneousScaling = false; //!< set to true to force homogeneous scaling QualityLevel quality = QualityLevel::LOW; //!< Upscaling quality diff --git a/filament/src/FrameInfo.cpp b/filament/src/FrameInfo.cpp index 89084725b2..5a4fb88713 100644 --- a/filament/src/FrameInfo.cpp +++ b/filament/src/FrameInfo.cpp @@ -26,6 +26,17 @@ namespace filament { using namespace utils; using namespace details; +namespace details { +// this is to avoid a call to memmove +template +static inline +void move_backward(InputIterator first, InputIterator last, OutputIterator result) { + while (first != last) { + *--result = *--last; + } +} +} // namespace details + FrameInfoManager::FrameInfoManager(FEngine& engine) : mEngine(engine) { backend::DriverApi& driver = mEngine.getDriverApi(); for (auto& query : mQueries) { @@ -42,14 +53,16 @@ void FrameInfoManager::terminate() { } } -void FrameInfoManager::beginFrame(uint32_t frameId) { +void FrameInfoManager::beginFrame(Config const& config, uint32_t frameId) { backend::DriverApi& driver = mEngine.getDriverApi(); driver.beginTimerQuery(mQueries[mIndex]); uint64_t elapsed = 0; if (driver.getTimerQueryValue(mQueries[mLast], &elapsed)) { mLast = (mLast + 1) % POOL_COUNT; + // convertion to our duration happens here mFrameTime = std::chrono::duration(elapsed); } + update(config,mFrameTime); } void FrameInfoManager::endFrame() { @@ -58,5 +71,48 @@ void FrameInfoManager::endFrame() { mIndex = (mIndex + 1) % POOL_COUNT; } +void FrameInfoManager::update(Config const& config, FrameInfoManager::duration lastFrameTime) { + const float kFeedbackConstant = (1.0f - std::exp(-config.oneOverTau)); + + // keep an history of frame times + auto& history = mFrameTimeHistory; + + // this is like doing { pop_back(); push_front(); } + details::move_backward(history.begin(), history.end() - 1, history.end()); + history[0].frameTime = lastFrameTime; + + mFrameTimeHistorySize = std::min(++mFrameTimeHistorySize, size_t(MAX_FRAMETIME_HISTORY)); + if (UTILS_UNLIKELY(mFrameTimeHistorySize < 3)) { + // not enough history to do anything usefull + history[0].valid = false; + return; + } + + // apply a median filter to get a good representation of the frame time of the last + // N frames. + std::array median; // NOLINT -- it's initialized below + size_t size = std::min(mFrameTimeHistorySize, std::min(config.historySize, median.size())); + for (size_t i = 0; i < size; ++i) { + median[i] = history[i].frameTime; + } + std::sort(median.begin(), median.begin() + size); + duration denoisedFrameTime = median[size / 2]; + + history[0].denoisedFrameTime = denoisedFrameTime; + + // how much we need to scale the current workload to fit in our target, at this instant + const float targetWithHeadroom = config.targetFrameTime * (1.0f - config.headRoomRatio); + const float workload = denoisedFrameTime.count() / targetWithHeadroom; + history[0].workLoad = workload; + history[0].smoothedWorkLoad = history[1].smoothedWorkLoad + + kFeedbackConstant * (workload - history[1].smoothedWorkLoad); + history[0].valid = true; + +// slog.d << history[0].frameTime.count() << ", " +// << history[0].denoisedFrameTime.count() << ", " +// << history[0].workLoad << ", " +// << history[0].smoothedWorkLoad << io::endl; +} + } // namespace filament diff --git a/filament/src/FrameInfo.h b/filament/src/FrameInfo.h index d8b3b2f5dd..85cd7794d7 100644 --- a/filament/src/FrameInfo.h +++ b/filament/src/FrameInfo.h @@ -21,6 +21,7 @@ #include "backend/Handle.h" +#include #include #include @@ -31,28 +32,54 @@ namespace details { class FEngine; } // namespace details +struct FrameInfo { + using duration = std::chrono::duration; + duration frameTime{}; // frame period + duration denoisedFrameTime{}; // frame period (median filter) + float workLoad{}; // instant workload (from denoised frame time) + float smoothedWorkLoad{}; // filtered workload + bool valid = false; +}; + class FrameInfoManager { static constexpr size_t POOL_COUNT = 8; + static constexpr size_t MAX_FRAMETIME_HISTORY = 32u; public: - using duration = std::chrono::duration; + using duration = FrameInfo::duration; + + struct Config { + float targetFrameTime; + float headRoomRatio; + float oneOverTau; + size_t historySize; + }; explicit FrameInfoManager(details::FEngine& engine); ~FrameInfoManager() noexcept; void terminate(); - void beginFrame(uint32_t frameId); // call this immediately after "make current" + void beginFrame(Config const& config, uint32_t frameId); // call this immediately after "make current" void endFrame(); // call this immediately before "swap buffers" - duration getLastFrameTime() const noexcept { - return mFrameTime; + FrameInfo const& getLastFrameInfo() const { + return mFrameTimeHistory[0]; } + duration getLastFrameTime() const noexcept { + return getLastFrameInfo().frameTime; + } + + private: + void update(Config const& config, duration lastFrameTime); details::FEngine& mEngine; backend::Handle mQueries[POOL_COUNT]; duration mFrameTime{}; uint32_t mIndex = 0; uint32_t mLast = 0; + + std::array mFrameTimeHistory; + size_t mFrameTimeHistorySize = 0; }; diff --git a/filament/src/Renderer.cpp b/filament/src/Renderer.cpp index f8e233ad7c..d75964d7c8 100644 --- a/filament/src/Renderer.cpp +++ b/filament/src/Renderer.cpp @@ -195,7 +195,7 @@ void FRenderer::renderJob(ArenaScope& arena, FView& view) { bool dithering = view.getDithering() == View::Dithering::TEMPORAL; bool fxaa = view.getAntiAliasing() == View::AntiAliasing::FXAA; uint8_t msaa = view.getSampleCount(); - float2 scale = view.updateScale(mFrameInfoManager.getLastFrameTime()); + float2 scale = view.updateScale(mFrameInfoManager.getLastFrameInfo()); const View::QualityLevel upscalingQuality = view.getDynamicResolutionOptions().quality; auto aoOptions = view.getAmbientOcclusionOptions(); if (!hasPostProcess) { @@ -695,8 +695,8 @@ bool FRenderer::beginFrame(FSwapChain* swapChain, uint64_t vsyncSteadyClockTimeN // 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 vsync{steady_clock::duration(vsyncSteadyClockTimeNano) }; - const time_point vsyncTp(vsyncSteadyClockTimeNano ? vsync : now); + const steady_clock::time_point userVsync{ steady_clock::duration(vsyncSteadyClockTimeNano) }; + const time_point appVsync(vsyncSteadyClockTimeNano ? userVsync : now); mFrameId++; @@ -715,7 +715,7 @@ bool FRenderer::beginFrame(FSwapChain* swapChain, uint64_t vsyncSteadyClockTimeN // NOTE: this makes synchronous calls to the driver driver.updateStreams(&driver); - driver.beginFrame(vsyncTp.time_since_epoch().count(), mFrameId, callback, user); + driver.beginFrame(appVsync.time_since_epoch().count(), mFrameId, callback, user); if (!mFrameSkipper.beginFrame()) { driver.endFrame(mFrameId); @@ -725,10 +725,49 @@ bool FRenderer::beginFrame(FSwapChain* swapChain, uint64_t vsyncSteadyClockTimeN // This need to occur after the backend beginFrame() because some backends need to start // a command buffer before creating a fence. - mFrameInfoManager.beginFrame(mFrameId); + mFrameInfoManager.beginFrame({ + .targetFrameTime = float(mFrameRateOptions.interval) / mDisplayInfo.refreshRate, + .headRoomRatio = mFrameRateOptions.headRoomRatio, + .oneOverTau = mFrameRateOptions.scaleRate, + .historySize = mFrameRateOptions.history + }, mFrameId); + +#if 0 // work-in-progress + if (vsyncSteadyClockTimeNano) { + const size_t interval = mFrameRateOptions.interval; // user requested swap-interval; + const steady_clock::duration refreshPeriod(uint64_t(1e9 / mDisplayInfo.refreshRate)); + const steady_clock::duration presentationDeadline(mDisplayInfo.presentationDeadlineNanos); + const steady_clock::duration vsyncOffset(mDisplayInfo.vsyncOffsetNanos); + + // hardware vsync timestamp + steady_clock::time_point hwVsync = appVsync - vsyncOffset; + + // compute our desired presentation time. We can't pick a desired presentation time + // that's too far, or we won't be able to dequeue buffers. + steady_clock::time_point desiredPresentationTime = hwVsync + 2 * interval * refreshPeriod; + + // Compute the deadline. This deadline is when the GPU must be finished. + // The deadline has 1ms backed in it on Android. + steady_clock::time_point deadline = desiredPresentationTime - presentationDeadline; + + // one important thing is to make sure that the deadline is comfortably later than + // when the gpu will finish, otherwise we'll have inconsistent latency/frames. + + // TODO: evaluate if we can make it in time, and if not why. + // If the problem is cpu+gpu latency we can try to push the desired presentation time + // further away, but this has limits, as only 2 buffers are dequeuable. + // If the problem is the gpu is overwhelmed, then we need to + // - see if there is more headroom in dynamic resolution + // - or start skipping frames. Ideally lower the framerate too. + + // presentation time is set to the middle of the period we're interested in + steady_clock::time_point presentationTime = desiredPresentationTime - refreshPeriod / 2; + driver.setPresentationTime(presentationTime.time_since_epoch().count()); + } +#endif // latch the frame time - std::chrono::duration time(vsync - mUserEpoch); + std::chrono::duration time(userVsync - mUserEpoch); float h = float(time.count()); float l = float(time.count() - h); mShaderUserTime = { h, l, 0, 0 }; @@ -883,4 +922,8 @@ void Renderer::setDisplayInfo(const DisplayInfo& info) noexcept { upcast(this)->setDisplayInfo(info); } +void Renderer::setFrameRateOptions(FrameRateOptions const& options) noexcept { + upcast(this)->setFrameRateOptions(options); +} + } // namespace filament diff --git a/filament/src/View.cpp b/filament/src/View.cpp index d08d7cc93a..8850f86670 100644 --- a/filament/src/View.cpp +++ b/filament/src/View.cpp @@ -118,24 +118,6 @@ void FView::setDynamicResolutionOptions(DynamicResolutionOptions const& options) if (dynamicResolution.enabled) { // if enabled, sanitize the parameters - // History can't be more than 32 frames (~0.5s) - dynamicResolution.history = std::min(dynamicResolution.history, uint8_t(MAX_FRAMETIME_HISTORY)); - - // History must at least be 3 frames - dynamicResolution.history = std::max(dynamicResolution.history, uint8_t(3)); - - // can't ask more 240 fps - dynamicResolution.targetFrameTimeMilli = - std::max(dynamicResolution.targetFrameTimeMilli, 1000.0f / 240.0f); - - // can't ask less than 1 fps - dynamicResolution.targetFrameTimeMilli = - std::min(dynamicResolution.targetFrameTimeMilli, 1000.0f); - - // headroom can't be larger than frame time, or less than 0 - dynamicResolution.headRoomRatio = std::min(dynamicResolution.headRoomRatio, 1.0f); - dynamicResolution.headRoomRatio = std::max(dynamicResolution.headRoomRatio, 0.0f); - // minScale cannot be 0 or negative dynamicResolution.minScale = max(dynamicResolution.minScale, float2(1.0f / 1024.0f)); @@ -145,11 +127,6 @@ void FView::setDynamicResolutionOptions(DynamicResolutionOptions const& options) // clamp maxScale to 2x because we're doing bilinear filtering, so super-sampling // is not useful above that. dynamicResolution.maxScale = min(dynamicResolution.maxScale, float2(2.0f)); - - // reset the history, so we start from a known (and current) state - mFrameTimeHistorySize = 0; - mScale = 1.0f; - mDynamicWorkloadScale = 1.0f; } } @@ -157,58 +134,16 @@ void FView::setDynamicLightingOptions(float zLightNear, float zLightFar) noexcep mFroxelizer.setOptions(zLightNear, zLightFar); } -// this is to avoid a call to memmove -template -static inline -void move_backward(InputIterator first, InputIterator last, OutputIterator result) { - while (first != last) { - *--result = *--last; - } -} - -float2 FView::updateScale(duration frameTime) noexcept { +float2 FView::updateScale(FrameInfo const& info) noexcept { DynamicResolutionOptions const& options = mDynamicResolution; if (options.enabled) { - - if (UTILS_UNLIKELY(frameTime.count() <= std::numeric_limits::epsilon())) { + if (!UTILS_UNLIKELY(info.valid)) { mScale = 1.0f; return mScale; } - // keep an history of frame times - auto& history = mFrameTimeHistory; - - // this is like doing { pop_back(); push_front(); } - details::move_backward(history.begin(), history.end() - 1, history.end()); - history.front() = frameTime; - mFrameTimeHistorySize = std::min(++mFrameTimeHistorySize, size_t(MAX_FRAMETIME_HISTORY)); - - if (UTILS_UNLIKELY(mFrameTimeHistorySize < 3)) { - // don't make any decision if we don't have enough data - mScale = 1.0f; - return mScale; - } - - // apply a median filter to get a good representation of the frame time of the last - // N frames. - std::array median; // NOLINT -- it's initialized below - size_t size = std::min(mFrameTimeHistorySize, median.size()); - std::uninitialized_copy_n(history.begin(), size, median.begin()); - std::sort(median.begin(), median.begin() + size); - duration filteredFrameTime = median[size / 2]; - - // how much we need to scale the current workload to fit in our target, at this instant - const float targetWithHeadroom = options.targetFrameTimeMilli * (1 - options.headRoomRatio); - const float workloadScale = targetWithHeadroom / filteredFrameTime.count(); - - // low-pass: y += b * (x - y) - const float oneOverTau = options.scaleRate; - const float x = mScale.x * mScale.y * workloadScale; - mDynamicWorkloadScale += (1.0f - std::exp(-oneOverTau)) * (x - mDynamicWorkloadScale); - // scaling factor we need to apply on the whole surface - const float scale = mDynamicWorkloadScale; - + const float scale = (mScale.x * mScale.y) / info.smoothedWorkLoad; const float w = mViewport.width; const float h = mViewport.height; if (scale < 1.0f && !options.homogeneousScaling) { @@ -236,24 +171,22 @@ float2 FView::updateScale(duration frameTime) noexcept { mScale = std::sqrt(scale); } - // now tweak the scaling factor to get multiples of 4 (to help quad-shading) - mScale = (floor(mScale * float2{ w, h } / 4) * 4) / float2{ w, h }; + // now tweak the scaling factor to get multiples of 8 (to help quad-shading) + // i.e. 8x8=64 fragments, to try to help with warp sizes. + mScale = (floor(mScale * float2{ w, h } / 8) * 8) / float2{ w, h }; // always clamp to the min/max scale range mScale = clamp(mScale, options.minScale, options.maxScale); //#define DEBUG_DYNAMIC_RESOLUTION -#if !defined(NDEBUG) && defined(DEBUG_DYNAMIC_RESOLUTION) +#if defined(DEBUG_DYNAMIC_RESOLUTION) static int sLogCounter = 15; if (!--sLogCounter) { sLogCounter = 15; - slog.d << frameTime.count() - << ", " << filteredFrameTime.count() - << ", " << workloadScale - << ", " << mDynamicWorkloadScale + slog.d << info.denoisedFrameTime.count() * 1000.0f << " ms" + << ", " << info.smoothedWorkLoad << ", " << mScale.x << ", " << mScale.y - << ", " << mScale.x * mScale.y << ", " << mViewport.width * mScale.x << ", " << mViewport.height * mScale.y << io::endl; @@ -262,6 +195,7 @@ float2 FView::updateScale(duration frameTime) noexcept { } else { mScale = 1.0f; } + return mScale; } diff --git a/filament/src/details/Renderer.h b/filament/src/details/Renderer.h index a663a66823..d80f8d82cf 100644 --- a/filament/src/details/Renderer.h +++ b/filament/src/details/Renderer.h @@ -58,6 +58,8 @@ class ShadowMap; * A concrete implementation of the Renderer Interface. */ class FRenderer : public Renderer { + static constexpr size_t MAX_FRAMETIME_HISTORY = 32u; + public: explicit FRenderer(FEngine& engine); ~FRenderer() noexcept; @@ -95,6 +97,24 @@ public: mDisplayInfo = info; } + void setFrameRateOptions(FrameRateOptions const& options) noexcept { + FrameRateOptions& frameRateOptions = mFrameRateOptions; + frameRateOptions = options; + + // History can't be more than 32 frames (~0.5s) + frameRateOptions.history = std::min(frameRateOptions.history, + uint8_t(MAX_FRAMETIME_HISTORY)); + + // History must at least be 3 frames + frameRateOptions.history = std::max(frameRateOptions.history, uint8_t(3)); + + frameRateOptions.interval = std::max(uint8_t(1), frameRateOptions.interval); + + // headroom can't be larger than frame time, or less than 0 + frameRateOptions.headRoomRatio = std::min(frameRateOptions.headRoomRatio, 1.0f); + frameRateOptions.headRoomRatio = std::max(frameRateOptions.headRoomRatio, 0.0f); + } + private: friend class Renderer; using Command = RenderPass::Command; @@ -159,6 +179,7 @@ private: Epoch mUserEpoch; math::float4 mShaderUserTime{}; DisplayInfo mDisplayInfo; + FrameRateOptions mFrameRateOptions; // per-frame arena for this Renderer LinearAllocatorArena& mPerRenderPassArena; diff --git a/filament/src/details/View.h b/filament/src/details/View.h index f1aa2941b9..5d565ca48b 100644 --- a/filament/src/details/View.h +++ b/filament/src/details/View.h @@ -21,6 +21,7 @@ #include "upcast.h" +#include "FrameInfo.h" #include "UniformBuffer.h" #include "details/Allocators.h" @@ -238,7 +239,7 @@ public: return mHasPostProcessPass; } - math::float2 updateScale(std::chrono::duration frameTime) noexcept; + math::float2 updateScale(FrameInfo const& info) noexcept; void setDynamicResolutionOptions(View::DynamicResolutionOptions const& options) noexcept; @@ -338,8 +339,6 @@ public: UniformBuffer& getShadowUniforms() const { return mShadowUb; } private: - static constexpr size_t MAX_FRAMETIME_HISTORY = 32u; - void prepareVisibleRenderables(utils::JobSystem& js, Frustum const& frustum, FScene::RenderableSoa& renderableData) const noexcept; @@ -376,12 +375,12 @@ private: FCamera* mViewingCamera = nullptr; CameraInfo mViewingCameraInfo; - Frustum mCullingFrustum; + Frustum mCullingFrustum{}; mutable Froxelizer mFroxelizer; Viewport mViewport; - LinearColorA mClearColor; + LinearColorA mClearColor{}; bool mCulling = true; bool mFrontFaceWindingInverted = false; bool mClearTargetColor = true; @@ -403,13 +402,8 @@ private: BloomOptions mBloomOptions; FogOptions mFogOptions; - using duration = std::chrono::duration; DynamicResolutionOptions mDynamicResolution; - std::array mFrameTimeHistory; - size_t mFrameTimeHistorySize = 0; - math::float2 mScale = 1.0f; - float mDynamicWorkloadScale = 1.0f; bool mIsDynamicResolutionSupported = false; RenderQuality mRenderQuality;