From a6a74b4b445f0726a0d8d033c8b38a2684e375b1 Mon Sep 17 00:00:00 2001 From: daniele natale Date: Wed, 5 Aug 2026 22:47:15 +0200 Subject: [PATCH 1/3] Fix 360 and 180 stereo VR videos rendering mono VRLayer defaults useSameLayerForBothEyes to true. When it is set, OpenXRLayerBase::GetNumXRLayers() returns 1, so a single equirect layer is submitted with XR_EYE_VISIBILITY_BOTH and only the left eye's UV transform is applied. Both eyes then receive the same half of the frame and stereo video plays back mono. create180LRProjectionLayer already disabled the flag, but create360StereoProjectionLayer and create180TBProjectionLayer did not. The default is now documented where it is declared, since the path that got it right said nothing about it. Verified on a Pico Neo 3 (OpenXR) with an equirect test video whose top half is red and bottom half blue: before this change both eyes showed red, afterwards the left eye shows red and the right eye blue. --- app/src/main/cpp/VRLayer.cpp | 1 + app/src/main/cpp/VRVideo.cpp | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/app/src/main/cpp/VRLayer.cpp b/app/src/main/cpp/VRLayer.cpp index f3961df63e..c9ec577028 100644 --- a/app/src/main/cpp/VRLayer.cpp +++ b/app/src/main/cpp/VRLayer.cpp @@ -30,6 +30,7 @@ struct VRLayer::State { std::function pendingEvent; std::string name; bool composited; + // Clear for layers drawing per-eye content, or both eyes get the left one. bool useSameLayerForBothEyes; State(): initialized(false), diff --git a/app/src/main/cpp/VRVideo.cpp b/app/src/main/cpp/VRVideo.cpp index f7803bb11f..e33ab625ab 100644 --- a/app/src/main/cpp/VRVideo.cpp +++ b/app/src/main/cpp/VRVideo.cpp @@ -267,6 +267,9 @@ struct VRVideo::State { rightTransform.ScaleInPlace(vrb::Vector(1.0f, 0.5f, 1.0f)); equirect->SetUVTransform(device::Eye::Right, rightTransform); + // Per-eye UV transforms need one layer per eye, else mono. + equirect->SetUseSameLayerForBothEyes(false); + leftEye = vrb::Toggle::Create(create); leftEye->AddNode(VRLayerNode::Create(create, equirect)); rightEye = vrb::Toggle::Create(create); @@ -346,6 +349,9 @@ struct VRVideo::State { uvTransform.TranslateInPlace(vrb::Vector(0.0f, 0.5f, 0.0f)); equirect->SetUVTransform(device::Eye::Right, uvTransform); + // Per-eye UV transforms need one layer per eye, else mono. + equirect->SetUseSameLayerForBothEyes(false); + leftEye = create180LayerToggle(equirect); rightEye = create180LayerToggle(equirect); } From fbd309a105dad4a454ce21a0d43c065cabcf10dd Mon Sep 17 00:00:00 2001 From: daniele natale Date: Wed, 5 Aug 2026 22:47:46 +0200 Subject: [PATCH 2/3] Center 180 degree VR video hemispheres in front of the viewer The 180 degree paths use a horizontal UV scale of 2 so the texture maps onto half the sphere. Without a matching -0.5 horizontal bias that half ends up 90 degrees off to the side: looking straight ahead shows the edge of the image, with black filling the rest of the view. create180LRProjectionLayer already applied the bias; create180ProjectionLayer and create180TBProjectionLayer did not. Verified on a Pico Neo 3 (OpenXR): the hemisphere is now centred on the forward direction instead of starting at the centre of the view. --- app/src/main/cpp/VRVideo.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/src/main/cpp/VRVideo.cpp b/app/src/main/cpp/VRVideo.cpp index e33ab625ab..f4029f56dd 100644 --- a/app/src/main/cpp/VRVideo.cpp +++ b/app/src/main/cpp/VRVideo.cpp @@ -295,6 +295,8 @@ struct VRVideo::State { vrb::Matrix uvTransform = vrb::Matrix::Identity(); uvTransform.ScaleInPlace(vrb::Vector(2.0f, 1.0f, 1.0f)); + // centers the hemisphere in front of the viewer. + uvTransform.TranslateInPlace(vrb::Vector(-0.5f, 0.0f, 0.0f)); equirect->SetUVTransform(device::Eye::Left, uvTransform); equirect->SetUVTransform(device::Eye::Right, uvTransform); @@ -345,6 +347,8 @@ struct VRVideo::State { vrb::Matrix uvTransform = vrb::Matrix::Identity(); uvTransform.ScaleInPlace(vrb::Vector(2.0f, 0.5f, 1.0f)); + // centers the hemisphere in front of the viewer. + uvTransform.TranslateInPlace(vrb::Vector(-0.5f, 0.0f, 0.0f)); equirect->SetUVTransform(device::Eye::Left, uvTransform); uvTransform.TranslateInPlace(vrb::Vector(0.0f, 0.5f, 0.0f)); equirect->SetUVTransform(device::Eye::Right, uvTransform); From d24fba8d00040207e6be153af4c1614bbd533ec2 Mon Sep 17 00:00:00 2001 From: daniele natale Date: Wed, 5 Aug 2026 22:48:22 +0200 Subject: [PATCH 3/3] Add a 360 stereo left-right video projection mode Full-sphere stereo was only available as over/under. Side-by-side existed for 180 degree and flat 3D video, but not for 360, so side-by-side 360 content could not be viewed in stereo at all. Adds VIDEO_PROJECTION_360_STEREO_LEFT_RIGHT with both a mesh and a compositor-layer implementation, a menu entry with icon, and the mozVideoProjection=360lr query parameter. 360tb is accepted as an alias for 360s so the query-string vocabulary matches 180tb/180lr/3dtb; 360s keeps working. The specific values are matched before the bare 360 prefix, as already needed for 180lr and 180tb. Neither the "360 Stereo" label nor its icon said that it means over/under, which makes diagnosing content format problems harder than it needs to be. Both 360 modes now follow the naming scheme and the divider-bar icon convention already used by the 180 modes: 360 Stereo -> Stereo 360 Top to Bottom (new) -> Stereo 360 Left to Right Translations are updated for de and en-rGB only; the remaining locales fall back to English until they are handled by the l10n process. The string key is kept so existing translations are not orphaned. Verified on a Pico Neo 3 (OpenXR) with an equirect test video whose left half is red and right half blue: the left eye shows red, the right blue. --- .../ui/widgets/NavigationBarWidget.java | 3 +- .../menus/VideoProjectionMenuWidget.java | 11 +++++-- app/src/main/cpp/VRVideo.cpp | 30 +++++++++++++++++++ app/src/main/cpp/VRVideo.h | 1 + .../ic_icon_videoplayback_360_stereo.xml | 3 ++ ...con_videoplayback_360_stereo_leftright.xml | 13 ++++++++ app/src/main/res/values-de/strings.xml | 5 +++- app/src/main/res/values-en-rGB/strings.xml | 5 +++- app/src/main/res/values/strings.xml | 5 +++- 9 files changed, 70 insertions(+), 6 deletions(-) create mode 100644 app/src/main/res/drawable/ic_icon_videoplayback_360_stereo_leftright.xml diff --git a/app/src/common/shared/com/igalia/wolvic/ui/widgets/NavigationBarWidget.java b/app/src/common/shared/com/igalia/wolvic/ui/widgets/NavigationBarWidget.java index 6d6301d3d2..c740e5b424 100644 --- a/app/src/common/shared/com/igalia/wolvic/ui/widgets/NavigationBarWidget.java +++ b/app/src/common/shared/com/igalia/wolvic/ui/widgets/NavigationBarWidget.java @@ -932,7 +932,8 @@ private void enterVRVideo(@VideoProjectionMenuWidget.VideoProjectionFlags int aP mediaHeight = mAttachedWindow.getWindowHeight(); } final boolean resetBorder = aProjection == VideoProjectionMenuWidget.VIDEO_PROJECTION_360 || - aProjection == VideoProjectionMenuWidget.VIDEO_PROJECTION_360_STEREO; + aProjection == VideoProjectionMenuWidget.VIDEO_PROJECTION_360_STEREO || + aProjection == VideoProjectionMenuWidget.VIDEO_PROJECTION_360_STEREO_LEFT_RIGHT; mAttachedWindow.enableVRVideoMode(mediaWidth, mediaHeight, resetBorder); // Handle video resize while in VR video playback if (mFullScreenMedia != null) { diff --git a/app/src/common/shared/com/igalia/wolvic/ui/widgets/menus/VideoProjectionMenuWidget.java b/app/src/common/shared/com/igalia/wolvic/ui/widgets/menus/VideoProjectionMenuWidget.java index 4e04165233..f8bf1957f3 100644 --- a/app/src/common/shared/com/igalia/wolvic/ui/widgets/menus/VideoProjectionMenuWidget.java +++ b/app/src/common/shared/com/igalia/wolvic/ui/widgets/menus/VideoProjectionMenuWidget.java @@ -22,7 +22,7 @@ public class VideoProjectionMenuWidget extends MenuWidget { @IntDef(value = { VIDEO_PROJECTION_NONE, VIDEO_PROJECTION_3D_SIDE_BY_SIDE, VIDEO_PROJECTION_360, VIDEO_PROJECTION_360_STEREO, VIDEO_PROJECTION_180, VIDEO_PROJECTION_180_STEREO_LEFT_RIGHT, VIDEO_PROJECTION_180_STEREO_TOP_BOTTOM, - VIDEO_PROJECTION_3D_TOP_BOTTOM }) + VIDEO_PROJECTION_3D_TOP_BOTTOM, VIDEO_PROJECTION_360_STEREO_LEFT_RIGHT }) public @interface VideoProjectionFlags {} public static final int VIDEO_PROJECTION_NONE = -1; @@ -33,6 +33,7 @@ public class VideoProjectionMenuWidget extends MenuWidget { public static final int VIDEO_PROJECTION_180_STEREO_LEFT_RIGHT = 4; public static final int VIDEO_PROJECTION_180_STEREO_TOP_BOTTOM = 5; public static final int VIDEO_PROJECTION_3D_TOP_BOTTOM = 6; + public static final int VIDEO_PROJECTION_360_STEREO_LEFT_RIGHT = 7; public interface Delegate { void onVideoProjectionClick(@VideoProjectionFlags int aProjection); @@ -108,6 +109,9 @@ private void createMenuItems() { mItems.add(new ProjectionMenuItem(VIDEO_PROJECTION_360_STEREO, getContext().getString(R.string.video_mode_360_stereo), R.drawable.ic_icon_videoplayback_360_stereo)); + mItems.add(new ProjectionMenuItem(VIDEO_PROJECTION_360_STEREO_LEFT_RIGHT, getContext().getString(R.string.video_mode_360_stereo_leftright), + R.drawable.ic_icon_videoplayback_360_stereo_leftright)); + mItems.add(new ProjectionMenuItem(VIDEO_PROJECTION_180, getContext().getString(R.string.video_mode_180), R.drawable.ic_icon_videoplayback_180)); @@ -165,7 +169,10 @@ public void setSelectedProjection(@VideoProjectionFlags int aProjection) { autoEnter.set(projection.endsWith("_auto")); - if (projection.startsWith("360s")) { + if (projection.startsWith("360lr")) { + return VIDEO_PROJECTION_360_STEREO_LEFT_RIGHT; + } else if (projection.startsWith("360tb") || projection.startsWith("360s")) { + // "360s" predates the tb/lr distinction and is kept as an alias. return VIDEO_PROJECTION_360_STEREO; } else if (projection.startsWith("360")) { return VIDEO_PROJECTION_360; diff --git a/app/src/main/cpp/VRVideo.cpp b/app/src/main/cpp/VRVideo.cpp index f4029f56dd..e60c05b949 100644 --- a/app/src/main/cpp/VRVideo.cpp +++ b/app/src/main/cpp/VRVideo.cpp @@ -124,6 +124,10 @@ struct VRVideo::State { leftEye = createSphereProjection(false, device::EyeRect(0.0f, 0.5f, 1.0f, 0.5f)); rightEye = createSphereProjection(false, device::EyeRect(0.0f, 0.0f, 1.0f, 0.5f)); break; + case VRVideoProjection::VIDEO_PROJECTION_360_STEREO_LEFT_RIGHT: + leftEye = createSphereProjection(false, device::EyeRect(0.0f, 0.0f, 0.5f, 1.0f)); + rightEye = createSphereProjection(false, device::EyeRect(0.5f, 0.0f, 0.5f, 1.0f)); + break; case VRVideoProjection::VIDEO_PROJECTION_180: leftEye = createSphereProjection(true, device::EyeRect(0.0f, 0.0f, 1.0f, 1.0f)); break; @@ -152,6 +156,9 @@ struct VRVideo::State { case VRVideoProjection::VIDEO_PROJECTION_360_STEREO: create360StereoProjectionLayer(); break; + case VRVideoProjection::VIDEO_PROJECTION_360_STEREO_LEFT_RIGHT: + create360LRProjectionLayer(); + break; case VRVideoProjection::VIDEO_PROJECTION_180: create180ProjectionLayer(); break; @@ -276,6 +283,29 @@ struct VRVideo::State { rightEye->AddNode(VRLayerNode::Create(create, equirect)); } + void create360LRProjectionLayer() { + vrb::CreationContextPtr create = context.lock(); + DeviceDelegatePtr device = deviceWeak.lock(); + VRLayerEquirectPtr equirect = device->CreateLayerEquirect(window->GetLayer()); + layer = equirect; + + vrb::Matrix leftTransform = vrb::Matrix::Identity(); + leftTransform.ScaleInPlace(vrb::Vector(0.5f, 1.0f, 1.0f)); + equirect->SetUVTransform(device::Eye::Left, leftTransform); + + vrb::Matrix rightTransform = vrb::Matrix::Position(vrb::Vector(0.5f, 0.0f, 0.0f)); + rightTransform.ScaleInPlace(vrb::Vector(0.5f, 1.0f, 1.0f)); + equirect->SetUVTransform(device::Eye::Right, rightTransform); + + // Per-eye UV transforms need one layer per eye, else mono. + equirect->SetUseSameLayerForBothEyes(false); + + leftEye = vrb::Toggle::Create(create); + leftEye->AddNode(VRLayerNode::Create(create, equirect)); + rightEye = vrb::Toggle::Create(create); + rightEye->AddNode(VRLayerNode::Create(create, equirect)); + } + vrb::TogglePtr create180LayerToggle(const VRLayerEquirectPtr& aLayer) { vrb::CreationContextPtr create = context.lock(); vrb::TogglePtr result = vrb::Toggle::Create(create); diff --git a/app/src/main/cpp/VRVideo.h b/app/src/main/cpp/VRVideo.h index 4e77fdc8fa..6451b5ad8f 100644 --- a/app/src/main/cpp/VRVideo.h +++ b/app/src/main/cpp/VRVideo.h @@ -37,6 +37,7 @@ class VRVideo { VIDEO_PROJECTION_180_STEREO_LEFT_RIGHT = 4, VIDEO_PROJECTION_180_STEREO_TOP_BOTTOM = 5, VIDEO_PROJECTION_3D_TOP_BOTTOM = 6, + VIDEO_PROJECTION_360_STEREO_LEFT_RIGHT = 7, }; static VRVideoPtr Create(vrb::CreationContextPtr aContext, const WidgetPtr& aWindow, diff --git a/app/src/main/res/drawable/ic_icon_videoplayback_360_stereo.xml b/app/src/main/res/drawable/ic_icon_videoplayback_360_stereo.xml index 54a5290999..947e67efe1 100644 --- a/app/src/main/res/drawable/ic_icon_videoplayback_360_stereo.xml +++ b/app/src/main/res/drawable/ic_icon_videoplayback_360_stereo.xml @@ -7,4 +7,7 @@ + diff --git a/app/src/main/res/drawable/ic_icon_videoplayback_360_stereo_leftright.xml b/app/src/main/res/drawable/ic_icon_videoplayback_360_stereo_leftright.xml new file mode 100644 index 0000000000..2ad790c49e --- /dev/null +++ b/app/src/main/res/drawable/ic_icon_videoplayback_360_stereo_leftright.xml @@ -0,0 +1,13 @@ + + + + diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 2bd63c244a..7946be72ab 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -917,7 +917,10 @@ 360 - 360 Stereo + Stereo 360, oben nach unten + + + Stereo 360, links nach rechts 180 diff --git a/app/src/main/res/values-en-rGB/strings.xml b/app/src/main/res/values-en-rGB/strings.xml index ddb30c6273..f2f0b7bb38 100644 --- a/app/src/main/res/values-en-rGB/strings.xml +++ b/app/src/main/res/values-en-rGB/strings.xml @@ -968,7 +968,10 @@ 360 - 360 Stereo + Stereo 360 Top to Bottom + + + Stereo 360 Left to Right 180 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d758e430c2..fc6af9857f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1156,7 +1156,10 @@ 360 - 360 Stereo + Stereo 360 Top to Bottom + + + Stereo 360 Left to Right 180