From 7cd308fcaed59530bc7cd2f0454d587003ac6439 Mon Sep 17 00:00:00 2001 From: vkedwardli Date: Mon, 26 Jun 2023 17:56:56 +0800 Subject: [PATCH] Realtime Video Routing (#1126) * Syphon video routing with OpenGL and Vulkan (MacOS) * Spout video routing with DX11 and OpenGL (Windows) * Fix Xcode detection --- .gitmodules | 6 ++ CMakeLists.txt | 32 +++++++ core/cfg/option.cpp | 5 ++ core/cfg/option.h | 5 ++ core/deps/Spout | 1 + core/deps/Syphon | 1 + core/rend/dx11/dx11_renderer.cpp | 88 +++++++++++++++++++ core/rend/dx11/dx11_renderer.h | 5 ++ core/rend/dx11/dx11context.cpp | 19 ++++ core/rend/dx11/dx11context.h | 1 + core/rend/dx11/oit/dx11_oitrenderer.cpp | 1 + core/rend/gl4/gles.cpp | 3 + core/rend/gles/gles.cpp | 40 +++++++++ core/rend/gles/gles.h | 7 ++ core/rend/gui.cpp | 35 +++++++- core/rend/vulkan/vulkan_context.cpp | 28 +++++- core/rend/vulkan/vulkan_context.h | 2 + core/rend/vulkan/vulkan_renderer.h | 19 ++++ core/windows/winmain.cpp | 62 +++++++++++++ core/wsi/context.h | 1 + core/wsi/gl_context.cpp | 5 ++ core/wsi/sdl.cpp | 22 ++++- core/wsi/sdl.h | 1 + .../emulator-osx/emulator-osx/osx-main.mm | 59 ++++++++++++- 24 files changed, 443 insertions(+), 5 deletions(-) create mode 160000 core/deps/Spout create mode 160000 core/deps/Syphon diff --git a/.gitmodules b/.gitmodules index 899318949..42e04c601 100644 --- a/.gitmodules +++ b/.gitmodules @@ -22,3 +22,9 @@ [submodule "core/deps/oboe"] path = core/deps/oboe url = https://github.com/google/oboe.git +[submodule "core/deps/Syphon"] + path = core/deps/Syphon + url=https://github.com/vkedwardli/Syphon-Framework.git +[submodule "core/deps/Spout"] + path = core/deps/Spout + url = https://github.com/leadedge/Spout2.git diff --git a/CMakeLists.txt b/CMakeLists.txt index 1962e6955..08a0be015 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -214,10 +214,15 @@ else() $<$:-Wall>) endif() +if(${CMAKE_OSX_SYSROOT} MATCHES "\/MacOSX.platform\/Developer\/SDKs") + set(TARGET_MAC ON) +endif() + target_compile_definitions(${PROJECT_NAME} PRIVATE $<$:GL_SILENCE_DEPRECATION> $<$:DEBUGFAST> $<$:TARGET_IPHONE> + $<$:TARGET_MAC> $<$:GLES> $<$:GLES3> $<$:GLES_SILENCE_DEPRECATION> @@ -1246,6 +1251,7 @@ if(USE_VULKAN) target_compile_definitions(${PROJECT_NAME} PUBLIC VK_USE_PLATFORM_WIN32_KHR) elseif(APPLE) target_compile_definitions(${PROJECT_NAME} PUBLIC VK_USE_PLATFORM_MACOS_MVK) + target_compile_definitions(${PROJECT_NAME} PUBLIC VK_USE_PLATFORM_METAL_EXT) endif() add_subdirectory(core/deps/Vulkan-Headers) @@ -1623,6 +1629,20 @@ if(NOT LIBRETRO) COMMAND ldid -S${CMAKE_CURRENT_SOURCE_DIR}/shell/apple/emulator-ios/emulator/flycast.entitlements ${CMAKE_CURRENT_BINARY_DIR}/Payload/Flycast.app/Flycast COMMAND ${CMAKE_COMMAND} -E tar "cfv" "${CMAKE_CURRENT_BINARY_DIR}/$-${CMAKE_OSX_SYSROOT}/Flycast.ipa" --format=zip ${CMAKE_CURRENT_BINARY_DIR}/Payload) else() + + add_subdirectory(core/deps/Syphon) + target_link_libraries(${PROJECT_NAME} PRIVATE Syphon) + if(${CMAKE_GENERATOR} MATCHES "^Xcode.*") + add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy ${SYPHON_METALLIB} + ${CMAKE_CURRENT_BINARY_DIR}/$/Flycast.app/Contents/Resources/) + else() + add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy ${SYPHON_METALLIB} + ${CMAKE_CURRENT_BINARY_DIR}/$/Flycast.app/Contents/Resources/) + endif() + target_compile_definitions(${PROJECT_NAME} PRIVATE VIDEO_ROUTING) + target_sources(${PROJECT_NAME} PRIVATE shell/apple/common/http_client.mm shell/apple/emulator-osx/emulator-osx/SDLMain.h @@ -1659,6 +1679,7 @@ if(NOT LIBRETRO) find_library(FOUNDATION_LIBRARY Foundation) find_library(AUDIO_TOOLBOX_LIBRARY AudioToolbox) find_library(OPENGL_LIBRARY OpenGL) + find_library(METAL_LIBRARY Metal) find_library(IOSURFACE_LIBRARY IOSurface) find_library(MULTITOUCH_SUPPORT_LIBRARY MultitouchSupport /System/Library/PrivateFrameworks) @@ -1667,6 +1688,7 @@ if(NOT LIBRETRO) ${FOUNDATION_LIBRARY} ${AUDIO_TOOLBOX_LIBRARY} ${OPENGL_LIBRARY} + ${METAL_LIBRARY} ${IOSURFACE_LIBRARY} ${MULTITOUCH_SUPPORT_LIBRARY}) @@ -1713,6 +1735,16 @@ if(NOT LIBRETRO) shell/uwp/http_client.cpp) set_target_properties(${PROJECT_NAME} PROPERTIES RESOURCE "${ResourceFiles}") else() + target_include_directories(${PROJECT_NAME} PRIVATE core/deps/Spout/SPOUTSDK/SpoutGL) + target_include_directories(${PROJECT_NAME} PRIVATE core/deps/Spout/SPOUTSDK/SpoutDirectX/SpoutDX) + SET(SPOUT_BUILD_CMT OFF CACHE BOOL "For Visual Studio - build /MT to link runtime libraries" FORCE) + SET(SPOUT_BUILD_SPOUTDX ON CACHE BOOL "Build SpoutDX DirectX11 support library" FORCE) + SET(SPOUT_BUILD_LIBRARY OFF CACHE BOOL "Build C-compatible cross compiler library" FORCE) + SET(SKIP_INSTALL_ALL ON CACHE BOOL "Install headers and libraries" FORCE) + add_subdirectory(core/deps/Spout) + target_link_libraries(${PROJECT_NAME} PRIVATE Spout_static SpoutDX_static) + target_compile_definitions(${PROJECT_NAME} PRIVATE VIDEO_ROUTING) + target_sources(${PROJECT_NAME} PRIVATE shell/windows/flycast.rc) target_link_libraries(${PROJECT_NAME} PRIVATE dsound opengl32 winmm ws2_32 wsock32 xinput9_1_0 cfgmgr32 wininet) endif() diff --git a/core/cfg/option.cpp b/core/cfg/option.cpp index b09660195..4452dce76 100644 --- a/core/cfg/option.cpp +++ b/core/cfg/option.cpp @@ -106,6 +106,11 @@ Option DupeFrames("rend.DupeFrames", false); Option PerPixelLayers("rend.PerPixelLayers", 32); Option NativeDepthInterpolation("rend.NativeDepthInterpolation", false); Option EmulateFramebuffer("rend.EmulateFramebuffer", false); +#ifdef VIDEO_ROUTING +Option VideoRouting("rend.VideoRouting", false); +Option VideoRoutingScale("rend.VideoRoutingScale", false); +Option VideoRoutingVRes("rend.VideoRoutingVRes", 720); +#endif // Misc diff --git a/core/cfg/option.h b/core/cfg/option.h index aa1777a04..a4d784894 100644 --- a/core/cfg/option.h +++ b/core/cfg/option.h @@ -465,6 +465,11 @@ extern Option ThreadedRendering; extern Option DupeFrames; extern Option NativeDepthInterpolation; extern Option EmulateFramebuffer; +#ifdef VIDEO_ROUTING +extern Option VideoRouting; +extern Option VideoRoutingScale; +extern Option VideoRoutingVRes; +#endif // Misc diff --git a/core/deps/Spout b/core/deps/Spout new file mode 160000 index 000000000..122c85196 --- /dev/null +++ b/core/deps/Spout @@ -0,0 +1 @@ +Subproject commit 122c8519685f1f221a0dcdda96e23424ffce3f08 diff --git a/core/deps/Syphon b/core/deps/Syphon new file mode 160000 index 000000000..486dd8e4e --- /dev/null +++ b/core/deps/Syphon @@ -0,0 +1 @@ +Subproject commit 486dd8e4ef3bead482ea26de99770f4389e24e37 diff --git a/core/rend/dx11/dx11_renderer.cpp b/core/rend/dx11/dx11_renderer.cpp index d865b93c8..65d174651 100644 --- a/core/rend/dx11/dx11_renderer.cpp +++ b/core/rend/dx11/dx11_renderer.cpp @@ -185,6 +185,10 @@ void DX11Renderer::Term() quad.reset(); deviceContext.reset(); device.reset(); + vrStagingTexture.reset(); + vrStagingTextureSRV.reset(); + vrScaledTexture.reset(); + vrScaledRenderTarget.reset(); } void DX11Renderer::createDepthTexAndView(ComPtr& texture, ComPtr& view, int width, int height, DXGI_FORMAT format, UINT bindFlags) @@ -460,6 +464,7 @@ bool DX11Renderer::Render() deviceContext->OMSetRenderTargets(1, &theDX11Context.getRenderTarget().get(), nullptr); displayFramebuffer(); DrawOSD(false); + renderVideoRouting(); theDX11Context.setFrameRendered(); #else ID3D11RenderTargetView *nullView = nullptr; @@ -960,6 +965,7 @@ void DX11Renderer::RenderFramebuffer(const FramebufferInfo& info) deviceContext->OMSetRenderTargets(1, &theDX11Context.getRenderTarget().get(), nullptr); displayFramebuffer(); DrawOSD(false); + renderVideoRouting(); theDX11Context.setFrameRendered(); #else ID3D11RenderTargetView *nullView = nullptr; @@ -1294,6 +1300,88 @@ void DX11Renderer::writeFramebufferToVRAM() WriteFramebuffer<2, 1, 0, 3>(width, height, (u8 *)tmp_buf.data(), texAddress, pvrrc.fb_W_CTRL, linestride, xClip, yClip); } +void DX11Renderer::renderVideoRouting() +{ +#ifdef VIDEO_ROUTING + if (config::VideoRouting) + { + extern void os_VideoRoutingPublishFrameTexture(ID3D11Texture2D* pTexture); + + ID3D11RenderTargetView* pRenderTargetView = theDX11Context.getRenderTarget().get(); + + // Backbuffer texture would be different after reszing, fetching new address everytime + ID3D11Resource* pResource = nullptr; + pRenderTargetView->GetResource(&pResource); + ID3D11Texture2D* backBufferTexture = nullptr; + pResource->QueryInterface(__uuidof(ID3D11Texture2D), (void**)&backBufferTexture); + + if (config::VideoRoutingScale) + { + static int targetWidth, targetHeight, vrStagingWidth, vrStagingHeight; + static D3D11_VIEWPORT scaledViewPort{}; + + auto updateScaledTexture = [this]() -> void { + targetWidth = config::VideoRoutingVRes * settings.display.width / settings.display.height; + targetHeight = config::VideoRoutingVRes; + + vrScaledTexture.reset(); + vrScaledRenderTarget.reset(); + createTexAndRenderTarget(vrScaledTexture, vrScaledRenderTarget, targetWidth, targetHeight); + + scaledViewPort.Width = targetWidth; + scaledViewPort.Height = targetHeight; + scaledViewPort.MinDepth = 0.f; + scaledViewPort.MaxDepth = 1.f; + }; + + D3D11_TEXTURE2D_DESC bbDesc = {}; + backBufferTexture->GetDesc(&bbDesc); + + // Window resized + if (bbDesc.Width != vrStagingWidth || bbDesc.Height != vrStagingHeight) + { + vrStagingTexture.reset(); + vrStagingTextureSRV.reset(); + + D3D11_TEXTURE2D_DESC srvDesc = bbDesc; + srvDesc.BindFlags = D3D11_BIND_SHADER_RESOURCE; + srvDesc.Usage = D3D11_USAGE_DEFAULT; + device->CreateTexture2D(&srvDesc, nullptr, &vrStagingTexture.get()); + + D3D11_SHADER_RESOURCE_VIEW_DESC viewDesc{}; + viewDesc.Format = DXGI_FORMAT_B8G8R8A8_UNORM; + viewDesc.ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2D; + viewDesc.Texture2D.MipLevels = 1; + + device->CreateShaderResourceView(vrStagingTexture.get(), &viewDesc, &vrStagingTextureSRV.get()); + + updateScaledTexture(); + } + + // Scale down value changed + if (targetHeight != config::VideoRoutingVRes) + { + updateScaledTexture(); + } + + deviceContext->OMSetRenderTargets(1, &vrScaledRenderTarget.get(), nullptr); + deviceContext->RSSetViewports(1, &scaledViewPort); + deviceContext->CopyResource(vrStagingTexture.get(), backBufferTexture); + quad->draw(vrStagingTextureSRV, samplers->getSampler(true)); + os_VideoRoutingPublishFrameTexture(vrScaledTexture); + + deviceContext->OMSetRenderTargets(1, &theDX11Context.getRenderTarget().get(), nullptr); + + } else { + os_VideoRoutingPublishFrameTexture(backBufferTexture); + } + + backBufferTexture->Release(); + pResource->Release(); + } +#endif +} + Renderer *rend_DirectX11() { return new DX11Renderer(); diff --git a/core/rend/dx11/dx11_renderer.h b/core/rend/dx11/dx11_renderer.h index 0c87afc1c..b257bfa07 100644 --- a/core/rend/dx11/dx11_renderer.h +++ b/core/rend/dx11/dx11_renderer.h @@ -96,6 +96,7 @@ protected: void setCullMode(int mode); virtual void setRTTSize(int width, int height) {} void writeFramebufferToVRAM(); + void renderVideoRouting(); ComPtr device; ComPtr deviceContext; @@ -154,6 +155,10 @@ private: ComPtr fbScaledTexture; ComPtr fbScaledTextureView; ComPtr fbScaledRenderTarget; + ComPtr vrStagingTexture; + ComPtr vrStagingTextureSRV; + ComPtr vrScaledTexture; + ComPtr vrScaledRenderTarget; ComPtr rasterCullNone, rasterCullFront, rasterCullBack; diff --git a/core/rend/dx11/dx11context.cpp b/core/rend/dx11/dx11context.cpp index 510e50065..0c54677a3 100644 --- a/core/rend/dx11/dx11context.cpp +++ b/core/rend/dx11/dx11context.cpp @@ -161,6 +161,8 @@ bool DX11Context::init(bool keepCurrentWindow) NOTICE_LOG(RENDERER, "No system-provided shader cache"); } + initVideoRouting(); + imguiDriver = std::unique_ptr(new DX11Driver(pDevice, pDeviceContext)); resize(); shaders.init(pDevice, &D3DCompile); @@ -171,6 +173,19 @@ bool DX11Context::init(bool keepCurrentWindow) return success; } +void DX11Context::initVideoRouting() +{ + #ifdef VIDEO_ROUTING + extern void os_VideoRoutingTermDX(); + extern void os_VideoRoutingInitSpoutDXWithDevice(ID3D11Device* pDevice); + os_VideoRoutingTermDX(); + if (config::VideoRouting) + { + os_VideoRoutingInitSpoutDXWithDevice(pDevice.get()); + } + #endif +} + void DX11Context::term() { NOTICE_LOG(RENDERER, "DX11 Context terminating"); @@ -195,6 +210,10 @@ void DX11Context::term() FreeLibrary(d3dcompilerHandle); d3dcompilerHandle = NULL; } +#ifdef VIDEO_ROUTING + extern void os_VideoRoutingTermDX(); + os_VideoRoutingTermDX(); +#endif } void DX11Context::Present() diff --git a/core/rend/dx11/dx11context.h b/core/rend/dx11/dx11context.h index 8e8d909c0..cdb409c4f 100644 --- a/core/rend/dx11/dx11context.h +++ b/core/rend/dx11/dx11context.h @@ -33,6 +33,7 @@ class DX11Context : public GraphicsContext { public: bool init(bool keepCurrentWindow = false); + void initVideoRouting() override; void term() override; void EndImGuiFrame(); void Present(); diff --git a/core/rend/dx11/oit/dx11_oitrenderer.cpp b/core/rend/dx11/oit/dx11_oitrenderer.cpp index 0267c1301..1ea29b9c4 100644 --- a/core/rend/dx11/oit/dx11_oitrenderer.cpp +++ b/core/rend/dx11/oit/dx11_oitrenderer.cpp @@ -667,6 +667,7 @@ struct DX11OITRenderer : public DX11Renderer deviceContext->OMSetRenderTargets(1, &theDX11Context.getRenderTarget().get(), nullptr); displayFramebuffer(); DrawOSD(false); + renderVideoRouting(); theDX11Context.setFrameRendered(); #else ID3D11RenderTargetView *nullView = nullptr; diff --git a/core/rend/gl4/gles.cpp b/core/rend/gl4/gles.cpp index 413d175cf..dc6d79119 100644 --- a/core/rend/gl4/gles.cpp +++ b/core/rend/gl4/gles.cpp @@ -671,6 +671,7 @@ struct OpenGL4Renderer : OpenGLRenderer gl.ofbo2.ready = false; frameRendered = true; } + renderVideoRouting(); restoreCurrentFramebuffer(); return true; @@ -779,6 +780,8 @@ static void resize(int w, int h) bool OpenGL4Renderer::renderFrame(int width, int height) { + initVideoRoutingFrameBuffer(); + const bool is_rtt = pvrrc.isRTT; TransformMatrix matrices(pvrrc, is_rtt ? pvrrc.getFramebufferWidth() : width, diff --git a/core/rend/gles/gles.cpp b/core/rend/gles/gles.cpp index 9ff78586d..cee1710e7 100644 --- a/core/rend/gles/gles.cpp +++ b/core/rend/gles/gles.cpp @@ -445,6 +445,7 @@ void termGLCommon() gl.dcfb.tex = 0; gl.ofbo2.framebuffer.reset(); gl.fbscaling.framebuffer.reset(); + gl.videorouting.framebuffer.reset(); #ifdef LIBRETRO postProcessor.term(); termVmuLightgun(); @@ -1151,6 +1152,8 @@ static void upload_vertex_indices() bool OpenGLRenderer::renderFrame(int width, int height) { + initVideoRoutingFrameBuffer(); + bool is_rtt = pvrrc.isRTT; float vtx_min_fZ = 0.f; //pvrrc.fZ_min; @@ -1378,6 +1381,22 @@ bool OpenGLRenderer::renderFrame(int width, int height) return !is_rtt; } +void OpenGLRenderer::initVideoRoutingFrameBuffer() +{ +#ifdef VIDEO_ROUTING + if (config::VideoRouting) + { + int targetWidth = (config::VideoRoutingScale ? config::VideoRoutingVRes * settings.display.width / settings.display.height : settings.display.width); + int targetHeight = (config::VideoRoutingScale ? config::VideoRoutingVRes : settings.display.height); + if (gl.videorouting.framebuffer != nullptr + && (gl.videorouting.framebuffer->getWidth() != targetWidth || gl.videorouting.framebuffer->getHeight() != targetHeight)) + gl.videorouting.framebuffer.reset(); + if (gl.videorouting.framebuffer == nullptr) + gl.videorouting.framebuffer = std::make_unique(targetWidth, targetHeight, true, true); + } +#endif +} + void OpenGLRenderer::Term() { TexCache.Clear(); @@ -1399,11 +1418,32 @@ bool OpenGLRenderer::Render() frameRendered = true; gl.ofbo2.ready = false; } + + renderVideoRouting(); restoreCurrentFramebuffer(); return true; } +void OpenGLRenderer::renderVideoRouting() +{ +#ifdef VIDEO_ROUTING + if (config::VideoRouting) + { + glBindFramebuffer(GL_READ_FRAMEBUFFER, gl.ofbo.origFbo); + gl.videorouting.framebuffer->bind(GL_DRAW_FRAMEBUFFER); + glcache.Disable(GL_SCISSOR_TEST); + int targetWidth = (config::VideoRoutingScale ? config::VideoRoutingVRes * settings.display.width / settings.display.height : settings.display.width); + int targetHeight = (config::VideoRoutingScale ? config::VideoRoutingVRes : settings.display.height); + glBlitFramebuffer(0, 0, settings.display.width, settings.display.height, + 0, 0, targetWidth, targetHeight, + GL_COLOR_BUFFER_BIT, GL_LINEAR); + extern void os_VideoRoutingPublishFrameTexture(GLuint texID, GLuint texTarget, float w, float h); + os_VideoRoutingPublishFrameTexture(gl.videorouting.framebuffer->getTexture(), GL_TEXTURE_2D, targetWidth, targetHeight); + } +#endif +} + Renderer* rend_GLES2() { return new OpenGLRenderer(); diff --git a/core/rend/gles/gles.h b/core/rend/gles/gles.h index 8617e98ea..f0def4a1e 100755 --- a/core/rend/gles/gles.h +++ b/core/rend/gles/gles.h @@ -305,6 +305,11 @@ struct gl_ctx bool ready = false; } ofbo2; + struct + { + std::unique_ptr framebuffer; + } videorouting; + const char *gl_version; const char *glsl_version_header; int gl_major; @@ -545,6 +550,7 @@ protected: } bool renderLastFrame(); + void renderVideoRouting(); private: bool renderFrame(int width, int height); @@ -553,6 +559,7 @@ protected: bool frameRendered = false; int width = 640; int height = 480; + void initVideoRoutingFrameBuffer(); }; void initQuad(); diff --git a/core/rend/gui.cpp b/core/rend/gui.cpp index a7c3005df..7764da428 100644 --- a/core/rend/gui.cpp +++ b/core/rend/gui.cpp @@ -1576,7 +1576,7 @@ static void gui_display_settings() if (ImGui::Button("Change")) gui_state = GuiState::Onboarding; #endif -#if defined(__APPLE__) && TARGET_OS_OSX +#ifdef TARGET_MAC ImGui::SameLine(ImGui::GetContentRegionAvail().x - ImGui::CalcTextSize("Reveal in Finder").x - ImGui::GetStyle().FramePadding.x); if (ImGui::Button("Reveal in Finder")) { @@ -2143,6 +2143,39 @@ static void gui_display_settings() OptionCheckbox("Load Custom Textures", config::CustomTextures, "Load custom/high-res textures from data/textures/"); } +#ifdef VIDEO_ROUTING +#ifdef __APPLE__ + header("Video Routing (Syphon)"); +#elif defined(_WIN32) + ((renderApi == 0) || (renderApi == 3)) ? header("Video Routing (Spout)") : header("Video Routing (Only available with OpenGL or DirectX 11)"); +#endif + { +#ifdef __APPLE__ + if (OptionCheckbox("Send video content to another application", config::VideoRouting, + "e.g. Route GPU texture to OBS Studio directly instead of using CPU intensive Display/Window Capture")) +#elif defined(_WIN32) + DisabledScope scope( !( (renderApi == 0) || (renderApi == 3)) ); + if (OptionCheckbox("Send video content to another program", config::VideoRouting, + "e.g. Route GPU texture to OBS Studio directly instead of using CPU intensive Display/Window Capture")) +#endif + { + GraphicsContext::Instance()->initVideoRouting(); + } + { + DisabledScope scope(!config::VideoRouting); + OptionCheckbox("Scale down before sending", config::VideoRoutingScale, "Could increase performance when sharing a smaller texture, YMMV"); + { + DisabledScope scope(!config::VideoRoutingScale); + static int vres = config::VideoRoutingVRes; + if( ImGui::InputInt("Output vertical resolution", &vres) ) + { + config::VideoRoutingVRes = vres; + } + } + ImGui::Text("Output texture size: %d x %d", config::VideoRoutingScale ? config::VideoRoutingVRes * settings.display.width / settings.display.height : settings.display.width, config::VideoRoutingScale ? config::VideoRoutingVRes : settings.display.height); + } + } +#endif ImGui::PopStyleVar(); ImGui::EndTabItem(); diff --git a/core/rend/vulkan/vulkan_context.cpp b/core/rend/vulkan/vulkan_context.cpp index 97476cab8..d623ecfa1 100644 --- a/core/rend/vulkan/vulkan_context.cpp +++ b/core/rend/vulkan/vulkan_context.cpp @@ -19,6 +19,7 @@ along with Flycast. If not, see . */ #include "vulkan_context.h" +#include "vulkan_renderer.h" #include "imgui/imgui.h" #include "imgui/backends/imgui_impl_vulkan.h" #include "../gui.h" @@ -413,6 +414,8 @@ bool VulkanContext::InitDevice() } else if (!strcmp(property.extensionName, "VK_KHR_portability_subset")) deviceExtensions.push_back("VK_KHR_portability_subset"); + else if (!strcmp(property.extensionName, "VK_EXT_metal_objects")) + deviceExtensions.push_back("VK_EXT_metal_objects"); #ifdef VK_DEBUG else if (!strcmp(property.extensionName, VK_EXT_DEBUG_MARKER_EXTENSION_NAME)) { @@ -514,6 +517,8 @@ bool VulkanContext::InitDevice() + std::to_string(props.driverVersion / 10000) + "." + std::to_string((props.driverVersion % 10000) / 100) + "." + std::to_string(props.driverVersion % 100); + + initVideoRouting(); #else driverVersion = std::to_string(VK_API_VERSION_MAJOR(props.driverVersion)) + "." + std::to_string(VK_API_VERSION_MINOR(props.driverVersion)) + "." @@ -538,6 +543,19 @@ bool VulkanContext::InitDevice() return false; } +void VulkanContext::initVideoRouting() +{ +#if defined(VIDEO_ROUTING) && defined(TARGET_MAC) + extern void os_VideoRoutingTermVk(); + extern void os_VideoRoutingInitSyphonWithVkDevice(const vk::UniqueDevice& device); + os_VideoRoutingTermVk(); + if (config::VideoRouting) + { + os_VideoRoutingInitSyphonWithVkDevice(device); + } +#endif +} + void VulkanContext::CreateSwapChain() { try @@ -626,8 +644,8 @@ void VulkanContext::CreateSwapChain() if (surfaceCapabilities.maxImageCount != 0) imageCount = std::min(imageCount, surfaceCapabilities.maxImageCount); vk::ImageUsageFlags usage = vk::ImageUsageFlagBits::eColorAttachment; -#ifdef TEST_AUTOMATION - // for final screenshot +#if defined(TEST_AUTOMATION) || (defined(VIDEO_ROUTING) && defined(TARGET_MAC)) + // for final screenshot or Syphon usage |= vk::ImageUsageFlagBits::eTransferSrc; #endif vk::SwapchainCreateInfoKHR swapChainCreateInfo(vk::SwapchainCreateFlagsKHR(), GetSurface(), imageCount, colorFormat, vk::ColorSpaceKHR::eSrgbNonlinear, @@ -967,6 +985,8 @@ void VulkanContext::PresentFrame(vk::Image image, vk::ImageView imageView, const DrawOverlay(settings.display.uiScale, config::FloatVMUs, true); renderer->DrawOSD(false); EndFrame(overlayCmdBuffer); + static_cast(renderer)->RenderVideoRouting(); + } catch (const InvalidVulkanContext& err) { } } @@ -1020,6 +1040,10 @@ void VulkanContext::term() renderCompleteSemaphores.clear(); drawFences.clear(); allocator.Term(); +#if defined(VIDEO_ROUTING) && defined(TARGET_MAC) + extern void os_VideoRoutingTermVk(); + os_VideoRoutingTermVk(); +#endif #ifndef USE_SDL surface.reset(); #else diff --git a/core/rend/vulkan/vulkan_context.h b/core/rend/vulkan/vulkan_context.h index b1af59857..2e5e72b49 100644 --- a/core/rend/vulkan/vulkan_context.h +++ b/core/rend/vulkan/vulkan_context.h @@ -61,6 +61,7 @@ public: void Present() noexcept; void PresentFrame(vk::Image image, vk::ImageView imageView, const vk::Extent2D& extent, float aspectRatio) noexcept; void PresentLastFrame(); + void initVideoRouting() override; vk::PhysicalDevice GetPhysicalDevice() const { return physicalDevice; } vk::Device GetDevice() const { return *device; } @@ -69,6 +70,7 @@ public: vk::CommandBuffer GetCurrentCommandBuffer() const { return *commandBuffers[GetCurrentImageIndex()]; } vk::DescriptorPool GetDescriptorPool() const { return *descriptorPool; } vk::Extent2D GetViewPort() const { return { (u32)settings.display.width, (u32)settings.display.height }; } + vk::SwapchainKHR GetSwapChain() const { return *swapChain; } u32 GetSwapChainSize() const { return (u32)imageViews.size(); } int GetCurrentImageIndex() const { return currentImage; } void WaitIdle() const; diff --git a/core/rend/vulkan/vulkan_renderer.h b/core/rend/vulkan/vulkan_renderer.h index d9b3b765f..6329a5231 100644 --- a/core/rend/vulkan/vulkan_renderer.h +++ b/core/rend/vulkan/vulkan_renderer.h @@ -232,6 +232,25 @@ public: fbCommandPool.EndFrame(); framebufferRendered = true; } + + void RenderVideoRouting() + { +#if defined(VIDEO_ROUTING) && defined(TARGET_MAC) + if (config::VideoRouting) + { + auto device = GetContext()->GetDevice(); + auto srcImage = device.getSwapchainImagesKHR(GetContext()->GetSwapChain())[GetContext()->GetCurrentImageIndex()]; + auto graphicsQueue = device.getQueue(GetContext()->GetGraphicsQueueFamilyIndex(), 0); + + int targetWidth = (config::VideoRoutingScale ? config::VideoRoutingVRes * settings.display.width / settings.display.height : settings.display.width); + int targetHeight = (config::VideoRoutingScale ? config::VideoRoutingVRes : settings.display.height); + + extern void os_VideoRoutingPublishFrameTexture(const vk::Device& device, const vk::Image& image, const vk::Queue& queue, float x, float y, float w, float h); + os_VideoRoutingPublishFrameTexture(device, srcImage, graphicsQueue, 0, 0, targetWidth, targetHeight); + } +#endif + } + protected: BaseVulkanRenderer() : viewport(640, 480) {} diff --git a/core/windows/winmain.cpp b/core/windows/winmain.cpp index a055aa22f..20730acdc 100644 --- a/core/windows/winmain.cpp +++ b/core/windows/winmain.cpp @@ -1002,3 +1002,65 @@ void os_RunInstance(int argc, const char *argv[]) WARN_LOG(BOOT, "Cannot launch Flycast instance: error %d", GetLastError()); } } + +#ifdef VIDEO_ROUTING +#include "SpoutSender.h" +#include "SpoutDX.h" + +static SpoutSender* spoutSender; +static spoutDX* spoutDXSender; + +void os_VideoRoutingInitSpout() +{ + if (spoutSender == nullptr) + { + spoutSender = new SpoutSender(); + } + + int boardID = cfgLoadInt("naomi", "BoardId", 0); + char buf[32] = {0}; + vsnprintf(buf, sizeof(buf), (boardID == 0 ? "Flycast - Video Content" : "Flycast - Video Content - %d"), std::va_list(&boardID)); + spoutSender->SetSenderName(buf); +} + +void os_VideoRoutingPublishFrameTexture(GLuint texID, GLuint texTarget, float w, float h) +{ + spoutSender->SendTexture(texID, texTarget, w, h, true); +} + +void os_VideoRoutingTermGL() +{ + if (spoutSender) + { + spoutSender->ReleaseSender(); + spoutSender = nullptr; + } +} + +void os_VideoRoutingInitSpoutDXWithDevice(ID3D11Device* pDevice) +{ + if (spoutDXSender == nullptr) + { + spoutDXSender = new spoutDX(); + } + spoutDXSender->OpenDirectX11(pDevice); + int boardID = cfgLoadInt("naomi", "BoardId", 0); + char buf[32] = {0}; + vsnprintf(buf, sizeof(buf), (boardID == 0 ? "Flycast - Video Content" : "Flycast - Video Content - %d"), std::va_list(&boardID)); + spoutDXSender->SetSenderName(buf); +} + +void os_VideoRoutingPublishFrameTexture(ID3D11Texture2D* pTexture) +{ + spoutDXSender->SendTexture(pTexture); +} + +void os_VideoRoutingTermDX() +{ + if (spoutDXSender) + { + spoutDXSender->ReleaseSender(); + spoutDXSender = nullptr; + } +} +#endif diff --git a/core/wsi/context.h b/core/wsi/context.h index ec1891f0f..ce29795c3 100644 --- a/core/wsi/context.h +++ b/core/wsi/context.h @@ -34,6 +34,7 @@ public: virtual std::string getDriverName() = 0; virtual std::string getDriverVersion() = 0; virtual bool hasPerPixel() { return false; } + virtual void initVideoRouting() {} void setWindow(void *window, void *display = nullptr) { this->window = window; diff --git a/core/wsi/gl_context.cpp b/core/wsi/gl_context.cpp index 3fff82f32..981964a66 100644 --- a/core/wsi/gl_context.cpp +++ b/core/wsi/gl_context.cpp @@ -56,6 +56,11 @@ void GLGraphicsContext::preTerm() #ifndef LIBRETRO imguiDriver.reset(); #endif +#ifdef VIDEO_ROUTING + extern void os_VideoRoutingTermGL(); + os_VideoRoutingTermGL(); +#endif + instance = nullptr; } diff --git a/core/wsi/sdl.cpp b/core/wsi/sdl.cpp index 4ef7854ba..6d31865a6 100644 --- a/core/wsi/sdl.cpp +++ b/core/wsi/sdl.cpp @@ -116,10 +116,30 @@ bool SDLGLGraphicsContext::init() glGetError(); eglGetError(); #endif - + + initVideoRouting(); + return true; } +void SDLGLGraphicsContext::initVideoRouting() +{ +#ifdef VIDEO_ROUTING + extern void os_VideoRoutingTermGL(); + os_VideoRoutingTermGL(); + if (config::VideoRouting) + { +#ifdef TARGET_MAC + extern void os_VideoRoutingInitSyphonWithGLContext(void* glContext); + os_VideoRoutingInitSyphonWithGLContext(glcontext); +#elif defined(_WIN32) + extern void os_VideoRoutingInitSpout(); + os_VideoRoutingInitSpout(); +#endif + } +#endif +} + void SDLGLGraphicsContext::swap() { do_swap_automation(); diff --git a/core/wsi/sdl.h b/core/wsi/sdl.h index ed049b5e7..0fd49459b 100644 --- a/core/wsi/sdl.h +++ b/core/wsi/sdl.h @@ -42,6 +42,7 @@ public: bool init(); void term() override; void swap(); + void initVideoRouting() override; private: SDL_GLContext glcontext = nullptr; diff --git a/shell/apple/emulator-osx/emulator-osx/osx-main.mm b/shell/apple/emulator-osx/emulator-osx/osx-main.mm index 7c1fc44fa..c83c4585c 100644 --- a/shell/apple/emulator-osx/emulator-osx/osx-main.mm +++ b/shell/apple/emulator-osx/emulator-osx/osx-main.mm @@ -33,7 +33,8 @@ int darw_printf(const char* text, ...) va_end(args); NSString* log = [NSString stringWithCString:temp encoding: NSUTF8StringEncoding]; - if (getenv("TERM") == NULL) //Xcode console does not support colors + static bool isXcode = [[[NSProcessInfo processInfo] environment][@"OS_ACTIVITY_DT_MODE"] boolValue]; + if (isXcode) // Xcode console does not support colors { log = [log stringByReplacingOccurrencesOfString:@"\x1b[0m" withString:@""]; log = [log stringByReplacingOccurrencesOfString:@"\x1b[92m" withString:@"ℹ️ "]; @@ -202,3 +203,59 @@ void os_RunInstance(int argc, const char *argv[]) die("execv failed"); } } + +#import +#import +#include "rend/vulkan/vulkan.h" +static SyphonOpenGLServer* syphonGLServer; +static SyphonMetalServer* syphonMtlServer; + +void os_VideoRoutingInitSyphonWithGLContext(void* glContext) +{ + int boardID = cfgLoadInt("naomi", "BoardId", 0); + syphonGLServer = [[SyphonOpenGLServer alloc] initWithName:[NSString stringWithFormat:(boardID == 0 ? @"Video Content" : @"Video Content - %d"), boardID] context:[(__bridge NSOpenGLContext*)glContext CGLContextObj] options:nil]; +} + +void os_VideoRoutingPublishFrameTexture(GLuint texID, GLuint texTarget, float w, float h) +{ + CGLLockContext([syphonGLServer context]); + [syphonGLServer publishFrameTexture:texID textureTarget:texTarget imageRegion:NSMakeRect(0, 0, w, h) textureDimensions:NSMakeSize(w, h) flipped:NO]; + CGLUnlockContext([syphonGLServer context]); +} + +void os_VideoRoutingTermGL() +{ + [syphonGLServer stop]; + [syphonGLServer release]; + syphonGLServer = NULL; +} + +void os_VideoRoutingInitSyphonWithVkDevice(const vk::UniqueDevice& device) +{ + vk::ExportMetalDeviceInfoEXT deviceInfo; + auto objectsInfo = vk::ExportMetalObjectsInfoEXT(&deviceInfo); + device->exportMetalObjectsEXT(&objectsInfo); + + int boardID = cfgLoadInt("naomi", "BoardId", 0); + syphonMtlServer = [[SyphonMetalServer alloc] initWithName:[NSString stringWithFormat:(boardID == 0 ? @"Video Content" : @"Video Content - %d"), boardID] device:deviceInfo.mtlDevice options:nil]; +} + +void os_VideoRoutingPublishFrameTexture(const vk::Device& device, const vk::Image& image, const vk::Queue& queue, float x, float y, float w, float h) +{ + auto textureInfo = vk::ExportMetalTextureInfoEXT(image); + auto commandInfo = vk::ExportMetalCommandQueueInfoEXT(queue); + commandInfo.pNext = &textureInfo; + auto objectsInfo = vk::ExportMetalObjectsInfoEXT(&commandInfo); + device.exportMetalObjectsEXT(&objectsInfo); + + auto commandBuffer = [commandInfo.mtlCommandQueue commandBufferWithUnretainedReferences]; + [syphonMtlServer publishFrameTexture:textureInfo.mtlTexture onCommandBuffer:commandBuffer imageRegion:NSMakeRect(x, y, w, h) flipped:YES]; + [commandBuffer commit]; +} + +void os_VideoRoutingTermVk() +{ + [syphonMtlServer stop]; + [syphonMtlServer release]; + syphonMtlServer = NULL; +}