--- a/src/api/wayfire/scene.hpp +++ b/src/api/wayfire/scene.hpp @@ -468,9 +468,11 @@ TOP = 3, UNMANAGED = 4, OVERLAY = 5, - LOCK = 6, + /** Desktop effects stop below this layer. Session locks remain above it. */ + SCREENSAVER = 6, + LOCK = 7, // For compatibility with workspace-manager, to be removed - DWIDGET = 7, + DWIDGET = 8, /** Not a real layer, but a placeholder for the number of layers. */ ALL_LAYERS, }; --- a/src/core/scene.cpp +++ b/src/core/scene.cpp @@ -175,6 +175,7 @@ "top", "unmanaged", "overlay", + "screensaver", "lock", "dwidget" }; --- a/src/view/layer-shell/layer-shell.cpp +++ b/src/view/layer-shell/layer-shell.cpp @@ -480,7 +480,7 @@ /* Keep the OLED screensaver above every normal overlay. */ if (std::string{nonull(lsurface->namespace_t)} == "screensaver") { - return wf::scene::layer::LOCK; + return wf::scene::layer::SCREENSAVER; } static const std::vector desktop_widget_ids = { --- a/src/api/wayfire/plugin.hpp +++ b/src/api/wayfire/plugin.hpp @@ -107,7 +107,7 @@ /** * The version is defined as macro as well, to allow conditional compilation. */ -#define WAYFIRE_API_ABI_VERSION_MACRO 2026'07'26 +#define WAYFIRE_API_ABI_VERSION_MACRO 2026'09'18 /** * The version of Wayfire's API/ABI --- a/plugins/ipc-rules/ipc-rules-common.hpp +++ b/plugins/ipc-rules/ipc-rules-common.hpp @@ -121,6 +121,9 @@ case wf::scene::layer::LOCK: return "lock"; + + case wf::scene::layer::SCREENSAVER: + return "screensaver"; case wf::scene::layer::DWIDGET: return "dew"; --- a/src/output/workspace-stream.cpp +++ b/src/output/workspace-stream.cpp @@ -48,6 +48,15 @@ for (auto& output_node : wf::collect_output_nodes(wf::get_core().scene(), self->output)) { + // A workspace contains desktop content only. In particular, cube/expo + // subpasses must never fold a screensaver or lock into the desktop. + if (output_node == self->output->node_for_layer(scene::layer::SCREENSAVER) || + output_node == self->output->node_for_layer(scene::layer::LOCK) || + output_node == self->output->node_for_layer(scene::layer::DWIDGET)) + { + continue; + } + for (auto& ch : output_node->get_children()) { if (ch->is_enabled()) @@ -150,7 +159,7 @@ }; auto color = self->background.value_or(background_color_opt); - data.pass->clear(data.damage, color); + data.pass->clear(data.damage, color, data.target); } void presentation_feedback(wf::output_t *output) override --- a/src/api/wayfire/render-manager.hpp +++ b/src/api/wayfire/render-manager.hpp @@ -72,6 +72,17 @@ ~render_manager(); /** + * Render the composed scene strictly below SCREENSAVER to an offscreen target. + * Uses layout coordinates and the actual scene order (including custom nodes + * such as cube), without modifying live visibility, damage or frame callbacks. + * Excludes screensavers, locks, software cursors and final output postprocessing. + * Call from the compositor event loop, outside an active output render pass. + * Returns false for inhibited outputs, invalid targets or a failed submit. + * No work is performed between calls. The caller owns the target buffer. + */ + bool render_desktop(const render_target_t& target); + + /** * Rendering an output is done on demand, that is, when the output is * damaged. Some plugins however need to redraw the output as often as * possible, for ex. when displaying some kind of animation. --- a/src/api/wayfire/render.hpp +++ b/src/api/wayfire/render.hpp @@ -528,6 +528,8 @@ * Do not clear the background areas. */ RPASS_CLEAR_BACKGROUND = (1 << 1), + /** Suppress presentation feedback, including feedback from nested subpasses. */ + RPASS_NO_PRESENTATION = (1 << 2), }; /** @@ -644,6 +646,10 @@ */ void clear(const wf::regionf_t& region, const wf::color_t& color); + /** Clear a region expressed in an instruction's translated/scaled coordinates. */ + void clear(const wf::regionf_t& region, const wf::color_t& color, + const wf::render_target_t& adjusted_target); + /** * Add a texture rendering operation to the pass. */ --- a/src/render.cpp +++ b/src/render.cpp @@ -809,6 +809,18 @@ wf::regionf_t wf::render_pass_t::run_partial() { + // Subpasses (cube faces, transformers, etc.) may supply their own reference + // output. An offscreen sample must not claim that any of these were presented. + static unsigned no_presentation_depth = 0; + struct feedback_guard_t + { + unsigned& depth; + bool suppress; + feedback_guard_t(unsigned& depth, bool suppress) : depth(depth), suppress(suppress) + { if (suppress) { ++depth; } } + ~feedback_guard_t() { if (suppress) { --depth; } } + } feedback_guard{no_presentation_depth, bool(params.flags & RPASS_NO_PRESENTATION)}; + auto accumulated_damage = params.damage; if (params.flags & RPASS_EMIT_SIGNALS) { @@ -844,7 +856,7 @@ { instr.pass = this; instr.instance->render(instr); - if (params.reference_output) + if (params.reference_output && !no_presentation_depth) { instr.instance->presentation_feedback(params.reference_output); } @@ -876,8 +888,14 @@ void wf::render_pass_t::clear(const wf::regionf_t& region, const wf::color_t& color) { - wlr_box box{0, 0, params.target.get_size().width, params.target.get_size().height}; - auto damage = params.target.framebuffer_region_from_geometry_region(region); + clear(region, color, params.target); +} + +void wf::render_pass_t::clear(const wf::regionf_t& region, const wf::color_t& color, + const wf::render_target_t& adjusted_target) +{ + wlr_box box{0, 0, adjusted_target.get_size().width, adjusted_target.get_size().height}; + auto damage = adjusted_target.framebuffer_region_from_geometry_region(region); wlr_render_rect_options opts; opts.blend_mode = WLR_RENDER_BLEND_MODE_NONE; --- a/src/output/render-manager.cpp +++ b/src/output/render-manager.cpp @@ -537,6 +537,11 @@ class depth_buffer_manager_t { public: + bool is_required() const + { + return required_counter > 0; + } + void ensure_depth_buffer(int fb, int width, int height) { /* If the backend doesn't have its own framebuffer, then the @@ -1563,6 +1568,62 @@ return pimpl->postprocessing->get_target_framebuffer(); } +bool render_manager::render_desktop(const render_target_t& target) +{ + auto output = pimpl->output; + if (output->is_inhibited() || pimpl->output_inhibit_counter || pimpl->current_pass || + !target.get_buffer() || target.geometry.width <= 0 || target.geometry.height <= 0) + { + return false; + } + + auto root = wf::get_core().scene(); + std::vector instances; + bool below_screensaver = false; + for (const auto& child : root->get_children()) + { + if (child == root->layers[(size_t)scene::layer::SCREENSAVER]) + { + below_screensaver = true; + continue; + } + + if (below_screensaver && child->is_enabled()) + { + child->gen_render_instances(instances, [] (const wf::regionf_t&) {}, output); + } + } + + if (!below_screensaver) + { + return false; + } + + render_pass_params_t params; + params.instances = &instances; + params.target = target; + params.damage = wf::regionf_t{output->get_layout_geometry()} & target.geometry; + params.background_color = pimpl->background_color_opt; + params.renderer = output->handle->renderer; + params.flags = RPASS_CLEAR_BACKGROUND | RPASS_EMIT_SIGNALS | RPASS_NO_PRESENTATION; + // Cube and other 3D nodes require depth testing in offscreen passes too. + // Use an independent attachment; never resize or reuse the live output's. + depth_buffer_manager_t snapshot_depth; + if (wf::get_core().is_gles2() && pimpl->depth_buffer_manager->is_required()) + { + snapshot_depth.set_required(true); + const auto size = target.get_size(); + snapshot_depth.ensure_depth_buffer(gles::ensure_render_buffer_fb_id(target), + size.width, size.height); + } + + render_pass_t pass{params}; + pass.run_partial(); + bool submitted = pass.submit(); + snapshot_depth.frame_done(); + return submitted; +} + void render_manager::set_require_depth_buffer(bool require) { return pimpl->depth_buffer_manager->set_required(require); --- a/plugins/cube/cube.cpp +++ b/plugins/cube/cube.cpp @@ -106,7 +106,9 @@ auto cws = self->cube->output->wset()->get_current_workspace(); output_background = std::make_shared( self->cube->output, cws); - output_background->background = wf::color_t{0.0, 0.0, 0.0, 0.0}; + // This pass owns the desktop beneath the transparent cube. + // Clear uncovered areas with the normal desktop background; + // leaving them transparent retains old pixels on live outputs. output_background->include_workspace_views = false; output_background->exclude_desktop_environment = self->cube->rotate_layer_shell; auto push_background_damage = [=] (const wf::regionf_t& damage) --- a/plugins/common/workspace-wall.cpp +++ b/plugins/common/workspace-wall.cpp @@ -190,7 +190,7 @@ void render(const wf::scene::render_instruction_t& data) override { - data.pass->clear(data.damage, self->wall->background_color); + data.pass->clear(data.damage, self->wall->background_color, data.target); auto damage = data.target.framebuffer_region_from_geometry_region(data.damage); for (int i = 0; i < (int)self->workspaces.size(); i++) --- a/plugins/single_plugins/switcher.cpp +++ b/plugins/single_plugins/switcher.cpp @@ -810,7 +810,7 @@ void render(const wf::scene::render_instruction_t& data) { - data.pass->clear(data.target.geometry, {0, 0, 0, 1}); + data.pass->clear(data.target.geometry, {0, 0, 0, 1}, data.target); auto local_target = data.target.translated(-wf::origin(render_node->get_bounding_box())); for (auto view : get_background_views())