Skip to content

Fix stereo and 180 degree video rendering, add 360 stereo side-by-side - #2038

Closed
NataleVIL wants to merge 3 commits into
Igalia:mainfrom
vil-it:upstream-video-fixes
Closed

Fix stereo and 180 degree video rendering, add 360 stereo side-by-side#2038
NataleVIL wants to merge 3 commits into
Igalia:mainfrom
vil-it:upstream-video-fixes

Conversation

@NataleVIL

@NataleVIL NataleVIL commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

With the move from OculusVR to OpenXR several VR video formats stopped working and were fixed one format at a time — 3D side-by-side in 6f5793b, Stereo 180 Left to Right in 01cca10 (Fixes #562). The remaining equirect projections were never revisited and still carry both of the defects #562 described. Three independent commits, each builds on its own.

Both eyes get the same half of the frame — #562: "flat (no depth) with same image in each eye". VRLayer defaults useSameLayerForBothEyes to true, so GetNumXRLayers() submits a single equirect layer with XR_EYE_VISIBILITY_BOTH and applies only the left eye's UV transform. create180LRProjectionLayer() got the fix in 01cca10; create360StereoProjectionLayer() and create180TBProjectionLayer() did not. The default is now documented where it is declared, since nothing marked that call as load-bearing.

The 180 degree hemisphere is not centred — #562: "stretched to a dome almost to the extent when there should be black there". A horizontal UV scale of 2 maps the texture onto half the sphere, so without the matching -0.5 bias that half sits 90 degrees off to the side and looking forward shows the edge of the image. Also fixed for left-to-right only in 01cca10; create180ProjectionLayer() and create180TBProjectionLayer() were left out.

Adds VIDEO_PROJECTION_360_STEREO_LEFT_RIGHT. Full-sphere stereo existed only as over/under. Adds mesh and compositor-layer implementations, a menu entry and mozVideoProjection=360lr, plus 360tb as an alias for 360s. Neither the "360 Stereo" label nor its icon said that it means over/under, so both 360 modes now follow the naming and divider-bar icon conventions of the 180 modes: 360 Stereo → Stereo 360 Top to Bottom, new → Stereo 360 Left to Right. The string key is kept so existing translations are not orphaned; de and en-rGB are updated. Happy to use a new key, or hand the icons to someone with a better eye, if you prefer.

Tested on a Pico Neo 3 (OpenXR, Gecko) with generated equirect videos where one half is red and the other blue. Before, all three stereo modes showed red in both eyes; now the left eye shows red and the right blue, and 180 top/bottom is centred. Can post the ffmpeg and adb commands if useful.

@svillar svillar left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wow, this is awesome work. Thanks for the contributions. I just have some minor comments.

Comment thread app/src/main/cpp/VRLayer.cpp Outdated
Comment thread app/src/main/cpp/VRVideo.cpp Outdated
@svillar

svillar commented Aug 7, 2026

Copy link
Copy Markdown
Member

BTW, it'd be awesome if you could paste here a few examples that you tried to verify how it works

@svillar svillar added the release_candidate PR that should be part of the next release label Aug 7, 2026
@svillar

svillar commented Aug 11, 2026

Copy link
Copy Markdown
Member

hi @NataleVIL do you have plans to work on this? I think it'd be awesome to include it in the next upcoming release

@NataleVIL

Copy link
Copy Markdown
Contributor Author

hi @NataleVIL do you have plans to work on this? I think it'd be awesome to include it in the next upcoming release

Yes I am looking into it right now :)

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.
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.
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. 360s is replaced by 360tb so
the query-string vocabulary matches 180tb/180lr/3dtb throughout. The
specific values are matched before the bare 360 prefix, as already
needed for 180lr and 180tb. The menu lists left-right before top-bottom,
following the order the 180 degree modes already use.

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, en-rGB and it 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.
@NataleVIL
NataleVIL force-pushed the upstream-video-fixes branch from dde3b01 to 031b403 Compare August 11, 2026 11:36
@NataleVIL

Copy link
Copy Markdown
Contributor Author

@svillar
I addressed all review comments: comments removed, 360s dropped. The 360 menu
entries now list left-right before top-bottom, matching the order the 180 modes
already used. Rebased into the original three commits rather than added on top,
so the branch is force-pushed.

I was not able to find a video for each mode, so I used AI to build this small local test suite:

Test recipe

Reproducing this needs nothing but ffmpeg, python3 and a tethered headset.
It only covers the modes this PR touches.

1. Generate the clips. Nothing is downloaded; the frames are drawn by
ffmpeg itself from lavfi sources:

# One eye: a colour fill, a faint grid, and a white cross on the frame centre.
# Left eye red, right eye blue — a mono render therefore shows one colour in both.
eye() {
  local colour=$1 w=$2 h=$3
  echo "color=c=$colour:s=${w}x${h}:d=20"\
",drawgrid=w=$((w / 16)):h=$((h / 8)):t=2:c=white@0.3"\
",drawbox=x=$((w / 2 - 8)):y=0:w=16:h=$h:c=white@0.9:t=fill"\
",drawbox=x=0:y=$((h / 2 - 8)):w=$w:h=16:c=white@0.9:t=fill"
}

# Two eyes stacked into one frame. $4 is hstack (side-by-side) or vstack (top-bottom).
mk() {
  local name=$1 w=$2 h=$3 stack=$4
  ffmpeg -loglevel error -y \
    -f lavfi -i "$(eye 0x902020 "$w" "$h")" \
    -f lavfi -i "$(eye 0x202090 "$w" "$h")" \
    -filter_complex "[0:v][1:v]$stack[out]" -map "[out]" \
    -c:v libx264 -pix_fmt yuv420p -r 30 "$name.mp4"
}

mkdir -p wolvic-test && cd wolvic-test
mk 360tb 2048 1024 vstack   # frame 2048x2048
mk 360lr 2048 1024 hstack   # frame 4096x1024
mk 180tb 1024 1024 vstack   # frame 1024x2048
mk 180lr 1024 1024 hstack   # frame 2048x1024

The sizes passed to mk are per eye, and they follow from
createSphereProjection, which sweeps 2 * M_PI horizontally for 360 and
M_PI for 180, over M_PI vertically in both cases — so 2:1 per eye for 360
and 1:1 per eye for 180. Worth checking before drawing conclusions from a clip:

for f in *.mp4; do
  ffprobe -v error -select_streams v:0 -show_entries stream=width,height -of csv=p=0 "$f"
done

2. Serve them. mozVideoProjection is read from the page URL, so each mode
is just a link:

cat > index.html <<'HTML'
<meta charset="utf-8">
<video controls autoplay loop muted playsinline style="width:100%"></video>
<p id="links" style="font-size:22px"></p>
<script>
  const q = new URLSearchParams(location.search);
  document.querySelector('video').src = (q.get('v') || '360tb') + '.mp4';
  document.getElementById('links').innerHTML = ['360tb','360lr','180tb','180lr']
    .map(m => `<a href="?v=${m}&mozVideoProjection=${m}" style="margin-right:18px">${m}</a>`)
    .join('');
</script>
HTML

python3 -m http.server 8000

With the headset tethered, forward the port from another terminal:

adb reverse tcp:8000 tcp:8000

3. Open http://localhost:8000/ in Wolvic, pick a mode, tap the video and
go fullscreen. The projection only applies in fullscreen.

What to look for

clip before after
360tb both eyes red — rendered mono left eye red, right eye blue
360lr mode did not exist left eye red, right eye blue
180tb, 180lr both eyes red — rendered mono left eye red, right eye blue
either 180 clip the hemisphere is offset by half its width, so looking straight ahead does not land on the centre of the image the cross sits straight ahead

The grid only makes seams and distortion visible; it is not part of any check.

Verified on a Pico Neo 3

@NataleVIL
NataleVIL requested a review from svillar August 11, 2026 16:48

@svillar svillar left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should keep these 3 commits in 3 different PRs to discuss them individually. My recommendation would be to move them to other PRs so we could merge them, the two first look great but the 360º one cannot be correct.

You're doing great changes but you aren't touching the youtube extension. This means that "360lr" branch won't ever be reached, same for "360tb". The YouTube extension only generates "360_auto" and "360_auto"

@svillar

svillar commented Aug 19, 2026

Copy link
Copy Markdown
Member

OK let's close this then. Moved to #2052, #2053 and #2054.

@svillar svillar closed this Aug 19, 2026
@svillar svillar removed the release_candidate PR that should be part of the next release label Aug 19, 2026
@NataleVIL
NataleVIL deleted the upstream-video-fixes branch August 27, 2026 13:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Stereo 180 Left to Right projection broken

2 participants