From bd416423b3848c8b356ccd2a00479995e61df2ba Mon Sep 17 00:00:00 2001 From: ecker Date: Wed, 2 Sep 2026 21:58:25 -0500 Subject: [PATCH] headless window + renderering (or attempts to) --- Makefile | 8 +- client/client/ext.cpp | 105 +++++++++++++++++- client/main.cpp | 11 +- engine/inc/uf/config.h | 9 +- engine/inc/uf/engine/ext.h | 1 + engine/inc/uf/ext/vulkan/device.h | 1 + engine/inc/uf/ext/vulkan/swapchain.h | 2 + engine/inc/uf/spec/context/null.h | 6 + engine/inc/uf/spec/controller/null.h | 5 + engine/inc/uf/spec/null.h | 6 + engine/inc/uf/spec/window/null.h | 86 ++++++++++++++ engine/inc/uf/utils/singletons/pre_main.h | 1 + engine/inc/uf/utils/thread/thread.h | 7 +- engine/src/engine/asset/asset.cpp | 6 +- engine/src/engine/ext/ext.cpp | 5 + engine/src/ext/vulkan/device.cpp | 115 ++++++++++++------- engine/src/ext/vulkan/rendermodes/base.cpp | 42 ++++--- engine/src/ext/vulkan/swapchain.cpp | 63 +++++++++++ engine/src/spec/window/linux.cpp | 2 +- engine/src/spec/window/null.cpp | 123 +++++++++++++++++++++ engine/src/utils/singletons/pre_main.cpp | 17 ++- engine/src/utils/thread/kos.cpp | 6 +- engine/src/utils/thread/thread.cpp | 6 +- program.sh | 7 +- 24 files changed, 563 insertions(+), 77 deletions(-) create mode 100644 engine/inc/uf/spec/context/null.h create mode 100644 engine/inc/uf/spec/controller/null.h create mode 100644 engine/inc/uf/spec/null.h create mode 100644 engine/inc/uf/spec/window/null.h create mode 100644 engine/src/spec/window/null.cpp diff --git a/Makefile b/Makefile index 7de14cba..def4fea1 100644 --- a/Makefile +++ b/Makefile @@ -66,6 +66,11 @@ OPTIMIZATIONS = -O3 -fstrict-aliasing -DUF_NO_EXCEPTIONS WARNINGS = -Wall -Wno-unknown-pragmas -Wno-unused-function -Wno-unused-variable -Wno-switch -Wno-reorder -Wno-sign-compare -Wno-unused-but-set-variable -Wno-ignored-attributes -Wno-narrowing -Wno-misleading-indentation FLAGS += -std=c++2b $(OPTIMIZATIONS) $(WARNINGS) -fdiagnostics-color=always +# Headless variant +ifeq ($(HEADLESS),1) + FLAGS += -DUF_HEADLESS +endif + # Base Library Definitions LIB_NAME += uf EXT_LIB_NAME += ext @@ -135,7 +140,8 @@ $(PREFIX): $(EX_DLL) $(EXT_EX_DLL) $(TARGET) $(TARGET_SHADERS) $(CC) $(FLAGS) $(INCS) -c $< -o $@ ifneq ($(ARCH),dreamcast) -$(TARGET): $(OBJS) + +$(TARGET): $(OBJS) $(EX_DLL) $(EXT_EX_DLL) $(CXX) $(FLAGS) $(OBJS) $(LIBS) $(INCS) $(LINKS) -l$(LIB_NAME) -l$(EXT_LIB_NAME) -o $(TARGET) endif diff --git a/client/client/ext.cpp b/client/client/ext.cpp index e4118e56..771d56a6 100644 --- a/client/client/ext.cpp +++ b/client/client/ext.cpp @@ -10,8 +10,33 @@ #include #include #include +#include #include // yuck +#if UF_USE_LUA +#include +#endif + +#if UF_ENV_HEADLESS +#include +#include +#include +#include + +namespace { + // stdin command channel: a detached thread reads lines into a queue that + // the main thread drains in client::tick via the console + struct HeadlessIO { + std::atomic running = false; + bool eof = false; + bool eofAnnounced = false; + std::mutex mutex; + uf::stl::vector queue; + }; + // intentionally leaked so the reader thread never touches freed memory + HeadlessIO* headlessIO = nullptr; +} +#endif bool client::ready = false; bool client::terminated = false; @@ -28,25 +53,38 @@ void client::initialize() { /* Initialize window */ { // Window size pod::Vector2i size = uf::vector::decode( client::config["window"]["size"], pod::Vector2i{} ); + #if UF_ENV_HEADLESS + // no display to query; fall back to a fixed size + if ( size.x <= 0 && size.y <= 0 ) size = { 1280, 720 }; + // fake a refresh rate so the auto frame limiter still works + uf::config["window"]["refresh rate"] = 60; + // servers usually have no audio device; keep the engine quiet + uf::config["engine"]["audio"]["mute"] = true; + // missing assets should log, not kill a headless session + uf::config["engine"]["debug"]["loader"]["assert"] = false; + #else // request system size if ( size.x <= 0 && size.y <= 0 ) { auto resolution = client::window.getResolution(); client::config["window"]["size"][0] = (size.x = resolution.x); client::config["window"]["size"][1] = (size.y = resolution.y); } + #endif // Window title uf::stl::string title; { title = client::config["window"]["title"].as(); } + #if !UF_ENV_HEADLESS // Terminal window; spec::terminal.setVisible( client::config["window"]["terminal"]["visible"].as() ); + #endif // Ncurses uf::IoStream::ncurses = client::config["window"]["terminal"]["ncurses"].as(); // Window's context settings uf::renderer::settings::width = size.x; uf::renderer::settings::height = size.y; client::window.create( size, title ); - #if !UF_ENV_DREAMCAST + #if !UF_ENV_DREAMCAST && !UF_ENV_HEADLESS // Set refresh rate uf::config["window"]["refresh rate"] = client::window.getRefreshRate(); // Miscellaneous @@ -123,11 +161,46 @@ void client::initialize() { uf::renderer::states::resized = true; } ); } -#if !UF_ENV_DREAMCAST +#if !UF_ENV_DREAMCAST && !UF_ENV_HEADLESS if ( client::config["window"]["mode"].as() == "fullscreen" ) client::window.toggleFullscreen(); else if ( client::config["window"]["mode"].as() == "borderless" ) client::window.toggleFullscreen( true ); #endif +#if UF_ENV_HEADLESS + /* Headless command channel */ { + // extra command so an agent can poke the engine from outside + uf::console::registerCommand( "lua", "Executes a Lua snippet in the engine state", [&]( const uf::stl::string& code ) -> uf::stl::string { + if ( code.empty() ) return "invalid invocation: lua ''"; + #if UF_USE_LUA + auto result = ext::lua::state.safe_script( code, sol::script_pass_on_error ); + if ( !result.valid() ) { + sol::error err = result; + return "Lua error: " + uf::stl::string( err.what() ); + } + return "Lua executed"; + #else + return "lua is not available in this build"; + #endif + }); + + headlessIO = new HeadlessIO(); + headlessIO->running = true; + std::thread( [](){ + uf::stl::string line; + while ( headlessIO->running.load() && std::getline( std::cin, line ) ) { + while ( !line.empty() && (line.back() == '\r' || line.back() == '\n') ) line.pop_back(); + if ( line.empty() ) continue; + std::lock_guard lock( headlessIO->mutex ); + headlessIO->queue.emplace_back( line ); + } + std::lock_guard lock( headlessIO->mutex ); + headlessIO->eof = true; + }).detach(); + + UF_MSG_INFO("Headless mode: command channel ready (send command lines on stdin, 'help' for a list)"); + } +#endif + client::ready = true; #if UF_ENV_DREAMCAST client::window.pollEvents(); @@ -135,6 +208,30 @@ void client::initialize() { } void client::tick() { +#if UF_ENV_HEADLESS + // no window input; drain the stdin command channel on the main thread + if ( headlessIO ) { + uf::stl::vector lines; + bool announceEOF = false; + { + std::lock_guard lock( headlessIO->mutex ); + lines.swap( headlessIO->queue ); + if ( headlessIO->eof && !headlessIO->eofAnnounced ) { + headlessIO->eofAnnounced = true; + announceEOF = true; + } + } + for ( auto& line : lines ) { + auto output = uf::console::execute( line ); + if ( !output.empty() ) UF_MSG_INFO("Console: {}", output); + } + if ( announceEOF ) { + headlessIO->running = false; + UF_MSG_INFO("Headless: stdin closed; command channel disabled ('quit' or SIGTERM stops the engine)"); + } + } + return; +#else client::window.bufferInputs(); client::window.pollEvents(); @@ -177,6 +274,7 @@ void client::tick() { #endif } } +#endif } void client::render() { @@ -184,5 +282,8 @@ void client::render() { } void client::terminate() { +#if UF_ENV_HEADLESS + if ( headlessIO ) headlessIO->running = false; +#endif client::window.terminate(); } \ No newline at end of file diff --git a/client/main.cpp b/client/main.cpp index 46b68d7a..983f3a97 100644 --- a/client/main.cpp +++ b/client/main.cpp @@ -6,6 +6,7 @@ #include #include +#include #include @@ -15,8 +16,13 @@ namespace { bool killing = true; + volatile sig_atomic_t signalExit = 0; namespace handlers { + void term( int sig ) { + signalExit = 1; + } + void exit() { #if UF_ENV_DREAMCAST arch_stk_trace(1); @@ -74,6 +80,7 @@ namespace { } int main(int argc, char** argv){ + uf::StaticInitialization::runAll(); for ( size_t i = 0; i < argc; ++i ) { char* c_str = argv[i]; std::string string(argv[i]); @@ -82,6 +89,8 @@ int main(int argc, char** argv){ std::atexit(::handlers::exit); signal(SIGABRT, ::handlers::abrt); signal(SIGSEGV, ::handlers::segv); + signal(SIGINT, ::handlers::term); + signal(SIGTERM, ::handlers::term); client::initialize(); uf::initialize(); @@ -100,7 +109,7 @@ int main(int argc, char** argv){ } } - while ( client::ready && uf::ready ) { + while ( client::ready && uf::ready && !signalExit ) { ++uf::time::frame; #if UF_EXCEPTIONS diff --git a/engine/inc/uf/config.h b/engine/inc/uf/config.h index 8d9a4e07..173f27ae 100644 --- a/engine/inc/uf/config.h +++ b/engine/inc/uf/config.h @@ -12,7 +12,14 @@ #undef UF_ENV_UNKNOWN #endif -#if defined(_WIN32) || defined(__WIN32__) || defined(__CYGWIN__) +#if defined(UF_HEADLESS) + // Headless (null windowing + VK_EXT_headless_surface); targets Linux servers, so the Linux spec implementations stay active + #define UF_ENV "Headless" + #define UF_ENV_HEADLESS 1 + #define UF_ENV_LINUX 1 + #define UF_ENV_HEADER "null.h" + +#elif defined(_WIN32) || defined(__WIN32__) || defined(__CYGWIN__) // Windows #define UF_ENV "Windows" #define UF_ENV_WINDOWS 1 diff --git a/engine/inc/uf/engine/ext.h b/engine/inc/uf/engine/ext.h index 7b22dad4..8c247340 100644 --- a/engine/inc/uf/engine/ext.h +++ b/engine/inc/uf/engine/ext.h @@ -5,6 +5,7 @@ namespace uf { extern bool UF_API ready; + extern bool UF_API headless; extern uf::stl::vector UF_API arguments; extern uf::Serializer UF_API config; diff --git a/engine/inc/uf/ext/vulkan/device.h b/engine/inc/uf/ext/vulkan/device.h index 104a650e..76502697 100644 --- a/engine/inc/uf/ext/vulkan/device.h +++ b/engine/inc/uf/ext/vulkan/device.h @@ -59,6 +59,7 @@ namespace ext { VkInstance instance; VkDebugUtilsMessengerEXT debugMessenger; VkSurfaceKHR surface; + bool surfaceless = false; VkPhysicalDevice physicalDevice; VkDevice logicalDevice; struct { diff --git a/engine/inc/uf/ext/vulkan/swapchain.h b/engine/inc/uf/ext/vulkan/swapchain.h index 5611959b..6059eff7 100644 --- a/engine/inc/uf/ext/vulkan/swapchain.h +++ b/engine/inc/uf/ext/vulkan/swapchain.h @@ -15,6 +15,8 @@ namespace ext { uf::stl::vector presentCompleteSemaphores; uf::stl::vector images; + // surfaceless (no VkSurfaceKHR): offscreen images stand in for swapchain images + uf::stl::vector allocations; // helpers VkResult acquireNextImage( uint32_t* imageIndex, VkSemaphore, VkFence = VK_NULL_HANDLE ); diff --git a/engine/inc/uf/spec/context/null.h b/engine/inc/uf/spec/context/null.h new file mode 100644 index 00000000..217ea806 --- /dev/null +++ b/engine/inc/uf/spec/context/null.h @@ -0,0 +1,6 @@ +#pragma once + +#include + +#include "linux.h" + diff --git a/engine/inc/uf/spec/controller/null.h b/engine/inc/uf/spec/controller/null.h new file mode 100644 index 00000000..8d986df6 --- /dev/null +++ b/engine/inc/uf/spec/controller/null.h @@ -0,0 +1,5 @@ +#pragma once + +#include + +#include "linux.h" diff --git a/engine/inc/uf/spec/null.h b/engine/inc/uf/spec/null.h new file mode 100644 index 00000000..d097193e --- /dev/null +++ b/engine/inc/uf/spec/null.h @@ -0,0 +1,6 @@ +#pragma once + +#include + +// reuse linux +#include "linux.h" diff --git a/engine/inc/uf/spec/window/null.h b/engine/inc/uf/spec/window/null.h new file mode 100644 index 00000000..12f208e8 --- /dev/null +++ b/engine/inc/uf/spec/window/null.h @@ -0,0 +1,86 @@ +#pragma once + +#include +#include "universal.h" + +#if UF_ENV_HEADLESS + #if UF_USE_VULKAN + #include + #endif + + // these macros (from X11, pulled in by vulkan.h) interfere with other things + #ifdef Success + #undef Success + #endif + #ifdef None + #undef None + #endif + +namespace spec { + namespace null { + // No windowing system; gives the engine a size to render at and + // (when the driver supports it) a headless surface to render into + class UF_API Window : public spec::uni::Window { + public: + typedef void* handle_t; + typedef void* context_t; + + protected: + vector_t m_size; + title_t m_title; + + public: + UF_API_CALL Window(); + UF_API_CALL Window( const vector_t& size, const title_t& title = "Window" ); + ~Window(); + + void UF_API_CALL create( const vector_t& size, const title_t& title = "Window" ); + void UF_API_CALL terminate(); + + handle_t UF_API_CALL getDisplay() const; + handle_t UF_API_CALL getHandle() const; + vector_t UF_API_CALL getPosition() const; + vector_t UF_API_CALL getSize() const; + size_t UF_API_CALL getRefreshRate() const; + + void UF_API_CALL setPosition( const vector_t& position ); + void UF_API_CALL centerWindow(); + void UF_API_CALL setMousePosition( const vector_t& position ); + vector_t UF_API_CALL getMousePosition(); + void UF_API_CALL setSize( const vector_t& size ); + void UF_API_CALL setTitle( const title_t& title ); + void UF_API_CALL setIcon( const vector_t& size, uint8_t* pixels ); + void UF_API_CALL setVisible( bool visibility ); + void UF_API_CALL setCursorVisible( bool visibility ); + void UF_API_CALL setKeyRepeatEnabled( bool state ); + void UF_API_CALL setMouseGrabbed( bool state ); + + static bool UF_API_CALL isKeyPressed( const uf::stl::string& key ); + + void UF_API_CALL requestFocus(); + bool UF_API_CALL hasFocus() const; + + void UF_API_CALL bufferInputs(); + void UF_API_CALL processEvents(); + bool UF_API_CALL pollEvents( bool block = false ); + void UF_API_CALL grabMouse( bool state ); + + static pod::Vector2ui UF_API_CALL getResolution(); + void UF_API_CALL toggleFullscreen( bool borderless = false ); + + #if UF_USE_VULKAN + uf::stl::vector UF_API_CALL getExtensions( bool validationEnabled ); + void UF_API_CALL createSurface( VkInstance instance, VkSurfaceKHR& surface ); + #endif + + void display(); + }; + } + typedef spec::null::Window Window; +} + +namespace uf { + using Window = spec::null::Window; +} + +#endif \ No newline at end of file diff --git a/engine/inc/uf/utils/singletons/pre_main.h b/engine/inc/uf/utils/singletons/pre_main.h index 7dc80485..4e65f7e9 100644 --- a/engine/inc/uf/utils/singletons/pre_main.h +++ b/engine/inc/uf/utils/singletons/pre_main.h @@ -7,5 +7,6 @@ namespace uf { struct UF_API StaticInitialization { public: StaticInitialization(std::function); + static void UF_API runAll(); }; } \ No newline at end of file diff --git a/engine/inc/uf/utils/thread/thread.h b/engine/inc/uf/utils/thread/thread.h index 6ab17ae0..d65644f2 100644 --- a/engine/inc/uf/utils/thread/thread.h +++ b/engine/inc/uf/utils/thread/thread.h @@ -45,9 +45,9 @@ namespace uf { namespace thread { - extern UF_API uf::stl::string mainThreadName; - extern UF_API uf::stl::string workerThreadName; - extern UF_API uf::stl::string asyncThreadName; + inline constexpr const char* mainThreadName = "Main"; + inline constexpr const char* workerThreadName = "Worker"; + inline constexpr const char* asyncThreadName = "Async"; } } @@ -155,6 +155,7 @@ namespace uf { /* Easy to use async helper functions */ pod::Thread& UF_API fetchWorker( const uf::stl::string& name = uf::thread::workerThreadName ); + inline pod::Thread& UF_API fetchWorker( const char* s ) { return fetchWorker( uf::stl::string( s ) ); } // ugh pod::Thread::Tasks UF_API schedule( bool multithread, bool waits = true ); pod::Thread::Tasks UF_API schedule( const uf::stl::string& name = uf::thread::workerThreadName, bool waits = true ); std::shared_ptr UF_API execute( pod::Thread::Tasks& tasks ); diff --git a/engine/src/engine/asset/asset.cpp b/engine/src/engine/asset/asset.cpp index 2c440bf4..06eed018 100644 --- a/engine/src/engine/asset/asset.cpp +++ b/engine/src/engine/asset/asset.cpp @@ -75,7 +75,8 @@ void uf::asset::processQueue() { if ( jobs.empty() && finishedJobs.empty() ) return; - auto tasks = uf::thread::schedule(uf::asset::asyncQueue ? uf::thread::asyncThreadName : uf::thread::mainThreadName, false); + // to-do: check if const char* overload works again + auto tasks = uf::thread::schedule( uf::stl::string( uf::asset::asyncQueue ? uf::thread::asyncThreadName : uf::thread::mainThreadName ), false ); if ( !finishedJobs.empty() ) { tasks.queue([jobs = std::move(finishedJobs)]() { @@ -125,7 +126,8 @@ void uf::asset::processIO( const uf::asset::Stream::container_t& pendingStreams, return uf::asset::processIO( {}, pendingStreams, async, wait ); } void uf::asset::processIO( const uf::asset::Read::container_t& pendingReads, const uf::asset::Stream::container_t& pendingStreams, bool async, bool wait ) { - auto tasks = uf::thread::schedule(async ? uf::thread::asyncThreadName : uf::thread::mainThreadName, wait); + // to-do: check if const char* overload works again + auto tasks = uf::thread::schedule( uf::stl::string( async ? uf::thread::asyncThreadName : uf::thread::mainThreadName ), wait ); if ( pendingReads.empty() && pendingStreams.empty() ) return; for ( auto& [filename, requests] : pendingReads ) { diff --git a/engine/src/engine/ext/ext.cpp b/engine/src/engine/ext/ext.cpp index 2aab643a..cf0ad7fd 100644 --- a/engine/src/engine/ext/ext.cpp +++ b/engine/src/engine/ext/ext.cpp @@ -52,6 +52,11 @@ #endif bool uf::ready = false; +#if UF_ENV_HEADLESS +bool uf::headless = true; +#else +bool uf::headless = false; +#endif uf::stl::vector uf::arguments; uf::Serializer uf::config; diff --git a/engine/src/ext/vulkan/device.cpp b/engine/src/ext/vulkan/device.cpp index 4f008a42..747e02a5 100644 --- a/engine/src/ext/vulkan/device.cpp +++ b/engine/src/ext/vulkan/device.cpp @@ -160,6 +160,8 @@ namespace { } // { + if ( device.surfaceless ) return deviceInfo; + VkSurfaceCapabilitiesKHR capabilities; uf::stl::vector formats; uf::stl::vector presentModes; @@ -970,6 +972,19 @@ void ext::vulkan::Device::initialize() { validateRequestedExtensions( extensions.properties.instance, requestedExtensions, extensions.supported.instance ); } + // + #if UF_ENV_HEADLESS + // can't create a headless surface; render offscreen with no VkSurfaceKHR and no swapchain + if ( std::find( extensions.supported.instance.begin(), extensions.supported.instance.end(), uf::stl::string( VK_EXT_HEADLESS_SURFACE_EXTENSION_NAME ) ) == extensions.supported.instance.end() ) { + this->surfaceless = true; + UF_MSG_ERROR("Driver does not expose VK_EXT_headless_surface; continuing surfaceless -- offscreen rendering, no presentation (no VkSurfaceKHR, no swapchain)"); + // un-request the surface extensions so the instance can be created without them + requestedExtensions.erase( std::remove( requestedExtensions.begin(), requestedExtensions.end(), uf::stl::string( VK_KHR_SURFACE_EXTENSION_NAME ) ), requestedExtensions.end() ); + requestedExtensions.erase( std::remove( requestedExtensions.begin(), requestedExtensions.end(), uf::stl::string( VK_EXT_HEADLESS_SURFACE_EXTENSION_NAME ) ), requestedExtensions.end() ); + extensions.supported.instance.erase( std::remove( extensions.supported.instance.begin(), extensions.supported.instance.end(), uf::stl::string( VK_KHR_SURFACE_EXTENSION_NAME ) ), extensions.supported.instance.end() ); + extensions.supported.instance.erase( std::remove( extensions.supported.instance.begin(), extensions.supported.instance.end(), uf::stl::string( VK_EXT_HEADLESS_SURFACE_EXTENSION_NAME ) ), extensions.supported.instance.end() ); + } + #endif // Create instance { uf::stl::vector instanceExtensions; @@ -1018,7 +1033,13 @@ void ext::vulkan::Device::initialize() { createInfo.enabledLayerCount = static_cast(cInstanceLayers.size()); createInfo.ppEnabledLayerNames = cInstanceLayers.data(); + #if UF_ENV_HEADLESS + // lacks VK_EXT_headless_surface, so a failure here is generic and unexpected + VkResult instanceResult = vkCreateInstance( &createInfo, nullptr, &this->instance ); + if ( instanceResult != VK_SUCCESS ) UF_EXCEPTION("{}", ext::vulkan::errorString( instanceResult )); + #else VK_CHECK_RESULT( vkCreateInstance( &createInfo, nullptr, &this->instance )); + #endif VK_REGISTER_HANDLE( this->instance ); { @@ -1043,7 +1064,7 @@ void ext::vulkan::Device::initialize() { } // Create surface { - window->createSurface( instance, surface ); + if ( !surfaceless ) window->createSurface( instance, surface ); } // Create physical device @@ -1162,9 +1183,8 @@ void ext::vulkan::Device::initialize() { validateRequestedExtensions( extensions.properties.device, requestedExtensions, extensions.supported.device ); } - uf::stl::vector deviceExtensions = { - VK_KHR_SWAPCHAIN_EXTENSION_NAME - }; + uf::stl::vector deviceExtensions; + if ( !surfaceless ) deviceExtensions.emplace_back( VK_KHR_SWAPCHAIN_EXTENSION_NAME ); for ( auto& s : extensions.supported.device ) { VK_VALIDATION_MESSAGE("Enabled device extension: {}", s); deviceExtensions.emplace_back( s ); @@ -1222,7 +1242,7 @@ void ext::vulkan::Device::initialize() { } VkBool32 presentSupport = false; - vkGetPhysicalDeviceSurfaceSupportKHR( this->physicalDevice, i, surface, &presentSupport ); + if ( !surfaceless ) vkGetPhysicalDeviceSurfaceSupportKHR( this->physicalDevice, i, surface, &presentSupport ); if ( queueFamily.queueCount > 0 && presentSupport ) { presentQueueNodeIndex = i; } @@ -1282,7 +1302,7 @@ void ext::vulkan::Device::initialize() { } // Create the logical device representation - if ( useSwapChain ) { + if ( useSwapChain && !surfaceless ) { // If the device will be used for presenting to a display via a swapchain we need to request the swapchain extension deviceExtensions.emplace_back(VK_KHR_SWAPCHAIN_EXTENSION_NAME); } @@ -1490,50 +1510,63 @@ void ext::vulkan::Device::initialize() { } // Set formats { - uf::stl::vector formats; - uint32_t formatCount; vkGetPhysicalDeviceSurfaceFormatsKHR( this->physicalDevice, device.surface, &formatCount, nullptr); - formats.resize( formatCount ); - vkGetPhysicalDeviceSurfaceFormatsKHR( this->physicalDevice, device.surface, &formatCount, formats.data() ); - - bool SRGB = true; - auto TARGET_FORMAT = SRGB ? VK_FORMAT_B8G8R8A8_SRGB : VK_FORMAT_B8G8R8A8_UNORM; - auto TARGET_COLORSPACE = VK_COLOR_SPACE_SRGB_NONLINEAR_KHR; - if ( ext::vulkan::settings::pipelines::hdr ) { - TARGET_FORMAT = VK_FORMAT_R32G32B32A32_SFLOAT; - TARGET_COLORSPACE = VK_COLOR_SPACE_HDR10_ST2084_EXT; - } - - // If the surface format list only includes one entry with VK_FORMAT_UNDEFINED, - // there is no preferered format, so we assume VK_FORMAT_B8G8R8A8_SRGB - if ( formatCount == 1 && formats[0].format == VK_FORMAT_UNDEFINED ) { - ext::vulkan::settings::formats::color = TARGET_FORMAT; - ext::vulkan::settings::formats::colorSpace = formats[0].colorSpace; - } else { - // iterate over the list of available surface format and - // check for the presence of VK_FORMAT_B8G8R8A8_SRGB - bool found = false; - for ( auto&& surfaceFormat : formats ) { - if ( surfaceFormat.format == ext::vulkan::settings::formats::color ) { - ext::vulkan::settings::formats::color = surfaceFormat.format; - ext::vulkan::settings::formats::colorSpace = surfaceFormat.colorSpace; - found = true; - break; - } + if ( surfaceless ) { + // no surface to query formats from; assume the B8G8R8A8_SRGB (or HDR) default + bool SRGB = true; + auto TARGET_FORMAT = SRGB ? VK_FORMAT_B8G8R8A8_SRGB : VK_FORMAT_B8G8R8A8_UNORM; + auto TARGET_COLORSPACE = VK_COLOR_SPACE_SRGB_NONLINEAR_KHR; + if ( ext::vulkan::settings::pipelines::hdr ) { + TARGET_FORMAT = VK_FORMAT_R32G32B32A32_SFLOAT; + TARGET_COLORSPACE = VK_COLOR_SPACE_HDR10_ST2084_EXT; } - if ( !found ) { + ext::vulkan::settings::formats::color = TARGET_FORMAT; + ext::vulkan::settings::formats::colorSpace = TARGET_COLORSPACE; + } else { + uf::stl::vector formats; + uint32_t formatCount; vkGetPhysicalDeviceSurfaceFormatsKHR( this->physicalDevice, device.surface, &formatCount, nullptr); + formats.resize( formatCount ); + vkGetPhysicalDeviceSurfaceFormatsKHR( this->physicalDevice, device.surface, &formatCount, formats.data() ); + + bool SRGB = true; + auto TARGET_FORMAT = SRGB ? VK_FORMAT_B8G8R8A8_SRGB : VK_FORMAT_B8G8R8A8_UNORM; + auto TARGET_COLORSPACE = VK_COLOR_SPACE_SRGB_NONLINEAR_KHR; + if ( ext::vulkan::settings::pipelines::hdr ) { + TARGET_FORMAT = VK_FORMAT_R32G32B32A32_SFLOAT; + TARGET_COLORSPACE = VK_COLOR_SPACE_HDR10_ST2084_EXT; + } + + // If the surface format list only includes one entry with VK_FORMAT_UNDEFINED, + // there is no preferered format, so we assume VK_FORMAT_B8G8R8A8_SRGB + if ( formatCount == 1 && formats[0].format == VK_FORMAT_UNDEFINED ) { + ext::vulkan::settings::formats::color = TARGET_FORMAT; + ext::vulkan::settings::formats::colorSpace = formats[0].colorSpace; + } else { + // iterate over the list of available surface format and + // check for the presence of VK_FORMAT_B8G8R8A8_SRGB + bool found = false; for ( auto&& surfaceFormat : formats ) { - if ( surfaceFormat.format == TARGET_FORMAT ) { + if ( surfaceFormat.format == ext::vulkan::settings::formats::color ) { ext::vulkan::settings::formats::color = surfaceFormat.format; ext::vulkan::settings::formats::colorSpace = surfaceFormat.colorSpace; found = true; break; } } - // in case VK_FORMAT_B8G8R8A8_SRGB is not available - // select the first available color format if ( !found ) { - ext::vulkan::settings::formats::color = formats[0].format; - ext::vulkan::settings::formats::colorSpace = formats[0].colorSpace; + for ( auto&& surfaceFormat : formats ) { + if ( surfaceFormat.format == TARGET_FORMAT ) { + ext::vulkan::settings::formats::color = surfaceFormat.format; + ext::vulkan::settings::formats::colorSpace = surfaceFormat.colorSpace; + found = true; + break; + } + } + // in case VK_FORMAT_B8G8R8A8_SRGB is not available + // select the first available color format + if ( !found ) { + ext::vulkan::settings::formats::color = formats[0].format; + ext::vulkan::settings::formats::colorSpace = formats[0].colorSpace; + } } } } diff --git a/engine/src/ext/vulkan/rendermodes/base.cpp b/engine/src/ext/vulkan/rendermodes/base.cpp index 4fd7a14b..4cad5b4d 100644 --- a/engine/src/ext/vulkan/rendermodes/base.cpp +++ b/engine/src/ext/vulkan/rendermodes/base.cpp @@ -67,14 +67,16 @@ void ext::vulkan::BaseRenderMode::initialize( Device& device ) { renderTarget.initialize( device ); // set sync objects - for ( auto i = 0; i < ext::vulkan::swapchain.buffers; ++i ) { - auto& presentCompleteSemaphore = swapchain.presentCompleteSemaphores.emplace_back(); - VkSemaphoreCreateInfo semaphoreCreateInfo = {}; - semaphoreCreateInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO; - semaphoreCreateInfo.pNext = nullptr; + if ( !device.surfaceless ) { + for ( auto i = 0; i < ext::vulkan::swapchain.buffers; ++i ) { + auto& presentCompleteSemaphore = swapchain.presentCompleteSemaphores.emplace_back(); + VkSemaphoreCreateInfo semaphoreCreateInfo = {}; + semaphoreCreateInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO; + semaphoreCreateInfo.pNext = nullptr; - VK_CHECK_RESULT(vkCreateSemaphore(device, &semaphoreCreateInfo, nullptr, &presentCompleteSemaphore)); - VK_REGISTER_HANDLE(presentCompleteSemaphore); + VK_CHECK_RESULT(vkCreateSemaphore(device, &semaphoreCreateInfo, nullptr, &presentCompleteSemaphore)); + VK_REGISTER_HANDLE(presentCompleteSemaphore); + } } } @@ -109,8 +111,13 @@ VkSubmitInfo ext::vulkan::BaseRenderMode::queue() { VkSubmitInfo submitInfo = {}; submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; submitInfo.pWaitDstStageMask = waitStageMask; - submitInfo.pWaitSemaphores = &swapchain.presentCompleteSemaphores[states::currentBuffer]; - submitInfo.waitSemaphoreCount = 1; + if ( device->surfaceless ) { + submitInfo.pWaitSemaphores = nullptr; + submitInfo.waitSemaphoreCount = 0; + } else { + submitInfo.pWaitSemaphores = &swapchain.presentCompleteSemaphores[states::currentBuffer]; + submitInfo.waitSemaphoreCount = 1; + } // the present-wait semaphore is paired with the acquired image, so it is only reused once that image is re-acquired submitInfo.pSignalSemaphores = &renderCompleteSemaphores[states::imageIndex]; submitInfo.signalSemaphoreCount = 1; @@ -135,10 +142,12 @@ void ext::vulkan::BaseRenderMode::render() { VK_CHECK_QUEUE_CHECKPOINT( queue, res ); } - { - VkQueue queue = device->getQueue( QueueEnum::PRESENT ); - auto lock = device->lockQueue( queue ); - VK_CHECK_RESULT(swapchain.queuePresent( queue, states::imageIndex, renderCompleteSemaphores[states::imageIndex])); + if ( !device->surfaceless ) { + { + VkQueue queue = device->getQueue( QueueEnum::PRESENT ); + auto lock = device->lockQueue( queue ); + VK_CHECK_RESULT(swapchain.queuePresent( queue, states::imageIndex, renderCompleteSemaphores[states::imageIndex])); + } } states::currentBuffer = (states::currentBuffer + 1) % ext::vulkan::swapchain.buffers; @@ -170,7 +179,12 @@ void ext::vulkan::BaseRenderMode::createCommandBuffers( const uf::stl::vectorsurfaceless ) { + // no swapchain to acquire; the offscreen pool is double-buffered by currentBuffer + states::imageIndex = states::currentBuffer; + } else { + VK_CHECK_RESULT(swapchain.acquireNextImage(&states::imageIndex, swapchain.presentCompleteSemaphores[states::currentBuffer])); + } VK_CHECK_RESULT(vkResetFences(*device, 1, &fences[states::currentBuffer])); ::acquired = true; } diff --git a/engine/src/ext/vulkan/swapchain.cpp b/engine/src/ext/vulkan/swapchain.cpp index b5d0f0c0..c38214ca 100644 --- a/engine/src/ext/vulkan/swapchain.cpp +++ b/engine/src/ext/vulkan/swapchain.cpp @@ -7,6 +7,10 @@ #include VkResult ext::vulkan::Swapchain::acquireNextImage( uint32_t* imageIndex, VkSemaphore presentCompleteSemaphore, VkFence acquireFence ) { + if ( device && device->surfaceless ) { + *imageIndex = ext::vulkan::states::currentBuffer; + return VK_SUCCESS; + } #if UF_USE_FFX_FSR || UF_USE_FFX_SDK if ( ext::fsr::frameInterpolation ) { return ext::fsr::acquireNextImage( imageIndex, presentCompleteSemaphore, acquireFence ); @@ -17,6 +21,7 @@ VkResult ext::vulkan::Swapchain::acquireNextImage( uint32_t* imageIndex, VkSemap } VkResult ext::vulkan::Swapchain::queuePresent( VkQueue queue, uint32_t imageIndex, VkSemaphore waitSemaphore ) { + if ( device && device->surfaceless ) return VK_SUCCESS; #if UF_USE_FFX_FSR || UF_USE_FFX_SDK if ( ext::fsr::frameInterpolation ) { return ext::fsr::queuePresent( queue, imageIndex, waitSemaphore ); @@ -43,6 +48,49 @@ void ext::vulkan::Swapchain::initialize( Device& device ) { // if ( width == 0 ) width = ext::vulkan::settings::width; // if ( height == 0 ) height = ext::vulkan::settings::height; } + // a pool of offscreen images and presenting is a no-op + if ( device.surfaceless ) { + // a re-initialization (e.g. a resize) replaces the previous pool + for ( size_t i = 0; i < images.size(); ++i ) { + if ( images[i] != VK_NULL_HANDLE ) { + vmaDestroyImage( ext::vulkan::allocator, images[i], allocations[i] ); + VK_UNREGISTER_HANDLE( images[i] ); + } + } + images.clear(); + allocations.clear(); + + buffers = 2; + images.resize( buffers ); + allocations.resize( buffers ); + + auto size = device.window->getSize(); + for ( auto i = 0u; i < buffers; ++i ) { + VkImageCreateInfo imageCreateInfo = {}; + imageCreateInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO; + imageCreateInfo.imageType = VK_IMAGE_TYPE_2D; + imageCreateInfo.format = ext::vulkan::settings::formats::color; + imageCreateInfo.extent = { + static_cast( size[0] ), + static_cast( size[1] ), + 1 + }; + imageCreateInfo.mipLevels = 1; + imageCreateInfo.arrayLayers = 1; + imageCreateInfo.samples = VK_SAMPLE_COUNT_1_BIT; + imageCreateInfo.tiling = VK_IMAGE_TILING_OPTIMAL; + imageCreateInfo.usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT; + imageCreateInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; + + VmaAllocationCreateInfo allocationCreateInfo = {}; + allocationCreateInfo.usage = VMA_MEMORY_USAGE_GPU_ONLY; + + VK_CHECK_RESULT(vmaCreateImage( ext::vulkan::allocator, &imageCreateInfo, &allocationCreateInfo, &images[i], &allocations[i], nullptr )); + VK_REGISTER_HANDLE( images[i] ); + } + UF_MSG_INFO("Surfaceless swapchain: {} offscreen image(s) at {}x{}", buffers, size[0], size[1] ); + return; + } // Set present VkPresentModeKHR swapchainPresentMode = VK_PRESENT_MODE_FIFO_KHR; { @@ -215,6 +263,21 @@ void ext::vulkan::Swapchain::initialize( Device& device ) { void ext::vulkan::Swapchain::destroy() { if ( !device ) return; + if ( device->surfaceless ) { + // offscreen images are regular VMA allocations; image and memory go together + for ( size_t i = 0; i < swapchain.images.size(); ++i ) { + if ( swapchain.images[i] != VK_NULL_HANDLE ) { + vmaDestroyImage( ext::vulkan::allocator, swapchain.images[i], swapchain.allocations[i] ); + VK_UNREGISTER_HANDLE( swapchain.images[i] ); + swapchain.images[i] = VK_NULL_HANDLE; + } + } + swapchain.images.clear(); + swapchain.allocations.clear(); + device = VK_NULL_HANDLE; + return; + } + for ( auto& image : swapchain.images ) { // vkDestroyImage( *device, image, nullptr ); // destroyed via vkDestroySwapchainKHR //VK_UNREGISTER_HANDLE( image ); diff --git a/engine/src/spec/window/linux.cpp b/engine/src/spec/window/linux.cpp index c9e5296c..07dcaa64 100644 --- a/engine/src/spec/window/linux.cpp +++ b/engine/src/spec/window/linux.cpp @@ -1,6 +1,6 @@ #include -#if UF_ENV_LINUX +#if UF_ENV_LINUX && !UF_ENV_HEADLESS #include #include #include diff --git a/engine/src/spec/window/null.cpp b/engine/src/spec/window/null.cpp new file mode 100644 index 00000000..3110ebcf --- /dev/null +++ b/engine/src/spec/window/null.cpp @@ -0,0 +1,123 @@ +#include + +#if UF_ENV_HEADLESS +#include + +#if UF_USE_VULKAN + #include +#endif + +namespace spec { + namespace null { + Window::Window() : Window( { 1280, 720 } ) { + } + Window::Window( const vector_t& size, const title_t& title ) { + create( size, title ); + } + Window::~Window() { + } + + void Window::create( const vector_t& size, const title_t& title ) { + // there is nothing to create; just remember what the client wants + m_size = size; + m_title = title; + } + void Window::terminate() { + } + + Window::handle_t Window::getDisplay() const { + return nullptr; + } + Window::handle_t Window::getHandle() const { + return nullptr; + } + spec::null::Window::vector_t Window::getPosition() const { + return { 0, 0 }; + } + spec::null::Window::vector_t Window::getSize() const { + // drives the swapchain extent fallback for unbounded surfaces + return m_size; + } + size_t Window::getRefreshRate() const { + // no display to query; pretend 60Hz + return 60; + } + + void Window::setPosition( const vector_t& position ) { + } + void Window::centerWindow() { + } + void Window::setMousePosition( const vector_t& position ) { + } + spec::null::Window::vector_t Window::getMousePosition() { + return { 0, 0 }; + } + void Window::setSize( const vector_t& size ) { + m_size = size; + } + void Window::setTitle( const title_t& title ) { + m_title = title; + } + void Window::setIcon( const vector_t& size, uint8_t* pixels ) { + } + void Window::setVisible( bool visibility ) { + } + void Window::setCursorVisible( bool visibility ) { + } + void Window::setKeyRepeatEnabled( bool state ) { + } + void Window::setMouseGrabbed( bool state ) { + } + + bool Window::isKeyPressed( const uf::stl::string& key ) { + return false; + } + + void Window::requestFocus() { + } + bool Window::hasFocus() const { + return true; + } + + void Window::bufferInputs() { + } + void Window::processEvents() { + } + bool Window::pollEvents( bool block ) { + return false; + } + void Window::grabMouse( bool state ) { + } + + pod::Vector2ui Window::getResolution() { + return { 1280, 720 }; + } + void Window::toggleFullscreen( bool borderless ) { + } + + #if UF_USE_VULKAN + uf::stl::vector Window::getExtensions( bool validationEnabled ) { + uf::stl::vector exts = { VK_KHR_SURFACE_EXTENSION_NAME, VK_EXT_HEADLESS_SURFACE_EXTENSION_NAME }; + if ( validationEnabled ) exts.push_back( VK_EXT_DEBUG_UTILS_EXTENSION_NAME ); + return exts; + } + void Window::createSurface( VkInstance instance, VkSurfaceKHR& surface ) { + // the extent is unbounded by spec; the swapchain falls back to getSize() + VkHeadlessSurfaceCreateInfoEXT info = {}; + info.sType = VK_STRUCTURE_TYPE_HEADLESS_SURFACE_CREATE_INFO_EXT; + + { + VkResult result = vkCreateHeadlessSurfaceEXT( instance, &info, nullptr, &surface ); + if ( result == VK_ERROR_EXTENSION_NOT_PRESENT ) { + UF_MSG_ERROR("Driver does not expose VK_EXT_headless_surface; the headless build requires a driver that does (Mesa/AMD/Intel, not NVIDIA proprietary)"); + } + VK_CHECK_RESULT(result); + } + } + #endif + + void Window::display() { + } + } +} +#endif diff --git a/engine/src/utils/singletons/pre_main.cpp b/engine/src/utils/singletons/pre_main.cpp index f7a4958c..a6b4b703 100644 --- a/engine/src/utils/singletons/pre_main.cpp +++ b/engine/src/utils/singletons/pre_main.cpp @@ -1,5 +1,20 @@ #include +#include + +namespace { + // queue of deferred initializers; the pointer is constant-initialized to NULL, so it is safe to touch during static initialization + uf::stl::vector>* queuedInitializations = NULL; +} + uf::StaticInitialization::StaticInitialization( std::function fun ) { - if ( fun ) fun(); + if ( queuedInitializations == NULL ) queuedInitializations = new uf::stl::vector>; + queuedInitializations->emplace_back( std::move(fun) ); +} + +void uf::StaticInitialization::runAll() { + if ( queuedInitializations == NULL ) return; + auto& queue = *queuedInitializations; + for ( auto& initializer : queue ) if ( initializer ) initializer(); + queue.clear(); } \ No newline at end of file diff --git a/engine/src/utils/thread/kos.cpp b/engine/src/utils/thread/kos.cpp index be6d1276..9deb59cb 100644 --- a/engine/src/utils/thread/kos.cpp +++ b/engine/src/utils/thread/kos.cpp @@ -14,9 +14,6 @@ float uf::thread::limiter = 1.0f / 120.0f; uint32_t uf::thread::workers = 1; uf::thread::id_t uf::thread::mainThreadId = nullptr; bool uf::thread::async = false; -uf::stl::string uf::thread::mainThreadName = "Main"; -uf::stl::string uf::thread::workerThreadName = "Worker"; -uf::stl::string uf::thread::asyncThreadName = "Async"; namespace { mutex_t global_mutex = MUTEX_INITIALIZER; @@ -77,7 +74,8 @@ pod::Thread& uf::thread::fetchWorker( const uf::stl::string& name ) { } pod::Thread::Tasks uf::thread::schedule( bool async, bool wait ) { - return schedule( async ? uf::thread::asyncThreadName : uf::thread::mainThreadName, wait ); + // to-do: check if const char* overload works again + return schedule( uf::stl::string( async ? uf::thread::asyncThreadName : uf::thread::mainThreadName ), wait ); } pod::Thread::Tasks uf::thread::schedule( const uf::stl::string& name, bool wait ) { diff --git a/engine/src/utils/thread/thread.cpp b/engine/src/utils/thread/thread.cpp index e5e87ef3..93e01ad9 100644 --- a/engine/src/utils/thread/thread.cpp +++ b/engine/src/utils/thread/thread.cpp @@ -8,9 +8,6 @@ float uf::thread::limiter = 1.0f / 120.0f; uint32_t uf::thread::workers = 1; uf::thread::id_t uf::thread::mainThreadId = std::this_thread::get_id(); bool uf::thread::async = false; -uf::stl::string uf::thread::mainThreadName = "Main"; -uf::stl::string uf::thread::workerThreadName = "Worker"; -uf::stl::string uf::thread::asyncThreadName = "Async"; namespace { std::mutex mutex; @@ -56,7 +53,8 @@ pod::Thread& uf::thread::fetchWorker( const uf::stl::string& name ) { UF_EXCEPTION("cannot find free worker"); } pod::Thread::Tasks uf::thread::schedule( bool async, bool wait ) { - return schedule( async ? uf::thread::workerThreadName : uf::thread::mainThreadName, wait ); + // to-do: check if const char* overload works again + return schedule( uf::stl::string( async ? uf::thread::workerThreadName : uf::thread::mainThreadName ), wait ); } pod::Thread::Tasks uf::thread::schedule( const uf::stl::string& name, bool wait ) { pod::Thread::Tasks tasks = { diff --git a/program.sh b/program.sh index cf26ef68..d4e7fed0 100644 --- a/program.sh +++ b/program.sh @@ -9,6 +9,9 @@ export PATH="$(pwd)/exe/lib/${ARCH}/:$(pwd)/exe/lib/${ARCH}/${CC}/:$(pwd)/exe/li export LD_LIBRARY_PATH="$(pwd)/exe/lib/${ARCH}/:$(pwd)/exe/lib/${ARCH}/${CC}/:$(pwd)/exe/lib/${ARCH}/${CC}/${RENDERER}/:${LD_LIBRARY_PATH}" echo PATH: ${PATH} -echo Executing ./exe/program.${ARCH}.${CC}.${RENDERER}.exe $@ -./exe/program.${ARCH}.${CC}.${RENDERER}.exe $@ +NAME=./exe/program.${ARCH}.${CC}.${RENDERER} +# Windows builds carry a .exe suffix; platform builds (e.g. Linux) do not +[ -f "${NAME}.exe" ] && NAME="${NAME}.exe" +echo Executing ${NAME} $@ +${NAME} $@ tskill program