#include "LunarWM_render.h" #include "LunarWM_core.h" #include "LunarWM_xr.h" #include "common.h" #include "vec.h" #include #include #include #include static inline SphericalCoord get_forward_spherical_with_nearest( Vector3 fwd, float r) { if (fabs(fwd.y) < 0.2f) { fwd.y = 0; } Vector3 vec = Vector3Scale(Vector3Normalize(fwd), r); return Vector3ToSpherical(vec); } static inline Matrix xr_matrix(XrPosef const pose) { Matrix const translation = MatrixTranslate(pose.position.x, pose.position.y, pose.position.z); Matrix const rotation = QuaternionToMatrix((Quaternion) { pose.orientation.x, pose.orientation.y, pose.orientation.z, pose.orientation.w, }); return MatrixMultiply(rotation, translation); } static inline Matrix xr_projection_matrix(XrFovf const fov) { static_assert(RL_CULL_DISTANCE_FAR > RL_CULL_DISTANCE_NEAR); Matrix matrix = {}; auto const near = (float)RL_CULL_DISTANCE_NEAR; auto const far = (float)RL_CULL_DISTANCE_FAR; float const tan_angle_left = tanf(fov.angleLeft); float const tan_angle_right = tanf(fov.angleRight); float const tan_angle_down = tanf(fov.angleDown); float const tan_angle_up = tanf(fov.angleUp); float const tan_angle_width = tan_angle_right - tan_angle_left; float const tan_angle_height = tan_angle_up - tan_angle_down; matrix.m0 = 2 / tan_angle_width; matrix.m4 = 0; matrix.m8 = (tan_angle_right + tan_angle_left) / tan_angle_width; matrix.m12 = 0; matrix.m1 = 0; matrix.m5 = 2 / tan_angle_height; matrix.m9 = (tan_angle_up + tan_angle_down) / tan_angle_height; matrix.m13 = 0; matrix.m2 = 0; matrix.m6 = 0; matrix.m10 = -(far + near) / (far - near); matrix.m14 = -(far * (near + near)) / (far - near); matrix.m3 = 0; matrix.m7 = 0; matrix.m11 = -1; matrix.m15 = 0; return matrix; } static void DrawBillboardNoShear( Camera3D const cam, Texture2D tex, Vector3 pos, float scale, Color tint) { Rectangle const src = { 0, 0, tex.width, tex.height }; Vector2 const size = { scale * fabsf(src.width / src.height), -scale }; Vector2 const origin = { size.x * 0.5f, size.y * 0.5f }; DrawBillboardPro(cam, tex, src, pos, cam.up, size, origin, 0.0f, tint); } void DrawTextureCyl( Texture2D tex, Vector3 center, float radius, float scale, bool y_flip) { if (!tex.id || scale <= 0.0f || radius == 0.0f) return; float r = fabsf(radius); float arc_len = (float)tex.width * scale; // arc length in world units float theta = arc_len / r; // radians across the panel float half_t = 0.5f * theta; float half_h = 0.5f * (float)tex.height * scale; // mid-angle around Y so the segment's middle sits at 'center' float a0 = atan2f(center.x, center.z); // shift so the cylinder surface midpoint matches 'center' Vector3 mid_ref = (Vector3) { sinf(a0) * r, center.y, cosf(a0) * r }; Vector3 delta = Vector3Subtract(center, mid_ref); // tessellation: about 3° per slice (min 8) int slices = (int)ceilf(fmaxf(theta * (180.0f / PI) / 3.0f, 8.0f)); if (slices > 1024) slices = 1024; float vt = y_flip ? 1.0f : 0.0f; float vb = y_flip ? 0.0f : 1.0f; rlDrawRenderBatchActive(); // flush any prior state rlSetTexture(tex.id); rlDisableBackfaceCulling(); rlColor4ub(255, 255, 255, 255); rlBegin(RL_QUADS); for (int i = 0; i < slices; ++i) { float u0 = (float)i / (float)slices; float u1 = (float)(i + 1) / (float)slices; float aL = a0 - half_t + theta * u0; float aR = a0 - half_t + theta * u1; Vector3 nL = (Vector3) { sinf(aL), 0.0f, cosf(aL) }; Vector3 nR = (Vector3) { sinf(aR), 0.0f, cosf(aR) }; if (radius < 0.0f) { nL = Vector3Negate(nL); nR = Vector3Negate(nR); } Vector3 pLT = Vector3Add( (Vector3) { nL.x * r, center.y + half_h, nL.z * r }, delta); Vector3 pLB = Vector3Add( (Vector3) { nL.x * r, center.y - half_h, nL.z * r }, delta); Vector3 pRT = Vector3Add( (Vector3) { nR.x * r, center.y + half_h, nR.z * r }, delta); Vector3 pRB = Vector3Add( (Vector3) { nR.x * r, center.y - half_h, nR.z * r }, delta); // match your flat-quad U flip (so WL textures look correct) float U0 = 1.0f - u0; float U1 = 1.0f - u1; // one normal per-vertex (simple cylindrical) rlNormal3f(nL.x, nL.y, nL.z); rlTexCoord2f(U0, vt); rlVertex3f(pLT.x, pLT.y, pLT.z); rlNormal3f(nR.x, nR.y, nR.z); rlTexCoord2f(U1, vt); rlVertex3f(pRT.x, pRT.y, pRT.z); rlNormal3f(nR.x, nR.y, nR.z); rlTexCoord2f(U1, vb); rlVertex3f(pRB.x, pRB.y, pRB.z); rlNormal3f(nL.x, nL.y, nL.z); rlTexCoord2f(U0, vb); rlVertex3f(pLB.x, pLB.y, pLB.z); } rlEnd(); rlSetTexture(0); rlEnableBackfaceCulling(); } static void DrawTextureCyl2(Texture2D tex, Vector3 sphere_center, SphericalCoord coord, float rad, float scale, bool y_flip) { if (!tex.id || scale <= 0.0f || rad == 0.0f) return; // midpoint on the sphere where the panel should sit (its center) Vector3 fwd = SphericalToVector3(coord); if (Vector3Length(fwd) < 1e-6f) fwd = (Vector3) { 0, 0, 1 }; fwd = Vector3Normalize(fwd); // build a local tangent frame at that point (right, up, forward) Vector3 worldUp = (Vector3) { 0, 1, 0 }; if (fabsf(Vector3DotProduct(worldUp, fwd)) > 0.99f) worldUp = (Vector3) { 1, 0, 0 }; Vector3 right = Vector3Normalize(Vector3CrossProduct(worldUp, fwd)); Vector3 up = Vector3Normalize(Vector3CrossProduct(fwd, right)); float r = fabsf(rad); float arc_len = (float)tex.width * scale; // world units across float theta = arc_len / r; // total horizontal FOV in radians float half_t = 0.5f * theta; float half_h = 0.5f * (float)tex.height * scale; // shift so cylinder's surface midpoint lands exactly at coord.r from // sphere_center Vector3 delta = Vector3Add(sphere_center, Vector3Add(Vector3Scale(fwd, coord.r - r), (Vector3) { 0, 0, 0 })); // tessellation: about 3° per slice (min 8, max 1024) int slices = (int)ceilf(fmaxf(theta * (180.0f / PI) / 3.0f, 8.0f)); if (slices > 1024) slices = 1024; float vt = y_flip ? 1.0f : 0.0f; float vb = y_flip ? 0.0f : 1.0f; rlDrawRenderBatchActive(); rlSetTexture(tex.id); rlDisableBackfaceCulling(); rlColor4ub(255, 255, 255, 255); rlBegin(RL_QUADS); for (int i = 0; i < slices; ++i) { float u0 = (float)i / (float)slices; float u1 = (float)(i + 1) / (float)slices; float aL = -half_t + theta * u0; float aR = -half_t + theta * u1; // local outward directions on the cylindrical surface Vector3 nL = Vector3Add( Vector3Scale(right, sinf(aL)), Vector3Scale(fwd, cosf(aL))); Vector3 nR = Vector3Add( Vector3Scale(right, sinf(aR)), Vector3Scale(fwd, cosf(aR))); if (rad < 0.0f) { nL = Vector3Negate(nL); nR = Vector3Negate(nR); } // surface points (center band), then top/bottom by +/- up*half_h Vector3 cL = Vector3Add(delta, Vector3Scale(nL, r)); Vector3 cR = Vector3Add(delta, Vector3Scale(nR, r)); Vector3 pLT = Vector3Add(cL, Vector3Scale(up, half_h)); Vector3 pLB = Vector3Add(cL, Vector3Scale(up, -half_h)); Vector3 pRT = Vector3Add(cR, Vector3Scale(up, half_h)); Vector3 pRB = Vector3Add(cR, Vector3Scale(up, -half_h)); // match the original horizontal flip so Wayland textures look correct float U0 = 1.0f - u0; float U1 = 1.0f - u1; rlNormal3f(nL.x, nL.y, nL.z); rlTexCoord2f(U0, vt); rlVertex3f(pLT.x, pLT.y, pLT.z); rlNormal3f(nR.x, nR.y, nR.z); rlTexCoord2f(U1, vt); rlVertex3f(pRT.x, pRT.y, pRT.z); rlNormal3f(nR.x, nR.y, nR.z); rlTexCoord2f(U1, vb); rlVertex3f(pRB.x, pRB.y, pRB.z); rlNormal3f(nL.x, nL.y, nL.z); rlTexCoord2f(U0, vb); rlVertex3f(pLB.x, pLB.y, pLB.z); } rlEnd(); rlSetTexture(0); rlEnableBackfaceCulling(); } static inline Vector3 RecenterPoint(LunarWM *wm, Vector3 p) { if (!wm->xr.recenter_active) return p; return Vector3Add(Vector3RotateByQuaternion(p, wm->xr.recenter_rot), wm->xr.recenter_trans); } static inline Quaternion RecenterOrient(LunarWM *wm, Quaternion q) { if (!wm->xr.recenter_active) return q; return QuaternionMultiply(wm->xr.recenter_rot, q); } static LunarWM_Toplevel *find_toplevel(LunarWM *this, int id) { for (size_t i = 0; i < vector_size(this->wayland.v_toplevels); i++) { auto *tl = this->wayland.v_toplevels[i]; if (tl->id == id) return tl; } return NULL; } void LunarWM_render_hud(LunarWM *this, float /*dt*/, int hud_size) { ClearBackground((Color) { 0, 0, 0, 0 }); float const text_size = this->cman->cfg.displays.hud.font_size; char const *txt = TextFormat("WAYLAND_DISPLAY=%s", getenv("WAYLAND_DISPLAY")); auto txt_w = MeasureText(txt, 24); DrawText( txt, hud_size / 2 - txt_w / 2, hud_size - text_size, text_size, WHITE); txt = TextFormat("DISPLAY=%s", getenv("DISPLAY")); txt_w = MeasureText(txt, text_size); DrawText(txt, hud_size / 2 - txt_w / 2, hud_size - text_size * 2, text_size, WHITE); { time_t t = time(NULL); struct tm *tm_info = localtime(&t); int hours = tm_info->tm_hour; int minutes = tm_info->tm_min; txt = TextFormat("%02d:%02d", hours, minutes); txt_w = MeasureText(txt, 32); DrawText(txt, hud_size / 2 - txt_w / 2, 0, text_size, WHITE); } } void LunarWM_render_windows(LunarWM *this, bool alpha_check) { for (size_t i = 0; i < vector_size(this->wm.workspaces[this->wm.active_workspace].v_windows); i++) { auto *window = &this->wm.workspaces[this->wm.active_workspace].v_windows[i]; auto *tl = window->tl; if (!tl || !tl->surface) { continue; } if (tl->gles_texture) { if (alpha_check && tl->composed_has_alpha) { continue; } Texture2D tex = tl->rl_texture; bool y_flip = false; if (IsRenderTextureValid(tl->surface_rt)) { tex = tl->surface_rt.texture; tex.width = tl->rl_texture.width; tex.height = tl->rl_texture.height; y_flip = true; } if (!tex.id) continue; float rad = window->coord.r - 0.01f * (float)i; DrawTextureCyl2(tex, Vector3Zero(), window->coord, rad, this->cman->cfg.space.window_scale, y_flip); } } } static void render_3d(LunarWM *this, float /*dt*/) { LunarWM_render_windows(this, true); for (int h = 0; h < 2; ++h) { auto *hand_info = &this->xr.hands[h]; for (size_t k = 0; k < XR_HAND_JOINT_COUNT_EXT; ++k) { auto const *jl = &hand_info->joint_locations[k]; Vector3 pos = { jl->pose.position.x, jl->pose.position.y, jl->pose.position.z, }; pos = RecenterPoint(this, pos); DrawSphere(pos, jl->radius, (Color) { 255, 0, 0, 255 }); } } Skybox_draw(this->renderer.skybox, this->renderer.camera.position); rlEnableColorBlend(); rlDisableDepthMask(); // don't write depth LunarWM_render_windows(this, false); // TODO: Replace with actual cursor texture. { // Cursor rlDrawRenderBatchActive(); rlDisableDepthTest(); Vector3 tip = SphericalToVector3(this->wm.pointer); Vector3 n = Vector3Normalize( Vector3Subtract(this->renderer.camera.position, tip)); Vector3 up_hint = (Vector3) { 0, 1, 0 }; if (fabsf(Vector3DotProduct(up_hint, n)) > 0.98f) up_hint = (Vector3) { 1, 0, 0 }; Vector3 right = Vector3Normalize(Vector3CrossProduct(up_hint, n)); Vector3 up = Vector3Normalize(Vector3CrossProduct(n, right)); Vector3 down = Vector3Negate(up); float const s = 0.03f; Vector3 v1 = tip; Vector3 v2 = Vector3Add(tip, Vector3Scale(down, s)); Vector3 v3 = Vector3Add(tip, Vector3Scale(right, s)); Vector3 normal = Vector3CrossProduct( Vector3Subtract(v2, v1), Vector3Subtract(v3, v1)); if (Vector3DotProduct(normal, n) < 0.0f) { Vector3 tmp = v2; v2 = v3; v3 = tmp; } DrawTriangle3D(v1, v2, v3, RED); rlDrawRenderBatchActive(); rlEnableDepthTest(); } rlEnableDepthMask(); if (IsTextureValid(this->renderer.hud_rt.texture)) { rlDrawRenderBatchActive(); rlDisableDepthTest(); Vector3 camPos = this->renderer.camera.position; Vector3 camDir = Vector3Normalize( Vector3Subtract(this->renderer.camera.target, camPos)); Vector3 up = this->renderer.camera.up; Vector3 right = Vector3Normalize(Vector3CrossProduct(camDir, up)); up = Vector3CrossProduct(right, camDir); Vector3 center = Vector3Add(camPos, Vector3Scale(camDir, 0.6f)); float heightMeters = 0.10f; DrawBillboardNoShear(this->renderer.camera, this->renderer.hud_rt.texture, center, heightMeters * 5, WHITE); rlDrawRenderBatchActive(); rlEnableDepthTest(); } } bool LunarWM_render_layer(LunarWM *this, LunarWM_RenderLayerInfo *info, float dt) { auto const view_count = (uint32_t)this->xr.view_configuration_views_count; assert(view_count == 2); XrView views[2] = {}; for (int i = 0; i < ARRAY_SZ(views); i++) { views[i] = (XrView) { .type = XR_TYPE_VIEW, }; } XrViewLocateInfo locInfo = { .type = XR_TYPE_VIEW_LOCATE_INFO, .viewConfigurationType = XR_VIEW_CONFIGURATION_TYPE_PRIMARY_STEREO, .displayTime = info->predicted_display_time, .space = this->xr.local_space, }; XrViewState viewState = { .type = XR_TYPE_VIEW_STATE }; uint32_t located = 0; if (xrLocateViews( this->xr.session, &locInfo, &viewState, view_count, &located, views) != XR_SUCCESS || located != view_count) { wlr_log(WLR_ERROR, "Failed to locate views"); return false; } // acquire swapchain images auto *color_sc = &this->xr.swapchains.v_color[0]; auto *depth_sc = &this->xr.swapchains.v_depth[0]; uint32_t col_idx = 0; uint32_t dep_idx = 0; if (!LunarWM_xr_acquire_wait(color_sc->swapchain, &col_idx) || !LunarWM_xr_acquire_wait(depth_sc->swapchain, &dep_idx)) { wlr_log(WLR_ERROR, "Swap-chain acquire failed"); return false; } GLuint color_tex = LunarWM_xr_get_swapchain_image(this, 0, col_idx); GLuint depth_tex = LunarWM_xr_get_swapchain_image(this, 1, dep_idx); // build FBO if (this->renderer.fbo == 0u) { glGenFramebuffers(1, &this->renderer.fbo); } glBindFramebuffer(GL_FRAMEBUFFER, this->renderer.fbo); rlFramebufferAttach(this->renderer.fbo, color_tex, RL_ATTACHMENT_COLOR_CHANNEL0, RL_ATTACHMENT_TEXTURE2D, 0); rlFramebufferAttach(this->renderer.fbo, depth_tex, RL_ATTACHMENT_DEPTH, RL_ATTACHMENT_TEXTURE2D, 0); assert(rlFramebufferComplete(this->renderer.fbo)); uint32_t const eye_w = this->xr.a_view_configuration_views[0].recommendedImageRectWidth; uint32_t const eye_h = this->xr.a_view_configuration_views[0].recommendedImageRectHeight; this->renderer.tmp_rt = (RenderTexture2D) { .id = this->renderer.fbo, .texture = { color_tex, (int)eye_w * view_count, (int)eye_h, 1, -1 }, .depth = { depth_tex, (int)eye_w * view_count, (int)eye_h, 1, -1 }, }; // head-space view matrix (matches rlOpenXR) XrSpaceLocation headLoc = { .type = XR_TYPE_SPACE_LOCATION }; xrLocateSpace(this->xr.view_space, this->xr.local_space, info->predicted_display_time, &headLoc); auto const head_view = MatrixInvert(xr_matrix(headLoc.pose)); // per-eye projection + view-offset Matrix const view_off_l = MatrixMultiply(xr_matrix(views[0].pose), head_view); Matrix const view_off_r = MatrixMultiply(xr_matrix(views[1].pose), head_view); Matrix const proj_r = xr_projection_matrix(views[0].fov); Matrix const proj_l = xr_projection_matrix(views[1].fov); int const hud_size = this->cman->cfg.displays.hud.size; if (!IsTextureValid(this->renderer.hud_rt.texture)) { this->renderer.hud_rt = LoadRenderTexture(hud_size, hud_size); } if (IsTextureValid(this->renderer.hud_rt.texture)) { BeginTextureMode(this->renderer.hud_rt); { LunarWM_render_hud(this, dt, hud_size); } EndTextureMode(); } // draw if (!IsTextureValid(this->renderer.main_rt.texture)) { this->renderer.main_rt = LoadRenderTexture(eye_w * view_count, eye_h); } BeginTextureMode(this->renderer.main_rt); rlEnableStereoRender(); rlSetMatrixProjectionStereo(proj_r, proj_l); rlSetMatrixViewOffsetStereo(view_off_r, view_off_l); glViewport(0, 0, (GLsizei)eye_w * view_count, (GLsizei)eye_h); rlClearColor(0, 0, 10, 255); rlClearScreenBuffers(); for (int i = 0; i < 1; i++) { XrTime const time = info->predicted_display_time; XrSpaceLocation view_location = { .type = XR_TYPE_SPACE_LOCATION }; XrResult const result = xrLocateSpace( this->xr.view_space, this->xr.local_space, time, &view_location); if (result != XR_SUCCESS) { break; } if ((view_location.locationFlags & XR_SPACE_LOCATION_POSITION_VALID_BIT) != 0u) { auto const pos = view_location.pose.position; this->renderer.camera.position = (Vector3) { pos.x, pos.y, pos.z }; } if ((view_location.locationFlags & XR_SPACE_LOCATION_ORIENTATION_VALID_BIT) != 0u) { auto const rot = view_location.pose.orientation; auto const forward = Vector3RotateByQuaternion((Vector3) { 0, 0, -1 }, (Quaternion) { rot.x, rot.y, rot.z, rot.w }); auto const up = Vector3RotateByQuaternion((Vector3) { 0, 1, 0 }, (Quaternion) { rot.x, rot.y, rot.z, rot.w }); this->renderer.camera.target = Vector3Add(this->renderer.camera.position, forward); this->renderer.camera.up = up; } if (this->xr.recenter_active) { Vector3 pos = this->renderer.camera.position; Vector3 fwd = Vector3Normalize( Vector3Subtract(this->renderer.camera.target, pos)); Vector3 up = this->renderer.camera.up; pos = Vector3Add( Vector3RotateByQuaternion(pos, this->xr.recenter_rot), this->xr.recenter_trans); fwd = Vector3RotateByQuaternion(fwd, this->xr.recenter_rot); up = Vector3RotateByQuaternion(up, this->xr.recenter_rot); this->renderer.camera.position = pos; this->renderer.camera.target = Vector3Add(pos, fwd); this->renderer.camera.up = up; } } BeginMode3D(this->renderer.camera); { ClearBackground(RED); render_3d(this, dt); } EndMode3D(); rlDisableStereoRender(); EndTextureMode(); if (!IsShaderValid(this->renderer.linear_srgb)) { static char const linear_srgb[] = { #embed "../assets/linear_srgb.fs" , 0 }; this->renderer.linear_srgb = LoadShaderFromMemory(NULL, linear_srgb); } BeginTextureMode(this->renderer.tmp_rt); rlDisableColorBlend(); ClearBackground(BLACK); BeginShaderMode(this->renderer.linear_srgb); DrawTexturePro(this->renderer.main_rt.texture, (Rectangle) { 0, 0, this->renderer.main_rt.texture.width, -this->renderer.main_rt.texture.height, }, (Rectangle) { 0, 0, this->renderer.tmp_rt.texture.width, this->renderer.tmp_rt.texture.height, }, (Vector2) { 0, 0 }, 0, WHITE); EndShaderMode(); EndTextureMode(); rlEnableColorBlend(); // release swapchain images XrSwapchainImageReleaseInfo const ri = { .type = XR_TYPE_SWAPCHAIN_IMAGE_RELEASE_INFO }; xrReleaseSwapchainImage(color_sc->swapchain, &ri); xrReleaseSwapchainImage(depth_sc->swapchain, &ri); // fill projection layer info->layer_projection_views_count = view_count; for (uint32_t i = 0; i < view_count; ++i) { int32_t const xOff = i * eye_w; auto *pv = &info->layer_projection_views[i]; pv->pose = views[i].pose; pv->fov = views[i].fov; pv->subImage.swapchain = color_sc->swapchain; pv->subImage.imageRect.offset = (XrOffset2Di) { .x = xOff, .y = 0 }; pv->subImage.imageRect.extent = (XrExtent2Di) { .width = eye_w, .height = eye_h }; pv->subImage.imageArrayIndex = 0; } info->layer_projection = (XrCompositionLayerProjection) { .type = XR_TYPE_COMPOSITION_LAYER_PROJECTION, .layerFlags = XR_COMPOSITION_LAYER_BLEND_TEXTURE_SOURCE_ALPHA_BIT | XR_COMPOSITION_LAYER_CORRECT_CHROMATIC_ABERRATION_BIT, .space = this->xr.local_space, .viewCount = view_count, .views = info->layer_projection_views, }; info->layers_count = 0; info->layers[info->layers_count++] = (XrCompositionLayerBaseHeader *)&info->layer_projection; if (this->renderer.first_frame) { LunarWM_set_recenter_from_camera(this); this->renderer.first_frame = false; this->wm.pointer = get_forward_spherical_with_nearest( this->renderer.camera.target, this->cman->cfg.space.radius); } return true; }