Skip to content

Commit 12433d9

Browse files
utkarshdalalUtkarsh Dalal
andauthored
Immersive mode: fix gamma, raise quad resolution, stop duplicate compositing (#1846)
* Skip hidden-surface compositing while the immersive session owns presentation The native render loop parks on its condvar and only renders when a content update dirties it. With the XR bridge attached, the quad samples the AHardwareBuffer directly, so forwarding the frame and returning before nativeUpdateWindowContentAHB leaves the native Vulkan compositor idle — no scene draw and no present to the invisible surface, per game frame. Flat mode is unchanged; the bridge is null there. * Raise the immersive swapchain to 1920x1080 and fix gamma via an sRGB swapchain The 720p intermediate was the quality ceiling: game frames were sampled down into it and back up onto the quad. The prior 1080p attempt measured worse, but that was against a pipeline still paying the per-frame EGLImage recreate and the duplicate hidden-surface compositor, both since removed. Gamma: the swapchain preferred linear GL_RGBA8 while the content is sRGB-encoded, so the compositor double-applied the transfer (washed-out output). Prefer GL_SRGB8_ALPHA8 and linearize sampled content in the shaders, since GLES re-encodes on write to sRGB attachments; a linear-format fallback keeps old behavior when sRGB is not offered. libxrimmersive.so rebuilt (NDK 27.3.13750724) for both XR flavors. * Render the composited scene into a shared AHardwareBuffer for the immersive session * Let the immersive session run at the system default refresh rate * Forward ASurfaceRenderer game buffers to the immersive session * Gate the immersive bridge attach points to XR builds * Drop the ASR immersive forwarding; keep bridge re-attach on renderer change * Harden the XR scene target: resize handling, memory-type guard, write-completion fence, real extents * Follow the container screen size and a per-container refresh rate in immersive mode --------- Co-authored-by: Utkarsh Dalal <utkarsh.dalal@toptal.com>
1 parent 14607a4 commit 12433d9

32 files changed

Lines changed: 425 additions & 56 deletions

File tree

4.75 KB
Binary file not shown.
39.8 KB
Binary file not shown.

‎app/src/main/cpp/winlator/VulkanRendererContext.cpp‎

Lines changed: 189 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ VulkanRendererContext::~VulkanRendererContext() {
3939
if (wt.stg != VK_NULL_HANDLE) { vk_.DestroyBuffer(device, wt.stg, nullptr); vk_.FreeMemory(device, wt.stgMem, nullptr); }
4040
}
4141
deleteQueue.clear();
42+
destroyXrTargetResources();
4243
cleanupSwapchain(); cleanupCursorTex();
4344

4445
vk_.DestroySampler(device, sampler, nullptr);
@@ -746,14 +747,22 @@ void VulkanRendererContext::recordCmdBuf(VkCommandBuffer cb, uint32_t imgIdx,
746747
0, 0, nullptr, 0, nullptr, (uint32_t)postUpload.size(), postUpload.data());
747748

748749

750+
bool toXr = xrTargetActive.load() && xrFb!=VK_NULL_HANDLE;
751+
VkExtent2D tgtExt = toXr ? xrExt : swapchainExt;
749752
VkRenderPassBeginInfo rpi{}; rpi.sType=VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO;
750-
rpi.renderPass=renderPass; rpi.framebuffer=swapchainFBs[imgIdx]; rpi.renderArea={{0,0},swapchainExt};
753+
rpi.renderPass = toXr ? xrRp : renderPass;
754+
rpi.framebuffer = toXr ? xrFb : swapchainFBs[imgIdx];
755+
rpi.renderArea={{0,0},tgtExt};
751756
VkClearValue clr={{{0.f,0.f,0.f,1.f}}}; rpi.clearValueCount=1; rpi.pClearValues=&clr;
752757

753758
vk_.CmdBeginRenderPass(cb, &rpi, VK_SUBPASS_CONTENTS_INLINE);
754-
VkViewport vp{0,0,(float)swapchainExt.width,(float)swapchainExt.height,0,1};
759+
// The immersive quad flips vertically to suit the per-window game buffers, so the scene
760+
// target must match their orientation: render it upside down via a negative viewport.
761+
VkViewport vp = toXr
762+
? VkViewport{0,(float)tgtExt.height,(float)tgtExt.width,-(float)tgtExt.height,0,1}
763+
: VkViewport{0,0,(float)tgtExt.width,(float)tgtExt.height,0,1};
755764
vk_.CmdSetViewport(cb, 0, 1, &vp);
756-
VkRect2D sc{{0,0},swapchainExt}; vk_.CmdSetScissor(cb, 0, 1, &sc);
765+
VkRect2D sc{{0,0},tgtExt}; vk_.CmdSetScissor(cb, 0, 1, &sc);
757766

758767
vk_.CmdBindPipeline(cb, VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline);
759768
for (auto& d : draws) {
@@ -773,8 +782,8 @@ void VulkanRendererContext::recordCmdBuf(VkCommandBuffer cb, uint32_t imgIdx,
773782
pc.brightness = activeBrightness;
774783
pc.contrast = activeContrast;
775784
pc.gamma = activeGamma;
776-
pc.outW = std::max(1.0f, (float)d.w * sx / cw * (float)swapchainExt.width);
777-
pc.outH = std::max(1.0f, (float)d.h * sy / ch * (float)swapchainExt.height);
785+
pc.outW = std::max(1.0f, (float)d.w * sx / cw * (float)tgtExt.width);
786+
pc.outH = std::max(1.0f, (float)d.h * sy / ch * (float)tgtExt.height);
778787
vk_.CmdPushConstants(cb, pipeLayout, VK_SHADER_STAGE_VERTEX_BIT|VK_SHADER_STAGE_FRAGMENT_BIT, 0, sizeof(pc), &pc);
779788
vk_.CmdDraw(cb, 4, 1, 0, 0);
780789
}
@@ -810,6 +819,142 @@ void VulkanRendererContext::recordCmdBuf(VkCommandBuffer cb, uint32_t imgIdx,
810819
}
811820
}
812821

822+
823+
bool VulkanRendererContext::createXrTargetResources(uint32_t w, uint32_t h) {
824+
AHardwareBuffer_Desc d{};
825+
d.width=w; d.height=h; d.layers=1;
826+
d.format=AHARDWAREBUFFER_FORMAT_R8G8B8A8_UNORM;
827+
d.usage=AHARDWAREBUFFER_USAGE_GPU_SAMPLED_IMAGE|AHARDWAREBUFFER_USAGE_GPU_FRAMEBUFFER;
828+
if (AHardwareBuffer_allocate(&d,&xrAhb)!=0||!xrAhb) { RLOG_E("xrTarget: AHB alloc %ux%u failed",w,h); return false; }
829+
830+
VkAndroidHardwareBufferPropertiesANDROID props{};
831+
props.sType=VK_STRUCTURE_TYPE_ANDROID_HARDWARE_BUFFER_PROPERTIES_ANDROID;
832+
if (!vk_.GetAndroidHardwareBufferPropertiesANDROID ||
833+
vk_.GetAndroidHardwareBufferPropertiesANDROID(device,xrAhb,&props)!=VK_SUCCESS) {
834+
RLOG_E("xrTarget: AHB props failed"); destroyXrTargetResources(); return false;
835+
}
836+
837+
VkExternalMemoryImageCreateInfo emi{};
838+
emi.sType=VK_STRUCTURE_TYPE_EXTERNAL_MEMORY_IMAGE_CREATE_INFO;
839+
emi.handleTypes=VK_EXTERNAL_MEMORY_HANDLE_TYPE_ANDROID_HARDWARE_BUFFER_BIT_ANDROID;
840+
VkImageCreateInfo ii{};
841+
ii.sType=VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO; ii.pNext=&emi;
842+
ii.imageType=VK_IMAGE_TYPE_2D; ii.format=VK_FORMAT_R8G8B8A8_UNORM;
843+
ii.extent={w,h,1}; ii.mipLevels=1; ii.arrayLayers=1; ii.samples=VK_SAMPLE_COUNT_1_BIT;
844+
ii.tiling=VK_IMAGE_TILING_OPTIMAL;
845+
ii.usage=VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT|VK_IMAGE_USAGE_SAMPLED_BIT;
846+
ii.sharingMode=VK_SHARING_MODE_EXCLUSIVE; ii.initialLayout=VK_IMAGE_LAYOUT_UNDEFINED;
847+
if (vk_.CreateImage(device,&ii,nullptr,&xrImg)!=VK_SUCCESS) { RLOG_E("xrTarget: image"); destroyXrTargetResources(); return false; }
848+
849+
VkImportAndroidHardwareBufferInfoANDROID imp{};
850+
imp.sType=VK_STRUCTURE_TYPE_IMPORT_ANDROID_HARDWARE_BUFFER_INFO_ANDROID; imp.buffer=xrAhb;
851+
VkMemoryDedicatedAllocateInfo ded{};
852+
ded.sType=VK_STRUCTURE_TYPE_MEMORY_DEDICATED_ALLOCATE_INFO; ded.pNext=&imp; ded.image=xrImg;
853+
uint32_t idx=0; while (idx<32 && !(props.memoryTypeBits&(1u<<idx))) idx++;
854+
if (idx>=32) { RLOG_E("xrTarget: no compatible memory type (bits=0x%x)", props.memoryTypeBits); destroyXrTargetResources(); return false; }
855+
VkMemoryAllocateInfo mai{};
856+
mai.sType=VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO; mai.pNext=&ded;
857+
mai.allocationSize=props.allocationSize; mai.memoryTypeIndex=idx;
858+
if (vk_.AllocateMemory(device,&mai,nullptr,&xrMem)!=VK_SUCCESS ||
859+
vk_.BindImageMemory(device,xrImg,xrMem,0)!=VK_SUCCESS) {
860+
RLOG_E("xrTarget: memory"); destroyXrTargetResources(); return false;
861+
}
862+
863+
VkImageViewCreateInfo vi{};
864+
vi.sType=VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;
865+
vi.image=xrImg; vi.viewType=VK_IMAGE_VIEW_TYPE_2D; vi.format=VK_FORMAT_R8G8B8A8_UNORM;
866+
vi.subresourceRange={VK_IMAGE_ASPECT_COLOR_BIT,0,1,0,1};
867+
if (vk_.CreateImageView(device,&vi,nullptr,&xrView)!=VK_SUCCESS) { RLOG_E("xrTarget: view"); destroyXrTargetResources(); return false; }
868+
869+
VkAttachmentDescription att{};
870+
att.format=VK_FORMAT_R8G8B8A8_UNORM; att.samples=VK_SAMPLE_COUNT_1_BIT;
871+
att.loadOp=VK_ATTACHMENT_LOAD_OP_CLEAR; att.storeOp=VK_ATTACHMENT_STORE_OP_STORE;
872+
att.stencilLoadOp=VK_ATTACHMENT_LOAD_OP_DONT_CARE; att.stencilStoreOp=VK_ATTACHMENT_STORE_OP_DONT_CARE;
873+
att.initialLayout=VK_IMAGE_LAYOUT_UNDEFINED; att.finalLayout=VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
874+
VkAttachmentReference ref{0,VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL};
875+
VkSubpassDescription sub{}; sub.pipelineBindPoint=VK_PIPELINE_BIND_POINT_GRAPHICS;
876+
sub.colorAttachmentCount=1; sub.pColorAttachments=&ref;
877+
VkSubpassDependency dep{}; dep.srcSubpass=VK_SUBPASS_EXTERNAL; dep.dstSubpass=0;
878+
dep.srcStageMask=VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; dep.srcAccessMask=0;
879+
dep.dstStageMask=VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
880+
dep.dstAccessMask=VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
881+
VkRenderPassCreateInfo rci{}; rci.sType=VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO;
882+
rci.attachmentCount=1; rci.pAttachments=&att; rci.subpassCount=1; rci.pSubpasses=&sub;
883+
rci.dependencyCount=1; rci.pDependencies=&dep;
884+
if (vk_.CreateRenderPass(device,&rci,nullptr,&xrRp)!=VK_SUCCESS) { RLOG_E("xrTarget: renderpass"); destroyXrTargetResources(); return false; }
885+
886+
VkFramebufferCreateInfo fi{};
887+
fi.sType=VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO;
888+
fi.renderPass=xrRp; fi.attachmentCount=1; fi.pAttachments=&xrView;
889+
fi.width=w; fi.height=h; fi.layers=1;
890+
if (vk_.CreateFramebuffer(device,&fi,nullptr,&xrFb)!=VK_SUCCESS) { RLOG_E("xrTarget: framebuffer"); destroyXrTargetResources(); return false; }
891+
892+
xrExt={w,h};
893+
RLOG("xrTarget: created %ux%u ahb=%p", w, h, (void*)xrAhb);
894+
return true;
895+
}
896+
897+
void VulkanRendererContext::destroyXrTargetResources() {
898+
if (xrFb !=VK_NULL_HANDLE){vk_.DestroyFramebuffer(device,xrFb,nullptr); xrFb=VK_NULL_HANDLE;}
899+
if (xrRp !=VK_NULL_HANDLE){vk_.DestroyRenderPass(device,xrRp,nullptr); xrRp=VK_NULL_HANDLE;}
900+
if (xrView!=VK_NULL_HANDLE){vk_.DestroyImageView(device,xrView,nullptr); xrView=VK_NULL_HANDLE;}
901+
if (xrImg !=VK_NULL_HANDLE){vk_.DestroyImage(device,xrImg,nullptr); xrImg=VK_NULL_HANDLE;}
902+
if (xrMem !=VK_NULL_HANDLE){vk_.FreeMemory(device,xrMem,nullptr); xrMem=VK_NULL_HANDLE;}
903+
if (xrAhb){AHardwareBuffer_release(xrAhb); xrAhb=nullptr;}
904+
xrExt={0,0};
905+
}
906+
907+
int64_t VulkanRendererContext::enableXrTarget() {
908+
std::unique_lock<std::shared_mutex> fl(frameMutex);
909+
std::lock_guard<std::mutex> lk(renderMutex);
910+
if (device==VK_NULL_HANDLE) return 0;
911+
// A surface resize may still be queued for the render loop; process it here so the
912+
// target is sized from the current swapchain extent, not the stale one.
913+
if (fbResized.load()) {
914+
for (auto& f:inFlightFences) vk_.WaitForFences(device,1,&f,VK_TRUE,UINT64_MAX);
915+
cleanupSwapchain();
916+
try {
917+
createSwapchain(); createFramebuffers(); createCmdBufs();
918+
imgInFlight.assign(swapchainImages.size(),VK_NULL_HANDLE);
919+
fbResized.store(false);
920+
} catch(...) { return 0; }
921+
}
922+
uint32_t w=swapchainExt.width, h=swapchainExt.height;
923+
if (w==0||h==0){ w=(uint32_t)surfaceWidth; h=(uint32_t)surfaceHeight; }
924+
if (w==0||h==0) return 0;
925+
// The immersive quad swapchain follows the container screen size (min width 1280);
926+
// rendering the scene any larger only burns GPU on pixels the quad downsamples away.
927+
// Keep the surface aspect.
928+
float qw = (float)std::max(containerWidth, 1280);
929+
float qh = (float)containerHeight * qw / (float)std::max(containerWidth, 1);
930+
float fit = std::min({qw/(float)w, qh/(float)h, 1.f});
931+
w = std::max(16u, (uint32_t)((float)w*fit) & ~1u);
932+
h = std::max(16u, (uint32_t)((float)h*fit) & ~1u);
933+
if (xrTargetActive.load() && xrAhb!=nullptr && xrExt.width==w && xrExt.height==h)
934+
return (int64_t)(intptr_t)xrAhb;
935+
vk_.DeviceWaitIdle(device);
936+
if (xrAhb!=nullptr) { xrTargetActive.store(false); destroyXrTargetResources(); }
937+
if (!createXrTargetResources(w,h)) return 0;
938+
xrTargetActive.store(true);
939+
needsRender.store(true); dirtyCV.notify_one();
940+
return (int64_t)(intptr_t)xrAhb;
941+
}
942+
943+
int64_t VulkanRendererContext::xrTargetExtentPacked() {
944+
std::lock_guard<std::mutex> lk(renderMutex);
945+
return ((int64_t)xrExt.width<<32) | (int64_t)xrExt.height;
946+
}
947+
948+
void VulkanRendererContext::disableXrTarget() {
949+
std::unique_lock<std::shared_mutex> fl(frameMutex);
950+
std::lock_guard<std::mutex> lk(renderMutex);
951+
if (!xrTargetActive.load() && xrAhb==nullptr) return;
952+
if (device!=VK_NULL_HANDLE) vk_.DeviceWaitIdle(device);
953+
xrTargetActive.store(false);
954+
destroyXrTargetResources();
955+
needsRender.store(true); dirtyCV.notify_one();
956+
}
957+
813958
void VulkanRendererContext::renderLoop() {
814959

815960
while (isRunning) {
@@ -873,30 +1018,37 @@ ok=true;}catch(...){}
8731018
}
8741019

8751020
if (currentFrame >= cmdBufs.size() || cmdBufs[currentFrame] == VK_NULL_HANDLE) return;
1021+
bool toXr = xrTargetActive.load() && xrFb!=VK_NULL_HANDLE;
8761022
bool currentFenceWaited = false;
877-
if (!vk_.GetFenceStatus || vk_.GetFenceStatus(device, inFlightFences[currentFrame]) == VK_NOT_READY) {
1023+
if (toXr) {
1024+
for (auto& f:inFlightFences) vk_.WaitForFences(device,1,&f,VK_TRUE,UINT64_MAX);
1025+
currentFenceWaited = true;
1026+
} else if (!vk_.GetFenceStatus || vk_.GetFenceStatus(device, inFlightFences[currentFrame]) == VK_NOT_READY) {
8781027
vk_.WaitForFences(device,1,&inFlightFences[currentFrame],VK_TRUE,UINT64_MAX);
8791028
currentFenceWaited = true;
8801029
}
8811030

882-
uint32_t imgIdx;
883-
VkResult res=vk_.AcquireNextImageKHR(device,swapchain,UINT64_MAX,imgAvailSems[currentFrame],VK_NULL_HANDLE,&imgIdx);
884-
if (res==VK_ERROR_OUT_OF_DATE_KHR||res==VK_ERROR_SURFACE_LOST_KHR){fbResized.store(true);return;}
885-
if (res!=VK_SUCCESS&&res!=VK_SUBOPTIMAL_KHR) return;
886-
if (imgIdx >= swapchainFBs.size() || imgIdx >= swapchainImages.size()) {
887-
RLOG_E("renderFrame: invalid acquired image index=%u (fb=%zu images=%zu)",
888-
imgIdx, swapchainFBs.size(), swapchainImages.size());
889-
return;
890-
}
1031+
uint32_t imgIdx = 0;
1032+
VkResult res = VK_SUCCESS;
1033+
if (!toXr) {
1034+
res=vk_.AcquireNextImageKHR(device,swapchain,UINT64_MAX,imgAvailSems[currentFrame],VK_NULL_HANDLE,&imgIdx);
1035+
if (res==VK_ERROR_OUT_OF_DATE_KHR||res==VK_ERROR_SURFACE_LOST_KHR){fbResized.store(true);return;}
1036+
if (res!=VK_SUCCESS&&res!=VK_SUBOPTIMAL_KHR) return;
1037+
if (imgIdx >= swapchainFBs.size() || imgIdx >= swapchainImages.size()) {
1038+
RLOG_E("renderFrame: invalid acquired image index=%u (fb=%zu images=%zu)",
1039+
imgIdx, swapchainFBs.size(), swapchainImages.size());
1040+
return;
1041+
}
8911042

892-
if (imgInFlight.size()!=swapchainImages.size()) imgInFlight.assign(swapchainImages.size(),VK_NULL_HANDLE);
893-
if (imgInFlight[imgIdx]!=VK_NULL_HANDLE &&
894-
(!currentFenceWaited || imgInFlight[imgIdx] != inFlightFences[currentFrame])) {
895-
if (!vk_.GetFenceStatus || vk_.GetFenceStatus(device, imgInFlight[imgIdx]) == VK_NOT_READY) {
896-
vk_.WaitForFences(device,1,&imgInFlight[imgIdx],VK_TRUE,UINT64_MAX);
1043+
if (imgInFlight.size()!=swapchainImages.size()) imgInFlight.assign(swapchainImages.size(),VK_NULL_HANDLE);
1044+
if (imgInFlight[imgIdx]!=VK_NULL_HANDLE &&
1045+
(!currentFenceWaited || imgInFlight[imgIdx] != inFlightFences[currentFrame])) {
1046+
if (!vk_.GetFenceStatus || vk_.GetFenceStatus(device, imgInFlight[imgIdx]) == VK_NOT_READY) {
1047+
vk_.WaitForFences(device,1,&imgInFlight[imgIdx],VK_TRUE,UINT64_MAX);
1048+
}
8971049
}
1050+
imgInFlight[imgIdx]=inFlightFences[currentFrame];
8981051
}
899-
imgInFlight[imgIdx]=inFlightFences[currentFrame];
9001052

9011053
vk_.ResetCommandBuffer(cmdBufs[currentFrame],0);
9021054

@@ -965,9 +1117,11 @@ ok=true;}catch(...){}
9651117
VkSemaphore wSem[]={imgAvailSems[currentFrame]}, sSem[]={renderDoneSems[currentFrame]};
9661118
VkPipelineStageFlags wStage[]={VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT};
9671119
VkSubmitInfo si{}; si.sType=VK_STRUCTURE_TYPE_SUBMIT_INFO;
968-
si.waitSemaphoreCount=1; si.pWaitSemaphores=wSem; si.pWaitDstStageMask=wStage;
1120+
if (!toXr) {
1121+
si.waitSemaphoreCount=1; si.pWaitSemaphores=wSem; si.pWaitDstStageMask=wStage;
1122+
si.signalSemaphoreCount=1; si.pSignalSemaphores=sSem;
1123+
}
9691124
si.commandBufferCount=1; si.pCommandBuffers=&cmdBufs[currentFrame];
970-
si.signalSemaphoreCount=1; si.pSignalSemaphores=sSem;
9711125

9721126
vk_.ResetFences(device,1,&inFlightFences[currentFrame]);
9731127
if (vk_.QueueSubmit(graphicsQueue,1,&si,inFlightFences[currentFrame])!=VK_SUCCESS) {
@@ -976,11 +1130,18 @@ ok=true;}catch(...){}
9761130
vk_.CreateFence(device,&fi,nullptr,&inFlightFences[currentFrame]);
9771131
return;
9781132
}
979-
VkSwapchainKHR scs[]={swapchain};
980-
VkPresentInfoKHR pi{}; pi.sType=VK_STRUCTURE_TYPE_PRESENT_INFO_KHR;
981-
pi.waitSemaphoreCount=1; pi.pWaitSemaphores=sSem; pi.swapchainCount=1; pi.pSwapchains=scs; pi.pImageIndices=&imgIdx;
982-
res=vk_.QueuePresentKHR(graphicsQueue,&pi);
983-
if (res==VK_ERROR_OUT_OF_DATE_KHR||res==VK_ERROR_SURFACE_LOST_KHR) fbResized.store(true);
1133+
if (!toXr) {
1134+
VkSwapchainKHR scs[]={swapchain};
1135+
VkPresentInfoKHR pi{}; pi.sType=VK_STRUCTURE_TYPE_PRESENT_INFO_KHR;
1136+
pi.waitSemaphoreCount=1; pi.pWaitSemaphores=sSem; pi.swapchainCount=1; pi.pSwapchains=scs; pi.pImageIndices=&imgIdx;
1137+
res=vk_.QueuePresentKHR(graphicsQueue,&pi);
1138+
if (res==VK_ERROR_OUT_OF_DATE_KHR||res==VK_ERROR_SURFACE_LOST_KHR) fbResized.store(true);
1139+
} else {
1140+
// The XR session samples xrAhb from its own GL context with no fence handoff;
1141+
// blocking here means the buffer is fully written whenever this thread is idle,
1142+
// leaving only the active write window unsynchronized (a tear, not stale data).
1143+
vk_.WaitForFences(device,1,&inFlightFences[currentFrame],VK_TRUE,UINT64_MAX);
1144+
}
9841145
currentFrame=(currentFrame+1)%MAX_FRAMES_IN_FLIGHT;
9851146
}
9861147

‎app/src/main/cpp/winlator/VulkanRendererContext.h‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,9 @@ struct WindowPushConstants {
128128

129129
class VulkanRendererContext {
130130
public:
131+
int64_t enableXrTarget();
132+
void disableXrTarget();
133+
int64_t xrTargetExtentPacked();
131134
VulkanRendererContext(ANativeWindow* window, int cWidth, int cHeight, void* adrenotoolsHandle = nullptr);
132135
~VulkanRendererContext();
133136

@@ -309,6 +312,20 @@ class VulkanRendererContext {
309312
std::vector<VkImageView> swapchainViews;
310313
std::vector<VkFramebuffer> swapchainFBs;
311314

315+
// XR offscreen scene target: when active, renderFrame renders the composited scene into
316+
// this AHB-backed image (no surface acquire/present) so the immersive session samples it
317+
// directly. Enabled/disabled from JNI; resources recreated on resize.
318+
AHardwareBuffer* xrAhb = nullptr;
319+
VkImage xrImg = VK_NULL_HANDLE;
320+
VkDeviceMemory xrMem = VK_NULL_HANDLE;
321+
VkImageView xrView = VK_NULL_HANDLE;
322+
VkFramebuffer xrFb = VK_NULL_HANDLE;
323+
VkRenderPass xrRp = VK_NULL_HANDLE;
324+
VkExtent2D xrExt {0,0};
325+
std::atomic<bool> xrTargetActive{false};
326+
bool createXrTargetResources(uint32_t w, uint32_t h);
327+
void destroyXrTargetResources();
328+
312329
VkRenderPass renderPass = VK_NULL_HANDLE;
313330
VkDescriptorSetLayout dsLayout = VK_NULL_HANDLE;
314331
VkPipelineLayout pipeLayout = VK_NULL_HANDLE;

‎app/src/main/cpp/winlator/vulkan_jni.cpp‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,21 @@ Java_com_winlator_renderer_VulkanRenderer_nativeUpdateWindowContentAHB(
9999
auto* r=reinterpret_cast<VulkanRendererContext*>(handle);
100100
if (r&&ahbPtr) r->updateWindowContentAHB(id,reinterpret_cast<AHardwareBuffer*>(ahbPtr),w,h,x,y);
101101
}
102+
extern "C" JNIEXPORT jlong JNICALL
103+
Java_com_winlator_renderer_VulkanRenderer_nativeEnableXrTarget(JNIEnv*, jobject, jlong handle) {
104+
auto* r=reinterpret_cast<VulkanRendererContext*>(handle);
105+
return r ? (jlong)r->enableXrTarget() : 0;
106+
}
107+
extern "C" JNIEXPORT void JNICALL
108+
Java_com_winlator_renderer_VulkanRenderer_nativeDisableXrTarget(JNIEnv*, jobject, jlong handle) {
109+
auto* r=reinterpret_cast<VulkanRendererContext*>(handle);
110+
if (r) r->disableXrTarget();
111+
}
112+
extern "C" JNIEXPORT jlong JNICALL
113+
Java_com_winlator_renderer_VulkanRenderer_nativeGetXrTargetExtent(JNIEnv*, jobject, jlong handle) {
114+
auto* r=reinterpret_cast<VulkanRendererContext*>(handle);
115+
return r ? (jlong)r->xrTargetExtentPacked() : 0;
116+
}
102117
extern "C" JNIEXPORT void JNICALL
103118
Java_com_winlator_renderer_VulkanRenderer_nativeSetTransform(
104119
JNIEnv*, jobject, jlong handle, jfloat ox, jfloat oy, jfloat sx, jfloat sy)

‎app/src/main/cpp/xrimmersive/jni_bridge.cpp‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,13 +37,15 @@ NativeHandle *LiveHandle(jlong handlePtr) {
3737
extern "C" {
3838

3939
JNIEXPORT jlong JNICALL
40-
Java_app_gamenative_ui_screen_xr_XrNative_nativeCreate(JNIEnv *env, jclass, jobject activity) {
40+
Java_app_gamenative_ui_screen_xr_XrNative_nativeCreate(JNIEnv *env, jclass, jobject activity,
41+
jint quadWidth, jint quadHeight, jfloat refreshRate) {
4142
JavaVM *vm = nullptr;
4243
env->GetJavaVM(&vm);
4344

4445
auto *handle = new NativeHandle();
4546
handle->activityGlobalRef = env->NewGlobalRef(activity);
4647
handle->session = new xrimmersive::XrImmersiveSession();
48+
handle->session->configure(quadWidth, quadHeight, refreshRate);
4749
handle->session->initialize(vm, handle->activityGlobalRef);
4850
{
4951
std::lock_guard<std::mutex> lock(gHandleMutex);

0 commit comments

Comments
 (0)