From 46b356cb2c9868e7bf0929c9fb5736f333dbbe0f Mon Sep 17 00:00:00 2001 From: prewettg Date: Thu, 18 Mar 2021 12:49:20 -0400 Subject: [PATCH] Fix blanking windows not being drawn into on macOS (#3616) * Fix blanking windows not being drawn into on macOS * Refactored to create swap chain info object; added fixes for resizing; added sample. * CocoaGLSwapChain should inherit from Platform::SwapChain * Link to SDL directly for multiple_windows example * SDL apparently redefines main to SDL_main and requires C++ linkage * Fix main to have SDL-approved signature --- .../backend/src/opengl/PlatformCocoaGL.mm | 92 ++++- samples/CMakeLists.txt | 2 + samples/multiple_windows.cpp | 379 ++++++++++++++++++ 3 files changed, 452 insertions(+), 21 deletions(-) create mode 100644 samples/multiple_windows.cpp diff --git a/filament/backend/src/opengl/PlatformCocoaGL.mm b/filament/backend/src/opengl/PlatformCocoaGL.mm index 2070f4373d..50939e57b7 100644 --- a/filament/backend/src/opengl/PlatformCocoaGL.mm +++ b/filament/backend/src/opengl/PlatformCocoaGL.mm @@ -34,12 +34,17 @@ namespace filament { using namespace backend; +struct CocoaGLSwapChain : public Platform::SwapChain { + NSView* view; + NSRect previousBounds; + NSRect previousWindowFrame; +}; + struct PlatformCocoaGLImpl { NSOpenGLContext* mGLContext = nullptr; - NSView* mCurrentView = nullptr; + CocoaGLSwapChain* mCurrentSwapChain = nullptr; std::vector mHeadlessSwapChains; - NSRect mPreviousBounds = {}; - void updateOpenGLContext(NSView *nsView, bool resetView); + void updateOpenGLContext(NSView *nsView, bool resetView, bool clearView); }; PlatformCocoaGL::PlatformCocoaGL() @@ -86,50 +91,90 @@ Platform::SwapChain* PlatformCocoaGL::createSwapChain(void* nativewindow, uint64 flags &= ~backend::SWAP_CHAIN_CONFIG_TRANSPARENT; NSView* nsView = (__bridge NSView*)nativewindow; + CocoaGLSwapChain* swapChain = new CocoaGLSwapChain(); + swapChain->view = nsView; + swapChain->previousBounds = NSZeroRect; + swapChain->previousWindowFrame = NSZeroRect; + // If the SwapChain is being recreated (e.g. if the underlying surface has been resized), // then we need to force an update to occur in the subsequent makeCurrent, which can be done by // simply resetting the current view. In multi-window situations, this happens automatically. - if (pImpl->mCurrentView == nsView) { - pImpl->mCurrentView = nullptr; + if (pImpl->mCurrentSwapChain && pImpl->mCurrentSwapChain->view == nsView) { + pImpl->mCurrentSwapChain = nullptr; } - return (SwapChain*) nativewindow; + return swapChain; } Platform::SwapChain* PlatformCocoaGL::createSwapChain(uint32_t width, uint32_t height, uint64_t& flags) noexcept { NSView* nsView = [[NSView alloc] initWithFrame:NSMakeRect(0, 0, width, height)]; + // adding the pointer to the array retains the NSView pImpl->mHeadlessSwapChains.push_back(nsView); - return (__bridge SwapChain*)nsView; + + CocoaGLSwapChain* swapChain = new CocoaGLSwapChain(); + swapChain->view = nsView; + swapChain->previousBounds = NSZeroRect; + swapChain->previousWindowFrame = NSZeroRect; + + return swapChain; } void PlatformCocoaGL::destroySwapChain(Platform::SwapChain* swapChain) noexcept { + CocoaGLSwapChain* cocoaSwapChain = static_cast(swapChain); + if (pImpl->mCurrentSwapChain == cocoaSwapChain) { + pImpl->mCurrentSwapChain = nullptr; + } + auto& v = pImpl->mHeadlessSwapChains; - NSView* nsView = (__bridge NSView*)swapChain; - auto it = std::find(v.begin(), v.end(), nsView); + auto it = std::find(v.begin(), v.end(), cocoaSwapChain->view); if (it != v.end()) { // removing the pointer from the array releases the NSView v.erase(it); } + delete cocoaSwapChain; } void PlatformCocoaGL::makeCurrent(Platform::SwapChain* drawSwapChain, Platform::SwapChain* readSwapChain) noexcept { ASSERT_PRECONDITION_NON_FATAL(drawSwapChain == readSwapChain, "ContextManagerCocoa does not support using distinct draw/read swap chains."); - NSView *nsView = (__bridge NSView*) drawSwapChain; - - NSRect currentBounds = [nsView convertRectToBacking:nsView.bounds]; + CocoaGLSwapChain* swapChain = (CocoaGLSwapChain*)drawSwapChain; + NSRect currentBounds = [swapChain->view convertRectToBacking:swapChain->view.bounds]; + NSRect currentWindowFrame = swapChain->view.window.frame; // Check if the view has been swapped out or resized. - if (pImpl->mCurrentView != nsView) { - pImpl->mCurrentView = nsView; - pImpl->updateOpenGLContext(nsView, true); - } else if (!CGRectEqualToRect(currentBounds, pImpl->mPreviousBounds)) { - pImpl->updateOpenGLContext(nsView, false); + // updateOpenGLContext() needs to call -clearDrawable if the view was + // resized, otherwise we do not want to call -clearDrawable, as in addition + // to disassociating the context from the view as the documentation says, + // it also does what its name implies and clears the drawable pixels. + // This is problematic with multiple windows, but still necessary if the + // window has resized. + if (pImpl->mCurrentSwapChain != swapChain) { + pImpl->mCurrentSwapChain = swapChain; + if (!NSEqualRects(currentBounds, swapChain->previousBounds)) { + // A window is being resized or moved, but the last draw was a + // different window (for example, it updates on a timer): + // just call -setView first, otherwise -clearDrawable would clear + // the other window. But if we do this the first time that the swap + // chain has been created then resizing will show a black image. + if (!NSIsEmptyRect(swapChain->previousBounds)) { + pImpl->updateOpenGLContext(swapChain->view, true, false); + } + // Now call -clearDrawable, otherwise we get garbage during if we + // are resizing. + pImpl->updateOpenGLContext(swapChain->view, true, true); + } else { + // We are drawing another window: only call -setView. + pImpl->updateOpenGLContext(swapChain->view, true, false); + } + } else if (!NSEqualRects(currentWindowFrame, swapChain->previousWindowFrame)) { + // Same window has moved or resized: need to clear and set view. + pImpl->updateOpenGLContext(swapChain->view, true, true); } - pImpl->mPreviousBounds = currentBounds; + swapChain->previousBounds = currentBounds; + swapChain->previousWindowFrame = currentWindowFrame; } void PlatformCocoaGL::commit(Platform::SwapChain* swapChain) noexcept { @@ -144,7 +189,8 @@ bool PlatformCocoaGL::pumpEvents() noexcept { return true; } -void PlatformCocoaGLImpl::updateOpenGLContext(NSView *nsView, bool resetView) { +void PlatformCocoaGLImpl::updateOpenGLContext(NSView *nsView, bool resetView, + bool clearView) { NSOpenGLContext* glContext = mGLContext; // NOTE: This is not documented well (if at all) but NSOpenGLContext requires "setView" and @@ -154,14 +200,18 @@ void PlatformCocoaGLImpl::updateOpenGLContext(NSView *nsView, bool resetView) { if (![NSThread isMainThread]) { dispatch_sync(dispatch_get_main_queue(), ^(void) { if (resetView) { - [glContext clearDrawable]; + if (clearView) { + [glContext clearDrawable]; + } [glContext setView:nsView]; } [glContext update]; }); } else { if (resetView) { - [glContext clearDrawable]; + if (clearView) { + [glContext clearDrawable]; + } [glContext setView:nsView]; } [glContext update]; diff --git a/samples/CMakeLists.txt b/samples/CMakeLists.txt index de486b0af6..4b60fbf1ab 100644 --- a/samples/CMakeLists.txt +++ b/samples/CMakeLists.txt @@ -228,6 +228,7 @@ if (NOT ANDROID) add_demo(hellotriangle) add_demo(lightbulb) add_demo(material_sandbox) + add_demo(multiple_windows) add_demo(point_sprites) add_demo(rendertarget) add_demo(sample_cloth) @@ -245,6 +246,7 @@ if (NOT ANDROID) target_link_libraries(gltf_viewer PRIVATE gltf-resources gltfio viewer) target_link_libraries(gltf_instances PRIVATE gltf-resources gltfio viewer) target_link_libraries(hellopbr PRIVATE filameshio suzanne-resources) + target_link_libraries(multiple_windows PRIVATE filameshio suzanne-resources) target_link_libraries(rendertarget PRIVATE filameshio suzanne-resources) target_link_libraries(sample_cloth PRIVATE filameshio) target_link_libraries(sample_normal_map PRIVATE filameshio) diff --git a/samples/multiple_windows.cpp b/samples/multiple_windows.cpp new file mode 100644 index 0000000000..99d580addb --- /dev/null +++ b/samples/multiple_windows.cpp @@ -0,0 +1,379 @@ +/* + * 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 + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "generated/resources/resources.h" +#include "generated/resources/monkey.h" + +using namespace filament; + +namespace { + static constexpr Engine::Backend kBackend = Engine::Backend::OPENGL; + static constexpr int kWidth = 640; + static constexpr int kHeight = 480; + static constexpr double kFieldOfViewDeg = 60.0; + static constexpr double kNearPlane = 0.1; + static constexpr double kFarPlane = 50.0; + static constexpr const char* kIBLFolder = "default_env"; + static constexpr double kRotationDegPerSec = 36.0; + static constexpr math::float3 kCameraCenter = {0.0f, 0.0f, 0.0f}; + static constexpr math::float3 kCameraUp = {0.0f, 1.0f, 0.0f}; + static constexpr float kCameraDist = 3.0f; + + struct Window { + std::function onNewFrame; + + SDL_Window* sdl_window = nullptr; + Renderer* renderer = nullptr; + SwapChain* swapChain = nullptr; + utils::Entity cameraEntity; + Camera* camera = nullptr; + View* view = nullptr; + Scene* scene = nullptr; + IBL* ibl = nullptr; + Material* material = nullptr; + MaterialInstance* materialInstance = nullptr; + filamesh::MeshReader::Mesh mesh; + + bool needsDraw = true; + double time = 0.0; + double lastDrawTime = 0.0; + }; +} + +void setup_window(Window& w, Engine* engine); +void destroy_window(Window& w, Engine* engine); +void resize_window(Window& w, Engine* engine); + +void setup_static_scene(Window& w, Engine* engine); +void setup_animating_scene(Window& w, Engine* engine); +void animation_new_frame(Window& w, double dt); +IBL* load_IBL(const utils::Path& iblDirectory, Engine* engine); + +#ifdef __cplusplus +extern "C" +#endif +int main(int argc, char *argv[]) { + // ---- initialize ---- + ASSERT_POSTCONDITION(SDL_Init(SDL_INIT_EVENTS) == 0, "SDL_Init Failure"); + + std::vector windows = { Window(), Window() }; + uint32_t windowFlags = SDL_WINDOW_SHOWN | SDL_WINDOW_ALLOW_HIGHDPI + | SDL_WINDOW_RESIZABLE; + int n = 1; + int x = 50, y = 50; + for (auto &w : windows) { + auto title = std::string("Filament - Window ") + std::to_string(n); + w.sdl_window = SDL_CreateWindow(title.c_str(), x, y, kWidth, kHeight, + windowFlags); + x += 50; + y += 50; + n += 1; + } + + // Create SDL windows first, so that the Engine's context is current + // if we are single-threaded. But we can't create the Filament objects + // until after we have created the engine. + auto engine = Engine::create(kBackend); + + for (auto &w : windows) { + setup_window(w, engine); + } + setup_animating_scene(windows[0], engine); + setup_static_scene(windows[1], engine); + + // ---- event loop ---- + size_t nClosed = 0; + SDL_Event event; + Uint64 lastTime = 0; + const Uint64 kCounterFrequency = SDL_GetPerformanceFrequency(); + + while (nClosed < windows.size()) { + if (!UTILS_HAS_THREADING) { + engine->execute(); + } + + while (SDL_PollEvent(&event) != 0) { + switch (event.type) { + case SDL_QUIT: + nClosed = windows.size(); + break; + case SDL_WINDOWEVENT: + switch (event.window.event) { + case SDL_WINDOWEVENT_RESIZED: + for (auto &w : windows) { + if (event.window.windowID == SDL_GetWindowID(w.sdl_window)) { + resize_window(w, engine); + break; + } + } + break; + case SDL_WINDOWEVENT_CLOSE: + for (auto &w : windows) { + if (event.window.windowID == SDL_GetWindowID(w.sdl_window)) { + SDL_HideWindow(w.sdl_window); + break; + } + } + nClosed++; + break; + default: + break; + } + break; + default: + break; + } + } + + Uint64 now = SDL_GetPerformanceCounter(); + const double dt = lastTime > 0 ? (double(now - lastTime) / kCounterFrequency) : (1.0 / 60.0); + lastTime = now; + for (auto &w : windows) { + w.time += dt; + if (w.onNewFrame) { + w.onNewFrame(w, dt); + } + } + + for (auto &w : windows) { + if (!w.needsDraw) { continue; } + + if (w.renderer->beginFrame(w.swapChain)) { + w.renderer->render(w.view); + w.renderer->endFrame(); + } + w.needsDraw = false; + w.lastDrawTime = w.time; + } + + SDL_Delay(16); + } + + // ---- cleanup ---- + for (auto &w : windows) { + destroy_window(w, engine); + } + + Engine::destroy(&engine); + + SDL_Quit(); + return 0; +} + +void setup_window(Window& w, Engine* engine) { + w.renderer = engine->createRenderer(); + + void* nativeWindow = ::getNativeWindow(w.sdl_window); + void* nativeSwapChain = nativeWindow; +#if defined(__APPLE__) + void* metalLayer = nullptr; + if (kBackend == filament::Engine::Backend::METAL) { + metalLayer = setUpMetalLayer(nativeWindow); + // The swap chain on Metal is a CAMetalLayer. + nativeSwapChain = metalLayer; + } +#if defined(FILAMENT_DRIVER_SUPPORTS_VULKAN) + if (kBackend == filament::Engine::Backend::VULKAN) { + // We request a Metal layer for rendering via MoltenVK. + setUpMetalLayer(nativeWindow); + } +#endif +#endif + w.swapChain = engine->createSwapChain(nativeSwapChain); + + utils::EntityManager& em = utils::EntityManager::get(); + em.create(1, &w.cameraEntity); + w.camera = engine->createCamera(w.cameraEntity); + + w.view = engine->createView(); + w.view->setCamera(w.camera); + + w.scene = engine->createScene(); + w.view->setScene(w.scene); + + resize_window(w, engine); +} + +void destroy_window(Window& w, Engine* engine) { + delete w.ibl; + + engine->destroy(w.mesh.renderable); + engine->destroy(w.materialInstance); + engine->destroy(w.material); + w.view->setScene(nullptr); + engine->destroy(w.scene); + engine->destroy(w.view); + engine->destroyCameraComponent(w.cameraEntity); + engine->destroy(w.cameraEntity); + engine->destroy(w.swapChain); + engine->destroy(w.renderer); + + SDL_DestroyWindow(w.sdl_window); +} + +void resize_window(Window& w, Engine* engine) { +#if defined(__APPLE__) + void* nativeWindow = ::getNativeWindow(w.sdl_window); + if (kBackend == filament::Engine::Backend::METAL) { + resizeMetalLayer(nativeWindow); + } +#if defined(FILAMENT_DRIVER_SUPPORTS_VULKAN) + if (kBackend == filament::Engine::Backend::VULKAN) { + resizeMetalLayer(nativeWindow); + } +#endif +#endif + + int width, height; + SDL_GL_GetDrawableSize(w.sdl_window, &width, &height); + w.view->setViewport({0, 0, uint32_t(width), uint32_t(height)}); + + w.camera->setProjection(kFieldOfViewDeg, double(width) / double(height), + kNearPlane, kFarPlane); + + w.needsDraw = true; +} + +void setup_static_scene(Window& w, Engine* engine) { + auto iblDir = FilamentApp::getRootAssetsPath() + kIBLFolder; + w.ibl = load_IBL(iblDir, engine); + if (w.ibl) { + w.ibl->getIndirectLight()->setIntensity(10000); + w.scene->setIndirectLight(w.ibl->getIndirectLight()); + w.scene->setSkybox(w.ibl->getSkybox()); + } + + w.material = Material::Builder().package(RESOURCES_SANDBOXLIT_DATA, + RESOURCES_SANDBOXLIT_SIZE) + .build(*engine); + w.materialInstance = w.material->createInstance(); + w.materialInstance->setParameter("baseColor", RgbType::sRGB, + {0.50f, 0.90f, 0.80f}); + w.materialInstance->setParameter("roughness", 0.90f); + w.materialInstance->setParameter("metallic", 0.01f); + w.materialInstance->setParameter("reflectance", 0.00f); + w.materialInstance->setParameter("sheenColor", 0.00f); + w.materialInstance->setParameter("clearCoat", 1.00f); + w.materialInstance->setParameter("clearCoatRoughness", 0.00f); + w.mesh = filamesh::MeshReader::loadMeshFromBuffer(engine, MONKEY_SUZANNE_DATA, nullptr, nullptr, w.materialInstance); + w.scene->addEntity(w.mesh.renderable); + + int width, height; + SDL_GL_GetDrawableSize(w.sdl_window, &width, &height); + w.camera->setProjection(kFieldOfViewDeg, double(width) / double(height), + kNearPlane, kFarPlane); + w.camera->lookAt({0.0f, 0.0f, kCameraDist}, kCameraCenter, kCameraUp); + + w.needsDraw = true; +} + +void setup_animating_scene(Window& w, Engine* engine) { + auto iblDir = FilamentApp::getRootAssetsPath() + kIBLFolder; + w.ibl = load_IBL(iblDir, engine); + if (w.ibl) { + w.ibl->getIndirectLight()->setIntensity(10000); + w.scene->setIndirectLight(w.ibl->getIndirectLight()); + w.scene->setSkybox(w.ibl->getSkybox()); + } + + w.material = Material::Builder().package(RESOURCES_SANDBOXLIT_DATA, + RESOURCES_SANDBOXLIT_SIZE) + .build(*engine); + w.materialInstance = w.material->createInstance(); + w.materialInstance->setParameter("baseColor", RgbType::sRGB, + {1.00f, 0.85f, 0.57f}); + w.materialInstance->setParameter("roughness", 0.40f); + w.materialInstance->setParameter("metallic", 0.99f); + w.materialInstance->setParameter("reflectance", 0.75f); + w.materialInstance->setParameter("sheenColor", 0.00f); + w.materialInstance->setParameter("clearCoat", 0.00f); + w.materialInstance->setParameter("clearCoatRoughness", 0.00f); + w.mesh = filamesh::MeshReader::loadMeshFromBuffer(engine, MONKEY_SUZANNE_DATA, nullptr, nullptr, w.materialInstance); + w.scene->addEntity(w.mesh.renderable); + + int width, height; + SDL_GL_GetDrawableSize(w.sdl_window, &width, &height); + w.camera->setProjection(kFieldOfViewDeg, double(width) / double(height), + kNearPlane, kFarPlane); + w.camera->lookAt({0.0f, 0.0f, kCameraDist}, kCameraCenter, kCameraUp); + + w.needsDraw = true; + + w.onNewFrame = animation_new_frame; +} + +void animation_new_frame(Window& w, double dt) { + // Don't animate every frame or the frames queue up and get very laggy. + if ((w.time - w.lastDrawTime) < 0.040) { + return; + } + + double theta = w.time * kRotationDegPerSec * 3.141592653589793 / 180.0; + math::float3 eye = {kCameraDist * std::sin(theta), + 0.0f, + kCameraDist * std::cos(theta)}; + w.camera->lookAt(eye, kCameraCenter, kCameraUp); + + w.needsDraw = true; +} + +IBL* load_IBL(const utils::Path& iblDirectory, Engine* engine) { + utils::Path iblPath(iblDirectory); + + if (!iblPath.exists()) { + std::cerr << "The specified IBL path does not exist: " << iblPath << std::endl; + return nullptr; + } + + if (!iblPath.isDirectory()) { + std::cerr << "The specified IBL path is not a directory: " << iblPath << std::endl; + return nullptr; + } + + IBL* ibl= new IBL(*engine); + if (!ibl->loadFromDirectory(iblPath)) { + std::cerr << "Could not load the specified IBL: " << iblPath << std::endl; + delete ibl; + return nullptr; + } + + return ibl; +}