Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions Runtime/DisplayXRLocal2D.cs
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,9 @@ private Vector2Int RtSize
/// <summary>The RenderTexture capturing the Canvas content.</summary>
public RenderTexture OverlayTexture { get; private set; }

private static readonly Vector3 kCanvasWorldPos = new Vector3(0, 100000f, 0);
// This component's private stage (see DisplayXROverlayStage): its canvas and
// camera sit there so no other overlay's camera sees this canvas.
private int m_Stage = -1;
private const int kPrivateLayer = 30;

private Canvas m_Canvas;
Expand Down Expand Up @@ -310,7 +312,9 @@ void OnEnable()

// WorldSpace canvas parked far away on a private layer.
m_Canvas.renderMode = RenderMode.WorldSpace;
m_CanvasRect.position = kCanvasWorldPos;
if (m_Stage < 0) m_Stage = DisplayXROverlayStage.Acquire();
Vector3 stagePos = DisplayXROverlayStage.Position(m_Stage);
m_CanvasRect.position = stagePos;
m_CanvasRect.rotation = Quaternion.identity;
m_CanvasRect.localScale = new Vector3(0.01f, 0.01f, 0.01f);
m_CanvasRect.sizeDelta = resolution;
Expand All @@ -321,7 +325,7 @@ void OnEnable()
var camGO = new GameObject("DisplayXR_Local2DCam");
camGO.transform.SetParent(transform, false);
camGO.hideFlags = HideFlags.HideAndDontSave;
camGO.transform.position = kCanvasWorldPos + new Vector3(0, 0, 1);
camGO.transform.position = stagePos + new Vector3(0, 0, 1);
camGO.transform.rotation = Quaternion.LookRotation(Vector3.back, Vector3.down);

m_OverlayCamera = camGO.AddComponent<Camera>();
Expand Down Expand Up @@ -459,6 +463,8 @@ private void ReleaseBridgeTex()

void OnDisable()
{
DisplayXROverlayStage.Release(m_Stage);
m_Stage = -1;
UnregisterContentDirtyTracking(); // #244 — drop Graphic dirty callbacks
ReleaseBridgeTex();

Expand Down
47 changes: 47 additions & 0 deletions Runtime/DisplayXROverlayStage.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// Copyright 2024-2026, DisplayXR contributors
// SPDX-License-Identifier: Apache-2.0

using System.Collections.Generic;
using UnityEngine;

namespace DisplayXR
{
/// <summary>
/// Private world-space "stage" positions for the offscreen canvases of
/// <see cref="DisplayXRLocal2D"/> and <see cref="DisplayXRWindowSpaceUI"/>.
///
/// Each component parks its canvas far from the scene on a private layer and
/// renders it with its own ortho camera that culls to that layer. They all used
/// one shared spot, so with two or more overlays every camera also rendered the
/// other canvases: a HUD showed up inside the Local2D bubble and vice versa (the
/// layer can't separate them — they share it). Giving each live overlay its own
/// stage, spaced far beyond any canvas's extent and the cameras' 10-unit far
/// plane, keeps each camera on its own canvas.
/// </summary>
internal static class DisplayXROverlayStage
{
static readonly Vector3 kOrigin = new Vector3(0f, 100000f, 0f);
// A 4096-unit canvas at the 0.01 canvas scale is ~41 world units wide.
const float kSpacing = 1000f;

static readonly HashSet<int> s_Used = new HashSet<int>();

/// <summary>Take the lowest free stage index.</summary>
public static int Acquire()
{
int i = 0;
while (s_Used.Contains(i)) i++;
s_Used.Add(i);
return i;
}

/// <summary>Give a stage back. Negative indices are ignored.</summary>
public static void Release(int index)
{
if (index >= 0) s_Used.Remove(index);
}

/// <summary>World position of the canvas on stage <paramref name="index"/>.</summary>
public static Vector3 Position(int index) => kOrigin + new Vector3(index * kSpacing, 0f, 0f);
}
}
11 changes: 11 additions & 0 deletions Runtime/DisplayXROverlayStage.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

52 changes: 48 additions & 4 deletions Runtime/DisplayXRWindowSpaceUI.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

using UnityEngine;
using UnityEngine.Experimental.Rendering;
using UnityEngine.Rendering;
using UnityEngine.Rendering; // RenderPipelineManager, ScriptableRenderContext

namespace DisplayXR
{
Expand Down Expand Up @@ -110,7 +110,18 @@ public class DisplayXRWindowSpaceUI : MonoBehaviour
// We park the WorldSpace canvas at this fixed position, far from any
// scene content, so the dedicated camera looking at it sees nothing
// else that might bleed into our RT.
private static readonly Vector3 kCanvasWorldPos = new Vector3(0, 100000f, 0);
// This component's private stage (see DisplayXROverlayStage): its canvas and
// camera sit there so no other overlay's camera sees this canvas.
private int m_Stage = -1;

// The URP transparent-overlay foreground clip (DisplayXR/ForegroundClipURP) is a
// full-screen pass on the renderer, so it also runs for this overlay camera and,
// keyed on _DXRForegroundFar (z>0.5 = armed), discards the whole canvas: the RT
// comes out fully transparent and the HUD never shows. Same fix as
// DisplayXRLocal2D: zero the global for our camera's render, restore after.
private static readonly int s_ForegroundFarId = Shader.PropertyToID("_DXRForegroundFar");
private Vector4 m_SavedForegroundFar;
private bool m_CamRenderHooked;
// Dedicated layer: we put the canvas + children on this layer and give
// ONLY our overlay camera that layer in its cullingMask. We pick a
// mid-range layer that's typically unused (Unity reserves 0-7 for
Expand Down Expand Up @@ -185,7 +196,9 @@ void OnEnable()
m_Canvas.renderMode = RenderMode.WorldSpace;
// worldCamera is assigned to the OverlayCamera below (after creation)
// so GraphicRaycaster can project screen-cursor input onto the canvas.
m_CanvasRect.position = kCanvasWorldPos;
if (m_Stage < 0) m_Stage = DisplayXROverlayStage.Acquire();
Vector3 stagePos = DisplayXROverlayStage.Position(m_Stage);
m_CanvasRect.position = stagePos;
m_CanvasRect.rotation = Quaternion.identity;
// Use the canvas's existing reference width as the scale baseline.
// 1 world unit per UI unit at scale 1 → set scale so the RT
Expand All @@ -208,7 +221,7 @@ void OnEnable()
// pipeline and the swapchain image our native blit feeds the
// runtime compositor with. Without this the panel reads
// upside-down in the runtime preview window.
camGO.transform.position = kCanvasWorldPos + new Vector3(0, 0, 1);
camGO.transform.position = stagePos + new Vector3(0, 0, 1);
camGO.transform.rotation = Quaternion.LookRotation(Vector3.back, Vector3.down);

m_OverlayCamera = camGO.AddComponent<Camera>();
Expand Down Expand Up @@ -236,6 +249,12 @@ void OnEnable()
// Mirrors DisplayXRLocal2D (which was migrated off manual Render for the
// canvas-rebuild race — same enabled=true resolution).
m_OverlayCamera.enabled = true;
if (!m_CamRenderHooked)
{
RenderPipelineManager.beginCameraRendering += OnBeginOverlayCamera;
RenderPipelineManager.endCameraRendering += OnEndOverlayCamera;
m_CamRenderHooked = true;
}

// Wire OverlayCamera as the canvas's event camera. GraphicRaycaster
// needs a camera reference to project screen-cursor input onto a
Expand Down Expand Up @@ -450,9 +469,34 @@ private void ReleaseBridgeTex()
m_BridgePtr = System.IntPtr.Zero;
}

// Scope the URP foreground clip out of our overlay camera (see the field doc).
// Camera-identity gated, so other cameras are untouched.
void OnBeginOverlayCamera(ScriptableRenderContext ctx, Camera cam)
{
if (cam != m_OverlayCamera) return;
m_SavedForegroundFar = Shader.GetGlobalVector(s_ForegroundFarId);
if (m_SavedForegroundFar.z > 0.5f)
Shader.SetGlobalVector(s_ForegroundFarId, Vector4.zero);
}

void OnEndOverlayCamera(ScriptableRenderContext ctx, Camera cam)
{
if (cam != m_OverlayCamera) return;
if (m_SavedForegroundFar.z > 0.5f)
Shader.SetGlobalVector(s_ForegroundFarId, m_SavedForegroundFar);
}

void OnDisable()
{
ReleaseSlot();
if (m_CamRenderHooked)
{
RenderPipelineManager.beginCameraRendering -= OnBeginOverlayCamera;
RenderPipelineManager.endCameraRendering -= OnEndOverlayCamera;
m_CamRenderHooked = false;
}
DisplayXROverlayStage.Release(m_Stage);
m_Stage = -1;

// Restore the canvas's original mode + transform + layer.
if (m_StateSaved && m_Canvas != null)
Expand Down
Loading