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__)