From e3457aa0a79f5345e5b847a523f91c2259fec4f5 Mon Sep 17 00:00:00 2001 From: Philip Rideout Date: Fri, 24 Aug 2018 16:37:12 -0700 Subject: [PATCH] Add single-threaded config to Filament. (#130) * Add single-threaded config to Filament. This adds a tick method to Engine and disables a couple components in Renderer (FrameSkipper and FrameInfoManager). This will make it easier to support WebGL, and will allow us to remove some of the command buffer debugging stuff that we added for Vulkan. * tick => execute, and other review feedback * Restore the ASSERT for FFence::wait. --- filament/include/filament/Engine.h | 7 +++ filament/src/Engine.cpp | 67 ++++++++++++++++------ filament/src/Fence.cpp | 4 ++ filament/src/Renderer.cpp | 32 ++++++++--- filament/src/details/Engine.h | 2 + filament/src/driver/CommandBufferQueue.cpp | 8 ++- filament/src/driver/CommandBufferQueue.h | 2 +- libs/utils/include/utils/compiler.h | 5 ++ libs/utils/src/JobSystem.cpp | 2 +- samples/app/FilamentApp.cpp | 44 ++++++++------ 10 files changed, 123 insertions(+), 50 deletions(-) diff --git a/filament/include/filament/Engine.h b/filament/include/filament/Engine.h index da63b822a0..6054f53825 100644 --- a/filament/include/filament/Engine.h +++ b/filament/include/filament/Engine.h @@ -335,6 +335,13 @@ public: destroy(camera->getEntity()); } + /** + * Invokes one iteration of the render loop, used only on single-threaded platforms. + * + * This should be called every time the windowing system needs to paint (e.g. at 60 Hz). + */ + void execute(); + DebugRegistry& getDebugRegistry() noexcept; protected: diff --git a/filament/src/Engine.cpp b/filament/src/Engine.cpp index 878890a0c2..261258bd43 100644 --- a/filament/src/Engine.cpp +++ b/filament/src/Engine.cpp @@ -74,11 +74,22 @@ static std::mutex sEnginesLock; FEngine* FEngine::create(Backend backend, ExternalContext* externalContext, void* sharedGLContext) { FEngine* instance = new FEngine(backend, externalContext, sharedGLContext); - slog.i << "FEngine (" << sizeof(void*) * 8 << " bits) created at " << instance << io::endl; + slog.i << "FEngine (" << sizeof(void*) * 8 << " bits) created at " << instance << " " + << "(threading is " << (UTILS_HAS_THREADING ? "enabled)" : "disabled)") << io::endl; // initialize all fields that need an instance of FEngine // (this cannot be done safely in the ctor) + // Normally we launch a thread and create the context and Driver from there (see FEngine::loop). + // In the single-threaded case, we do so in the here and now. + if (!UTILS_HAS_THREADING) { + instance->mExternalContext = ExternalContext::create(&instance->mBackend); + instance->mDriver = instance->mExternalContext->createDriver(sharedGLContext); + instance->init(); + instance->execute(); + return instance; + } + // start the driver thread instance->mDriverThread = std::thread(&FEngine::loop, instance); @@ -292,13 +303,18 @@ void FEngine::shutdown() { // There might be commands added by the terminate() calls flushCommandBuffer(mCommandBufferQueue); + if (!UTILS_HAS_THREADING) { + execute(); + } /* * terminate the rendering engine */ mCommandBufferQueue.requestExit(); - mDriverThread.join(); + if (UTILS_HAS_THREADING) { + mDriverThread.join(); + } mTerminated = true; // detach this thread from the jobsystem @@ -362,28 +378,18 @@ int FEngine::loop() { JobSystem::setThreadName("FEngine::loop"); JobSystem::setThreadPriority(JobSystem::Priority::DISPLAY); - // FIXME: we should do this based on the CPUs we actually have - uint32_t affinityMask = (std::thread::hardware_concurrency() >= 6) ? 0xF0 : 0; - - auto& commandBufferQueue = mCommandBufferQueue; while (true) { - // wait until we get command buffers to be executed (or thread exit requested) - auto buffers = commandBufferQueue.waitForCommands(); - if (UTILS_UNLIKELY(!buffers.size())) { - break; - } + + // FIXME: we should do this based on the CPUs we actually have + uint32_t affinityMask = (std::thread::hardware_concurrency() >= 6) ? 0xF0 : 0; if (affinityMask) { // looks like thread affinity needs to be reset regularly (on Android) JobSystem::setThreadAffinity(affinityMask); } - // execute all command buffers - for (auto& item : buffers) { - if (UTILS_LIKELY(item.begin)) { - mCommandStream.execute(item.begin); - mCommandBufferQueue.releaseBuffer(item); - } + if (!execute()) { + break; } } @@ -699,6 +705,25 @@ void* FEngine::streamAlloc(size_t size, size_t alignment) noexcept { return getDriverApi().allocate(size, alignment); } +bool FEngine::execute() { + + // wait until we get command buffers to be executed (or thread exit requested) + auto buffers = mCommandBufferQueue.waitForCommands(); + if (UTILS_UNLIKELY(!buffers.size())) { + return false; + } + + // execute all command buffers + for (auto& item : buffers) { + if (UTILS_LIKELY(item.begin)) { + mCommandStream.execute(item.begin); + mCommandBufferQueue.releaseBuffer(item); + } + } + + return true; +} + // --------------------------------------------------------------------------------------------- EnginePerformanceTest::~EnginePerformanceTest() noexcept = default; @@ -871,6 +896,14 @@ void* Engine::streamAlloc(size_t size, size_t alignment) noexcept { return upcast(this)->streamAlloc(size, alignment); } +// The external-facing execute does a flush, and is meant only for single-threaded environments. +// It also discards the boolean return value, which would otherwise indicate a thread exit. +void Engine::execute() { + ASSERT_PRECONDITION(!UTILS_HAS_THREADING, "Execute is meant for single-threaded platforms."); + upcast(this)->flush(); + upcast(this)->execute(); +} + DebugRegistry& Engine::getDebugRegistry() noexcept { return upcast(this)->getDebugRegistry(); } diff --git a/filament/src/Fence.cpp b/filament/src/Fence.cpp index f3fb6d7e02..8be6f121bb 100644 --- a/filament/src/Fence.cpp +++ b/filament/src/Fence.cpp @@ -20,6 +20,8 @@ #include +#include + namespace filament { using namespace driver; @@ -66,6 +68,8 @@ FenceStatus FFence::waitAndDestroy(FFence* fence, Mode mode) noexcept { UTILS_NOINLINE FenceStatus FFence::wait(Mode mode, uint64_t timeout) noexcept { + ASSERT_PRECONDITION(UTILS_HAS_THREADING || timeout == 0, "Non-zero timeout requires threads."); + FEngine& engine = mEngine; if (mode == Mode::FLUSH) { diff --git a/filament/src/Renderer.cpp b/filament/src/Renderer.cpp index 6aecada28a..4b5481aebf 100644 --- a/filament/src/Renderer.cpp +++ b/filament/src/Renderer.cpp @@ -58,7 +58,9 @@ void FRenderer::init() noexcept { mRenderTarget = driver.createDefaultRenderTarget(); mIsRGB16FSupported = driver.isRenderTargetFormatSupported(driver::TextureFormat::RGB16F); mIsRGB8Supported = driver.isRenderTargetFormatSupported(driver::TextureFormat::RGB8); - mFrameInfoManager.run(); + if (UTILS_HAS_THREADING) { + mFrameInfoManager.run(); + } } FRenderer::~FRenderer() noexcept { @@ -83,8 +85,14 @@ void FRenderer::terminate(FEngine& engine) { // 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...) - Fence::waitAndDestroy(engine.createFence()); - mFrameInfoManager.terminate(); + if (UTILS_HAS_THREADING) { + Fence::waitAndDestroy(engine.createFence()); + mFrameInfoManager.terminate(); + } 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(); + } } void FRenderer::render(FView const* view) { @@ -233,7 +241,9 @@ bool FRenderer::beginFrame(FSwapChain* swapChain) { assert(swapChain); mFrameId++; - mFrameInfoManager.beginFrame(mFrameId); + if (UTILS_HAS_THREADING) { + mFrameInfoManager.beginFrame(mFrameId); + } { // scope for frame id trace char buf[64]; @@ -273,12 +283,16 @@ void FRenderer::endFrame() { FEngine::DriverApi& driver = engine.getDriverApi(); RenderTargetPool& rtp = engine.getRenderTargetPool(); - // on debug builds this helps catching cases where we're writing to - // the buffer form another thread, which is currently not allowed. - driver.debugThreading(); - FrameInfoManager& frameInfoManager = mFrameInfoManager; - frameInfoManager.endFrame(); + + if (UTILS_HAS_THREADING) { + + // on debug builds this helps catching cases where we're writing to + // the buffer form another thread, which is currently not allowed. + driver.debugThreading(); + + frameInfoManager.endFrame(); + } mFrameSkipper.endFrame(); driver.endFrame(mFrameId); diff --git a/filament/src/details/Engine.h b/filament/src/details/Engine.h index 0101585ef9..14e6a64db5 100644 --- a/filament/src/details/Engine.h +++ b/filament/src/details/Engine.h @@ -343,6 +343,8 @@ public: return mDebugRegistry; } + bool execute(); + private: FEngine(Backend backend, ExternalContext* externalContext, void* sharedGLContext); void init(); diff --git a/filament/src/driver/CommandBufferQueue.cpp b/filament/src/driver/CommandBufferQueue.cpp index d3d92b46e6..03b78009d8 100644 --- a/filament/src/driver/CommandBufferQueue.cpp +++ b/filament/src/driver/CommandBufferQueue.cpp @@ -102,9 +102,11 @@ void CommandBufferQueue::flush() noexcept { } std::vector CommandBufferQueue::waitForCommands() const { - std::unique_lock lock(mLock); - while (mCommandBuffersToExecute.empty() && !mExitRequested) { - mCondition.wait(lock); + if (UTILS_HAS_THREADING) { + std::unique_lock lock(mLock); + while (mCommandBuffersToExecute.empty() && !mExitRequested) { + mCondition.wait(lock); + } } return std::move(mCommandBuffersToExecute); } diff --git a/filament/src/driver/CommandBufferQueue.h b/filament/src/driver/CommandBufferQueue.h index 007838d0cd..8894c9b392 100644 --- a/filament/src/driver/CommandBufferQueue.h +++ b/filament/src/driver/CommandBufferQueue.h @@ -28,7 +28,7 @@ namespace filament { /* - * A produdcer-consumer command queue that uses a CircularBuffer as main storage + * A producer-consumer command queue that uses a CircularBuffer as main storage */ class CommandBufferQueue { struct Slice { diff --git a/libs/utils/include/utils/compiler.h b/libs/utils/include/utils/compiler.h index 4bc55b3242..4814543b9b 100644 --- a/libs/utils/include/utils/compiler.h +++ b/libs/utils/include/utils/compiler.h @@ -72,6 +72,11 @@ # define UTILS_HAS_HYPER_THREADING 0 #endif +#if defined(__EMSCRIPTEN__) +# define UTILS_HAS_THREADING 0 +#else +# define UTILS_HAS_THREADING 1 +#endif #if __has_attribute(noinline) #define UTILS_NOINLINE __attribute__((noinline)) diff --git a/libs/utils/src/JobSystem.cpp b/libs/utils/src/JobSystem.cpp index 06bef35fe8..4e7aebd297 100644 --- a/libs/utils/src/JobSystem.cpp +++ b/libs/utils/src/JobSystem.cpp @@ -120,7 +120,7 @@ JobSystem::JobSystem(size_t threadCount, size_t adoptableThreadsCount) noexcept threadCount = hwThreads - 1; } } - threadCount = std::min(size_t(32), threadCount); + threadCount = std::min(size_t(UTILS_HAS_THREADING ? 32 : 0), threadCount); mThreadStates = aligned_vector(threadCount + adoptableThreadsCount); mThreadCount = uint16_t(threadCount); diff --git a/samples/app/FilamentApp.cpp b/samples/app/FilamentApp.cpp index 96414ea433..0b9f6e93b4 100644 --- a/samples/app/FilamentApp.cpp +++ b/samples/app/FilamentApp.cpp @@ -74,29 +74,27 @@ FilamentApp::~FilamentApp() { SDL_Quit(); } -void FilamentApp::run(const Config& config,SetupCallback setupCallback, +void FilamentApp::run(const Config& config, SetupCallback setupCallback, CleanupCallback cleanupCallback, ImGuiCallback imguiCallback, PreRenderCallback preRender, PostRenderCallback postRender, size_t width, size_t height) { - mEngine = Engine::create(config.backend); - - mDepthMaterial = Material::Builder() - .package((void*) DEPTH_VISUALIZER_PACKAGE, sizeof(DEPTH_VISUALIZER_PACKAGE)) - .build(*mEngine); - - mDepthMI = mDepthMaterial->createInstance(); - - mTransparentMaterial = Material::Builder() - .package((void*) TRANSPARENT_COLOR_PACKAGE, sizeof(TRANSPARENT_COLOR_PACKAGE)) - .build(*mEngine); - - mDefaultMaterial = Material::Builder() - .package((void*) AI_DEFAULT_MAT_PACKAGE, sizeof(AI_DEFAULT_MAT_PACKAGE)) - .build(*mEngine); - std::unique_ptr window( new FilamentApp::Window(this, config, config.title, width, height)); + mDepthMaterial = Material::Builder() + .package((void*) DEPTH_VISUALIZER_PACKAGE, sizeof(DEPTH_VISUALIZER_PACKAGE)) + .build(*mEngine); + + mDepthMI = mDepthMaterial->createInstance(); + + mDefaultMaterial = Material::Builder() + .package((void*) AI_DEFAULT_MAT_PACKAGE, sizeof(AI_DEFAULT_MAT_PACKAGE)) + .build(*mEngine); + + mTransparentMaterial = Material::Builder() + .package((void*) TRANSPARENT_COLOR_PACKAGE, sizeof(TRANSPARENT_COLOR_PACKAGE)) + .build(*mEngine); + std::unique_ptr cameraCube(new Cube(*mEngine, mTransparentMaterial, {1,0,0})); // we can't cull the light-frustum because it's not applied a rigid transform // and currently, filament assumes that for culling @@ -206,6 +204,10 @@ void FilamentApp::run(const Config& config,SetupCallback setupCallback, while (!mClosed) { + if (!UTILS_HAS_THREADING) { + mEngine->execute(); + } + // Allow the app to animate the scene if desired. if (mAnimation) { double now = (double) SDL_GetPerformanceCounter() / SDL_GetPerformanceFrequency(); @@ -423,10 +425,14 @@ FilamentApp::Window::Window(FilamentApp* filamentApp, : mFilamentApp(filamentApp) { const int x = SDL_WINDOWPOS_CENTERED; const int y = SDL_WINDOWPOS_CENTERED; - const uint32_t windowFlags = SDL_WINDOW_SHOWN | SDL_WINDOW_RESIZABLE - | SDL_WINDOW_ALLOW_HIGHDPI | SDL_WINDOW_OPENGL; + const uint32_t windowFlags = SDL_WINDOW_SHOWN | SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI; mWindow = SDL_CreateWindow(title.c_str(), x, y, (int) w, (int) h, windowFlags); + // Create the Engine after the window in case this happens to be a single-threaded platform. + // For single-threaded platforms, we need to ensure that Filament's OpenGL context is current, + // rather than the one created by SDL. + mFilamentApp->mEngine = Engine::create(config.backend); + // HACK: We don't use SDL's 2D rendering functionality, but by invoking it we cause // SDL to create a Metal backing layer, which allows us to run Vulkan apps via MoltenVK. #if defined(FILAMENT_DRIVER_SUPPORTS_VULKAN) && defined(__APPLE__)