From 1e8687d8b9a3f435c144d89de3c40796c1b0467b Mon Sep 17 00:00:00 2001 From: bjorn Date: Fri, 17 Jul 2026 19:30:46 -0700 Subject: [PATCH] Window; --- CMakeLists.txt | 1 + etc/boot.lua | 1 + src/api/api.h | 5 + src/api/l_event.c | 21 +- src/api/l_headset.c | 124 +-- src/api/l_headset_window.c | 314 +++++++ src/modules/audio/audio.c | 10 +- src/modules/event/event.c | 4 + src/modules/event/event.h | 9 +- src/modules/headset/headset.c | 1466 ++++++++++++++++++++------------ src/modules/headset/headset.h | 34 +- src/modules/headset/headset.js | 95 ++- src/util.h | 12 + 13 files changed, 1448 insertions(+), 648 deletions(-) create mode 100644 src/api/l_headset_window.c diff --git a/CMakeLists.txt b/CMakeLists.txt index db74298ad..348a58bcb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -528,6 +528,7 @@ if(LOVR_ENABLE_HEADSET) target_sources(lovr PRIVATE src/api/l_headset.c src/api/l_headset_layer.c + src/api/l_headset_window.c ) if(NOT EMSCRIPTEN) diff --git a/etc/boot.lua b/etc/boot.lua index 29d3f688d..33e0b4c4c 100644 --- a/etc/boot.lua +++ b/etc/boot.lua @@ -52,6 +52,7 @@ local conf = { dynamicresolution = true, submitdepth = true, overlay = false, + window = true, controllerskeleton = 'controller', extensions = nil }, diff --git a/src/api/api.h b/src/api/api.h index 5eda20975..3f8d89f59 100644 --- a/src/api/api.h +++ b/src/api/api.h @@ -189,6 +189,11 @@ struct Material* luax_optmaterial(lua_State* L, int index); struct ColoredString* luax_checkcoloredstrings(lua_State* L, int index, uint32_t* count, struct ColoredString* stack); #endif +#ifndef LOVR_DISABLE_HEADSET +struct Image; +void luax_checkimages(lua_State* L, int index, struct Image** images, uint32_t capacity, uint32_t* count, uint32_t* layers); +#endif + #ifndef LOVR_DISABLE_MATH uint64_t luax_checkrandomseed(lua_State* L, int index); #endif diff --git a/src/api/l_event.c b/src/api/l_event.c index 61acbd604..0e4b43f1e 100644 --- a/src/api/l_event.c +++ b/src/api/l_event.c @@ -56,12 +56,20 @@ static int nextEvent(lua_State* L) { case EVENT_VISIBLE: lua_pushboolean(L, event.data.visible.visible); - luax_pushenum(L, DisplayType, event.data.visible.display); + if (event.data.visible.window) { + luax_pushtype(L, Window, event.data.visible.window); + } else { + luax_pushenum(L, DisplayType, event.data.visible.display); + } return 3; case EVENT_FOCUS: lua_pushboolean(L, event.data.focus.focused); - luax_pushenum(L, DisplayType, event.data.focus.display); + if (event.data.focus.window) { + luax_pushtype(L, Window, event.data.focus.window); + } else { + luax_pushenum(L, DisplayType, event.data.focus.display); + } return 3; case EVENT_MOUNT: @@ -75,8 +83,13 @@ static int nextEvent(lua_State* L) { return 1; case EVENT_RESIZE: - lua_pushinteger(L, event.data.resize.width); - lua_pushinteger(L, event.data.resize.height); + lua_pushnumber(L, event.data.resize.width); + lua_pushnumber(L, event.data.resize.height); + if (event.data.resize.window) { + lua_pushnumber(L, event.data.resize.depth); + luax_pushtype(L, Window, event.data.resize.window); + return 5; + } return 3; case EVENT_KEYPRESSED: diff --git a/src/api/l_headset.c b/src/api/l_headset.c index f5834e5fe..b6ad81769 100644 --- a/src/api/l_headset.c +++ b/src/api/l_headset.c @@ -160,6 +160,7 @@ static int l_lovrHeadsetGetFeatures(lua_State* L) { lua_pushboolean(L, features.proximity), lua_setfield(L, -2, "proximity"); lua_pushboolean(L, features.refreshRate), lua_setfield(L, -2, "refreshRate"); lua_pushboolean(L, features.viveTrackers), lua_setfield(L, -2, "viveTrackers"); + lua_pushboolean(L, features.window), lua_setfield(L, -2, "window"); return 1; } @@ -787,7 +788,7 @@ static int l_lovrHeadsetAnimate(lua_State* L) { return 1; } -static void luax_checkimages(lua_State* L, int index, Image** images, uint32_t capacity, uint32_t* count, uint32_t* layers) { +void luax_checkimages(lua_State* L, int index, Image** images, uint32_t capacity, uint32_t* count, uint32_t* layers) { if (lua_istable(L, index)) { uint32_t length = luax_len(L, 1); luax_check(L, length <= capacity, "Too many images!"); @@ -812,97 +813,26 @@ static void luax_checkimages(lua_State* L, int index, Image** images, uint32_t c } } -static int l_lovrHeadsetSetBackground(lua_State* L) { - uint32_t width = 0; - uint32_t height = 0; - uint32_t layers = 0; - Image* images[6]; - uint32_t imageCount = 0; - Texture* texture = NULL; - - if (lua_isnoneornil(L, 1)) { - lovrHeadsetSetBackground(0, 0, 0); - return 0; - } else if ((texture = luax_totype(L, 1, Texture)) != NULL) { - const TextureInfo* info = lovrTextureGetInfo(texture); - width = info->width; - height = info->height; - layers = info->layers; - } else { - luax_checkimages(L, 1, images, COUNTOF(images), &imageCount, &layers); - luax_check(L, imageCount > 1, "Must have at least 1 image"); - width = lovrImageGetWidth(images[0], 0); - height = lovrImageGetHeight(images[0], 0); - } - - luax_check(L, layers == 1 || layers == 6, "Currently, background must have 1 or 6 layers"); - - Texture* background = lovrHeadsetSetBackground(width, height, layers); +extern int l_lovrWindowSetBackground(lua_State* L); +extern int l_lovrWindowGetLayers(lua_State* L); +extern int l_lovrWindowSetLayers(lua_State* L); - if (!background) { - for (uint32_t i = 0; i < imageCount; i++) { - lovrRelease(images[i], lovrImageDestroy); - } - luax_assert(L, false); - } - - if (texture) { - uint32_t srcOffset[4] = { 0 }; - uint32_t dstOffset[4] = { 0 }; - uint32_t extent[3] = { width, height, layers }; - luax_assert(L, lovrTextureCopy(texture, background, srcOffset, dstOffset, extent)); - } else if (imageCount > 0) { - for (uint32_t i = 0; i < imageCount; i++) { - uint32_t texOffset[4] = { 0, 0, i, 0 }; - uint32_t imgOffset[4] = { 0, 0, 0, 0 }; - uint32_t extent[3] = { width, height, lovrImageGetLayerCount(images[i]) }; - luax_assert(L, lovrTextureSetPixels(background, images[i], texOffset, imgOffset, extent)); - lovrRelease(images[i], lovrImageDestroy); - } - } - - return 0; +static int l_lovrHeadsetSetBackground(lua_State* L) { + luax_pushtype(L, Window, lovrHeadsetGetWindow()); + lua_insert(L, 1); + return l_lovrWindowSetBackground(L); } static int l_lovrHeadsetGetLayers(lua_State* L) { - bool main; - uint32_t count; - Layer** layers = lovrHeadsetGetLayers(&count, &main); - lua_createtable(L, (int) count, 0); - for (uint32_t i = 0; i < count; i++) { - luax_pushtype(L, Layer, layers[i]); - lua_rawseti(L, -2, (int) i + 1); - } - lua_pushboolean(L, main); - lua_setfield(L, -2, "main"); - return 1; + luax_pushtype(L, Window, lovrHeadsetGetWindow()); + lua_insert(L, 1); + return l_lovrWindowGetLayers(L); } static int l_lovrHeadsetSetLayers(lua_State* L) { - Layer* layers[MAX_LAYERS]; - uint32_t count = 0; - bool main = true; - if (lua_type(L, 1) == LUA_TTABLE) { - count = luax_len(L, 1); - luax_check(L, count <= MAX_LAYERS, "Too many layers (max is %d)", MAX_LAYERS); - for (uint32_t i = 0; i < count; i++) { - lua_rawgeti(L, 1, (int) i + 1); - layers[i] = luax_checktype(L, -1, Layer); - lua_pop(L, 1); - } - lua_getfield(L, 1, "main"); - if (!lua_isnil(L, -1)) main = lua_toboolean(L, -1); - lua_pop(L, 1); - } else { - count = lua_gettop(L); - luax_check(L, count <= MAX_LAYERS, "Too many layers (max is %d)", MAX_LAYERS); - for (uint32_t i = 0; i < count; i++) { - layers[i] = luax_checktype(L, (int) i + 1, Layer); - } - } - bool success = lovrHeadsetSetLayers(layers, count, main); - luax_assert(L, success); - return 0; + luax_pushtype(L, Window, lovrHeadsetGetWindow()); + lua_insert(L, 1); + return l_lovrWindowSetLayers(L); } static int l_lovrHeadsetGetTexture(lua_State* L) { @@ -1052,6 +982,21 @@ static int l_lovrHeadsetNewLayer(lua_State* L) { return 1; } +static int l_lovrHeadsetNewWindow(lua_State* L) { + WindowInfo info = { 0 }; + luax_readvec3(L, 1, info.size, NULL); + Window* window = lovrWindowCreate(&info); + luax_pushtype(L, Window, window); + lovrRelease(window, lovrWindowDestroy); + return 1; +} + +static int l_lovrHeadsetGetWindow(lua_State* L) { + Window* window = lovrHeadsetGetWindow(); + luax_pushtype(L, Window, window); + return 1; +} + static int l_lovrHeadsetGetHands(lua_State* L) { if (lua_istable(L, 1)) { lua_settop(L, 1); @@ -1154,6 +1099,8 @@ static const luaL_Reg lovrHeadset[] = { { "setPose", l_lovrHeadsetSetPose }, { "setButton", l_lovrHeadsetSetButton }, { "newLayer", l_lovrHeadsetNewLayer }, + { "newWindow", l_lovrHeadsetNewWindow }, + { "getWindow", l_lovrHeadsetGetWindow }, { "getHands", l_lovrHeadsetGetHands }, // Deprecated @@ -1164,11 +1111,13 @@ static const luaL_Reg lovrHeadset[] = { }; extern const luaL_Reg lovrLayer[]; +extern const luaL_Reg lovrWindow[]; int luaopen_lovr_headset(lua_State* L) { lua_newtable(L); luax_register(L, lovrHeadset); luax_registertype(L, Layer); + luax_registertype(L, Window); HeadsetConfig config = { .supersample = 1.f, @@ -1181,6 +1130,7 @@ int luaopen_lovr_headset(lua_State* L) { .submitDepth = true, .overlay = false, .overlayOrder = 0, + .window = true, .controllerSkeleton = SKELETON_CONTROLLER }; @@ -1233,6 +1183,10 @@ int luaopen_lovr_headset(lua_State* L) { config.overlayOrder = lua_type(L, -1) == LUA_TNUMBER ? luax_optu32(L, -1, 0) : 0; lua_pop(L, 1); + lua_getfield(L, -1, "window"); + config.window = lua_toboolean(L, -1); + lua_pop(L, 1); + lua_getfield(L, -1, "controllerskeleton"); if (!lua_isnil(L, -1)) config.controllerSkeleton = luax_checkenum(L, -1, ControllerSkeletonMode, NULL); lua_pop(L, 1); diff --git a/src/api/l_headset_window.c b/src/api/l_headset_window.c new file mode 100644 index 000000000..3125fca4e --- /dev/null +++ b/src/api/l_headset_window.c @@ -0,0 +1,314 @@ +#include "api.h" +#include "headset/headset.h" +#include "data/image.h" +#include "graphics/graphics.h" +#include "core/maf.h" +#include "util.h" + +static int l_lovrWindowIsOpen(lua_State* L) { + Window* window = luax_checktype(L, 1, Window); + bool open = lovrWindowIsOpen(window); + lua_pushboolean(L, open); + return 1; +} + +static int l_lovrWindowClose(lua_State* L) { + Window* window = luax_checktype(L, 1, Window); + lovrWindowClose(window); + return 0; +} + +static int l_lovrWindowIsVisible(lua_State* L) { + Window* window = luax_checktype(L, 1, Window); + bool visible = lovrWindowIsVisible(window); + lua_pushboolean(L, visible); + return 1; +} + +static int l_lovrWindowSetVisible(lua_State* L) { + Window* window = luax_checktype(L, 1, Window); + bool visible = lua_toboolean(L, 2); + luax_assert(L, lovrWindowSetVisible(window, visible)); + return 0; +} + +static int l_lovrWindowIsFocused(lua_State* L) { + Window* window = luax_checktype(L, 1, Window); + bool focused = lovrWindowIsFocused(window); + lua_pushboolean(L, focused); + return 1; +} + +static int l_lovrWindowIsFullscreen(lua_State* L) { + Window* window = luax_checktype(L, 1, Window); + bool fullscreen = lovrWindowIsFullscreen(window); + lua_pushboolean(L, fullscreen); + return 1; +} + +static int l_lovrWindowSetFullscreen(lua_State* L) { + Window* window = luax_checktype(L, 1, Window); + bool fullscreen = lua_toboolean(L, 2); + luax_assert(L, lovrWindowSetFullscreen(window, fullscreen)); + return 0; +} + +static int l_lovrWindowGetWidth(lua_State* L) { + Window* window = luax_checktype(L, 1, Window); + float bounds[3]; + luax_assert(L, lovrWindowGetBounds(window, bounds)); + lua_pushnumber(L, bounds[0]); + return 1; +} + +static int l_lovrWindowGetHeight(lua_State* L) { + Window* window = luax_checktype(L, 1, Window); + float bounds[3]; + luax_assert(L, lovrWindowGetBounds(window, bounds)); + lua_pushnumber(L, bounds[1]); + return 1; +} + +static int l_lovrWindowGetDepth(lua_State* L) { + Window* window = luax_checktype(L, 1, Window); + float bounds[3]; + luax_assert(L, lovrWindowGetBounds(window, bounds)); + lua_pushnumber(L, bounds[2]); + return 1; +} + +static int l_lovrWindowGetDimensions(lua_State* L) { + Window* window = luax_checktype(L, 1, Window); + float bounds[3]; + luax_assert(L, lovrWindowGetBounds(window, bounds)); + lua_pushnumber(L, bounds[0]); + lua_pushnumber(L, bounds[1]); + lua_pushnumber(L, bounds[2]); + return 3; +} + +static int l_lovrWindowGetPosition(lua_State* L) { + Window* window = luax_checktype(L, 1, Window); + float position[3], orientation[4]; + if (lovrWindowGetPose(window, position, orientation)) { + lua_pushnumber(L, position[0]); + lua_pushnumber(L, position[1]); + lua_pushnumber(L, position[2]); + } else { + lua_pushnumber(L, 0.); + lua_pushnumber(L, 0.); + lua_pushnumber(L, 0.); + } + return 3; +} + +static int l_lovrWindowGetOrientation(lua_State* L) { + Window* window = luax_checktype(L, 1, Window); + float position[3], orientation[4], angle, ax, ay, az; + if (lovrWindowGetPose(window, position, orientation)) { + quat_getAngleAxis(orientation, &angle, &ax, &ay, &az); + lua_pushnumber(L, angle); + lua_pushnumber(L, ax); + lua_pushnumber(L, ay); + lua_pushnumber(L, az); + } else { + lua_pushnumber(L, 0.); + lua_pushnumber(L, 0.); + lua_pushnumber(L, 0.); + lua_pushnumber(L, 0.); + } + return 4; +} + +static int l_lovrWindowGetPose(lua_State* L) { + Window* window = luax_checktype(L, 1, Window); + float position[3], orientation[4], angle, ax, ay, az; + if (lovrWindowGetPose(window, position, orientation)) { + lua_pushnumber(L, position[0]); + lua_pushnumber(L, position[1]); + lua_pushnumber(L, position[2]); + quat_getAngleAxis(orientation, &angle, &ax, &ay, &az); + lua_pushnumber(L, angle); + lua_pushnumber(L, ax); + lua_pushnumber(L, ay); + lua_pushnumber(L, az); + } else { + for (int i = 0; i < 7; i++) { + lua_pushnumber(L, 0.); + } + } + return 7; +} + +static int l_lovrWindowGetViewPose(lua_State* L) { + Window* window = luax_checktype(L, 1, Window); + float position[3], orientation[4]; + uint32_t view = luax_checku32(L, 2) - 1; + if (!lovrWindowGetViewPose(window, view, position, orientation)) { + lua_pushnil(L); + return 1; + } + float angle, ax, ay, az; + quat_getAngleAxis(orientation, &angle, &ax, &ay, &az); + lua_pushnumber(L, position[0]); + lua_pushnumber(L, position[1]); + lua_pushnumber(L, position[2]); + lua_pushnumber(L, angle); + lua_pushnumber(L, ax); + lua_pushnumber(L, ay); + lua_pushnumber(L, az); + return 7; +} + +static int l_lovrWindowGetViewAngles(lua_State* L) { + Window* window = luax_checktype(L, 1, Window); + float left, right, up, down; + uint32_t view = luax_checku32(L, 2) - 1; + if (!lovrWindowGetViewAngles(window, view, &left, &right, &up, &down)) { + lua_pushnil(L); + return 1; + } + lua_pushnumber(L, left); + lua_pushnumber(L, right); + lua_pushnumber(L, up); + lua_pushnumber(L, down); + return 4; +} + +static int l_lovrWindowGetTexture(lua_State* L) { + Window* window = luax_checktype(L, 1, Window); + Texture* texture = NULL; + luax_assert(L, lovrWindowGetTexture(window, &texture)); + luax_pushtype(L, Texture, texture); + return 1; +} + +static int l_lovrWindowGetPass(lua_State* L) { + Window* window = luax_checktype(L, 1, Window); + Pass* pass = NULL; + luax_assert(L, lovrWindowGetPass(window, &pass)); + luax_pushtype(L, Pass, pass); + return 1; +} + +int l_lovrWindowSetBackground(lua_State* L) { + Window* window = luax_checktype(L, 1, Window); + + uint32_t width = 0; + uint32_t height = 0; + uint32_t layers = 0; + Image* images[6]; + uint32_t imageCount = 0; + Texture* texture = NULL; + + if (lua_isnoneornil(L, 2)) { + lovrWindowSetBackground(window, 0, 0, 0); + return 0; + } else if ((texture = luax_totype(L, 2, Texture)) != NULL) { + const TextureInfo* info = lovrTextureGetInfo(texture); + width = info->width; + height = info->height; + layers = info->layers; + } else { + luax_checkimages(L, 2, images, COUNTOF(images), &imageCount, &layers); + luax_check(L, imageCount > 1, "Must have at least 1 image"); + width = lovrImageGetWidth(images[0], 0); + height = lovrImageGetHeight(images[0], 0); + } + + luax_check(L, layers == 1 || layers == 6, "Currently, background must have 1 or 6 layers"); + + Texture* background = lovrWindowSetBackground(window, width, height, layers); + + if (!background) { + for (uint32_t i = 0; i < imageCount; i++) { + lovrRelease(images[i], lovrImageDestroy); + } + luax_throw(L); + } + + if (texture) { + uint32_t srcOffset[4] = { 0 }; + uint32_t dstOffset[4] = { 0 }; + uint32_t extent[3] = { width, height, layers }; + luax_assert(L, lovrTextureCopy(texture, background, srcOffset, dstOffset, extent)); + } else if (imageCount > 0) { + for (uint32_t i = 0; i < imageCount; i++) { + uint32_t texOffset[4] = { 0, 0, i, 0 }; + uint32_t imgOffset[4] = { 0, 0, 0, 0 }; + uint32_t extent[3] = { width, height, lovrImageGetLayerCount(images[i]) }; + luax_assert(L, lovrTextureSetPixels(background, images[i], texOffset, imgOffset, extent)); + lovrRelease(images[i], lovrImageDestroy); + } + } + + return 0; +} + +int l_lovrWindowGetLayers(lua_State* L) { + Window* window = luax_checktype(L, 1, Window); + bool main; + uint32_t count; + Layer** layers = lovrWindowGetLayers(window, &count, &main); + lua_createtable(L, (int) count, 0); + for (uint32_t i = 0; i < count; i++) { + luax_pushtype(L, Layer, layers[i]); + lua_rawseti(L, -2, (int) i + 1); + } + lua_pushboolean(L, main); + lua_setfield(L, -2, "main"); + return 1; +} + +int l_lovrWindowSetLayers(lua_State* L) { + Window* window = luax_checktype(L, 1, Window); + Layer* layers[MAX_LAYERS]; + uint32_t count = 0; + bool main = true; + if (lua_type(L, 2) == LUA_TTABLE) { + count = luax_len(L, 2); + luax_check(L, count <= MAX_LAYERS, "Too many layers (max is %d)", MAX_LAYERS); + for (uint32_t i = 0; i < count; i++) { + lua_rawgeti(L, 2, (int) i + 1); + layers[i] = luax_checktype(L, -1, Layer); + lua_pop(L, 1); + } + lua_getfield(L, 2, "main"); + if (!lua_isnil(L, -1)) main = lua_toboolean(L, -1); + lua_pop(L, 1); + } else { + count = lua_gettop(L) - 1; + luax_check(L, count <= MAX_LAYERS, "Too many layers (max is %d)", MAX_LAYERS); + for (uint32_t i = 0; i < count; i++) { + layers[i] = luax_checktype(L, (int) i + 2, Layer); + } + } + bool success = lovrWindowSetLayers(window, layers, count, main); + luax_assert(L, success); + return 0; +} + +const luaL_Reg lovrWindow[] = { + { "isOpen", l_lovrWindowIsOpen }, + { "close", l_lovrWindowClose }, + { "isVisible", l_lovrWindowIsVisible }, + { "setVisible", l_lovrWindowSetVisible }, + { "isFocused", l_lovrWindowIsFocused }, + { "isFullscreen", l_lovrWindowIsFullscreen }, + { "setFullscreen", l_lovrWindowSetFullscreen }, + { "getWidth", l_lovrWindowGetWidth }, + { "getHeight", l_lovrWindowGetHeight }, + { "getDepth", l_lovrWindowGetDepth }, + { "getDimensions", l_lovrWindowGetDimensions }, + { "getPosition", l_lovrWindowGetPosition }, + { "getOrientation", l_lovrWindowGetOrientation }, + { "getPose", l_lovrWindowGetPose }, + { "getViewPose", l_lovrWindowGetViewPose }, + { "getViewAngles", l_lovrWindowGetViewAngles }, + { "getTexture", l_lovrWindowGetTexture }, + { "getPass", l_lovrWindowGetPass }, + { "setBackground", l_lovrWindowSetBackground }, + { "getLayers", l_lovrWindowGetLayers }, + { "setLayers", l_lovrWindowSetLayers }, + { NULL, NULL } +}; diff --git a/src/modules/audio/audio.c b/src/modules/audio/audio.c index ed02210ce..8c209dc2c 100644 --- a/src/modules/audio/audio.c +++ b/src/modules/audio/audio.c @@ -12,14 +12,8 @@ #include #include #include -#ifdef _MSC_VER -#include -#define CTZL _tzcnt_u64 -#else -#define CTZL __builtin_ctzl -#endif -#define FOREACH_SOURCE(mask, s) for (uint64_t m = mask; s = m ? state.activeSources[CTZL(m)] : NULL, m; m ^= (m & -m)) +#define FOREACH_SOURCE(mask, s) for (uint64_t m = mask; s = m ? state.activeSources[lovrTrailingZeros64(m)] : NULL, m; m ^= (m & -m)) #define AMBISONIC_ORDER(channels) ((channels / 6) + 1) #define OUTPUT_FORMAT SAMPLE_F32 #define MAX_OCCLUSION_SAMPLES 64 @@ -676,7 +670,7 @@ bool lovrSourcePlay(Source* source) { if (source->slot == ~0u) { uint64_t mask = state.activeSourceMask | state.pendingSourceMask; if (mask == ~0ull) return false; - uint32_t slot = mask ? CTZL(~mask) : 0; + uint32_t slot = mask ? lovrTrailingZeros64(~mask) : 0; state.pendingSourceMask |= (1ull << slot); state.activeSources[slot] = source; source->slot = slot; diff --git a/src/modules/event/event.c b/src/modules/event/event.c index 0619dda6c..a9f55d138 100644 --- a/src/modules/event/event.c +++ b/src/modules/event/event.c @@ -1,4 +1,5 @@ #include "event/event.h" +#include "headset/headset.h" #include "thread/thread.h" #include "util.h" #include @@ -28,6 +29,9 @@ void lovrEventDestroy(void) { for (size_t i = state.head; i < state.events.length; i++) { Event* event = &state.events.data[i]; switch (event->type) { + case EVENT_VISIBLE: lovrRelease(event->data.visible.window, lovrWindowDestroy); break; + case EVENT_FOCUS: lovrRelease(event->data.focus.window, lovrWindowDestroy); break; + case EVENT_RESIZE: lovrRelease(event->data.resize.window, lovrWindowDestroy); break; #ifndef LOVR_DISABLE_THREAD case EVENT_THREAD_ERROR: lovrRelease(event->data.thread.thread, lovrThreadDestroy); break; #endif diff --git a/src/modules/event/event.h b/src/modules/event/event.h index 0adad3b40..0fc1d402e 100644 --- a/src/modules/event/event.h +++ b/src/modules/event/event.h @@ -5,6 +5,7 @@ #pragma once struct Thread; +struct Window; union Variant; typedef enum { @@ -43,11 +44,13 @@ typedef struct { typedef struct { bool visible; DisplayType display; + struct Window* window; } VisibleEvent; typedef struct { bool focused; DisplayType display; + struct Window* window; } FocusEvent; typedef struct { @@ -55,8 +58,10 @@ typedef struct { } MountEvent; typedef struct { - uint32_t width; - uint32_t height; + struct Window* window; + float width; + float height; + float depth; } ResizeEvent; typedef struct { diff --git a/src/modules/headset/headset.c b/src/modules/headset/headset.c index b739c3cd4..b327b8d33 100644 --- a/src/modules/headset/headset.c +++ b/src/modules/headset/headset.c @@ -55,8 +55,10 @@ uintptr_t gpu_vk_get_queue(uint32_t* queueFamilyIndex, uint32_t* queueIndex); #define XR(f, s) do { XrResult r = f; if (XR_FAILED(r)) { xrthrow(r, s); return 0; } } while(0) #define XRG(f, s, j) do { XrResult r = f; if (XR_FAILED(r)) { xrthrow(r, s); goto j; } } while(0) -#define SESSION_RUNNING(s) (s >= XR_SESSION_STATE_READY && s <= XR_SESSION_STATE_FOCUSED) +#define SESSION_RUNNING(s) (state.extensions.spatialContainer ? s == XR_SESSION_STATE_IDLE : (s >= XR_SESSION_STATE_READY && s <= XR_SESSION_STATE_FOCUSED)) #define MAX_IMAGES 4 +#define MAX_VIEWS 4 +#define MAX_WINDOWS 32 #define MAX_HAND_JOINTS 27 #define XR_FOREACH(X)\ @@ -144,7 +146,17 @@ uintptr_t gpu_vk_get_queue(uint32_t* queueFamilyIndex, uint32_t* queueIndex); X(xrDestroyPassthroughLayerFB)\ X(xrGetPassthroughPreferencesMETA)\ X(xrGetRecommendedLayerResolutionMETA)\ - X(xrEnumerateColorSpacesSONY) + X(xrEnumerateColorSpacesSONY)\ + X(xrCreateSpatialContainerEXT)\ + X(xrDestroySpatialContainerEXT)\ + X(xrCreateSpatialContainerSpaceEXT)\ + X(xrGetSpatialContainerStateEXT)\ + X(xrRequestSpatialContainerVisibleEXT)\ + X(xrRequestSpatialContainerBoundsModeEXT)\ + X(xrGetSpatialContainerBoundsEXT)\ + X(xrBeginSpatialContainerRenderingEXT)\ + X(xrEndSpatialContainerRenderingEXT)\ + X(xrLocateSpatialContainerViewsEXT) #define XR_DECLARE(fn) static PFN_##fn fn; #define XR_LOAD(fn) xrGetInstanceProcAddr(state.instance, #fn, (PFN_xrVoidFunction*) &fn); @@ -198,6 +210,35 @@ struct Layer { XrCompositionLayerSettingsFB settings; }; +struct Window { + uint32_t ref; + uint32_t index; + XrSpatialContainerEXT handle; + XrSpace space; + XrViewStateFlags viewState; + XrView* views; + bool shouldRender; + bool hdr; + uint32_t renderWidth; + uint32_t renderHeight; + Swapchain swapchains[3]; + Pass* pass; + XrCompositionLayerProjection layer; + XrCompositionLayerProjectionView layerViews[4]; + XrCompositionLayerDepthInfoKHR depthInfo[4]; + union { + XrCompositionLayerBaseHeader header; + XrCompositionLayerCubeKHR cube; + XrCompositionLayerEquirectKHR equirect; + XrCompositionLayerEquirect2KHR equirect2; + } background; + XrCompositionLayerBaseHeader const* layerHeaders[MAX_LAYERS + 3]; + union { XrCompositionLayerQuad quad; XrCompositionLayerCylinderKHR cylinder; } stereoLayers[MAX_LAYERS]; + Layer* layers[MAX_LAYERS]; + uint32_t layerCount; + bool showMainLayer; +}; + typedef struct { XrRenderModelEXT handle; XrRenderModelPropertiesEXT properties; @@ -215,6 +256,64 @@ typedef struct { Pass* pass; } Simulator; +typedef struct { + bool battery; + bool bodyTracking; + bool cosmosController; + bool debug; + bool depth; + bool dynamicResolution; + bool focus3Controller; + bool foveatedInset; + bool foveation; + bool foveationConfig; + bool foveationVulkan; + bool frameController; + bool gaze; + bool genericController; + bool handInteraction; + bool handTracking; + bool handTrackingDataSource; + bool handTrackingElbow; + bool handTrackingMesh; + bool handTrackingMotionRange; + bool hdr; + bool headless; + bool interactionRenderModel; + bool keyboardTracking; + bool layerAutoFilter; + bool layerColor; + bool layerCube; + bool layerCurve; + bool layerDepthTest; + bool layerEquirect; + bool layerEquirect2; + bool layerSettings; + bool localFloor; + bool microgestures; + bool ml2Controller; + bool mxInk; + bool overlay; + bool palmPose; + bool passthroughPreferences; + bool picoController; + bool picoUltraController; + bool presence; + bool questPassthrough; + bool renderModel; + bool resize; + bool reverbController; + bool spatialContainer; + bool spatialContainerRendering; + bool swapchainUpdate; + bool refreshRate; + bool threadHint; + bool touchPro; + bool uuid; + bool visibilityMask; + bool viveTrackers; +} xr_extensions; + static atomic_uint ref; static struct { @@ -231,28 +330,12 @@ static struct { XrSpace referenceSpace; float* refreshRates; uint32_t refreshRateCount; - bool hdr; XrEnvironmentBlendMode* blendModes; XrEnvironmentBlendMode blendMode; uint32_t blendModeCount; XrSpace spaces[MAX_DEVICES]; XrSpace handSpaces[2][MAX_HAND_POSES]; TextureFormat depthFormat; - Pass* pass; - Swapchain swapchains[3]; - XrCompositionLayerProjection layer; - XrCompositionLayerProjectionView layerViews[4]; - XrCompositionLayerDepthInfoKHR depthInfo[4]; - XrCompositionLayerPassthroughFB passthroughLayer; - union { - XrCompositionLayerBaseHeader header; - XrCompositionLayerCubeKHR cube; - XrCompositionLayerEquirectKHR equirect; - XrCompositionLayerEquirect2KHR equirect2; - } background; - Layer* layers[MAX_LAYERS]; - uint32_t layerCount; - bool showMainLayer; Mesh* mask; XrFrameState frameState; XrTime lastDisplayTime; @@ -263,6 +346,8 @@ static struct { float clipFar; bool waited; bool began; + bool mounted; + bool mainSessionVisible; XrActionSet actionSet; XrAction actions[MAX_ACTIONS]; XrPath actionFilters[MAX_DEVICES]; @@ -276,65 +361,18 @@ static struct { bool foveationDynamic; XrPassthroughFB passthrough; XrPassthroughLayerFB passthroughLayerHandle; + XrCompositionLayerPassthroughFB passthroughLayer; bool passthroughActive; - bool mounted; - bool mainSessionVisible; XrDebugUtilsMessengerEXT messenger; - struct { - bool battery; - bool bodyTracking; - bool cosmosController; - bool debug; - bool depth; - bool dynamicResolution; - bool focus3Controller; - bool foveatedInset; - bool foveation; - bool foveationConfig; - bool foveationVulkan; - bool frameController; - bool gaze; - bool genericController; - bool handInteraction; - bool handTracking; - bool handTrackingDataSource; - bool handTrackingElbow; - bool handTrackingMesh; - bool handTrackingMotionRange; - bool hdr; - bool headless; - bool interactionRenderModel; - bool keyboardTracking; - bool layerAutoFilter; - bool layerColor; - bool layerCube; - bool layerCurve; - bool layerDepthTest; - bool layerEquirect; - bool layerEquirect2; - bool layerSettings; - bool localFloor; - bool microgestures; - bool ml2Controller; - bool mxInk; - bool overlay; - bool palmPose; - bool passthroughPreferences; - bool picoController; - bool picoUltraController; - bool presence; - bool questPassthrough; - bool renderModel; - bool resize; - bool reverbController; - bool swapchainUpdate; - bool refreshRate; - bool threadHint; - bool touchPro; - bool uuid; - bool visibilityMask; - bool viveTrackers; - } extensions; + Window windows[MAX_WINDOWS]; + Window* window; + uint32_t windowMask; + uint32_t openWindowMask; + uint32_t visibleWindowMask; + XrSpatialContainerLayerEXT windowLayers[MAX_WINDOWS]; + XrSpatialContainerCompositionLayerViewConfigurationEXT windowLayerViewConfigurations[MAX_WINDOWS]; // ... + XrView views[MAX_WINDOWS * MAX_VIEWS]; + xr_extensions extensions; } state; // Helpers @@ -350,14 +388,13 @@ static XrBool32 onMessage(XrDebugUtilsMessageSeverityFlagsEXT severity, XrDebugU static bool hasExtension(XrExtensionProperties* extensions, uint32_t count, const char* extension); static XrTime getCurrentXrTime(void); static bool updateViewSize(void); -static XrViewStateFlags locateViews(XrView views[4], uint32_t* count); -static bool createSwapchains(void); static bool createReferenceSpace(XrTime time); static XrAction getPoseActionForDevice(Device device); static XrHandTrackerEXT getHandTracker(Device device); static XrBodyTrackerBD getBodyTracker(void); static bool loadControllerModels(void); static bool loadVisibilityMask(void); +static Window* findWindow(XrSpatialContainerEXT handle); // Entry @@ -485,6 +522,8 @@ bool lovrHeadsetConnect(void) { { "XR_EXT_local_floor", &state.extensions.localFloor, true }, { "XR_EXT_palm_pose", &state.extensions.palmPose, true }, { "XR_EXT_render_model", &state.extensions.renderModel, true }, + { "XR_EXT_spatial_container", &state.extensions.spatialContainer, config->window }, + { "XR_EXT_spatial_container_self_rendering", &state.extensions.spatialContainerRendering, config->window }, { "XR_EXT_user_presence", &state.extensions.presence, true }, { "XR_EXT_uuid", &state.extensions.uuid, true }, { "XR_EXT_view_configuration_views_change", &state.extensions.resize, true }, @@ -539,6 +578,12 @@ bool lovrHeadsetConnect(void) { extension += strlen(extension) + 1; } + // Spatial containers don't work well with headless / dynamic resolution + if (state.extensions.spatialContainer) { + state.extensions.headless = false; + state.extensions.dynamicResolution = false; + } + lovrFree(extensionProperties); // Instance @@ -617,6 +662,7 @@ bool lovrHeadsetConnect(void) { XrSystemKeyboardTrackingPropertiesFB keyboardTrackingProperties = { .type = XR_TYPE_SYSTEM_KEYBOARD_TRACKING_PROPERTIES_FB }; XrSystemUserPresencePropertiesEXT presenceProperties = { .type = XR_TYPE_SYSTEM_USER_PRESENCE_PROPERTIES_EXT }; XrSystemPassthroughProperties2FB passthroughProperties = { .type = XR_TYPE_SYSTEM_PASSTHROUGH_PROPERTIES2_FB }; + XrSystemSpatialContainerPropertiesEXT spatialContainerProperties = { .type = XR_TYPE_SYSTEM_SPATIAL_CONTAINER_PROPERTIES_EXT }; if (state.extensions.gaze) { eyeGazeProperties.next = state.systemProperties.next; @@ -648,6 +694,11 @@ bool lovrHeadsetConnect(void) { state.systemProperties.next = &passthroughProperties; } + if (state.extensions.spatialContainer) { + spatialContainerProperties.next = state.systemProperties.next; + state.systemProperties.next = &spatialContainerProperties; + } + XRG(xrGetSystemProperties(state.instance, state.system, &state.systemProperties), "xrGetSystemProperties", fail); state.extensions.gaze = eyeGazeProperties.supportsEyeGazeInteraction; state.extensions.handTracking = handTrackingProperties.supportsHandTracking; @@ -655,6 +706,7 @@ bool lovrHeadsetConnect(void) { state.extensions.keyboardTracking = keyboardTrackingProperties.supportsKeyboardTracking; state.extensions.presence = presenceProperties.supportsUserPresence; state.extensions.questPassthrough = passthroughProperties.capabilities & XR_PASSTHROUGH_CAPABILITY_BIT_FB; + state.extensions.spatialContainer = spatialContainerProperties.supportsBounded && state.extensions.spatialContainerRendering; // View Configuration @@ -986,6 +1038,7 @@ void lovrHeadsetGetFeatures(HeadsetFeatures* features) { features->proximity = state.extensions.presence; features->refreshRate = state.extensions.refreshRate; features->viveTrackers = state.extensions.viveTrackers; + features->window = state.extensions.spatialContainer; } bool lovrHeadsetIsSeated(void) { @@ -1040,7 +1093,7 @@ bool lovrHeadsetStart(void) { } #endif - lovrAssert(hasGraphics || state.extensions.headless, "Graphics module is not available, and headless headset is not supported"); + lovrAssert(hasGraphics || state.extensions.headless, "Graphics module is not available, and headless mode is not supported"); #ifdef XR_EXTX_overlay XrSessionCreateInfoOverlayEXTX overlayInfo = { @@ -1054,6 +1107,15 @@ bool lovrHeadsetStart(void) { } #endif + XrSessionCreateInfoSpatialContainersEXT spatialContainerInfo = { + .type = XR_TYPE_SESSION_CREATE_INFO_SPATIAL_CONTAINERS_EXT + }; + + if (state.extensions.spatialContainer) { + spatialContainerInfo.next = info.next; + info.next = &spatialContainerInfo; + } + XrSessionActionSetsAttachInfo attachInfo = { .type = XR_TYPE_SESSION_ACTION_SETS_ATTACH_INFO, .countActionSets = 1, @@ -1120,7 +1182,7 @@ bool lovrHeadsetStart(void) { state.handSpaces[SIDE_RIGHT][HAND_PALM] = state.spaces[DEVICE_HAND_RIGHT_PALM]; } - // Swapchain + // Swapchain formats if (hasGraphics) { state.depthFormat = state.config.stencil ? FORMAT_D32FS8 : FORMAT_D32F; @@ -1128,12 +1190,6 @@ bool lovrHeadsetStart(void) { state.depthFormat = state.config.stencil ? FORMAT_D24S8 : FORMAT_D24; } - state.pass = lovrPassCreate("Headset"); - - if (!state.pass) { - goto stop; - } - #ifdef LOVR_VK int64_t nativeColorFormat = VK_FORMAT_R8G8B8A8_SRGB; int64_t nativeColorFormatHDR = VK_FORMAT_A2B10G10R10_UNORM_PACK32; @@ -1154,6 +1210,7 @@ bool lovrHeadsetStart(void) { bool supportsColor = false; bool supportsDepth = false; + bool supportsHDR = false; for (uint32_t i = 0; i < formatCount && (!supportsColor || !supportsDepth); i++) { if (formats[i] == nativeColorFormat) { @@ -1163,8 +1220,6 @@ bool lovrHeadsetStart(void) { } } - state.hdr = false; - if (state.extensions.hdr) { XrColorSpacesEnumerateInfoSONY colorSpaceInfo = { .type = XR_TYPE_COLOR_SPACES_ENUMERATE_INFO_SONY, @@ -1177,20 +1232,23 @@ bool lovrHeadsetStart(void) { if (XR_SUCCEEDED(xrEnumerateColorSpacesSONY(state.session, &colorSpaceInfo, COUNTOF(colorSpaces), &colorSpaceCount, colorSpaces))) { for (uint32_t i = 0; i < colorSpaceCount; i++) { if (colorSpaces[i] == XR_COLOR_SPACE_BT2020_PQ_SONY) { - state.hdr = true; + supportsHDR = true; break; } } } } - GraphicsFeatures features; - lovrGraphicsGetFeatures(&features); + if (!supportsHDR) { + state.extensions.hdr = false; + } + lovrAssertGoto(stop, supportsColor, "This VR runtime does not support sRGB rgba8 textures"); - if (!supportsDepth || !features.depthResolve) state.extensions.depth = false; - if (!createSwapchains()) { - goto stop; + GraphicsFeatures features; + lovrGraphicsGetFeatures(&features); + if (!supportsDepth || !features.depthResolve) { + state.extensions.depth = false; } } @@ -1256,9 +1314,22 @@ bool lovrHeadsetStart(void) { } } - state.showMainLayer = true; - return true; + state.window = lovrWindowCreate(&(WindowInfo) { + .size = { 0.f, 0.f, 0.f }, + .fullscreen = true, + .hdr = state.extensions.hdr + }); + + if (state.extensions.spatialContainer) { + for (uint32_t i = 0; i < MAX_WINDOWS; i++) { + state.windowLayerViewConfigurations[i] = (XrSpatialContainerCompositionLayerViewConfigurationEXT) { + .type = XR_TYPE_SPATIAL_CONTAINER_COMPOSITION_LAYER_VIEW_CONFIGURATION_EXT, + .viewConfigurationType = state.viewConfiguration + }; + } + } + return true; stop: lovrHeadsetStop(); return false; @@ -1286,16 +1357,11 @@ void lovrHeadsetStop(void) { state.refreshRateCount = 0; state.refreshRates = NULL; - for (uint32_t i = 0; i < state.layerCount; i++) { - lovrRelease(state.layers[i], lovrLayerDestroy); - state.layers[i] = NULL; + lovrBiterate(state.openWindowMask, index) { + lovrWindowClose(&state.windows[index]); } - state.layerCount = 0; - - lovrSwapchainDestroy(&state.swapchains[0]); - lovrSwapchainDestroy(&state.swapchains[1]); - lovrRelease(state.pass, lovrPassDestroy); - state.pass = NULL; + lovrRelease(state.window, lovrWindowDestroy); + state.window = NULL; lovrRelease(state.mask, lovrMeshDestroy); state.mask = NULL; @@ -1344,11 +1410,11 @@ bool lovrHeadsetIsActive(void) { bool lovrHeadsetIsVisible(bool* main) { *main = state.mainSessionVisible; - return state.sessionState >= XR_SESSION_STATE_VISIBLE; + return state.window && lovrWindowIsVisible(state.window); } bool lovrHeadsetIsFocused(void) { - return state.sessionState == XR_SESSION_STATE_FOCUSED; + return state.window && lovrWindowIsFocused(state.window); } bool lovrHeadsetIsMounted(void) { @@ -1394,6 +1460,11 @@ bool lovrHeadsetPollEvents(void) { bool isVisible = event->state >= XR_SESSION_STATE_VISIBLE; if (wasVisible != isVisible) { lovrEventPush((Event) { .type = EVENT_VISIBLE, .data.visible.visible = isVisible }); + if (isVisible) { + state.visibleWindowMask |= (1 << state.window->index); + } else { + state.visibleWindowMask &= ~(1 << state.window->index); + } } bool wasFocused = state.sessionState == XR_SESSION_STATE_FOCUSED; @@ -1443,18 +1514,82 @@ bool lovrHeadsetPollEvents(void) { XrEventDataViewConfigurationViewsChangedEXT* event = (XrEventDataViewConfigurationViewsChangedEXT*) &e; if (event->systemId == state.system && event->viewConfigurationType == state.viewConfiguration) { updateViewSize(); - if (!createSwapchains()) { - return false; - } + + // lovrWindowGetTexture will lazily reinitialize the swapchains + lovrSwapchainDestroy(&state.window->swapchains[SWAPCHAIN_COLOR]); + lovrSwapchainDestroy(&state.window->swapchains[SWAPCHAIN_DEPTH]); + } + } + case XR_TYPE_EVENT_DATA_SPATIAL_CONTAINER_VISIBLE_CHANGED_EXT: { + XrEventDataSpatialContainerVisibleChangedEXT* event = (XrEventDataSpatialContainerVisibleChangedEXT*) &e; + Window* window = findWindow(event->spatialContainer); + if (!window) break; + + lovrEventPush((Event) { + .type = EVENT_VISIBLE, + .data.visible.visible = event->visible, + .data.visible.window = window + }); + + if (event->visible) { + XrSpatialContainerBeginInfoEXT beginfo = { + .type = XR_TYPE_SPATIAL_CONTAINER_BEGIN_INFO_EXT, + .spatialContainer = event->spatialContainer, + .primaryViewConfigurationType = state.viewConfiguration + }; + XR(xrBeginSpatialContainerRenderingEXT(state.session, &beginfo), "xrBeginSpatialContainerRenderingEXT"); + state.visibleWindowMask |= (1 << window->index); + } else { + XrSpatialContainerEndInfoEXT endInfo = { + .type = XR_TYPE_SPATIAL_CONTAINER_END_INFO_EXT, + .spatialContainer = event->spatialContainer + }; + XR(xrEndSpatialContainerRenderingEXT(state.session, &endInfo), "xrEndSpatialContainerRenderingEXT"); + state.visibleWindowMask &= ~(1 << window->index); + } + break; + } + case XR_TYPE_EVENT_DATA_SPATIAL_CONTAINER_INTERACTABLE_CHANGED_EXT: { + XrEventDataSpatialContainerInteractableChangedEXT* event = (XrEventDataSpatialContainerInteractableChangedEXT*) &e; + Window* window = findWindow(event->spatialContainer); + if (!window) break; + lovrEventPush((Event) { + .type = EVENT_FOCUS, + .data.focus.focused = event->interactable, + .data.focus.window = window + }); + break; + } + case XR_TYPE_EVENT_DATA_SPATIAL_CONTAINER_BOUNDS_CHANGED_EXT: { + XrEventDataSpatialContainerBoundsChangedEXT* event = (XrEventDataSpatialContainerBoundsChangedEXT*) &e; + Window* window = findWindow(event->spatialContainer); + if (!window) break; + lovrEventPush((Event) { + .type = EVENT_RESIZE, + .data.resize.width = event->bounds.width, + .data.resize.height = event->bounds.height, + .data.resize.depth = event->bounds.depth, + .data.resize.window = window + }); + break; + } + case XR_TYPE_EVENT_DATA_SPATIAL_CONTAINER_CLOSED_EXT: { + XrEventDataSpatialContainerClosedEXT* event = (XrEventDataSpatialContainerClosedEXT*) &e; + Window* window = findWindow(event->spatialContainer); + if (!window) break; + lovrWindowClose(window); + if (state.openWindowMask == 0) { + lovrEventPush((Event) { .type = EVENT_QUIT, .data.quit.exitCode = 0 }); } + break; } default: break; } e.type = XR_TYPE_EVENT_DATA_BUFFER; } - if (SESSION_RUNNING(state.sessionState) && visibilityMaskDirty && !loadVisibilityMask()) { - lovrLog(LOG_WARN, "XR", "Failed to load headset mask: %s", lovrGetError()); + if (SESSION_RUNNING(state.sessionState) && visibilityMaskDirty) { + loadVisibilityMask(); } return true; @@ -1495,7 +1630,7 @@ bool lovrHeadsetUpdate(void) { if (state.extensions.dynamicResolution) { XrRecommendedLayerResolutionGetInfoMETA info = { .type = XR_TYPE_RECOMMENDED_LAYER_RESOLUTION_GET_INFO_META, - .layer = (XrCompositionLayerBaseHeader*) &state.layer, + .layer = (XrCompositionLayerBaseHeader*) &state.window->layer, .predictedDisplayTime = state.frameState.predictedDisplayTime }; @@ -1513,18 +1648,83 @@ bool lovrHeadsetUpdate(void) { } for (uint32_t i = 0; i < state.viewCount; i++) { - state.layerViews[i].subImage.imageRect.extent.width = state.width; - state.layerViews[i].subImage.imageRect.extent.height = state.height; - state.depthInfo[i].subImage.imageRect = state.layerViews[i].subImage.imageRect; + state.window->layerViews[i].subImage.imageRect.extent.width = state.width; + state.window->layerViews[i].subImage.imageRect.extent.height = state.height; + state.window->depthInfo[i].subImage.imageRect = state.window->layerViews[i].subImage.imageRect; } } } // Throttle when session is idle (but not too much, a desktop window might be rendering stuff) - if (state.sessionState == XR_SESSION_STATE_IDLE) { + if (state.sessionState == XR_SESSION_STATE_IDLE && !state.extensions.spatialContainer) { os_sleep(.001); } + // Locate views + if (state.extensions.spatialContainer) { + uint32_t windowMask = state.openWindowMask; + uint32_t windowCount = 0; + + XrSpatialContainerViewLocateInfoEXT viewLocateInfo[MAX_WINDOWS]; + XrSpatialContainerViewStateEXT viewState[MAX_WINDOWS]; + + lovrBiterate(windowMask, index) { + viewLocateInfo[windowCount] = (XrSpatialContainerViewLocateInfoEXT) { + .type = XR_TYPE_SPATIAL_CONTAINER_VIEW_LOCATE_INFO_EXT, + .viewConfigurationType = state.viewConfiguration, + .space = state.referenceSpace, + .spatialContainer = state.windows[index].handle + }; + + viewState[windowCount].type = XR_TYPE_SPATIAL_CONTAINER_VIEW_STATE_EXT; + viewState[windowCount].next = NULL; + windowCount++; + } + + XrSpatialContainerViewsLocateInfoEXT viewsLocateInfo = { + .type = XR_TYPE_SPATIAL_CONTAINER_VIEWS_LOCATE_INFO_EXT, + .displayTime = state.frameState.predictedDisplayTime, + .viewLocateInfoCount = windowCount, + .viewLocateInfos = viewLocateInfo + }; + + XR(xrLocateSpatialContainerViewsEXT(state.session, &viewsLocateInfo, windowCount, viewState, state.viewCount * windowCount, state.views), "xrLocateSpatialContainerViewsEXT"); + + uint32_t cursor = 0; + lovrBiterate(windowMask, index) { + state.windows[index].viewState = viewState[cursor].viewStateFlags; + state.windows[index].shouldRender = viewState[cursor].shouldSubmitLayers; + state.windows[index].renderWidth = viewState[cursor].recommendedImageExtent.width; + state.windows[index].renderHeight = viewState[cursor].recommendedImageExtent.height; + state.windows[index].views = state.views + cursor * state.viewCount; + cursor++; + } + } else { + XrViewLocateInfo viewLocateInfo = { + .type = XR_TYPE_VIEW_LOCATE_INFO, + .viewConfigurationType = state.viewConfiguration, + .displayTime = state.frameState.predictedDisplayTime, + .space = state.referenceSpace + }; + + for (uint32_t i = 0; i < state.viewCount; i++) { + state.views[i].type = XR_TYPE_VIEW; + state.views[i].next = NULL; + } + + uint32_t viewCount = state.viewCount; + XrViewState viewState = { .type = XR_TYPE_VIEW_STATE }; + if (XR_SUCCEEDED(xrLocateViews(state.session, &viewLocateInfo, &viewState, state.viewCount, &viewCount, state.views))) { + state.window->viewState = viewState.viewStateFlags; + state.window->shouldRender = state.frameState.shouldRender; + state.window->renderWidth = state.width; + state.window->renderHeight = state.height; + state.window->views = state.views; + } else { + state.window->viewState = 0; + } + } + return true; } @@ -1563,7 +1763,7 @@ void lovrHeadsetGetFoveation(FoveationLevel* level, bool* dynamic) { } bool lovrHeadsetSetFoveation(FoveationLevel level, bool dynamic) { - if (!state.session || !state.extensions.foveation) { + if (!state.session || !state.extensions.foveation || !state.window) { return level == FOVEATION_NONE; } @@ -1604,7 +1804,7 @@ bool lovrHeadsetSetFoveation(FoveationLevel level, bool dynamic) { .profile = profile }; - if (XR_FAILED(xrUpdateSwapchainFB(state.swapchains[SWAPCHAIN_COLOR].handle, (XrSwapchainStateBaseHeaderFB*) &foveationState))) { + if (XR_FAILED(xrUpdateSwapchainFB(state.window->swapchains[SWAPCHAIN_COLOR].handle, (XrSwapchainStateBaseHeaderFB*) &foveationState))) { return false; } @@ -1617,7 +1817,7 @@ bool lovrHeadsetSetFoveation(FoveationLevel level, bool dynamic) { } bool lovrHeadsetIsHDR(void) { - return state.hdr; + return state.extensions.hdr; } static XrEnvironmentBlendMode convertPassthroughMode(PassthroughMode mode) { @@ -1765,29 +1965,9 @@ bool lovrHeadsetGetViewPose(uint32_t view, float* position, float* orientation) vec3_init(position, state.simulator.poses[DEVICE_HEAD]); quat_init(orientation, state.simulator.poses[DEVICE_HEAD] + 3); return view == 0; - } - - uint32_t count; - XrView views[4]; - XrViewStateFlags flags = locateViews(views, &count); - - if (view >= count || !flags) { - return false; - } - - if (flags & XR_VIEW_STATE_POSITION_VALID_BIT) { - memcpy(position, &views[view].pose.position.x, 3 * sizeof(float)); } else { - memset(position, 0, 3 * sizeof(float)); - } - - if (flags & XR_VIEW_STATE_ORIENTATION_VALID_BIT) { - memcpy(orientation, &views[view].pose.orientation.x, 4 * sizeof(float)); - } else { - quat_identity(orientation); + return state.window && lovrWindowGetViewPose(state.window, view, position, orientation); } - - return true; } bool lovrHeadsetGetViewAngles(uint32_t view, float* left, float* right, float* up, float* down) { @@ -1801,21 +1981,9 @@ bool lovrHeadsetGetViewAngles(uint32_t view, float* left, float* right, float* u *up = fov; *down = fov; return view == 0; + } else { + return state.window && lovrWindowGetViewAngles(state.window, view, left, right, up, down); } - - uint32_t count; - XrView views[4]; - XrViewStateFlags flags = locateViews(views, &count); - - if (view >= count || !flags) { - return false; - } - - *left = -views[view].fov.angleLeft; - *right = views[view].fov.angleRight; - *up = views[view].fov.angleUp; - *down = -views[view].fov.angleDown; - return true; } void lovrHeadsetGetClipDistance(float* clipNear, float* clipFar) { @@ -2774,86 +2942,8 @@ bool lovrHeadsetAnimate(Model* model) { return false; } -Texture* lovrHeadsetSetBackground(uint32_t width, uint32_t height, uint32_t layers) { - Swapchain* swapchain = &state.swapchains[SWAPCHAIN_BACKGROUND]; - - if (width == 0 && height == 0) { - lovrSwapchainDestroy(swapchain); - memset(swapchain, 0, sizeof(Swapchain)); - return NULL; - } - - lovrCheck(state.extensions.layerCube || layers != 6, "This headset does not support cubemap backgrounds"); - lovrCheck(state.extensions.layerEquirect || state.extensions.layerEquirect2 || layers != 1, "This headset does not support equirectangular backgrounds"); - - if (!lovrSwapchainInit(swapchain, width, height, STATIC | (layers == 6 ? CUBE : 0))) { - return NULL; - } - - if (!lovrSwapchainAcquire(swapchain)) { - lovrSwapchainDestroy(swapchain); - memset(swapchain, 0, sizeof(Swapchain)); - return NULL; - } - - if (layers == 6) { - state.background.cube = (XrCompositionLayerCubeKHR) { - .type = XR_TYPE_COMPOSITION_LAYER_CUBE_KHR, - .eyeVisibility = XR_EYE_VISIBILITY_BOTH, - .swapchain = swapchain->handle, - .orientation.w = 1.f - }; - } else if (state.extensions.layerEquirect2) { - state.background.equirect2 = (XrCompositionLayerEquirect2KHR) { - .type = XR_TYPE_COMPOSITION_LAYER_EQUIRECT2_KHR, - .eyeVisibility = XR_EYE_VISIBILITY_BOTH, - .subImage = { swapchain->handle, { { 0, 0 }, { width, height } }, 0 }, - .pose.orientation.w = 1.f, - .centralHorizontalAngle = 2.f * (float) M_PI, - .upperVerticalAngle = (float) M_PI * .5f, - .lowerVerticalAngle = (float) -M_PI * .5f - }; - } else { - state.background.equirect = (XrCompositionLayerEquirectKHR) { - .type = XR_TYPE_COMPOSITION_LAYER_EQUIRECT_KHR, - .eyeVisibility = XR_EYE_VISIBILITY_BOTH, - .subImage = { swapchain->handle, { { 0, 0 }, { width, height } }, 0 }, - .pose.orientation.w = 1.f, - .scale = { 1.f, 1.f } - }; - } - - return state.swapchains[SWAPCHAIN_BACKGROUND].textures[0]; -} - -Layer** lovrHeadsetGetLayers(uint32_t* count, bool* main) { - *count = state.layerCount; - *main = state.showMainLayer; - return state.layers; -} - -bool lovrHeadsetSetLayers(Layer** layers, uint32_t count, bool main) { - uint32_t total = 0; - - for (uint32_t i = 0; i < count; i++) { - total += layers[i]->info.stereo ? 2 : 1; - } - - lovrCheck(total <= MAX_LAYERS, "Too many layers"); - - for (uint32_t i = 0; i < state.layerCount; i++) { - lovrRelease(state.layers[i], lovrLayerDestroy); - } - - state.layerCount = count; - for (uint32_t i = 0; i < count; i++) { - lovrRetain(layers[i]); - state.layers[i] = layers[i]; - } - - state.showMainLayer = main; - - return true; +Window* lovrHeadsetGetWindow(void) { + return state.window; } bool lovrHeadsetGetTexture(Texture** texture) { @@ -2891,47 +2981,12 @@ bool lovrHeadsetGetTexture(Texture** texture) { *texture = state.simulator.texture; return true; - } - - if (!SESSION_RUNNING(state.sessionState)) { - *texture = NULL; - return true; - } - - if (!state.began) { - XrFrameBeginInfo beginfo = { .type = XR_TYPE_FRAME_BEGIN_INFO }; - XR(xrBeginFrame(state.session, &beginfo), "xrBeginFrame"); - state.began = true; - } - - if (!state.frameState.shouldRender) { - *texture = NULL; - return true; - } - - *texture = lovrSwapchainAcquire(&state.swapchains[SWAPCHAIN_COLOR]); - return *texture != NULL; -} - -bool lovrHeadsetGetDepthTexture(Texture** texture) { - if (!SESSION_RUNNING(state.sessionState) || !state.extensions.depth) { - *texture = NULL; - return true; - } - - if (!state.began) { - XrFrameBeginInfo beginfo = { .type = XR_TYPE_FRAME_BEGIN_INFO }; - XR(xrBeginFrame(state.session, &beginfo), "xrBeginFrame"); - state.began = true; - } - - if (!state.frameState.shouldRender) { + } else if (state.window) { + return lovrWindowGetTexture(state.window, texture); + } else { *texture = NULL; return true; } - - *texture = lovrSwapchainAcquire(&state.swapchains[SWAPCHAIN_DEPTH]); - return *texture != NULL; } bool lovrHeadsetGetPass(Pass** pass) { @@ -2986,197 +3041,141 @@ bool lovrHeadsetGetPass(Pass** pass) { *pass = state.simulator.pass; return true; + } else if (state.window) { + return lovrWindowGetPass(state.window, pass); + } else { + *pass = NULL; + return true; } +} - if (state.began) { - *pass = state.frameState.shouldRender ? state.pass : NULL; +bool lovrHeadsetSubmit(void) { + if (!SESSION_RUNNING(state.sessionState)) { + state.waited = false; return true; } - Canvas canvas = { - .depthFormat = state.depthFormat, - .samples = state.config.antialias ? 4 : 1 + if (!state.began) { + XrFrameBeginInfo beginfo = { .type = XR_TYPE_FRAME_BEGIN_INFO }; + XR(xrBeginFrame(state.session, &beginfo), "xrBeginFrame"); + state.began = true; + } + + XrCompositionLayerDepthTestFB depthTestInfo = { + .type = XR_TYPE_COMPOSITION_LAYER_DEPTH_TEST_FB, + .depthMask = XR_TRUE, + .compareOp = XR_COMPARE_OP_LESS_OR_EQUAL_FB }; - if (!lovrHeadsetGetTexture(&canvas.color[0].texture) || !lovrHeadsetGetDepthTexture(&canvas.depth.texture)) { - return false; - } + XrFrameEndInfo info = { .type = XR_TYPE_FRAME_END_INFO }; + XrSpatialContainerLayerFrameEndInfoEXT spatialContainerInfo = { .type = XR_TYPE_SPATIAL_CONTAINER_LAYER_FRAME_END_INFO_EXT }; - if (!canvas.color[0].texture) { - *pass = NULL; - return true; - } + uint32_t windowCount = 0; - canvas.foveation = state.foveationLevel ? state.swapchains[SWAPCHAIN_COLOR].foveationTextures[state.swapchains[SWAPCHAIN_COLOR].textureIndex] : NULL; + lovrBiterate(state.visibleWindowMask, index) { + Window* window = &state.windows[index]; + const XrCompositionLayerBaseHeader** layers = window->layerHeaders; + uint32_t layerCount = 0; - if (!lovrPassSetCanvas(state.pass, &canvas)) { - return false; - } + if (window->shouldRender) { + lovrSwapchainRelease(&window->swapchains[SWAPCHAIN_COLOR]); + lovrSwapchainRelease(&window->swapchains[SWAPCHAIN_DEPTH]); + lovrSwapchainRelease(&window->swapchains[SWAPCHAIN_BACKGROUND]); - float background[4][4]; - LoadAction loads[4] = { LOAD_CLEAR }; - lovrGraphicsGetBackgroundColor(background[0]); - lovrPassSetClear(state.pass, loads, background, LOAD_CLEAR, 0.f); + // Passthrough layer + if (state.passthroughActive) { + layers[layerCount++] = (const XrCompositionLayerBaseHeader*) &state.passthroughLayer; + } - if (state.extensions.dynamicResolution) { - lovrPassSetViewport(state.pass, (float[6]) { 0.f, 0.f, (float) state.width, (float) state.height, 0.f, 1.f }); - lovrPassSetScissor(state.pass, (uint32_t[4]) { 0, 0, state.width, state.height }); - } - - uint32_t count; - XrView views[4]; - XrViewStateFlags flags = locateViews(views, &count); - - for (uint32_t i = 0; i < count; i++) { - state.layerViews[i].pose = views[i].pose; - state.layerViews[i].fov = views[i].fov; - - float viewMatrix[16]; - float projection[16]; - - if (flags & XR_VIEW_STATE_ORIENTATION_VALID_BIT) { - mat4_fromQuat(viewMatrix, &views[i].pose.orientation.x); - } else { - mat4_identity(viewMatrix); - } - - if (flags & XR_VIEW_STATE_POSITION_VALID_BIT) { - memcpy(viewMatrix + 12, &views[i].pose.position.x, 3 * sizeof(float)); - } - - mat4_invert(viewMatrix); - lovrPassSetViewMatrix(state.pass, i, viewMatrix); - - if (flags != 0) { - XrFovf* fov = &views[i].fov; - mat4_fov(projection, -fov->angleLeft, fov->angleRight, fov->angleUp, -fov->angleDown, state.clipNear, state.clipFar); - lovrPassSetProjection(state.pass, i, projection); - } - } - - if (state.extensions.visibilityMask && state.mask) { - Shader* shader = lovrGraphicsGetDefaultShader(SHADER_MASK); - lovrPassPush(state.pass, STACK_STATE); - lovrPassSetShader(state.pass, shader); - lovrPassSetColor(state.pass, (float[4]) { 0.f, 0.f, 0.f, 1.f }); - if (!shader || !lovrPassDrawMesh(state.pass, state.mask, NULL, 1)) { - lovrLog(LOG_WARN, "XR", "Failed to draw headset mask: %s", lovrGetError()); - lovrRelease(state.mask, lovrMeshDestroy); - state.mask = NULL; - } - lovrPassPop(state.pass, STACK_STATE); - } - - *pass = state.pass; - return true; -} - -bool lovrHeadsetSubmit(void) { - if (!SESSION_RUNNING(state.sessionState)) { - state.waited = false; - return true; - } - - if (!state.began) { - XrFrameBeginInfo beginfo = { .type = XR_TYPE_FRAME_BEGIN_INFO }; - XR(xrBeginFrame(state.session, &beginfo), "xrBeginFrame"); - state.began = true; - } + // Background layer + if (window->swapchains[SWAPCHAIN_BACKGROUND].handle) { + layers[layerCount++] = (const XrCompositionLayerBaseHeader*) &window->background.header; + window->background.header.space = state.referenceSpace; + } - XrCompositionLayerBaseHeader const* layers[MAX_LAYERS + 3]; + // Main layer + if (window->showMainLayer && window->swapchains[SWAPCHAIN_COLOR].handle) { + window->layer.next = NULL; - union { - XrCompositionLayerQuad quad; - XrCompositionLayerCylinderKHR cylinder; - } stereoLayers[MAX_LAYERS]; + if (state.extensions.layerDepthTest && state.extensions.depth && window->layerCount > 0) { + depthTestInfo.next = window->layer.next; + window->layer.next = &depthTestInfo; + } - XrCompositionLayerDepthTestFB depthTestInfo = { - .type = XR_TYPE_COMPOSITION_LAYER_DEPTH_TEST_FB, - .depthMask = XR_TRUE, - .compareOp = XR_COMPARE_OP_LESS_OR_EQUAL_FB - }; + if (state.extensions.depth) { + for (uint32_t i = 0; i < state.viewCount; i++) { + if (state.clipFar == 0.f) { + window->depthInfo[i].nearZ = +INFINITY; + window->depthInfo[i].farZ = state.clipNear; + } else { + window->depthInfo[i].nearZ = state.clipNear; + window->depthInfo[i].farZ = state.clipFar; + } + } + } - XrFrameEndInfo info = { - .type = XR_TYPE_FRAME_END_INFO, - .displayTime = state.frameState.predictedDisplayTime, - .environmentBlendMode = state.blendMode, - .layers = layers - }; + if (state.extensions.overlay || state.passthroughActive || state.blendMode != XR_ENVIRONMENT_BLEND_MODE_OPAQUE || window->layerCount > 0) { + window->layer.layerFlags = XR_COMPOSITION_LAYER_BLEND_TEXTURE_SOURCE_ALPHA_BIT; + } else { + window->layer.layerFlags = 0; + } - if (state.frameState.shouldRender) { - lovrSwapchainRelease(&state.swapchains[SWAPCHAIN_COLOR]); - lovrSwapchainRelease(&state.swapchains[SWAPCHAIN_DEPTH]); + window->layer.space = state.referenceSpace; + window->layer.viewCount = state.viewCount; - // Passthrough layer - if (state.passthroughActive) { - layers[info.layerCount++] = (const XrCompositionLayerBaseHeader*) &state.passthroughLayer; - } + layers[layerCount++] = (const XrCompositionLayerBaseHeader*) &window->layer; + } - // Background layer - if (state.swapchains[SWAPCHAIN_BACKGROUND].handle) { - layers[info.layerCount++] = (const XrCompositionLayerBaseHeader*) &state.background.header; - state.background.header.space = state.referenceSpace; - lovrSwapchainRelease(&state.swapchains[SWAPCHAIN_BACKGROUND]); - } + // Quad layers + for (uint32_t i = 0; i < window->layerCount; i++) { + Layer* layer = window->layers[i]; - // Main layer - if (state.showMainLayer) { - state.layer.next = NULL; + layers[layerCount++] = (const XrCompositionLayerBaseHeader*) &layer->header; + layer->header.space = layer->origin >= MAX_DEVICES ? state.referenceSpace : state.spaces[layer->origin]; + lovrSwapchainRelease(&layer->swapchain); - if (state.extensions.layerDepthTest && state.extensions.depth && state.layerCount > 0) { - depthTestInfo.next = state.layer.next; - state.layer.next = &depthTestInfo; - } + // Stereo layers require 2 composition layers (gr?). We make a temporary copy of the layer's + // data and change it to show up in the right eye with the second texture array layer. + if (layer->info.stereo) { + layers[layerCount++] = (const XrCompositionLayerBaseHeader*) &window->stereoLayers[i]; - if (state.extensions.depth) { - for (uint32_t i = 0; i < state.viewCount; i++) { - if (state.clipFar == 0.f) { - state.depthInfo[i].nearZ = +INFINITY; - state.depthInfo[i].farZ = state.clipNear; + if (layer->curve == 0.f) { + window->stereoLayers[i].quad = layer->quad; + window->stereoLayers[i].quad.eyeVisibility = XR_EYE_VISIBILITY_RIGHT; + window->stereoLayers[i].quad.subImage.imageArrayIndex = 1; } else { - state.depthInfo[i].nearZ = state.clipNear; - state.depthInfo[i].farZ = state.clipFar; + window->stereoLayers[i].cylinder = layer->cylinder; + window->stereoLayers[i].cylinder.eyeVisibility = XR_EYE_VISIBILITY_RIGHT; + window->stereoLayers[i].cylinder.subImage.imageArrayIndex = 1; } } } - - if (state.extensions.overlay || state.passthroughActive || state.blendMode != XR_ENVIRONMENT_BLEND_MODE_OPAQUE || state.layerCount > 0) { - state.layer.layerFlags = XR_COMPOSITION_LAYER_BLEND_TEXTURE_SOURCE_ALPHA_BIT; - } else { - state.layer.layerFlags = 0; - } - - state.layer.space = state.referenceSpace; - - layers[info.layerCount++] = (const XrCompositionLayerBaseHeader*) &state.layer; } - // Quad layers - for (uint32_t i = 0; i < state.layerCount; i++) { - Layer* layer = state.layers[i]; - - layers[info.layerCount++] = (const XrCompositionLayerBaseHeader*) &layer->header; - layer->header.space = layer->origin >= MAX_DEVICES ? state.referenceSpace : state.spaces[layer->origin]; - lovrSwapchainRelease(&layer->swapchain); - - // Stereo layers require 2 composition layers (gr?). We make a temporary copy of the layer's - // data and change it to show up in the right eye with the second texture array layer. - if (layer->info.stereo) { - layers[info.layerCount++] = (const XrCompositionLayerBaseHeader*) &stereoLayers[i]; - - if (layer->curve == 0.f) { - stereoLayers[i].quad = layer->quad; - stereoLayers[i].quad.eyeVisibility = XR_EYE_VISIBILITY_RIGHT; - stereoLayers[i].quad.subImage.imageArrayIndex = 1; - } else { - stereoLayers[i].cylinder = layer->cylinder; - stereoLayers[i].cylinder.eyeVisibility = XR_EYE_VISIBILITY_RIGHT; - stereoLayers[i].cylinder.subImage.imageArrayIndex = 1; - } - } + if (state.extensions.spatialContainer) { + state.windowLayers[windowCount++] = (XrSpatialContainerLayerEXT) { + .type = XR_TYPE_SPATIAL_CONTAINER_LAYER_EXT, + .spatialContainer = window->handle, + .retainPreviousSubmission = !window->shouldRender, + .layerCount = layerCount, + .layers = window->layerHeaders + }; + } else { + info.layerCount = layerCount; + info.layers = window->layerHeaders; } } + if (state.extensions.spatialContainer) { + spatialContainerInfo.containerLayers = state.windowLayers; + spatialContainerInfo.containerLayerCount = windowCount; + spatialContainerInfo.next = info.next; + info.next = &spatialContainerInfo; + } else if (state.frameState.shouldRender) { + info.displayTime = state.frameState.predictedDisplayTime; + info.environmentBlendMode = state.blendMode; + } + XR(xrEndFrame(state.session, &info), "xrEndFrame"); state.began = false; state.waited = false; @@ -3436,6 +3435,496 @@ Pass* lovrLayerGetPass(Layer* layer) { return layer->pass; } +// Window + +Window* lovrWindowCreate(const WindowInfo* info) { + lovrCheck(state.extensions.spatialContainer ? state.windowMask != ~0u : state.windowMask == 0, "Too many windows"); + lovrCheck(info->size[0] >= 0.f && info->size[1] >= 0.f && info->size[2] >= 0.f, "Window size can not be negative"); + + uint32_t index = lovrTrailingZeros(~state.windowMask); + + Window* window = &state.windows[index]; + window->ref = 1; + window->index = index; + window->hdr = info->hdr; + window->showMainLayer = true; + + state.windowMask |= (1 << index); + state.openWindowMask |= (1 << index); + lovrRetain(window); // Windows are retained until closed + + if (state.extensions.spatialContainer) { + XrSpatialContainerCreateInfoEXT containerInfo = { + .type = XR_TYPE_SPATIAL_CONTAINER_CREATE_INFO_EXT, + .graphicsPresentation = XR_SPATIAL_CONTAINER_GRAPHICS_PRESENTATION_SELF_RENDERING_EXT, + .suggestedBounds = { info->size[0], info->size[1], info->size[2] } + }; + + XR(xrCreateSpatialContainerEXT(state.session, &containerInfo, &window->handle), "xrCreateSpatialContainerEXT"); + + XrSpatialContainerSpaceCreateInfoEXT spaceInfo = { + .type = XR_TYPE_SPATIAL_CONTAINER_SPACE_CREATE_INFO_EXT, + .spatialContainer = window->handle + }; + + XR(xrCreateSpatialContainerSpaceEXT(state.session, &spaceInfo, &window->space), "xrCreateSpatialContainerSpaceEXT"); + + if (info->fullscreen) { + lovrWindowSetFullscreen(window, true); + } + + lovrWindowSetVisible(window, true); + } + + return window; +} + +void lovrWindowDestroy(void* ref) { + Window* window = ref; + + for (uint32_t i = 0; i < window->layerCount; i++) { + lovrRelease(window->layers[i], lovrLayerDestroy); + window->layers[i] = NULL; + } + window->layerCount = 0; + + for (uint32_t i = 0; i < COUNTOF(window->swapchains); i++) { + lovrSwapchainDestroy(&window->swapchains[i]); + } + + lovrRelease(window->pass, lovrPassDestroy); + + if (state.extensions.spatialContainer) { + if (window->handle) xrDestroySpatialContainerEXT(window->handle); + xrDestroySpace(window->space); + } + + state.visibleWindowMask &= ~(1 << window->index); + state.openWindowMask &= ~(1 << window->index); + state.windowMask &= ~(1 << window->index); + memset(window, 0, sizeof(Window)); +} + +bool lovrWindowIsOpen(Window* window) { + if (!state.extensions.spatialContainer) { + return true; + } else { + return window->handle; + } +} + +void lovrWindowClose(Window* window) { + if (window->handle) { + xrDestroySpatialContainerEXT(window->handle); + window->handle = XR_NULL_HANDLE; + state.openWindowMask &= ~(1 << window->index); + lovrRelease(window, lovrWindowDestroy); + } +} + +static bool lovrWindowGetState(Window* window, XrSpatialContainerStateEXT* windowState) { + if (!lovrWindowIsOpen(window)) return false; + *windowState = (XrSpatialContainerStateEXT) { .type = XR_TYPE_SPATIAL_CONTAINER_STATE_EXT }; + XrSpatialContainerStateGetInfoEXT info = { .type = XR_TYPE_SPATIAL_CONTAINER_STATE_GET_INFO_EXT }; + XrResult result = xrGetSpatialContainerStateEXT(window->handle, &info, windowState); + return XR_SUCCEEDED(result); +} + +bool lovrWindowIsVisible(Window* window) { + if (!state.extensions.spatialContainer) { + return state.sessionState >= XR_SESSION_STATE_VISIBLE; + } else { + XrSpatialContainerStateEXT windowState; + if (!lovrWindowGetState(window, &windowState)) return false; + return windowState.visible; + } +} + +bool lovrWindowSetVisible(Window* window, bool visible) { + if (!state.extensions.spatialContainer) return true; + + lovrCheck(lovrWindowIsOpen(window), "Window is not open"); + + XrSpatialContainerVisibleRequestInfoEXT info = { + .type = XR_TYPE_SPATIAL_CONTAINER_VISIBLE_REQUEST_INFO_EXT, + .visible = visible + }; + + XR(xrRequestSpatialContainerVisibleEXT(window->handle, &info), "xrRequestSpatialContainerVisibleEXT"); + return true; +} + +bool lovrWindowIsFocused(Window* window) { + if (!state.extensions.spatialContainer) { + return state.sessionState == XR_SESSION_STATE_FOCUSED; + } else { + XrSpatialContainerStateEXT windowState; + if (!lovrWindowGetState(window, &windowState)) return false; + return windowState.interactable; + } +} + +bool lovrWindowIsFullscreen(Window* window) { + if (!state.extensions.spatialContainer) { + return true; + } else { + XrSpatialContainerStateEXT windowState; + if (!lovrWindowGetState(window, &windowState)) return false; + return windowState.boundsMode == XR_SPATIAL_CONTAINER_BOUNDS_MODE_IMMERSIVE_EXT; + } +} + +bool lovrWindowSetFullscreen(Window* window, bool fullscreen) { + if (!state.extensions.spatialContainer) { + return fullscreen == true; + } else { + lovrCheck(lovrWindowIsOpen(window), "Window is not open"); + + XrSpatialContainerBoundsModeRequestInfoEXT info = { + .type = XR_TYPE_SPATIAL_CONTAINER_BOUNDS_MODE_REQUEST_INFO_EXT, + .boundsMode = fullscreen ? XR_SPATIAL_CONTAINER_BOUNDS_MODE_IMMERSIVE_EXT : XR_SPATIAL_CONTAINER_BOUNDS_MODE_BOUNDED_EXT + }; + + XR(xrRequestSpatialContainerBoundsModeEXT(window->handle, &info), "xrRequestSpatialContainerBoundsModeEXT"); + return true; + } +} + +bool lovrWindowGetBounds(Window* window, float* size) { + if (!state.extensions.spatialContainer) { + vec3_set(size, 0.f, 0.f, 0.f); + } else { + lovrCheck(lovrWindowIsOpen(window), "Window is not open"); + XrSpatialContainerBoundsGetInfoEXT info = { .type = XR_TYPE_SPATIAL_CONTAINER_BOUNDS_GET_INFO_EXT }; + XrSpatialContainerBoundsEXT bounds = { .type = XR_TYPE_SPATIAL_CONTAINER_BOUNDS_EXT }; + XR(xrGetSpatialContainerBoundsEXT(window->handle, &info, &bounds), "xrGetSpatialContainerBoundsEXT"); + memcpy(size, &bounds.bounds, 3 * sizeof(float)); + } + return true; +} + +bool lovrWindowGetPose(Window* window, float* position, float* orientation) { + if (!lovrWindowIsOpen(window) || state.frameState.predictedDisplayTime <= 0 || !state.extensions.spatialContainer) { + return false; + } + + XrSpaceLocation location = { .type = XR_TYPE_SPACE_LOCATION }; + xrLocateSpace(window->space, state.referenceSpace, state.frameState.predictedDisplayTime, &location); + memcpy(orientation, &location.pose.orientation, 4 * sizeof(float)); + memcpy(position, &location.pose.position, 3 * sizeof(float)); + return location.locationFlags & (XR_SPACE_LOCATION_POSITION_VALID_BIT | XR_SPACE_LOCATION_ORIENTATION_VALID_BIT); +} + +bool lovrWindowGetViewPose(Window* window, uint32_t view, float* position, float* orientation) { + if (view >= state.viewCount || !window->viewState) { + return false; + } + + if (window->viewState & XR_VIEW_STATE_POSITION_VALID_BIT) { + memcpy(position, &window->views[view].pose.position.x, 3 * sizeof(float)); + } else { + memset(position, 0, 3 * sizeof(float)); + } + + if (window->viewState & XR_VIEW_STATE_ORIENTATION_VALID_BIT) { + memcpy(orientation, &window->views[view].pose.orientation.x, 4 * sizeof(float)); + } else { + quat_identity(orientation); + } + + return true; +} + +bool lovrWindowGetViewAngles(Window* window, uint32_t view, float* left, float* right, float* up, float* down) { + if (view >= state.viewCount || !window->viewState) { + return false; + } + + *left = -window->views[view].fov.angleLeft; + *right = window->views[view].fov.angleRight; + *up = window->views[view].fov.angleUp; + *down = -window->views[view].fov.angleDown; + return true; +} + +bool lovrWindowGetTexture(Window* window, Texture** texture) { + if (!SESSION_RUNNING(state.sessionState)) { + *texture = NULL; + return true; + } + + if (!window->shouldRender) { + *texture = NULL; + return true; + } + + if (!window->swapchains[SWAPCHAIN_COLOR].handle) { + uint32_t width = state.width; + uint32_t height = state.height; + + // If dynamic resolution is active, ask it for the recommended maximum swapchain size. + // It's usually bigger than the recommended size in the view configuration. + if (state.extensions.dynamicResolution) { + XrRecommendedLayerResolutionGetInfoMETA info = { + .type = XR_TYPE_RECOMMENDED_LAYER_RESOLUTION_GET_INFO_META, + .layer = (XrCompositionLayerBaseHeader*) &(XrCompositionLayerProjection) { + .type = XR_TYPE_COMPOSITION_LAYER_PROJECTION, + .viewCount = state.viewCount, + .views = (XrCompositionLayerProjectionView[4]) { + [0].type = XR_TYPE_COMPOSITION_LAYER_PROJECTION_VIEW, + [1].type = XR_TYPE_COMPOSITION_LAYER_PROJECTION_VIEW, + [2].type = XR_TYPE_COMPOSITION_LAYER_PROJECTION_VIEW, + [3].type = XR_TYPE_COMPOSITION_LAYER_PROJECTION_VIEW + } + }, + .predictedDisplayTime = state.frameState.predictedDisplayTime + }; + + XrRecommendedLayerResolutionMETA resolution = { .type = XR_TYPE_RECOMMENDED_LAYER_RESOLUTION_META }; + + if (XR_SUCCEEDED(xrGetRecommendedLayerResolutionMETA(state.session, &info, &resolution)) && resolution.isValid) { + width = MAX(width, resolution.recommendedImageDimensions.width); + height = MAX(height, resolution.recommendedImageDimensions.height); + } else { + state.extensions.dynamicResolution = false; + } + } + + uint32_t flags = VIEW | FOVEATED | (window->hdr ? HDR : 0); + if (!lovrSwapchainInit(&window->swapchains[SWAPCHAIN_COLOR], width, height, flags)) { + return false; + } + + if (state.extensions.depth) { + if (!lovrSwapchainInit(&window->swapchains[SWAPCHAIN_DEPTH], width, height, VIEW | DEPTH)) { + return false; + } + } + + // Pre-init composition layer + window->layer = (XrCompositionLayerProjection) { + .type = XR_TYPE_COMPOSITION_LAYER_PROJECTION, + .viewCount = state.viewCount, + .views = window->layerViews + }; + + // Pre-init composition layer views + for (uint32_t i = 0; i < state.viewCount; i++) { + window->layerViews[i] = (XrCompositionLayerProjectionView) { + .type = XR_TYPE_COMPOSITION_LAYER_PROJECTION_VIEW, + .subImage = { window->swapchains[SWAPCHAIN_COLOR].handle, { { 0, 0 }, { width, height } }, i } + }; + } + + if (state.extensions.depth) { + for (uint32_t i = 0; i < state.viewCount; i++) { + window->layerViews[i].next = &window->depthInfo[i]; + window->depthInfo[i] = (XrCompositionLayerDepthInfoKHR) { + .type = XR_TYPE_COMPOSITION_LAYER_DEPTH_INFO_KHR, + .subImage.swapchain = window->swapchains[SWAPCHAIN_DEPTH].handle, + .subImage.imageRect = window->layerViews[i].subImage.imageRect, + .subImage.imageArrayIndex = i, + .minDepth = 0.f, + .maxDepth = 1.f + }; + } + } + + window->pass = lovrPassCreate("Window"); + + if (!window->pass) { + return false; + } + } + + if (!state.began) { + XrFrameBeginInfo beginfo = { .type = XR_TYPE_FRAME_BEGIN_INFO }; + XR(xrBeginFrame(state.session, &beginfo), "xrBeginFrame"); + state.began = true; + } + + *texture = lovrSwapchainAcquire(&window->swapchains[SWAPCHAIN_COLOR]); + return *texture != NULL; +} + +static bool lovrWindowGetDepthTexture(Window* window, Texture** texture) { + if (!SESSION_RUNNING(state.sessionState) || !state.extensions.depth) { + *texture = NULL; + return true; + } + + if (!state.began) { + XrFrameBeginInfo beginfo = { .type = XR_TYPE_FRAME_BEGIN_INFO }; + XR(xrBeginFrame(state.session, &beginfo), "xrBeginFrame"); + state.began = true; + } + + if (!window->shouldRender) { + *texture = NULL; + return true; + } + + *texture = lovrSwapchainAcquire(&window->swapchains[SWAPCHAIN_DEPTH]); + return *texture != NULL; +} + +bool lovrWindowGetPass(Window* window, Pass** pass) { + Canvas canvas = { + .depthFormat = state.depthFormat, + .samples = state.config.antialias ? 4 : 1 + }; + + if (!lovrWindowGetTexture(window, &canvas.color[0].texture) || !lovrWindowGetDepthTexture(window, &canvas.depth.texture)) { + return false; + } + + if (!canvas.color[0].texture) { + *pass = NULL; + return true; + } + + canvas.foveation = state.foveationLevel ? window->swapchains[SWAPCHAIN_COLOR].foveationTextures[window->swapchains[SWAPCHAIN_COLOR].textureIndex] : NULL; + + if (!lovrPassSetCanvas(window->pass, &canvas)) { + return false; + } + + float background[4][4]; + LoadAction loads[4] = { LOAD_CLEAR }; + lovrGraphicsGetBackgroundColor(background[0]); + lovrPassSetClear(window->pass, loads, background, LOAD_CLEAR, 0.f); + + if (state.extensions.dynamicResolution) { + lovrPassSetViewport(window->pass, (float[6]) { 0.f, 0.f, (float) state.width, (float) state.height, 0.f, 1.f }); + lovrPassSetScissor(window->pass, (uint32_t[4]) { 0, 0, state.width, state.height }); + } + + for (uint32_t i = 0; i < state.viewCount; i++) { + window->layerViews[i].pose = window->views[i].pose; + window->layerViews[i].fov = window->views[i].fov; + + float viewMatrix[16]; + float projection[16]; + + if (window->viewState & XR_VIEW_STATE_ORIENTATION_VALID_BIT) { + mat4_fromQuat(viewMatrix, &window->views[i].pose.orientation.x); + } else { + mat4_identity(viewMatrix); + } + + if (window->viewState & XR_VIEW_STATE_POSITION_VALID_BIT) { + memcpy(viewMatrix + 12, &window->views[i].pose.position.x, 3 * sizeof(float)); + } + + mat4_invert(viewMatrix); + lovrPassSetViewMatrix(window->pass, i, viewMatrix); + + if (window->viewState != 0) { + XrFovf* fov = &window->views[i].fov; + mat4_fov(projection, -fov->angleLeft, fov->angleRight, fov->angleUp, -fov->angleDown, state.clipNear, state.clipFar); + lovrPassSetProjection(window->pass, i, projection); + } + } + + if (state.extensions.visibilityMask && state.mask) { + Shader* shader = lovrGraphicsGetDefaultShader(SHADER_MASK); + lovrPassPush(window->pass, STACK_STATE); + lovrPassSetShader(window->pass, shader); + lovrPassSetColor(window->pass, (float[4]) { 0.f, 0.f, 0.f, 1.f }); + if (!shader || !lovrPassDrawMesh(window->pass, state.mask, NULL, 1)) { + lovrLog(LOG_WARN, "XR", "Failed to draw headset mask: %s", lovrGetError()); + lovrRelease(state.mask, lovrMeshDestroy); + state.mask = NULL; + } + lovrPassPop(window->pass, STACK_STATE); + } + + *pass = window->pass; + return true; +} + +Texture* lovrWindowSetBackground(Window* window, uint32_t width, uint32_t height, uint32_t layers) { + Swapchain* swapchain = &window->swapchains[SWAPCHAIN_BACKGROUND]; + + if (width == 0 && height == 0) { + lovrSwapchainDestroy(swapchain); + memset(swapchain, 0, sizeof(Swapchain)); + return NULL; + } + + lovrCheck(state.extensions.layerCube || layers != 6, "This headset does not support cubemap backgrounds"); + lovrCheck(state.extensions.layerEquirect || state.extensions.layerEquirect2 || layers != 1, "This headset does not support equirectangular backgrounds"); + + if (!lovrSwapchainInit(swapchain, width, height, STATIC | (layers == 6 ? CUBE : 0))) { + return NULL; + } + + if (!lovrSwapchainAcquire(swapchain)) { + lovrSwapchainDestroy(swapchain); + memset(swapchain, 0, sizeof(Swapchain)); + return NULL; + } + + if (layers == 6) { + window->background.cube = (XrCompositionLayerCubeKHR) { + .type = XR_TYPE_COMPOSITION_LAYER_CUBE_KHR, + .eyeVisibility = XR_EYE_VISIBILITY_BOTH, + .swapchain = swapchain->handle, + .orientation.w = 1.f + }; + } else if (state.extensions.layerEquirect2) { + window->background.equirect2 = (XrCompositionLayerEquirect2KHR) { + .type = XR_TYPE_COMPOSITION_LAYER_EQUIRECT2_KHR, + .eyeVisibility = XR_EYE_VISIBILITY_BOTH, + .subImage = { swapchain->handle, { { 0, 0 }, { width, height } }, 0 }, + .pose.orientation.w = 1.f, + .centralHorizontalAngle = 2.f * (float) M_PI, + .upperVerticalAngle = (float) M_PI * .5f, + .lowerVerticalAngle = (float) -M_PI * .5f + }; + } else { + window->background.equirect = (XrCompositionLayerEquirectKHR) { + .type = XR_TYPE_COMPOSITION_LAYER_EQUIRECT_KHR, + .eyeVisibility = XR_EYE_VISIBILITY_BOTH, + .subImage = { swapchain->handle, { { 0, 0 }, { width, height } }, 0 }, + .pose.orientation.w = 1.f, + .scale = { 1.f, 1.f } + }; + } + + return window->swapchains[SWAPCHAIN_BACKGROUND].textures[0]; +} + +Layer** lovrWindowGetLayers(Window* window, uint32_t* count, bool* main) { + *count = window->layerCount; + *main = window->showMainLayer; + return window->layers; +} + +bool lovrWindowSetLayers(Window* window, Layer** layers, uint32_t count, bool main) { + uint32_t total = 0; + + for (uint32_t i = 0; i < count; i++) { + total += layers[i]->info.stereo ? 2 : 1; + } + + lovrCheck(total <= MAX_LAYERS, "Too many layers"); + + for (uint32_t i = 0; i < window->layerCount; i++) { + lovrRelease(window->layers[i], lovrLayerDestroy); + } + + window->layerCount = count; + for (uint32_t i = 0; i < count; i++) { + lovrRetain(layers[i]); + window->layers[i] = layers[i]; + } + + window->showMainLayer = main; + + return true; +} + // Private void lovrHeadsetGetVulkanPhysicalDevice(void* instance, uintptr_t physicalDevice) { @@ -3507,7 +3996,7 @@ static bool lovrSwapchainInit(Swapchain* swapchain, uint32_t width, uint32_t hei bool cube = flags & CUBE; bool immutable = flags & STATIC; bool foveated = flags & FOVEATED && state.extensions.foveation; - bool hdr = flags & HDR && state.hdr; + bool hdr = flags & HDR && state.extensions.hdr; XrSwapchainCreateInfo info = { .type = XR_TYPE_SWAPCHAIN_CREATE_INFO, @@ -3756,108 +4245,6 @@ static bool updateViewSize(void) { return true; } -static XrViewStateFlags locateViews(XrView views[4], uint32_t* count) { - if (state.frameState.predictedDisplayTime <= 0) { - return 0; - } - - XrViewLocateInfo viewLocateInfo = { - .type = XR_TYPE_VIEW_LOCATE_INFO, - .viewConfigurationType = state.viewConfiguration, - .displayTime = state.frameState.predictedDisplayTime, - .space = state.referenceSpace - }; - - for (uint32_t i = 0; i < 4; i++) { - views[i].type = XR_TYPE_VIEW; - views[i].next = NULL; - } - - XrViewState viewState = { .type = XR_TYPE_VIEW_STATE }; - if (XR_FAILED(xrLocateViews(state.session, &viewLocateInfo, &viewState, state.viewCount, count, views))) { - return 0; - } - - return viewState.viewStateFlags; -} - -static bool createSwapchains(void) { - uint32_t width = state.width; - uint32_t height = state.height; - - // If dynamic resolution is active, ask it for the recommended maximum swapchain size. - // It's usually bigger than the recommended size in the view configuration. - if (state.extensions.dynamicResolution) { - XrRecommendedLayerResolutionGetInfoMETA info = { - .type = XR_TYPE_RECOMMENDED_LAYER_RESOLUTION_GET_INFO_META, - .layer = (XrCompositionLayerBaseHeader*) &(XrCompositionLayerProjection) { - .type = XR_TYPE_COMPOSITION_LAYER_PROJECTION, - .viewCount = state.viewCount, - .views = (XrCompositionLayerProjectionView[4]) { - [0].type = XR_TYPE_COMPOSITION_LAYER_PROJECTION_VIEW, - [1].type = XR_TYPE_COMPOSITION_LAYER_PROJECTION_VIEW, - [2].type = XR_TYPE_COMPOSITION_LAYER_PROJECTION_VIEW, - [3].type = XR_TYPE_COMPOSITION_LAYER_PROJECTION_VIEW - } - }, - .predictedDisplayTime = state.frameState.predictedDisplayTime - }; - - XrRecommendedLayerResolutionMETA resolution = { .type = XR_TYPE_RECOMMENDED_LAYER_RESOLUTION_META }; - - if (XR_SUCCEEDED(xrGetRecommendedLayerResolutionMETA(state.session, &info, &resolution)) && resolution.isValid) { - width = MAX(width, resolution.recommendedImageDimensions.width); - height = MAX(height, resolution.recommendedImageDimensions.height); - } else { - state.extensions.dynamicResolution = false; - } - } - - lovrSwapchainDestroy(&state.swapchains[SWAPCHAIN_COLOR]); - lovrSwapchainDestroy(&state.swapchains[SWAPCHAIN_DEPTH]); - - if (!lovrSwapchainInit(&state.swapchains[SWAPCHAIN_COLOR], width, height, VIEW | FOVEATED | HDR)) { - return false; - } - - if (state.extensions.depth && !lovrSwapchainInit(&state.swapchains[SWAPCHAIN_DEPTH], width, height, VIEW | DEPTH)) { - return false; - } - - // Pre-initialize layer structs - - state.layer = (XrCompositionLayerProjection) { - .type = XR_TYPE_COMPOSITION_LAYER_PROJECTION, - .viewCount = state.viewCount, - .views = state.layerViews - }; - - for (uint32_t i = 0; i < state.viewCount; i++) { - state.layerViews[i] = (XrCompositionLayerProjectionView) { - .type = XR_TYPE_COMPOSITION_LAYER_PROJECTION_VIEW, - .subImage.swapchain = state.swapchains[SWAPCHAIN_COLOR].handle, - .subImage.imageRect = { { 0, 0 }, { width, height } }, - .subImage.imageArrayIndex = i - }; - } - - if (state.extensions.depth) { - for (uint32_t i = 0; i < state.viewCount; i++) { - state.layerViews[i].next = &state.depthInfo[i]; - state.depthInfo[i] = (XrCompositionLayerDepthInfoKHR) { - .type = XR_TYPE_COMPOSITION_LAYER_DEPTH_INFO_KHR, - .subImage.swapchain = state.swapchains[SWAPCHAIN_DEPTH].handle, - .subImage.imageRect = { { 0, 0 }, { width, height } }, - .subImage.imageArrayIndex = i, - .minDepth = 0.f, - .maxDepth = 1.f - }; - } - } - - return true; -} - static bool createReferenceSpace(XrTime time) { if (time <= 0) { return false; @@ -4171,6 +4558,15 @@ static bool loadVisibilityMask(void) { return true; } +Window* findWindow(XrSpatialContainerEXT handle) { + lovrBiterate(state.openWindowMask, index) { + if (state.windows[index].handle == handle) { + return &state.windows[index]; + } + } + return NULL; +} + #ifdef _WIN32 #define EXPORT __declspec(dllexport) #else diff --git a/src/modules/headset/headset.h b/src/modules/headset/headset.h index e97a7c876..9a232786b 100644 --- a/src/modules/headset/headset.h +++ b/src/modules/headset/headset.h @@ -14,6 +14,7 @@ struct Texture; struct Pass; typedef struct Layer Layer; +typedef struct Window Window; typedef enum { SKELETON_NONE, @@ -32,6 +33,7 @@ typedef struct { bool hdr; bool submitDepth; bool overlay; + bool window; uint32_t overlayOrder; ControllerSkeletonMode controllerSkeleton; uint32_t extensionCount; @@ -60,6 +62,7 @@ typedef struct { bool proximity; bool refreshRate; bool viveTrackers; + bool window; } HeadsetFeatures; typedef enum { @@ -209,9 +212,7 @@ uint64_t* lovrHeadsetGetModelKeys(uint32_t* count); struct ModelData* lovrHeadsetNewModelData(uint64_t key); bool lovrHeadsetGetModelPose(struct Model* model, float* position, float* orientation); bool lovrHeadsetAnimate(struct Model* model); -struct Texture* lovrHeadsetSetBackground(uint32_t width, uint32_t height, uint32_t layers); -Layer** lovrHeadsetGetLayers(uint32_t* count, bool* main); -bool lovrHeadsetSetLayers(Layer** layers, uint32_t count, bool main); +Window* lovrHeadsetGetWindow(void); bool lovrHeadsetGetTexture(struct Texture** texture); bool lovrHeadsetGetPass(struct Pass** pass); bool lovrHeadsetSubmit(void); @@ -246,6 +247,33 @@ void lovrLayerSetViewport(Layer* layer, int32_t* viewport); struct Texture* lovrLayerGetTexture(Layer* layer); struct Pass* lovrLayerGetPass(Layer* layer); +// Window + +typedef struct { + float size[3]; + bool fullscreen; + bool hdr; +} WindowInfo; + +Window* lovrWindowCreate(const WindowInfo* info); +void lovrWindowDestroy(void* ref); +bool lovrWindowIsOpen(Window* window); +void lovrWindowClose(Window* window); +bool lovrWindowIsVisible(Window* window); +bool lovrWindowSetVisible(Window* window, bool visible); +bool lovrWindowIsFocused(Window* window); +bool lovrWindowIsFullscreen(Window* window); +bool lovrWindowSetFullscreen(Window* window, bool fullscreen); +bool lovrWindowGetBounds(Window* window, float* bounds); +bool lovrWindowGetPose(Window* window, float* position, float* orientation); +bool lovrWindowGetViewPose(Window* window, uint32_t view, float* position, float* orientation); +bool lovrWindowGetViewAngles(Window* window, uint32_t view, float* left, float* right, float* up, float* down); +bool lovrWindowGetTexture(Window* window, struct Texture** texture); +bool lovrWindowGetPass(Window* window, struct Pass** pass); +struct Texture* lovrWindowSetBackground(Window* window, uint32_t width, uint32_t height, uint32_t layers); +Layer** lovrWindowGetLayers(Window* window, uint32_t* count, bool* main); +bool lovrWindowSetLayers(Window* window, Layer** layers, uint32_t count, bool main); + // Private void lovrHeadsetGetVulkanPhysicalDevice(void* instance, uintptr_t physicalDevice); diff --git a/src/modules/headset/headset.js b/src/modules/headset/headset.js index e989151f5..f2b0332f0 100644 --- a/src/modules/headset/headset.js +++ b/src/modules/headset/headset.js @@ -252,20 +252,10 @@ var headset = { return false; }, - lovrHeadsetSetBackground(width, height, layers) { + lovrHeadsetGetWindow() { return 0; }, - lovrHeadsetGetLayers(count, main) { - HEAPU32[count >> 2] = 0; - HEAPU32[main >> 2] = 1; - return 0; - }, - - lovrHeadsetSetLayers(layers, count, main) { - return false; - }, - lovrHeadsetGetTexture__deps: ['$stackSave', '$stackAlloc', '$stackRestore'], lovrHeadsetGetTexture(texture) { if (!state.texture) { @@ -415,6 +405,8 @@ var headset = { state.poses[device].buttons |= (down << button); }, + // Layer + lovrLayerCreate(info) { return 0; }, @@ -479,6 +471,87 @@ var headset = { return 0; }, + // Window + + lovrWindowCreate(info) { + return 0; + }, + + lovrWindowDestroy(ref) { + return; + }, + + lovrWindowIsOpen(w) { + return false; + }, + + lovrWindowClose(w) { + // + }, + + lovrWindowIsVisible(w) { + return false; + }, + + lovrWindowSetVisible(w, visible) { + return false; + }, + + lovrWindowIsFocused(w) { + return false; + }, + + lovrWindowSetFocused(w, focused) { + return false; + }, + + lovrWindowIsFullscreen(w) { + return false; + }, + + lovrWindowSetFullscreen(w, fullscreen) { + return false; + }, + + lovrWindowGetBounds(w, bounds) { + return false; + }, + + lovrWindowGetPose(w, position, orientation) { + return false; + }, + + lovrWindowGetViewPose(w, view, position, orientation) { + return false; + }, + + lovrWindowGetViewAngles(w, view, left, right, up, down) { + return false; + }, + + lovrWindowGetTexture(w, texture) { + return false; + }, + + lovrWindowGetPass(w, pass) { + return false; + }, + + lovrWindowSetBackground(w, width, height, layers) { + return 0; + }, + + lovrWindowGetLayers(w, count, main) { + HEAPU32[count >> 2] = 0; + HEAPU32[main >> 2] = 1; + return 0; + }, + + lovrWindowSetLayers(w, layers, count, main) { + return false; + }, + + lovrHeadsetGetDisplayTime() { return 0; }, diff --git a/src/util.h b/src/util.h index 4b5a5d7be..cd7c054f7 100644 --- a/src/util.h +++ b/src/util.h @@ -135,6 +135,17 @@ void float16Init(void); float16 float32to16(float32 f); float32 float16to32(float16 f); +// Bits +#if defined(__clang__) || defined(__GNUC__) +#define lovrTrailingZeros(x) ((uint32_t) __builtin_ctz(x)) +#define lovrTrailingZeros64(x) ((uint64_t) __builtin_ctzl(x)) +#elif defined(_MSC_VER) +#include +#define lovrTrailingZeros(x) ((uint32_t) _tzcnt_u32(x)) +#define lovrTrailingZeros64(x) ((uint64_t) _tzcnt_u64(x)) +#endif +#define lovrBiterate(mask, index) for (uint32_t m = mask, index; index = lovrTrailingZeros(m), m; m ^= m & -m) + // Types typedef enum { T_NONE, @@ -159,6 +170,7 @@ typedef enum { T_Readback, T_Pass, T_Layer, + T_Window, T_Curve, T_Mat4, T_RandomGenerator,