mirror of
https://github.com/bearstonem/openmohaa-vr.git
synced 2026-10-28 17:00:29 +01:00
d9968210aae2e4caefe2bcd84cae2afd0d60cd76
22
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
d9968210aa |
fix(vr): correct visual glitches and reduce rendering overhead
Restore desktop HUD behavior and objective state, fix stereo text lifetimes, HUD placement and colors, sprite culling, and font shader passes. Handle swapchain timeouts and incomplete stereo frames safely, remove forced GPU waits, and correct rendering timing attribution. Track client-game source additions in CMake. Validated desktop and Android GL1/GL2 builds plus targeted regression and sanitizer checks. Headset appearance and performance remain unverified. |
||
|
|
bd72599ab7 |
feat(vr): the HUD comes up when asked, and the compass is whole
The group is hidden now. The off hand's X brings it up for vr_hudTime seconds and then it fades over 600ms. Almost everything it carries - how much ammo is left, which way the objective is - is only wanted at the moment it is asked for, and one that is always there is always in the way of a game about looking at things. The press is a timestamp rather than a toggle. The VR layer writes Sys_Milliseconds into vr_hudShow on the button's down edge and cgame reads it against cgi.Milliseconds, which is the same function, so there is no state to latch and no edge to clear on the far side - and a second press during the fade simply restarts it. Only the down edge counts: holding the button should not keep restarting the timer, and should not hold the group open either, because a button held down is a button the player has forgotten about. vr_hud 2 pins it up, which is what to use while arguing with the layout. X used to jump, and jump was also on the right hand's A, so it was a button spent twice. The HUD is worth one of them and jump keeps A. Left Y is the better candidate and is not used here: it still sends +scores for an objectives overlay that no longer appears on its own, since the objectives moved into this group and vr_objectivesAlways forces them on regardless. Moving the HUD there would cost nothing, and would give X back to jump. The compass has the two layers it was missing. The damage ring is stats[STAT_DAMAGEDIR] drawn through compasspain, fading over 1500ms from whenever the value last changed - it is a headingspinner, which unlike the objective is the variant that does subtract the view angles, so the direction arrives in world terms and has to be brought round to the head, and the +180 in that is because the stat points at the attacker while the ring marks where the damage came in. The objective is compassobjarrow turned to its angle rather than the pip that stood in for it while nothing here could draw anything rotated. vr_hudGroupY is 0.92, which is 0.63 of the eye. 0.68 is as low as this can reach at all - vr_hudScale compresses everything towards the middle of the display, so past that the cvar has nothing left to give and the size has to come down instead. And a guard where the font bug actually lives. glGenTextures never legitimately hands back 0 - it is the reserved name - so a zero means the call did not write, and nothing downstream notices: Upload32 uploads into texture zero, GL_Bind binds it, and the surface samples opaque black, which is what every string in the game looked like. R_CreateImage now asks a second time and says so if it still comes back empty. That is a guard and not the cure, because the cause is still unknown. What is known is that three of the four fonts recover and verdana-14 does not, that it is the first one created, and that its image is alive and correctly named with no texture attached - so it was never freed, and R_FreeImage and R_DeleteTextures both memset, which is how that was ruled out. The trace prints every font atlas as it is created, with its slot and its texnum, because from the outside an image that was never created and one created without a texture are indistinguishable by the time anything draws with it, and telling those apart is the next step. Until it is found, CG_HudDrawElements still draws its text as boxes. |
||
|
|
e6a7c76f84 |
feat(vr): one HUD in the middle of the view, and the two bugs that hid it
The retail HUD is five menus from ui/hud_*.urc, each aligned to a corner of a 4:3 screen. On a monitor those corners are a glance away; through a lens they are the far edge, present and unreadable. cg_vrhud.cpp draws the same information again as one group below the centre of the view. Nothing is read back out of the widgets, because there is nothing in them to read: every one is a formatter over cl.snap.ps.stats[], thirty-two integers, and the .urc files are pure layout - hud_health.urc is "playerstat 0, maxplayerstat 1, draw it as a vertical bar". cgame has the same array as cg.snap->ps.stats[]. The .urc files were still worth reading, as the spec for which stat index and which shader each element uses, and the decode of the two compass angles is taken from the widgets that already do it rather than guessed: the heading is the eye yaw less stats[STAT_COMPASSNORTH]/182, a sixteen bit angle, and the objective is stat/10 - 180 and is already relative to the view, because hud_compass.urc draws it as a plain spinner and not the headingspinner variant that subtracts the view angles itself. It is drawn from cgame rather than as another widget for a reason beyond taste. cgame's 2D goes through the one full screen window UIView3D::set2D sets up, and that window is converged as a whole. A widget would arrive with its own window, which is the first bug below. Convergence: the correction belongs to the display, not to the window. R_VRAdjust2DOrtho bent whatever ortho window it was handed, and in this engine that window is usually one widget - UIWidget::set2D cuts a viewport and an ortho window to each widget's own frame. The correction was therefore scaled to the widget, and a 150 pixel compass in a 1680 pixel eye got about a twelfth of the shift it needed while the objectives, drawn through the full screen window, got all of it. Every element ended up with a disparity in proportion to its own width, so each sat at a different apparent depth, and no vergence resolves that set - which is what "the HUD will not converge" was. It is R_VRAdjust2DRect now, applied to the viewport and scissor rectangle in eye buffer pixels, which every 2D window shares. The scissor has to move with it or a widget the shift has displaced is clipped away at where it used to be. vr_hudOffset goes back to 1 in autoexec.cfg: 0 was set on a measurement of the old bug, and it is the one value that cannot fuse at all, because the centre of each eye's buffer is about seven degrees off its own axis in opposite directions, so it asks the eyes to diverge by fourteen degrees. Text: the font atlases had no texture. Every string drew as a flat black rectangle - 0,0,0 with no glyph structure, measured off an eye dump rather than a compositor screenshot. The atlas is a 32 bit TGA with a real alpha channel and the shader is correct, so neither of the obvious causes held. The shader's own stage reported texnum 0, and GL_Bind binds image->texnum straight, so it was binding texture zero, which samples opaque black. The image is alive and correctly named, so it was never freed - R_FreeImage and R_DeleteTextures both memset, and the name survived. texnum was zero from creation. R_LoadFontShader now notices and reloads, but it has to free the image first: R_FindImageFileOld looks in the hash before it looks on disk, so asking by name returns the very image that has no texture. Three of the four fonts come back with a texture; verdana-14, the first one created, still comes back as zero on a fresh load from disk, which means qglGenTextures is not writing to it and the real bug is upstream in image creation. That is not fixed here, and CG_HudDrawElements still draws its text as boxes because of it. Also in here: R_DrawTrianglePic added to the cgame imports, which the UI has always had as re.DrawTrianglePic and is the only way to draw anything rotated, so the compass card can turn under a fixed needle the way hud_compass.urc does it. The retail corner HUD is hidden from the "Hide the HUD when necessary" block in UI_Update rather than from the explicit Display calls above it, because that branch returns early and is not the path taken - when it is not, UpdateViews walks the whole tree and draws every shown menu regardless, which is how hud_health and hud_ammo stayed on screen through the first attempt. Bars are R_DrawBox rather than a stretched gfx/2d/blank. That shader is declared alphaGen oneMinusVertex, which inverts the alpha: the bars asked for at 1 came out at 0 and the backdrop asked for at 0.35 came out at 0.65. vr_hudGroupY is 0.73 and does not mean what it looks like. It places the cluster in the flat frame, which is then scaled to vr_hudScale about the middle of the display and lifted by the eye's vertical frustum asymmetry, 170 pixels of 1760 here - so 0.73 is a little below the middle of what is actually seen and 0.62 was a little above it. The reachable range is about 0.13 to 0.68 of the eye. Not done: the damage direction ring, the objective as compassobjarrow rather than a pip, and the compass worth having, which is one the player takes out of a pocket and holds. |
||
|
|
bec1f4a8d5 |
fix(render): stop dynamic lights darkening the world, and record why
Firing in the Bocage multiplies roughly a sixth of the screen down by about 4x for as long as the trigger is held. It is the ground plane, with hard polygon edges, and it is not black - the texture is still there underneath. r_worldDlights 0 in autoexec.cfg stops the world responding to dynamic lights at all, which loses flashes lighting the scenery and is the smaller loss. The cause is not found. Seven theories were tried and all seven were wrong, so what this mostly commits is the evidence, in docs/markdown/04-coding/05-dlights.md next to the gl4es notes, so that picking it up again does not start from nothing: the cadence table and the frame difference that measured the 4x, a table of every dead theory with what killed it, the worked texcoord arithmetic showing the faces land correctly, and the one hard contradiction to resume on - every component of the dynamic lightmap path measures correct end to end, and switching the path off makes the symptom stop. Two things in there are recorded specifically because they look damning and are not. "texels 9275" against "rows 28" cannot fit, but R_RealDlightFace counts c_dlightTexels before it knows whether any light landed and then bails at !added without committing to dli.allocated, so failed surfaces are counted and take no space. And R_UploadDlights' guard on tr.pc.c_dlightSurfaces reads like it must always be zero from the back end, because R_IssueRenderCommands clears the counters before running the command list - it is not what happens, measured at 35 surfaces. That guard is gone anyway: the height below it is the same test done against the allocator's own state rather than against a debug counter. The workaround is its own cvar rather than a level of vr_traceDlight, which is what it was while it was being tested. A diagnostic switch that also changes behaviour means turning tracing off silently brings the bug back, and this port has already lost a day to a cvar that did not do what its name said. The diagnostics stay, off. 1 reports what each upload contained and where every dlit face landed in the atlas; 2 stains the whole atlas white and paints the lit texels magenta, which is read off the walls rather than out of the log; 3 skips ProjectDlightTexture, which was measured never to run here and is kept because proving that cost a run. The first stain was dark green and was useless in a level made of hedgerows - white can only brighten, so it cannot be mistaken for foliage. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
6f443949ae |
fix(vr): the death screen was drawn on the player's face
The stats screen the game raises when the player dies, and again at the end of
a mission, went into both eye buffers as screen space 2D - at the near plane, a
hand's width away, with no disparity to fuse and each eye warping it through its
own asymmetric frustum. That is the same fault
|
||
|
|
711cad20e6 |
feat(vr): reach into the world - use, holsters, ladders and thrown grenades
Four interactions that all needed the same thing: the hands' positions and velocities on the other side of the game module boundary. They share the plumbing, so they share a commit. use moves to B on the weapon hand, where the reference puts activate. The objectives list keeps the left hand's button; it had been bound to both, which was a button spent twice on one rarely opened screen. The trace also starts from the head's real position now rather than the body's notional eye - the two come apart exactly when leaning through a doorway to reach the thing on the far side. Weapons live on the body, the way Half-Life: Alyx does it: pistol at the right hip, grenades at the left, rifle and smg over the shoulders. Reaching back to where a weapon came from puts it away, reaching anywhere else swaps - holstering on any zone would have cost two gestures for every swap. Zones are measured from the head and rotated by the head's heading, so they stay on the body rather than wandering as the player turns. Ladders are climbed by pulling on them. MOHAA moves a climbing player by the root motion of the climb animation, with pmove skipped entirely, so this runs after the animation rather than instead of it: the animation still plays and the distance travelled is the distance the hand pulled. Both grips, averaged when both are held, and the grips stop meaning reload and rifle-steady while on a ladder. Grenades leave along the arm. Which instant of a swing to read is the whole problem and took several passes: averaging across the release blends the wind up with the follow through; the peak of the whole swing is usually the *backswing*, which threw grenades over the player's shoulder. What works is the natural gesture's own shape - take the direction the hand had as it let go, then find the fastest sample that agrees with it, which excludes the wind up by construction. Strength is a fraction of the game's own throw so all of MOHAA's grenade tuning survives. The sideways damping is the one deliberate lie. An underhand swing crosses the body, so the hand really is moving sideways at release - measured, lobs drifted left by up to 0.73, more sideways than forward. Damped in proportion to how gentle the throw is, so hard throws keep their true direction. A weapon wheel is included and is not finished: its input works and a tap of the same stick holsters, but the ring never appeared on screen and the drawing was not chased down. It is harmless - a hold selects nothing and does nothing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
214933c5fe |
perf(vr): render the eyes at the resolution the runtime asks for
vr_resolutionScale has been at 0.6 since the days when a frame cost 50-60 ms per
eye, which meant the headset was being given 1008x1056 against a native
1680x1760 - 36% of the pixels it has.
The measurement that justified it has since answered the opposite question.
Rendering at 36% of the pixels bought only 23% of the frame time back, which is
what proved the cost was never fill rate: it was CPU in rend2's back end binding
a GLSL program per surface per eye. Swapping to renderergl1 on gl4es took the eye
from 50-60 ms to 4-6, and nothing has claimed that budget since.
So it goes back to 1. The cvar stays rather than being deleted - it is still the
first question worth asking of any frame time regression, and it is the cheapest
thing to trade if a later feature wants budget it has not got.
Measured on the device afterwards, in the Bocage:
standing at spawn 90.0 fps eyes 8-9 ms
moving in the level 77-85 fps eyes 11-12 ms, most frames over the 11 ms period
So this does not hold 90 Hz while moving and the compositor is reprojecting a
good share of frames. Kept anyway, on the judgement of how it actually plays -
the picture is much sharper and the reprojection is not felt. The number is
recorded here so the trade is a known one rather than a surprise to whoever reads
the frame times next.
Two things follow. There is now no headroom for MSAA. And the frame split cannot
be taken at face value: it reports world 1 ms against hud 5 ms, which is the
Set2DWindow flush attributing the world's deferred draw calls to the first 2D
call that forces them out - the trap in QUEST_PORTING_GUIDE.md section 7.1. The
one figure that is trustworthy is gpuwait, still 3 ms, so the GPU remains idle
and whatever the 11 ms is, it is CPU side.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
7ce4e07352 |
fix(vr): scale the world by the game's own number, and the model by its own field
Reported as the hand and weapon models rendering too large. Two causes, one of
them the same knob failing to do what its name says.
**vr_worldscale was 32.** That is roughly the id Tech number for a game built in
inches and is simply the wrong engine's. MOHAA's art is authored at 16 units per
foot, which its own tik files state where they convert from centimetres:
models/weapons/bar.tik:
scale 0.52 // 16/30.5 since world is in 16 units per foot
16 / 0.3048 = 52.5 units to the metre, and the player dimensions agree - MAXS_Z
94 and DEFAULT_VIEWHEIGHT 82 come to 1.79 m standing and 1.56 m to the eye, where
32 would have made the player over two and a half metres tall.
Being out by a factor of 1.64 does not present as a scale error. Everything
measured in metres and handed to the game was too small, so a hand held half a
metre out was placed at 16 units rather than 26 - while the hand *model* is
authored at the game's true scale. A correctly sized model drawn far too close
reads as oversized, and there is nothing in the world to compare it against. Your
own hands are the only reference object, which is why this surfaced now and not
when the world first rendered.
Set in autoexec.cfg as well as changed in code, because vr_worldscale is
CVAR_ARCHIVE and the value already stored on the device beats any new default.
That trap is documented two sections above the one this was found in.
**The adjustment string's scale field did nothing to the model.** It is
scale,right,up,forward,pitch,yaw,roll, and the reference uses the first field as
the entity scale - VrInputWeaponAlign.c reads it into test_scale and cg_weapons.c
returns it as the model's scale. Here it only ever scaled the offset, so it was a
knob that appeared inert on a model that was the wrong size. Now applied to
model->scale, and applied before anything asks for a tag: TIKI_Orientation
multiplies the tag origin by the entity scale, so anchoring first and scaling
afterwards would slide the hand off the controller by whatever the scale changed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
4f5b54c74c |
feat(vr): a hand on each controller, placed in the renderer's frame
The first person model already has the hands as separate surfaces - triggerhand,
lefthand, garandhand and viewsleeves on USarmyplyr.skd - so putting one on each
controller needs no mesh work at all. The weapon hand's copy drops the left hand
surface and a second copy is drawn on the off hand carrying only that, using the
same Surface_NameToNum and MDL_SURFACE_NODRAW the game already uses to swap
lefthand for garandhand on rifles.
Two things had to be right first, and both were wrong in ways that looked like
tuning problems rather than faults.
**The frame.** cg.refdef.vieworg is not the camera. R_VRComposeView adds the
head's play space offset to it, rotated into the body's frame, after cgame is
finished (tr_vr.c). So anything anchored on vieworg alone is left behind by
exactly the head's offset within the play space - and because the head swings
about the neck, every look around moved it, and a snap turn left it behind
entirely since a stick turn moves the body frame that a head relative offset
knows nothing about.
Both hands are now reported measured from the tracking origin, the frame
VR_PoseToView puts the eyes in, and composed the way the renderer composes the
camera:
bodyAngles = (0, viewYaw - baseYaw, 0)
handWorld = vieworg + bodyAxis * trackingOffset
RTCWQuest reaches the same place by a different road - convertFromVR rotates a
head relative offset by viewangles[YAW] - hmdorientation[YAW] and the -64 plus
hmdposition[1] * worldScale rebase then replaces its height, which comes to the
same thing. Either route works. Taking half of each does not, and did not: a
head relative offset composed as though it were play space leaves the hand at
vieworg + R*(hand - head) while the camera is at vieworg + R*head, so the head's
movement is counted twice and every hand movement is amplified. That is what
"the right hand is accelerated" was, and it happened because VR_GetWeaponPose
was left reporting the old frame while its caller had moved to the new one - the
signature is identical either way, so nothing complained. The header now says so
at length.
**The anchor.** A hand drawn at the model origin sits wherever the animation put
it, which for a rifle is a long way out in front. The model is now moved so the
hand's own bone lands on the controller instead, inverting the composition the
engine itself uses in Entity::GetTagPositionAndOrientation. The off hand anchors
on Bip01 L Hand and the weapon hand on tag_weapon_right, because what the player
is holding there is the gun and the controller is its grip.
Every candidate anchor is measured and logged rather than trusted, which earned
itself immediately: tag_weapon_left was assumed to be an unused slot parked at
the origin and is nothing of the sort (23.5 units out), while the weapon hand's
lookup was returning -1 in silence and anchoring nothing. Both now report what
they found and how far out it is, and the early return says which precondition
failed.
Tracking is confirmed 1:1 on both controllers on the device. What is left is the
wrist bones' own rotation convention, which is a constant and lives in
vr_weaponAdjust and vr_offHandAdjust.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
bd480975a3 |
fix(vr): stop the driver encoding the engine's colour a second time
Reported as flat lighting - the whole scene lit from everywhere, shadowed faces
pulled up towards lit ones. That reads as a lighting fault and is not one: the
shading is untouched, and a defect that moves the entire scene by one curve is
the pipeline rather than any model's normals.
The swapchains have always been GL_SRGB8_ALPHA8, which is the right declaration.
This engine's output is display referred - the same values a monitor would have
been handed in 2002 - so telling the compositor the images are sRGB is what makes
it pass them through instead of encoding them again.
What the format does not do on its own is stop GL encoding on the way *in*.
ES 3.0 converts linear to sRGB on every write into an sRGB attachment, blits
included, and core ES has no state to turn that off - GL_EXT_sRGB_write_control
is what adds the switch, and it defaults to on. So the driver was encoding
already encoded colour and the compositor was faithfully passing it through:
stored = encode(V), displayed = encode(V)
which is the same lifted mid tones as declaring the images linear, reached from
the other side. The format line looks like the fix and is half of it, which is
why this survived so long - a port that has done only that half reads as one
that cannot possibly have the washed out bug.
Measured on the Homeworld port, where the same defect arrived by the linear
route: a mid tone the data puts at 0.19 came out at 0.45, against 0.473 for a
straight sRGB encode of it, and the ratio between the brightest and darkest
tenth of a hull collapsed from about 5:1 to 1.76:1.
Not routed through gl4es. gl4es keeps no copy of this enable and never sets it
itself, so disabling it once holds for the whole run, the engine's own rendering
through gl4es included.
Confirmed on the device: "sRGB write conversion off" in the log, and the picture
is correct - which means considerably darker. That is how the game is meant to
look.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
013b2f5bbf |
fix(vr): put in-game fullscreen menus on the panel, and load the test map late
Three things, all found by running it. The mission briefing was drawn onto the player's face. VR_UseScreenLayer only asked whether clc.state was CA_ACTIVE, so a fullscreen menu that comes up while the player is still in the world skipped the quad layer and was drawn as screen space 2D into both eye buffers - at the near plane, a hand's width away, with no disparity to fuse and each eye warping it through its own asymmetric frustum. That is exactly what the panel exists to prevent; nothing in-game was reaching for it. It now checks UI_FullscreenMenuUp as well. The log said so before the headset did: "view3d 0" with the eye passes still running means the 3D scene is not being drawn at all, which only happens when a fullscreen menu is covering it. map in autoexec.cfg never had a chance. That file is exec'd during Com_Init and the game's own startup - the intro stages, then the menu - runs afterwards and tears down anything loaded there. vr_startMap now waits until the client is sitting idle at the menu and loads it then. vr_testStart also prints unconditionally: a hook that silently does nothing is indistinguishable from one that never ran, and telling those apart has cost enough already. vr_captureEye is a plain archived cvar rather than a VR_TuningCvar. Those force their default at every start, which is right for a value still being argued with and wrong for a switch - it made the config's own setting impossible to honour and cost a device run to notice. The handoff warns about this exact trap. vr_traceFrame is off in the shipped config: three lines a second rolls anything that prints once out of logcat within a minute. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
db8dfae0bd |
feat(vr): reload on a tap of the weapon hand's grip
The reference's gesture (VrInputDefault.c): press and release the dominant grip inside vr_reloadtimeoutms and it sends +reload, then -reload a frame later because the game wants to see the key held for a tick before it is let go. A long hold is their weapon wheel, which this port does not have, so holding simply does nothing rather than reloading late. The weapon hand grip was a second binding for use. Use stays on the right stick click, which is where the reference has it too - +activate on ovrButton_Joystick - so nothing is lost. vr_reloadTapMs is their vr_reloadtimeoutms under this port's naming. Built, not yet run: the headset has been unreachable since the previous commit. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
046e2dd9a6 |
feat(vr): put the weapon in the player's hand, from the reference
Four pieces, each adapted from RTCWQuest rather than invented. Aim. Weapon::GetMuzzlePosition derives a shot from player->m_vViewAng, which is MOHAA's analogue of rtcw g_weapon.c:1904. It now takes the controller's pitch and the yaw the hand leads the head by, added to the view yaw the game already has - the controller's absolute yaw means nothing to the game, since the player can snap turn on the stick and the play space has an arbitrary forward. Only the trace moves; the camera stays on the head. The angles reach fgame through gameImport_t rather than the usercmd, which has no spare field - angles[3] is the view direction and buttons is packed. A dedicated server answers false and keeps the aim it had. Placement. The first person model is MOHAA's FPS view model, positioned by CG_OffsetFirstPersonView against the camera and given the walk bob. Both are wrong in a headset: the hand has its own position, and shaking the view model when the camera is the player's head is a way to make people ill. It now goes where the hand is - RTCWQuest's CG_CalculateVRWeaponPosition, including dropping the origin to the feet and adding the measured head height back so the weapon hangs at the height the player is really holding it, rather than off the camera where crouching would drag it about. Selection. Nothing was bound to it at all, and there is no button left - the triggers fire, both face buttons on each hand are objectives and jump, and the sticks and grips are duck and use. The reference does not spend a button either: the dominant thumbstick up is weapprev and down is weapnext, gated on the sideways component so it cannot be read as a turn and edge triggered so a held stick does not run the inventory. This port's turn already ignores that stick's Y axis, so nothing was displaced. Two handed hold. Off hand grip squeezed with the hands close together points the weapon along the line between them instead of along the weapon hand alone - how a rifle is actually held, and steadier, because a wobble at one hand is damped by the distance to the other. Roll is halved rather than taken from the line, which carries none. The off hand grip was a duplicate of use; the weapon hand's still opens doors. The suggested binding array was 16 with 14 in use. Now 32. Aim and placement are built but not yet confirmed on the device. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
862d36a9ba |
chore(vr): take the diagnostics back out
Removes r_flatColor, r_traceSurf, r_forceGenericStage, r_noCull, r_noDepth, r_noFog, r_invertCull, vr_debugPanel and its magenta panel clear, along with R_TraceSurface, R_DriverGet, R_FlatColorPush/Pop and the GetFloatv/IsEnabled entries added to qgl.h for them. Around 570 lines. Everything they were built to find is fixed, and the fixes are all still here: the cull face forced past gl4es's cache, RB_SurfaceFace's index type, the window resize vid_restart, the frustum built from the runtime's tangents, the terrain cone sized to the headset, and the VR layer's state calls routed through gl4es. Verified rendering on the device after the strip, with no overrides at all. vr_captureEye stays, defaulted off. It writes the eye buffer to a TGA and it is the one probe that consistently earned its keep - a picture settled in one look what rounds of description could not. Inert unless switched on. misc/android/autoexec.cfg is down to that single line. Nothing in it is load bearing any more. Also records a third instance of the session's recurring mistake in the gl4es doc: r_noDepth was declared, registered and tested for several device runs while the edit meant to read it had silently failed to apply. Every result attributed to it was a no-op, and it sat in the "known good" configuration taking credit for what disabling culling was actually doing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
b49e34a9ed |
fix(vr): stop the window resize restart, and the indices the world was drawn with
Two bugs, both found on the device, both with the evidence recorded in VR_RENDERER_HANDOFF.md sections 8 and 9. The menu was a renderer restart. SDL reports a window resize; the handler compares it against glconfig.vidWidth/Height, which in VR is the eye buffer and never the Android surface, so it can never match. It set r_customwidth, r_customheight and r_mode -1 - cvars the renderer then ignores, because the VR resolution comes from GetVRRenderResolution - and scheduled a vid_restart that destroyed and rebuilt the GL context and the OpenXR session under a running frame loop. The panel drew a full 256/256 samples up to CL_Vid_Restart_f and nothing ever after. VR_PORT_STATUS called that restart harmless; it was the whole bug. RB_SurfaceFace declared its output pointer as unsigned* and assigned tess.indexes to it, which is glIndex_t* - unsigned short under gl4es. Every index went in four bytes apart in a two byte array, so each triangle picked up whichever vertex the zero high halves pointed at. That was the world smeared into streaks that dragged its textures with it; models never come through that function, which is why they were always correct. ProjectDlightTexture had the same fault in hitIndexes. The compiler had been reporting it all along, under sixty other warnings in that file. Also: renderergl1 culled against the game's symmetric fov rather than the runtime's asymmetric per-eye frustum; the VR layer set viewport, cull, depth, blend, colour mask and scissor state natively while gl4es caches all of them and drops the engine's next request for a value its stale copy already claims; and GfxInfo_f reported a different draw route than R_DrawElements took. The remaining diagnostics are CVAR_TEMP and default off. vr_captureEye is worth keeping: it writes the eye buffer to a TGA, and one image settled questions that six rounds of verbal description could not. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
27011bd3eb |
docs: hand off the renderer swap with what is proven and what is not
renderergl1 on gl4es runs on the headset at 90fps and draws the engine's own geometry - the intro logo screen renders under a flat colour override. The main menu still draws nothing, and that is the one open problem. The largest blocker turned out to be the gl4es build itself: with CMake's flags it cleared framebuffers perfectly and rendered no geometry whatsoever, measured as 144 red from a clear and 0 green from a quad into the same buffer moments later. Matching RTCWQuest's Android.mk fixed it outright with no engine-side change. gl4es's own CMakeLists adds -DANDROID under the NDK toolchain and ndk-build never does, so the reference has only been exercised without it, and it gates the code that decides what the fixed function pipeline may emit. Recorded at length because it invalidated most of a session's work: while gl4es could not draw, every experiment ending "and nothing was drawn" returned the same answer whatever it was testing. Four changes were adopted on the strength of such results and are now marked for re-testing rather than trusted. The document says plainly which findings are measured and which are not. Also commits the framebuffer helpers the VR layer needs: gl4es has to own the GL_FRAMEBUFFER binding or it renders from its own table into a 16x16 pbuffer, while the swapchain texture attachment and the blit's read and draw bindings must stay with the driver - routing those through gl4es empties the blit and takes the cutscenes with it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
17771047c5 |
fix(vr): stop binding the eye framebuffer through gl4es
The headset was black while the frame loop ran at a contented 90 fps and the audio played. Everything was being drawn into the window nobody looks at. Routing the renderer's glBindFramebuffer through gl4es - so that gl4es would see every call the renderer makes - is right in general and wrong for this call in particular. gl4es can only bind framebuffers it created itself: its glBindFramebuffer looks the name up in its own table, and every framebuffer the VR layer owns comes from the driver's glGenFramebuffers. The lookup misses, it raises GL_INVALID_VALUE, returns without binding, and goes on believing framebuffer 0 is current. So the eye redirect quietly did nothing. RTCWQuest's renderer never binds a framebuffer at all - Quake 3's fixed function path predates them - and TBXR_Common binds the eye directly, leaving gl4es to draw into whatever the driver has. renderergl1 is the same renderer and this now does the same thing. What the renderer is still told about is the flush. gl4es batches geometry and issues it lazily, so work built for one eye would otherwise arrive in whichever target is bound when it finally goes out. RE_SetDefaultFramebuffer flushes while the old target is still current and binds nothing, which is also why the VR layer calls it before its own bind rather than after. readfboBegin and readfboEnd early out while gl4es's read and draw framebuffers are the same, which they always are here, so gl4es does not rebind behind this. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
d9960e1857 |
fix(vr): bind the engine's GL context against a pbuffer, as RTCWQuest does
The OpenXR session could not be created: eglGetCurrentContext returned nothing while SDL was certain a context was current. SDL binds the GL context against the window's EGL surface. In a headset there is no window being presented - every pixel reaches the display through the swapchains - so that surface never arrives. What SDL does then is the trap. SDL_EGL_MakeCurrent, handed no surface and without gl_allow_no_surface, calls eglMakeCurrent(EGL_NO_SURFACE, EGL_NO_CONTEXT) to unbind everything and returns success; SDL_GL_MakeCurrent then records the context as current. So SDL's bookkeeping and EGL's actual state disagree permanently, and asking SDL to bind it again does nothing, because SDL_GL_MakeCurrent sees its own record agree and returns early. RTCWQuest never meets this because it never depends on a window surface: it makes its context current against a 16x16 pbuffer (TBXR_Common.c, egl->TinySurface). Same here, except the context is the one SDL already made, since the engine renders in it. The EGLConfig comes from eglQueryContext, which VR_CreateSession was already recovering for the graphics binding. The pbuffer outlives the session deliberately - the session is torn down and rebuilt around renderer restarts, and unbinding the context in the middle of that is the thing this prevents. It is released in VR_Shutdown. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
a8a1314c2d |
fix(vr): ask for an ES context for the fixed function renderer, and check it is current
Two problems the first device run found, one hiding the other. GLimp_SetMode's fixedFunction branch offers exactly one context: desktop OpenGL 1.1. There is no such thing on Android, so window creation failed three times over and R_Init gave up with "Couldn't get a visual". renderergl2 never met this because it takes the other branch, which already knows how to ask for ES. gl4es is what makes the request answerable, and it wants the opposite of what the renderer believes it is getting: desktop GL 1.x presented upwards, OpenGL ES underneath. So the fixed function branch now asks for ES 3.0 and then ES 2.0, and the renderer is never told - it reads its version through gl4es, which reports desktop GL, and takes the desktop fixed function path on the strength of it. That got the renderer up, and revealed the second problem: the OpenXR session found no current EGL context. Which is where gl4es having its own opinion about glGetString stops being convenient. The engine's proof that a context is live is GL_VERSION coming back non-NULL, and gl4es answers VERSION, VENDOR and RENDERER out of constants without asking the driver - so a context that was created but never bound passes that test and is only noticed much later, somewhere else entirely. GLimp_SetMode now asks SDL directly whether the context it just made is current, restoring the check gl4es defeated. The session no longer assumes one is bound either. Being current is per thread, and a window that has not been shown yet has no surface to be current against, so VR_CreateSession asks for the binding through GLimp_MakeCurrent rather than requiring it to already be there, and reports the EGL handles when it cannot get one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
d2d52a5af4 |
fix(vr): tell the renderer before moving the framebuffer under it
The VR layer bound each eye, the UI buffer and the wrist panel directly and then told the renderer afterwards. That order was harmless while the renderer issued every call as it made it. It is not harmless under gl4es, which batches geometry and flushes lazily: it only learns to flush when something it knows about changes the target, so a batch left over from one pass would arrive in whatever was bound next - the previous eye's contents showing up in this one. Routing the transition through re.SetDefaultFramebuffer first gives gl4es the chance to flush into the target the geometry was drawn for. The direct bind stays where it was needed, since the renderer only binds framebuffers when its own framebuffer support is on; it is now a no-op against the same target. Also points the Android docs at renderergl1 with gl4es under it, and records that libgl4es.so rides into the APK as a dependency of libopenmohaa.so rather than being loaded by name. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
7872929f32 |
feat(vr): head aim, stick locomotion and comfort options
The game now knows where the player is looking. Until now head tracking only moved the camera the renderer drew from, so anything consulting the player's view angles - objectives that ask them to look at something, aim, hit detection - never saw it. CL_VRMove writes the head's heading and pitch into cl.viewangles, which is what all of that reads. Walking follows the off hand rather than the gaze, after the approach in Team Beef's RTCWQuest. Tying travel to where the player is looking means they cannot glance sideways without veering, which is what makes room scale movement feel like being dragged; steering with the free hand lets them look around and still walk a straight line. cl_vrWalkWithView restores gaze-directed movement. Turning snaps by default. A stick that rotates the world smoothly while the inner ear insists otherwise is the usual way to make people ill, and one flick gives one step rather than one per frame. cl_vrSnapAngle 45 degrees per flick cl_vrSmoothTurn 0 1 for smooth turning instead cl_vrTurnSpeed 90 degrees/second when smooth cl_vrDeadzone 0.2 cl_vrWalkWithView 0 1 to walk where the player looks All archived, so they are tunable on the headset rather than at build time, which is what section 5 of the porting plan asks for. Thumbsticks are bound on the Touch profile only: the simple controller has none, and offering a binding a runtime does not recognise makes it reject the whole set, which would cost the fallback its pointer too. The dominant hand's aim angles are carried through vrInput_t but not yet used. Aim is still on the head, which section 5 is right to call wrong for a shooter - that belongs with weapon handling, and looking at things had to work first. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
5aff8d336a |
feat(vr): stereo rendering and 6DoF head tracking through OpenXR
Runs on a Meta Quest 3 in stereo at the headset's native 1680x1760 per eye, with the world tracking head movement in six degrees of freedom. The session binds to the EGL context SDL already created rather than a private one, so there is a single GL context and the renderer needs no thread or context juggling. Each eye is rendered into an OpenXR swapchain image by substituting the framebuffer the renderer treats as the screen, which leaves the whole render path untouched. Exactly two eye passes per frame; there is deliberately no third pass to produce a flat image. The session and its swapchains belong to the GL context they were made with, so they are torn down and rebuilt around the renderer restarts the engine performs. Menus, the loading screen and cinematics go on a quad layer fixed in the room, placed once in front of wherever the player is facing. Drawing screen space content into both eyes instead gives it no stereo disparity while each eye still warps it through its own asymmetric frustum, and the result cannot be fused. The panel is drawn into one persistent buffer that is then copied into the swapchain image: a swapchain rotates through several images, and the engine's 2D path expects the screen to still hold what it drew last frame. Head tracking: - The per-eye asymmetric frustum from xrLocateViews feeds R_SetupProjection directly. A symmetric approximation misaligns the two images by a few degrees, which the eyes cannot fuse. - The pose is applied in RE_RenderScene, so it follows every camera the game has - the player's, cutscenes, ladders, death animations - without each needing to know about VR. - Orientation is carried as axes rather than pitch/yaw/roll, and composed with the game's heading as a rotation. Adding Euler angles only works while pitch and roll are near zero and turns every head movement into a twist once they are not. - Position is measured from where the head was when tracking started, so the game keeps supplying eye height and the headset contributes only leaning, ducking and stepping. The initial heading is subtracted too, or the play space's arbitrary forward is added to the character's and the player spawns facing the wrong way. The controllers point at the panel and click: the aim ray is intersected with the quad and the hit becomes the engine's cursor position, so the menus behave as they always did. The renderer's own framebuffer objects stay off on Android. Each eye is already a swapchain image, so routing through tr.renderFbo first would cost a second full resolution pass and resolve per eye for nothing. Known gaps: head orientation does not yet reach the player's view angles, so the game does not know where the player is looking; and there is no controller model or pointer ray. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |