diff --git a/CMakeLists.txt b/CMakeLists.txt index 0648972..75c8dd5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -51,6 +51,7 @@ add_executable(${PROJECT_NAME}) target_sources(${PROJECT_NAME} PUBLIC src/vec.c + src/RayExt.c src/Config.c src/LunarWM.c src/main.c diff --git a/assets/linear_srgb.fs b/assets/linear_srgb.fs new file mode 100644 index 0000000..89a36a0 --- /dev/null +++ b/assets/linear_srgb.fs @@ -0,0 +1,18 @@ +precision mediump float; + +varying vec2 fragTexCoord; +varying vec4 fragColor; +uniform sampler2D texture0; + +vec3 srgb_to_linear(vec3 c) { + bvec3 cutoff = lessThanEqual(c, vec3(0.04045)); + vec3 low = c / 12.92; + vec3 high = pow((c + 0.055) / 1.055, vec3(2.4)); + return mix(high, low, vec3(cutoff)); +} + +void main() { + vec4 c = texture2D(texture0, fragTexCoord) * fragColor; + c.rgb = srgb_to_linear(c.rgb); // decode to linear + gl_FragColor = c; +} diff --git a/assets/skybox.fs b/assets/skybox.fs new file mode 100644 index 0000000..0ea6876 --- /dev/null +++ b/assets/skybox.fs @@ -0,0 +1,31 @@ +#version 100 + +precision mediump float; + +// Input vertex attributes (from vertex shader) +varying vec3 fragPosition; + +// Input uniform values +uniform samplerCube environmentMap; +uniform bool vflipped; +uniform bool doGamma; + +void main() +{ + // Fetch color from texture map + vec4 texelColor = vec4(0.0); + + if (vflipped) texelColor = textureCube(environmentMap, vec3(fragPosition.x, -fragPosition.y, fragPosition.z)); + else texelColor = textureCube(environmentMap, fragPosition); + + vec3 color = vec3(texelColor.x, texelColor.y, texelColor.z); + + if (doGamma) // Apply gamma correction + { + color = color/(color + vec3(1.0)); + color = pow(color, vec3(1.0/2.2)); + } + + // Calculate final fragment color + gl_FragColor = vec4(color, 1.0); +} diff --git a/assets/skybox.vs b/assets/skybox.vs new file mode 100644 index 0000000..e440ace --- /dev/null +++ b/assets/skybox.vs @@ -0,0 +1,24 @@ +#version 100 + +// Input vertex attributes +attribute vec3 vertexPosition; + +// Input uniform values +uniform mat4 matProjection; +uniform mat4 matView; + +// Output vertex attributes (to fragment shader) +varying vec3 fragPosition; + +void main() +{ + // Calculate fragment position based on model transformations + fragPosition = vertexPosition; + + // Remove translation from the view matrix + mat4 rotView = mat4(mat3(matView)); + vec4 clipPos = matProjection*rotView*vec4(vertexPosition, 1.0); + + // Calculate final vertex position + gl_Position = clipPos; +} diff --git a/lunarwm/citrus_orchard_road_puresky_8k.hdr b/lunarwm/citrus_orchard_road_puresky_8k.hdr new file mode 100644 index 0000000..82f442b Binary files /dev/null and b/lunarwm/citrus_orchard_road_puresky_8k.hdr differ diff --git a/lunarwm/citrus_orchard_road_puresky_8k.png b/lunarwm/citrus_orchard_road_puresky_8k.png new file mode 100644 index 0000000..dd74db8 Binary files /dev/null and b/lunarwm/citrus_orchard_road_puresky_8k.png differ diff --git a/lunarwm/cubemap.png b/lunarwm/cubemap.png new file mode 100644 index 0000000..d09a9bb Binary files /dev/null and b/lunarwm/cubemap.png differ diff --git a/lunarwm/init.lua b/lunarwm/init.lua index c451d02..970fe56 100644 --- a/lunarwm/init.lua +++ b/lunarwm/init.lua @@ -2,8 +2,6 @@ function main(kbd) return "Super-" .. kbd end - - return { input = { keyboard = { @@ -29,4 +27,5 @@ return { resolution = { 2560, 1440 }, }, }, + cubemap = 'cubemap.png', } diff --git a/src/Config.c b/src/Config.c index 72a9c4b..8e099ab 100644 --- a/src/Config.c +++ b/src/Config.c @@ -321,6 +321,15 @@ int config_load_ref(lua_State *L, int idx, Config *out) } lua_pop(L, 1); + lua_getfield(L, -1, "cubemap"); + if (lua_isstring(L, -1)) { + char const *s = lua_tostring(L, -1); + if (s && s[0]) { + (void)dupstr(s, (char **)&out->cubemap); + } + } + lua_pop(L, 1); + lua_pop(L, 1); return 0; } @@ -342,6 +351,11 @@ void config_unref(lua_State *L, Config *cfg) free(cfg->input.keyboard.xkb_options); cfg->input.keyboard.xkb_options = NULL; + + if (cfg->cubemap) { + free((void *)cfg->cubemap); + cfg->cubemap = NULL; + } } void config_trigger_ref(lua_State *L, Config *cfg, int ref) @@ -430,9 +444,33 @@ int config_manager_reload(ConfigManager *cm) if (load_config_file(cm->L, cm->path) != 0) return -1; + int rc = config_load_ref(cm->L, -1, &cm->cfg); lua_pop(cm->L, 1); - return rc; + if (rc != 0) + return rc; + + if (cm->cfg.cubemap && cm->cfg.cubemap[0] != '/') { + char const *slash = strrchr(cm->path, '/'); + char const *dir = "."; + size_t dirlen = 1; + if (slash) { + dir = cm->path; + dirlen = (size_t)(slash - cm->path); + } + size_t n = dirlen + 1 + strlen(cm->cfg.cubemap) + + 1; // dir + '/' + hdri + '\0' + char *full = (char *)malloc(n); + if (full) { + memcpy(full, dir, dirlen); + full[dirlen] = '/'; + strcpy(full + dirlen + 1, cm->cfg.cubemap); + free((void *)cm->cfg.cubemap); + cm->cfg.cubemap = full; + } + } + + return 0; } lua_State *config_manager_lua(ConfigManager *cm) { return cm ? cm->L : NULL; } diff --git a/src/Config.h b/src/Config.h index c25d9e5..de4c250 100644 --- a/src/Config.h +++ b/src/Config.h @@ -47,6 +47,8 @@ typedef struct { Vector2 resolution; } virtual; } displays; + + char const *cubemap; } Config; char const *get_config_path(void); diff --git a/src/LunarWM.c b/src/LunarWM.c index df5b6af..a09a7b5 100644 --- a/src/LunarWM.c +++ b/src/LunarWM.c @@ -1,5 +1,6 @@ #include "LunarWM.h" +#include "RayExt.h" #include "common.h" #include @@ -21,6 +22,7 @@ #include #include +#include #include #include #include @@ -662,6 +664,157 @@ static void new_xdg_toplevel_listener_notify( } } +struct vo_client_res { + struct wl_resource *res; + struct wl_list link; // vo_client_res.link +}; + +static void vo_send_initial(struct virtual_output *vo, struct wl_resource *res) +{ + // wl_output v1..v4 ordering: geometry, mode, scale (v2+), name/desc (v4), + // done (v2+) + wl_output_send_geometry(res, vo->x, vo->y, vo->phys_w_mm, vo->phys_h_mm, + vo->subpixel, vo->make, vo->model, vo->transform); + + uint32_t flags = WL_OUTPUT_MODE_CURRENT | WL_OUTPUT_MODE_PREFERRED; + wl_output_send_mode(res, flags, vo->width, vo->height, vo->refresh_mhz); + + if (wl_resource_get_version(res) >= 2) + wl_output_send_scale(res, vo->scale); + + if (wl_resource_get_version(res) >= 4) { + wl_output_send_name(res, vo->name); + wl_output_send_description(res, vo->desc); + } + + if (wl_resource_get_version(res) >= 2) + wl_output_send_done(res); +} + +static void vo_resource_destroy(struct wl_resource *res) +{ + struct vo_client_res *cr = wl_resource_get_user_data(res); + if (!cr) + return; + wl_list_remove(&cr->link); + free(cr); +} + +// wl_output requests (only release exists in v3+) +static void output_release(struct wl_client *client, struct wl_resource *res) +{ + (void)client; + wl_resource_destroy(res); +} + +static const struct wl_output_interface output_impl = { + .release = output_release, +}; + +static void vo_bind( + struct wl_client *client, void *data, uint32_t version, uint32_t id) +{ + struct virtual_output *vo = data; + uint32_t ver = version; + if (ver > 4) + ver = 4; // we implement up to v4 + + struct wl_resource *res + = wl_resource_create(client, &wl_output_interface, ver, id); + if (!res) { + wl_client_post_no_memory(client); + return; + } + + struct vo_client_res *cr = calloc(1, sizeof *cr); + if (!cr) { + wl_client_post_no_memory(client); + wl_resource_destroy(res); + return; + } + cr->res = res; + wl_resource_set_implementation(res, &output_impl, cr, vo_resource_destroy); + wl_list_insert(&vo->clients, &cr->link); + + vo_send_initial(vo, res); +} + +static void vo_broadcast_mode(struct virtual_output *vo) +{ + struct vo_client_res *cr; + wl_list_for_each(cr, &vo->clients, link) + { + struct wl_resource *res = cr->res; + uint32_t flags = WL_OUTPUT_MODE_CURRENT | WL_OUTPUT_MODE_PREFERRED; + wl_output_send_mode(res, flags, vo->width, vo->height, vo->refresh_mhz); + if (wl_resource_get_version(res) >= 2) + wl_output_send_done(res); + } +} + +static void vo_broadcast_scale(struct virtual_output *vo) +{ + struct vo_client_res *cr; + wl_list_for_each(cr, &vo->clients, link) + { + struct wl_resource *res = cr->res; + if (wl_resource_get_version(res) >= 2) { + wl_output_send_scale(res, vo->scale); + wl_output_send_done(res); + } + } +} + +static void vo_destroy(struct virtual_output *vo) +{ + // destroy client resources + struct vo_client_res *cr, *tmp; + wl_list_for_each_safe(cr, tmp, &vo->clients, link) + { + wl_resource_destroy(cr->res); // calls vo_resource_destroy + } + if (vo->global) { + wl_global_destroy(vo->global); + vo->global = NULL; + } + free(vo); +} + +static struct virtual_output *vo_create(struct wl_display *display, + char const *name, char const *desc, int32_t w, int32_t h, + int32_t refresh_mhz, int32_t scale, char const *make, char const *model) +{ + struct virtual_output *vo = calloc(1, sizeof *vo); + if (!vo) + return NULL; + vo->display = display; + wl_list_init(&vo->clients); + + vo->x = 0; + vo->y = 0; + vo->phys_w_mm = 0; + vo->phys_h_mm = 0; // unknown; set if you care + vo->width = w; + vo->height = h; + vo->refresh_mhz = refresh_mhz; + vo->scale = scale > 0 ? scale : 1; + vo->subpixel = WL_OUTPUT_SUBPIXEL_UNKNOWN; + vo->transform = WL_OUTPUT_TRANSFORM_NORMAL; + vo->make = make ? make : "Lunar"; + vo->model = model ? model : "Virtual"; + vo->name = name; + vo->desc = desc ? desc : name; + + vo->global + = wl_global_create(display, &wl_output_interface, 4, vo, vo_bind); + if (!vo->global) { + free(vo); + return NULL; + } + + return vo; +} + static bool init_wayland(LunarWM *this) { wlr_log_init(WLR_DEBUG, nullptr); @@ -1413,10 +1566,36 @@ static bool init_xr(LunarWM *this) static void sync_config(LunarWM *this) { - UnloadTexture(this->renderer.hud_rt.texture); - this->renderer.hud_rt.texture.id = 0; - this->renderer.hud_rt.texture.width = 0; - this->renderer.hud_rt.texture.height = 0; + if (this->cman->cfg.cubemap) { + Skybox_init(&this->renderer.skybox, this->cman->cfg.cubemap); + } else { + Skybox_destroy(&this->renderer.skybox); + } + + if (IsTextureValid(this->renderer.hud_rt.texture)) { + UnloadTexture(this->renderer.hud_rt.texture); + this->renderer.hud_rt.texture.id = 0; + this->renderer.hud_rt.texture.width = 0; + this->renderer.hud_rt.texture.height = 0; + } + + if (this->wayland.custom_out_virtual) { + vo_destroy(this->wayland.custom_out_virtual); + this->wayland.custom_out_virtual = NULL; + } + if (this->wayland.custom_out_hud) { + vo_destroy(this->wayland.custom_out_hud); + this->wayland.custom_out_hud = NULL; + } + + int vw = (int)this->cman->cfg.displays.virtual.resolution.x; + int vh = (int)this->cman->cfg.displays.virtual.resolution.y; + int hud = this->cman->cfg.displays.hud.size; + + this->wayland.custom_out_virtual = vo_create(this->wayland.display, + "Virtual", "Virtual output", vw, vh, 60000, 1, "LunarWM", "Virtual"); + this->wayland.custom_out_hud = vo_create(this->wayland.display, "HUD", + "HUD output", hud, hud, 60000, 1, "LunarWM", "HUD"); } extern char **environ; @@ -1523,7 +1702,6 @@ bool LunarWM_init(LunarWM *this) lua_setglobal(L, "lunar"); config_manager_reload(this->cman); - sync_config(this); this->renderer.center = this->cman->cfg.space.initial_center; } @@ -1562,6 +1740,8 @@ bool LunarWM_init(LunarWM *this) return false; } + sync_config(this); + this->initialized = true; return true; @@ -1651,6 +1831,15 @@ static void cleanup_wayland(LunarWM *this) { assert(this); + if (this->wayland.custom_out_virtual) { + vo_destroy(this->wayland.custom_out_virtual); + this->wayland.custom_out_virtual = NULL; + } + if (this->wayland.custom_out_hud) { + vo_destroy(this->wayland.custom_out_hud); + this->wayland.custom_out_hud = NULL; + } + if (this->wayland.xwayland) { wlr_xwayland_destroy(this->wayland.xwayland); this->wayland.xwayland = NULL; @@ -2004,7 +2193,7 @@ void render_hud(LunarWM *this, float /*dt*/, int hud_size) void render_3d(LunarWM *this, float /*dt*/) { - DrawGrid(10, 1); + Skybox_draw(this->renderer.skybox, (Vector3) { 0, 0, 0 }); rlDisableBackfaceCulling(); for (size_t i = 0; i < vector_size(this->wayland.v_toplevels); i++) { @@ -2033,7 +2222,7 @@ void render_3d(LunarWM *this, float /*dt*/) jl->pose.position.y, jl->pose.position.z, }; - DrawSphere(pos, jl->radius, RED); + DrawSphere(pos, jl->radius, (Color) { 255, 0, 0, 255 }); } } @@ -2133,13 +2322,14 @@ static bool render_layer(LunarWM *this, LunarWM_RenderLayerInfo *info, float dt) auto const head_view = MatrixInvert(xr_matrix(headLoc.pose)); // per-eye projection + view-offset - Matrix const proj_r = xr_projection_matrix(views[0].fov); - Matrix const proj_l = xr_projection_matrix(views[1].fov); 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); @@ -2154,10 +2344,15 @@ static bool render_layer(LunarWM *this, LunarWM_RenderLayerInfo *info, float dt) } // draw - BeginTextureMode(this->renderer.tmp_rt); + + 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); // right, left (yes) + rlSetMatrixProjectionStereo(proj_r, proj_l); rlSetMatrixViewOffsetStereo(view_off_r, view_off_l); glViewport(0, 0, (GLsizei)eye_w * view_count, (GLsizei)eye_h); @@ -2202,6 +2397,33 @@ static bool render_layer(LunarWM *this, LunarWM_RenderLayerInfo *info, float dt) 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); + 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(); + // release swapchain images XrSwapchainImageReleaseInfo const ri = { .type = XR_TYPE_SWAPCHAIN_IMAGE_RELEASE_INFO }; diff --git a/src/LunarWM.h b/src/LunarWM.h index 7022d22..36a16d1 100644 --- a/src/LunarWM.h +++ b/src/LunarWM.h @@ -33,9 +33,27 @@ #include #include "Config.h" +#include "RayExt.h" struct LunarWM; +typedef struct virtual_output { + struct wl_global *global; + struct wl_display *display; + struct wl_list clients; // vo_client_res.link + + // state we advertise + int32_t x, y; // compositor space; keep 0,0 if not relevant + int32_t phys_w_mm, phys_h_mm; + int32_t width, height; // current mode + int32_t refresh_mhz; // mHz + int32_t scale; + enum wl_output_subpixel subpixel; + enum wl_output_transform transform; + char const *make, *model; + char const *name, *desc; +} LunarWM_VirtualOutput; + typedef struct { struct LunarWM *server; @@ -143,6 +161,9 @@ typedef struct LunarWM { struct wl_listener xwayland_associate_tmp; // per-surface temp struct wl_listener xwayland_dissociate_tmp; // per-surface temp + LunarWM_VirtualOutput *custom_out_virtual; + LunarWM_VirtualOutput *custom_out_hud; + LunarWM_Toplevel **v_toplevels; int current_focus; } wayland; @@ -175,10 +196,14 @@ typedef struct LunarWM { struct { GLuint fbo; RenderTexture2D tmp_rt; + RenderTexture2D main_rt; RenderTexture2D hud_rt; Camera3D camera; + Shader linear_srgb; Vector3 center; + + Skybox skybox; } renderer; ConfigManager *cman; diff --git a/src/RayExt.c b/src/RayExt.c new file mode 100644 index 0000000..b95468a --- /dev/null +++ b/src/RayExt.c @@ -0,0 +1,109 @@ +#include "RayExt.h" + +#include +#include +#include + +#if defined(GRAPHICS_API_OPENGL_ES3) +static char const *SKYBOX_VS = "#version 300 es\n" + "precision mediump float;\n" + "layout(location=0) in vec3 vertexPosition;\n" + "uniform mat4 mvp;\n" + "out vec3 vDir;\n" + "void main(){ vDir=vertexPosition; " + "gl_Position=mvp*vec4(vertexPosition,1.0); }\n"; +static char const *SKYBOX_FS = "#version 300 es\n" + "precision highp float;\n" + "in vec3 vDir;\n" + "uniform samplerCube environmentMap;\n" + "out vec4 finalColor;\n" + "void main(){ vec3 dir=normalize(vDir); " + "finalColor=texture(environmentMap, dir); }\n"; +#endif + +void Skybox_init(Skybox *skybox, char const *fp) +{ + if (skybox->ok) { + Skybox_destroy(skybox); + } + + // 1) Load cubemap from a 3x4 cross image + Image img = LoadImage(fp); + if (img.width == 0 || img.height == 0) { + TraceLog(LOG_ERROR, "Skybox: failed to load image: %s", fp); + skybox->ok = false; + return; + } + + TextureCubemap cubemap + = LoadTextureCubemap(img, CUBEMAP_LAYOUT_AUTO_DETECT); + UnloadImage(img); + if (cubemap.id == 0) { + TraceLog(LOG_ERROR, "Skybox: failed to create cubemap from %s", fp); + skybox->ok = false; + return; + } + + // 2) Make an inward-facing cube mesh + Mesh m = GenMeshCube( + 2.0f, 2.0f, 2.0f); // size doesn't matter; depth writes off + // Invert winding so we see the inside + for (int i = 0; i < m.triangleCount; ++i) { + unsigned short *idx = &m.indices[i * 3]; + unsigned short tmp = idx[1]; + idx[1] = idx[2]; + idx[2] = tmp; + } + UploadMesh(&m, false); + Model cube = LoadModelFromMesh(m); + + Shader sh = LoadShaderFromMemory(SKYBOX_VS, SKYBOX_FS); + + // make raylib aware which sampler is the cubemap + sh.locs[SHADER_LOC_MAP_CUBEMAP] = GetShaderLocation(sh, "environmentMap"); + + cube.materials[0].shader = sh; + + // put the cubemap in the expected material slot + cube.materials[0].maps[MATERIAL_MAP_CUBEMAP].texture = cubemap; + + // nicer defaults + SetTextureWrap(cubemap, TEXTURE_WRAP_CLAMP); + SetTextureFilter(cubemap, TEXTURE_FILTER_BILINEAR); + + skybox->cubemap = cubemap; + skybox->shader = sh; + skybox->cube = cube; + skybox->ok = true; +} + +void Skybox_destroy(Skybox *skybox) +{ + if (!skybox->ok) + return; + + UnloadModel(skybox->cube); // also unloads Mesh + UnloadTexture(skybox->cubemap); + UnloadShader(skybox->shader); + + *skybox = (Skybox) { 0 }; +} + +void Skybox_draw(Skybox const skybox, Vector3 position) +{ + if (!skybox.ok) + return; + + // Render behind everything without writing depth; cull disabled so we see + // inside faces + rlDisableColorBlend(); + rlDisableBackfaceCulling(); + rlDisableDepthMask(); + + DrawModel(skybox.cube, position, 60.0f, (Color) { 255, 255, 255, 255 }); + + rlDrawRenderBatchActive(); + rlEnableDepthMask(); + rlEnableBackfaceCulling(); + rlEnableColorBlend(); +} diff --git a/src/RayExt.h b/src/RayExt.h new file mode 100644 index 0000000..2237628 --- /dev/null +++ b/src/RayExt.h @@ -0,0 +1,18 @@ +#ifndef RAYEXT_H +#define RAYEXT_H + +#include + +typedef struct { + bool ok; + Model cube; + Shader shader; + TextureCubemap cubemap; +} Skybox; + +void Skybox_init(Skybox *skybox, char const *fp); +void Skybox_destroy(Skybox *skybox); + +void Skybox_draw(Skybox const skybox, Vector3 position); + +#endif // RAYEXT_H