headless window + renderering (or attempts to)

This commit is contained in:
ecker 2026-09-02 21:58:25 -05:00
parent df4e9dcb36
commit bd416423b3
24 changed files with 563 additions and 77 deletions

View File

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

View File

@ -10,8 +10,33 @@
#include <uf/utils/thread/thread.h>
#include <uf/utils/renderer/renderer.h>
#include <uf/utils/window/payloads.h>
#include <uf/utils/io/console.h>
#include <uf/ext/openvr/openvr.h> // yuck
#if UF_USE_LUA
#include <uf/ext/lua/lua.h>
#endif
#if UF_ENV_HEADLESS
#include <atomic>
#include <mutex>
#include <thread>
#include <iostream>
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<bool> running = false;
bool eof = false;
bool eofAnnounced = false;
std::mutex mutex;
uf::stl::vector<uf::stl::string> 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<std::string>();
}
#if !UF_ENV_HEADLESS
// Terminal window;
spec::terminal.setVisible( client::config["window"]["terminal"]["visible"].as<bool>() );
#endif
// Ncurses
uf::IoStream::ncurses = client::config["window"]["terminal"]["ncurses"].as<bool>();
// 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<std::string>() == "fullscreen" ) client::window.toggleFullscreen();
else if ( client::config["window"]["mode"].as<std::string>() == "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 '<code>'";
#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<std::mutex> lock( headlessIO->mutex );
headlessIO->queue.emplace_back( line );
}
std::lock_guard<std::mutex> 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<uf::stl::string> lines;
bool announceEOF = false;
{
std::lock_guard<std::mutex> 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();
}

View File

@ -6,6 +6,7 @@
#include <uf/utils/window/payloads.h>
#include <uf/utils/memory/pool.h>
#include <uf/utils/singletons/pre_main.h>
#include <uf/spec/renderer/universal.h>
@ -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

View File

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

View File

@ -5,6 +5,7 @@
namespace uf {
extern bool UF_API ready;
extern bool UF_API headless;
extern uf::stl::vector<uf::stl::string> UF_API arguments;
extern uf::Serializer UF_API config;

View File

@ -59,6 +59,7 @@ namespace ext {
VkInstance instance;
VkDebugUtilsMessengerEXT debugMessenger;
VkSurfaceKHR surface;
bool surfaceless = false;
VkPhysicalDevice physicalDevice;
VkDevice logicalDevice;
struct {

View File

@ -15,6 +15,8 @@ namespace ext {
uf::stl::vector<VkSemaphore> presentCompleteSemaphores;
uf::stl::vector<VkImage> images;
// surfaceless (no VkSurfaceKHR): offscreen images stand in for swapchain images
uf::stl::vector<VmaAllocation> allocations;
// helpers
VkResult acquireNextImage( uint32_t* imageIndex, VkSemaphore, VkFence = VK_NULL_HANDLE );

View File

@ -0,0 +1,6 @@
#pragma once
#include <uf/config.h>
#include "linux.h"

View File

@ -0,0 +1,5 @@
#pragma once
#include <uf/config.h>
#include "linux.h"

View File

@ -0,0 +1,6 @@
#pragma once
#include <uf/config.h>
// reuse linux
#include "linux.h"

View File

@ -0,0 +1,86 @@
#pragma once
#include <uf/config.h>
#include "universal.h"
#if UF_ENV_HEADLESS
#if UF_USE_VULKAN
#include <vulkan/vulkan.h>
#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::stl::string> 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

View File

@ -7,5 +7,6 @@ namespace uf {
struct UF_API StaticInitialization {
public:
StaticInitialization(std::function<void()>);
static void UF_API runAll();
};
}

View File

@ -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<pod::Thread::Tasks::Tracker> UF_API execute( pod::Thread::Tasks& tasks );

View File

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

View File

@ -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::stl::string> uf::arguments;
uf::Serializer uf::config;

View File

@ -160,6 +160,8 @@ namespace {
}
//
{
if ( device.surfaceless ) return deviceInfo;
VkSurfaceCapabilitiesKHR capabilities;
uf::stl::vector<VkSurfaceFormatKHR> formats;
uf::stl::vector<VkPresentModeKHR> 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<uf::stl::string> instanceExtensions;
@ -1018,7 +1033,13 @@ void ext::vulkan::Device::initialize() {
createInfo.enabledLayerCount = static_cast<uint32_t>(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<uf::stl::string> deviceExtensions = {
VK_KHR_SWAPCHAIN_EXTENSION_NAME
};
uf::stl::vector<uf::stl::string> 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<VkSurfaceFormatKHR> 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<VkSurfaceFormatKHR> 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;
}
}
}
}

View File

@ -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::vector<ex
void ext::vulkan::BaseRenderMode::_acquire() {
if ( ::acquired ) return;
VK_CHECK_RESULT(vkWaitForFences(*device, 1, &fences[states::currentBuffer], VK_TRUE, VK_DEFAULT_FENCE_TIMEOUT));
VK_CHECK_RESULT(swapchain.acquireNextImage(&states::imageIndex, swapchain.presentCompleteSemaphores[states::currentBuffer]));
if ( device->surfaceless ) {
// 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;
}

View File

@ -7,6 +7,10 @@
#include <uf/ext/ffx/fsr.h>
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<uint32_t>( size[0] ),
static_cast<uint32_t>( 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 );

View File

@ -1,6 +1,6 @@
#include <uf/spec/window/window.h>
#if UF_ENV_LINUX
#if UF_ENV_LINUX && !UF_ENV_HEADLESS
#include <uf/utils/io/inputs.h>
#include <uf/utils/io/iostream.h>
#include <uf/utils/serialize/serializer.h>

View File

@ -0,0 +1,123 @@
#include <uf/spec/window/window.h>
#if UF_ENV_HEADLESS
#include <uf/utils/io/iostream.h>
#if UF_USE_VULKAN
#include <uf/ext/vulkan/vulkan.h>
#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<uf::stl::string> Window::getExtensions( bool validationEnabled ) {
uf::stl::vector<uf::stl::string> 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

View File

@ -1,5 +1,20 @@
#include <uf/utils/singletons/pre_main.h>
#include <uf/utils/memory/vector.h>
namespace {
// queue of deferred initializers; the pointer is constant-initialized to NULL, so it is safe to touch during static initialization
uf::stl::vector<std::function<void()>>* queuedInitializations = NULL;
}
uf::StaticInitialization::StaticInitialization( std::function<void()> fun ) {
if ( fun ) fun();
if ( queuedInitializations == NULL ) queuedInitializations = new uf::stl::vector<std::function<void()>>;
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();
}

View File

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

View File

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

View File

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