Realtime Video Routing (#1126)

* Syphon video routing with OpenGL and Vulkan (MacOS)
* Spout video routing with DX11 and OpenGL (Windows)
* Fix Xcode detection
This commit is contained in:
vkedwardli 2023-06-26 17:56:56 +08:00 committed by GitHub
parent 6e6f75386b
commit 7cd308fcae
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 443 additions and 5 deletions

6
.gitmodules vendored
View File

@ -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

View File

@ -214,10 +214,15 @@ else()
$<$<COMPILE_LANGUAGE:CXX>:-Wall>)
endif()
if(${CMAKE_OSX_SYSROOT} MATCHES "\/MacOSX.platform\/Developer\/SDKs")
set(TARGET_MAC ON)
endif()
target_compile_definitions(${PROJECT_NAME} PRIVATE
$<$<BOOL:${APPLE}>:GL_SILENCE_DEPRECATION>
$<$<BOOL:${ENABLE_LOG}>:DEBUGFAST>
$<$<BOOL:${IOS}>:TARGET_IPHONE>
$<$<BOOL:${TARGET_MAC}>:TARGET_MAC>
$<$<BOOL:${IOS}>:GLES>
$<$<BOOL:${IOS}>:GLES3>
$<$<BOOL:${IOS}>: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}/$<CONFIG>-${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}/$<CONFIG>/Flycast.app/Contents/Resources/)
else()
add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy ${SYPHON_METALLIB}
${CMAKE_CURRENT_BINARY_DIR}/$<CONFIG>/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()

View File

@ -106,6 +106,11 @@ Option<bool> DupeFrames("rend.DupeFrames", false);
Option<int> PerPixelLayers("rend.PerPixelLayers", 32);
Option<bool> NativeDepthInterpolation("rend.NativeDepthInterpolation", false);
Option<bool> EmulateFramebuffer("rend.EmulateFramebuffer", false);
#ifdef VIDEO_ROUTING
Option<bool> VideoRouting("rend.VideoRouting", false);
Option<bool> VideoRoutingScale("rend.VideoRoutingScale", false);
Option<int> VideoRoutingVRes("rend.VideoRoutingVRes", 720);
#endif
// Misc

View File

@ -465,6 +465,11 @@ extern Option<bool> ThreadedRendering;
extern Option<bool> DupeFrames;
extern Option<bool> NativeDepthInterpolation;
extern Option<bool> EmulateFramebuffer;
#ifdef VIDEO_ROUTING
extern Option<bool> VideoRouting;
extern Option<bool> VideoRoutingScale;
extern Option<int> VideoRoutingVRes;
#endif
// Misc

1
core/deps/Spout Submodule

@ -0,0 +1 @@
Subproject commit 122c8519685f1f221a0dcdda96e23424ffce3f08

1
core/deps/Syphon Submodule

@ -0,0 +1 @@
Subproject commit 486dd8e4ef3bead482ea26de99770f4389e24e37

View File

@ -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<ID3D11Texture2D>& texture, ComPtr<ID3D11DepthStencilView>& 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();

View File

@ -96,6 +96,7 @@ protected:
void setCullMode(int mode);
virtual void setRTTSize(int width, int height) {}
void writeFramebufferToVRAM();
void renderVideoRouting();
ComPtr<ID3D11Device> device;
ComPtr<ID3D11DeviceContext> deviceContext;
@ -154,6 +155,10 @@ private:
ComPtr<ID3D11Texture2D> fbScaledTexture;
ComPtr<ID3D11ShaderResourceView> fbScaledTextureView;
ComPtr<ID3D11RenderTargetView> fbScaledRenderTarget;
ComPtr<ID3D11Texture2D> vrStagingTexture;
ComPtr<ID3D11ShaderResourceView> vrStagingTextureSRV;
ComPtr<ID3D11Texture2D> vrScaledTexture;
ComPtr<ID3D11RenderTargetView> vrScaledRenderTarget;
ComPtr<ID3D11RasterizerState> rasterCullNone, rasterCullFront, rasterCullBack;

View File

@ -161,6 +161,8 @@ bool DX11Context::init(bool keepCurrentWindow)
NOTICE_LOG(RENDERER, "No system-provided shader cache");
}
initVideoRouting();
imguiDriver = std::unique_ptr<ImGuiDriver>(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()

View File

@ -33,6 +33,7 @@ class DX11Context : public GraphicsContext
{
public:
bool init(bool keepCurrentWindow = false);
void initVideoRouting() override;
void term() override;
void EndImGuiFrame();
void Present();

View File

@ -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;

View File

@ -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<COORD_OPENGL> matrices(pvrrc, is_rtt ? pvrrc.getFramebufferWidth() : width,

View File

@ -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<GlFramebuffer>(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();

View File

@ -305,6 +305,11 @@ struct gl_ctx
bool ready = false;
} ofbo2;
struct
{
std::unique_ptr<GlFramebuffer> 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();

View File

@ -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/<game id>");
}
#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();

View File

@ -19,6 +19,7 @@
along with Flycast. If not, see <https://www.gnu.org/licenses/>.
*/
#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<BaseVulkanRenderer*>(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

View File

@ -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;

View File

@ -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) {}

View File

@ -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

View File

@ -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;

View File

@ -56,6 +56,11 @@ void GLGraphicsContext::preTerm()
#ifndef LIBRETRO
imguiDriver.reset();
#endif
#ifdef VIDEO_ROUTING
extern void os_VideoRoutingTermGL();
os_VideoRoutingTermGL();
#endif
instance = nullptr;
}

View File

@ -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();

View File

@ -42,6 +42,7 @@ public:
bool init();
void term() override;
void swap();
void initVideoRouting() override;
private:
SDL_GLContext glcontext = nullptr;

View File

@ -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 <Syphon/Syphon.h>
#import <cfg/cfg.h>
#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;
}