diff --git a/include/core/mir/geometry/dimensions.h b/include/core/mir/geometry/dimensions.h index 812b051af4d..f8518e6f2ad 100644 --- a/include/core/mir/geometry/dimensions.h +++ b/include/core/mir/geometry/dimensions.h @@ -200,6 +200,12 @@ template inline constexpr DeltaX operator/(DeltaX const& dx, Scalar scale) { return DeltaX{dx.as_value() / scale}; } template inline constexpr DeltaY operator/(DeltaY const& dy, Scalar scale) { return DeltaY{dy.as_value() / scale}; } + +// Can divide a coördinate by a distance to get a scalar +template +inline constexpr auto operator/(X const& x, Width const& width) { return x.as_value() / width.as_value(); } +template +inline constexpr auto operator/(Y const& y, Height const& height) { return y.as_value() / height.as_value(); } } // namespace // Converting between types is fine, as long as they are along the same axis diff --git a/include/core/mir/geometry/size.h b/include/core/mir/geometry/size.h index cd49b723df2..14885a0952e 100644 --- a/include/core/mir/geometry/size.h +++ b/include/core/mir/geometry/size.h @@ -20,6 +20,9 @@ #include "forward.h" #include "dimensions.h" #include +#include +#include +#include namespace mir { @@ -32,6 +35,13 @@ struct Point; template struct Displacement; +template +concept is_exactly_representable = requires +{ + typename std::enable_if::digits >= std::numeric_limits::digits, U>::type; + requires std::floating_point; +}; + template struct Size { @@ -41,6 +51,14 @@ struct Size constexpr Size(Size const&) noexcept = default; Size& operator=(Size const&) noexcept = default; + template + requires is_exactly_representable + constexpr Size(Size const& other) noexcept + : width{Width{other.width}}, + height{Height{other.height}} + { + } + template explicit constexpr Size(Size const& other) noexcept : width{Width{other.width}}, diff --git a/include/platform/mir/graphics/renderable.h b/include/platform/mir/graphics/renderable.h index 7cd9fb06bd7..5e3103151fb 100644 --- a/include/platform/mir/graphics/renderable.h +++ b/include/platform/mir/graphics/renderable.h @@ -51,6 +51,13 @@ class Renderable virtual std::shared_ptr buffer() const = 0; virtual geometry::Rectangle screen_position() const = 0; + /** + * The region of \ref buffer that should be sampled from. + * + * This is in buffer coordinates, so {{0, 0}, {buffer->size().width, buffer->size().height} means + * “use the whole buffer”. + */ + virtual geometry::RectangleD src_bounds() const = 0; virtual std::optional clip_area() const = 0; // These are from the old CompositingCriteria. There is a little bit diff --git a/src/gl/tessellation_helpers.cpp b/src/gl/tessellation_helpers.cpp index 3598b35cea5..26bfca6d405 100644 --- a/src/gl/tessellation_helpers.cpp +++ b/src/gl/tessellation_helpers.cpp @@ -20,26 +20,51 @@ namespace mg = mir::graphics; namespace mgl = mir::gl; namespace geom = mir::geometry; + +namespace +{ +struct SrcTexCoords +{ + GLfloat top; + GLfloat bottom; + GLfloat left; + GLfloat right; +}; + +auto tex_coords_from_rect(geom::Size buffer_size, geom::RectangleD sample_rect) -> SrcTexCoords +{ + /* GL Texture coordinates are normalised to the size of the buffer, so (0.0, 0.0) is the top-left + * and (1.0, 1.0) is the bottom-right + */ + SrcTexCoords coords; + coords.top = sample_rect.top() / buffer_size.height; + coords.bottom = sample_rect.bottom() / buffer_size.height; + coords.left = sample_rect.left() / buffer_size.width; + coords.right = sample_rect.right() / buffer_size.width; + return coords; +} +} + mgl::Primitive mgl::tessellate_renderable_into_rectangle( mg::Renderable const& renderable, geom::Displacement const& offset) { auto rect = renderable.screen_position(); rect.top_left = rect.top_left - offset; - GLfloat left = rect.top_left.x.as_int(); - GLfloat right = left + rect.size.width.as_int(); - GLfloat top = rect.top_left.y.as_int(); - GLfloat bottom = top + rect.size.height.as_int(); + GLfloat const left = rect.top_left.x.as_int(); + GLfloat const right = left + rect.size.width.as_int(); + GLfloat const top = rect.top_left.y.as_int(); + GLfloat const bottom = top + rect.size.height.as_int(); mgl::Primitive rectangle; rectangle.type = GL_TRIANGLE_STRIP; - GLfloat const tex_right = 1.0f; - GLfloat const tex_bottom = 1.0f; + auto const [tex_top, tex_bottom, tex_left, tex_right] = + tex_coords_from_rect(renderable.buffer()->size(), renderable.src_bounds()); auto& vertices = rectangle.vertices; - vertices[0] = {{left, top, 0.0f}, {0.0f, 0.0f}}; - vertices[1] = {{left, bottom, 0.0f}, {0.0f, tex_bottom}}; - vertices[2] = {{right, top, 0.0f}, {tex_right, 0.0f}}; + vertices[0] = {{left, top, 0.0f}, {tex_left, tex_top}}; + vertices[1] = {{left, bottom, 0.0f}, {tex_left, tex_bottom}}; + vertices[2] = {{right, top, 0.0f}, {tex_right, tex_top}}; vertices[3] = {{right, bottom, 0.0f}, {tex_right, tex_bottom}}; return rectangle; } diff --git a/src/server/frontend_wayland/CMakeLists.txt b/src/server/frontend_wayland/CMakeLists.txt index a56b23a75ff..e0e1ce015ed 100644 --- a/src/server/frontend_wayland/CMakeLists.txt +++ b/src/server/frontend_wayland/CMakeLists.txt @@ -63,6 +63,7 @@ set( ${PROJECT_SOURCE_DIR}/src/include/server/mir/frontend/pointer_input_dispatcher.h session_credentials.cpp ${PROJECT_SOURCE_DIR}/src/include/server/mir/frontend/buffer_stream.h + wp_viewporter.cpp wp_viewporter.h ) add_custom_command( diff --git a/src/server/frontend_wayland/wayland_connector.cpp b/src/server/frontend_wayland/wayland_connector.cpp index e7158fb5f04..f6486d70b0f 100644 --- a/src/server/frontend_wayland/wayland_connector.cpp +++ b/src/server/frontend_wayland/wayland_connector.cpp @@ -28,6 +28,7 @@ #include "wayland_executor.h" #include "desktop_file_manager.h" #include "foreign_toplevel_manager_v1.h" +#include "wp_viewporter.h" #include "mir/main_loop.h" #include "mir/thread_name.h" @@ -331,6 +332,8 @@ mf::WaylandConnector::WaylandConnector( shm_global = std::make_unique(display.get(), executor); + viewporter = std::make_unique(display.get()); + char const* wayland_display = nullptr; if (auto const display_name = getenv("WAYLAND_DISPLAY")) diff --git a/src/server/frontend_wayland/wayland_connector.h b/src/server/frontend_wayland/wayland_connector.h index 7f742bd17d3..096cc52eb28 100644 --- a/src/server/frontend_wayland/wayland_connector.h +++ b/src/server/frontend_wayland/wayland_connector.h @@ -85,6 +85,7 @@ class WlSeat; class WlShm; class WlSubcompositor; class WlSurface; +class WpViewporter; class DesktopFileManager; class WaylandExtensions @@ -210,6 +211,7 @@ class WaylandConnector : public Connector std::shared_ptr desktop_file_manager; std::unique_ptr data_device_manager_global; std::unique_ptr shm_global; + std::unique_ptr viewporter; std::shared_ptr const executor; std::shared_ptr const allocator; std::shared_ptr const shell; diff --git a/src/server/frontend_wayland/wl_surface.cpp b/src/server/frontend_wayland/wl_surface.cpp index 6345489f7b6..3d80c10c604 100644 --- a/src/server/frontend_wayland/wl_surface.cpp +++ b/src/server/frontend_wayland/wl_surface.cpp @@ -16,7 +16,7 @@ #include "wl_surface.h" -#include "mir/geometry/forward.h" +#include "viewporter_wrapper.h" #include "wayland_utils.h" #include "wl_surface_role.h" #include "wl_subcompositor.h" @@ -39,9 +39,12 @@ #include "mir/scene/surface.h" #include "mir/shell/surface_specification.h" #include "mir/log.h" +#include "wp_viewporter.h" #include #include +#include +#include #include namespace mf = mir::frontend; @@ -72,6 +75,9 @@ void mf::WlSurfaceState::update_from(WlSurfaceState const& source) begin(source.frame_callbacks), end(source.frame_callbacks)); + if (source.viewport) + viewport = source.viewport; + if (source.surface_data_invalidated) surface_data_invalidated = true; } @@ -331,7 +337,19 @@ void mf::WlSurface::commit(WlSurfaceState const& state) input_shape = state.input_shape.value(); if (state.scale) - inv_scale = 1.0f / state.scale.value(); + scale = state.scale.value(); + + if (state.viewport) + { + viewport = std::move(state.viewport); + } + + bool needs_buffer_submission = + state.scale || // If the scale has changed, or... + state.viewport || // ...we've added a viewport, or... + (viewport && viewport.value().changed_since_last_resolve()); // ...the viewport has changed... + // ...then we'll need to submit a new frame, even if the client hasn't + // attached a new buffer. auto const executor_send_frame_callbacks = [executor = wayland_executor, weak_self = mw::make_weak(this)]() { @@ -366,11 +384,10 @@ void mf::WlSurface::commit(WlSurfaceState const& state) } }); }; - std::shared_ptr mir_buffer; if (auto const shm_buffer = ShmBuffer::from(weak_buffer.value())) { - mir_buffer = allocator->buffer_from_shm( + current_buffer = allocator->buffer_from_shm( shm_buffer->data(), std::move(executor_send_frame_callbacks), std::move(release_buffer)); @@ -378,11 +395,11 @@ void mf::WlSurface::commit(WlSurfaceState const& state) mir_server_wayland, sw_buffer_committed, wl_resource_get_client(resource), - mir_buffer->id().as_value()); + current_buffer->id().as_value()); } else { - mir_buffer = allocator->buffer_from_resource( + current_buffer = allocator->buffer_from_resource( weak_buffer.value(), std::move(executor_send_frame_callbacks), std::move(release_buffer)); @@ -390,18 +407,10 @@ void mf::WlSurface::commit(WlSurfaceState const& state) mir_server_wayland, hw_buffer_committed, wl_resource_get_client(resource), - mir_buffer->id().as_value()); - } - - stream->submit_buffer(mir_buffer, mir_buffer->size() * inv_scale, {{0, 0}, geom::SizeD{mir_buffer->size()}}); - auto const new_buffer_size = mir_buffer->size() * inv_scale; - - if (std::make_optional(new_buffer_size) != buffer_size_) - { - state.invalidate_surface_data(); // input shape needs to be recalculated for the new size + current_buffer->id().as_value()); } - buffer_size_ = new_buffer_size; + needs_buffer_submission = true; } } else @@ -409,6 +418,31 @@ void mf::WlSurface::commit(WlSurfaceState const& state) frame_callback_executor->spawn(std::move(executor_send_frame_callbacks)); } + if (needs_buffer_submission) + { + geom::Size logical_size; + geom::RectangleD src_sample; + + if (viewport) + { + std::tie(src_sample, logical_size) = viewport.value().resolve_viewport(scale, current_buffer->size()); + } + else + { + src_sample = geom::RectangleD{{0, 0}, current_buffer->size()}; + logical_size = current_buffer->size() / scale; + } + + stream->submit_buffer(current_buffer, logical_size, src_sample); + + if (std::make_optional(logical_size) != buffer_size_) + { + state.invalidate_surface_data(); // input shape needs to be recalculated for the new size + } + + buffer_size_ = logical_size; + } + for (WlSubsurface* child: children) { child->parent_has_committed(); @@ -468,6 +502,15 @@ auto mf::WlSurface::confine_pointer_state() const -> MirPointerConfinementState return mir_pointer_unconfined; } +void mf::WlSurface::associate_viewport(wayland::Weak viewport) +{ + if (this->viewport || pending.viewport) + { + BOOST_THROW_EXCEPTION((std::logic_error{"Cannot associate a viewport to a window with an existing viewport"})); + } + pending.viewport = viewport; +} + void mir::frontend::WlSurface::update_surface_spec(shell::SurfaceSpecification const& spec) { pending.surface_spec.update_from(spec); diff --git a/src/server/frontend_wayland/wl_surface.h b/src/server/frontend_wayland/wl_surface.h index cefbd79e675..d658f5f8483 100644 --- a/src/server/frontend_wayland/wl_surface.h +++ b/src/server/frontend_wayland/wl_surface.h @@ -39,6 +39,7 @@ class Executor; namespace graphics { class GraphicBufferAllocator; +class Buffer; } namespace scene { @@ -57,6 +58,7 @@ namespace frontend class WlSurface; class WlSubsurface; class ResourceLifetimeTracker; +class Viewport; struct WlSurfaceState { @@ -83,6 +85,7 @@ struct WlSurfaceState std::optional offset; std::optional>> input_shape; std::vector> frame_callbacks; + wayland::Weak viewport; private: // only set to true if invalidate_surface_data() is called @@ -142,6 +145,13 @@ class WlSurface : public wayland::Surface void commit(WlSurfaceState const& state); auto confine_pointer_state() const -> MirPointerConfinementState; + /** + * Associate a viewport (buffer scale & crop metadata) with this surface + * + * \throws A std::logic_error if the surface already has a viewport associated + */ + void associate_viewport(wayland::Weak viewport); + std::shared_ptr const session; std::shared_ptr const stream; @@ -155,14 +165,24 @@ class WlSurface : public wayland::Surface NullWlSurfaceRole null_role; WlSurfaceRole* role; std::vector children; // ordering is from bottom to top + /* We might need to resubmit the current buffer, but with different metadata + * For example: if a client commits a wl_surface.buffer_scale() without attaching + * a new buffer, we need to generate a new frame with the same content, but at the + * new scale. + * + * To simplify other interfaces, keep a reference to the current content so we can + * hide this, here, in the frontend. + */ + std::shared_ptr current_buffer; WlSurfaceState pending; geometry::Displacement offset_; - float inv_scale{1.0f}; + int32_t scale{1}; std::optional buffer_size_; std::vector> frame_callbacks; std::optional> input_shape; std::vector scene_surface_created_callbacks; + wayland::Weak viewport; void send_frame_callbacks(); diff --git a/src/server/frontend_wayland/wp_viewporter.cpp b/src/server/frontend_wayland/wp_viewporter.cpp new file mode 100644 index 00000000000..3a65e05a064 --- /dev/null +++ b/src/server/frontend_wayland/wp_viewporter.cpp @@ -0,0 +1,238 @@ + +/* + * Copyright © Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "wp_viewporter.h" +#include "mir/geometry/forward.h" +#include "mir/wayland/protocol_error.h" +#include "mir/wayland/weak.h" +#include "wl_surface.h" + +#include + +namespace mf = mir::frontend; +namespace geom = mir::geometry; + +namespace +{ +class ViewporterInstance : public mir::wayland::Viewporter +{ +public: + explicit ViewporterInstance(wl_resource* resource) + : Viewporter(resource, Version<1>{}) + { + } + +private: + void get_viewport(wl_resource* id, wl_resource* surface) override + { + auto surf = mf::WlSurface::from(surface); + try + { + new mf::Viewport(id, surf); + } + catch (std::logic_error const&) + { + // We get a std::logic_error if the surface already had a viewport; translate to protocol exception here + throw mir::wayland::ProtocolError{ + resource, + Viewporter::Error::viewport_exists, + "Surface already has a viewport associated"}; + } + } +}; +} + +mf::WpViewporter::WpViewporter(wl_display* display) + : Global(display, Version<1>{}) +{ +} + +void mf::WpViewporter::bind(wl_resource* new_resource) +{ + new ViewporterInstance(new_resource); +} + +mf::Viewport::Viewport(wl_resource* new_viewport, WlSurface* surface) + : wayland::Viewport(new_viewport, Version<1>{}) +{ + surface->associate_viewport(wayland::make_weak(this)); + this->surface = wayland::make_weak(surface); +} + +mf::Viewport::~Viewport() +{ +} + +auto mf::Viewport::changed_since_last_resolve() -> bool +{ + return viewport_changed; +} + +namespace +{ +auto as_int_if_exact(double d) -> std::optional +{ + if (std::trunc(d) == d && d < std::numeric_limits::max()) + { + return static_cast(d); + } + return {}; +} + +auto as_int_size_if_exact(geom::SizeD size) -> std::optional +{ + auto width = as_int_if_exact(size.width.as_value()); + auto height = as_int_if_exact(size.height.as_value()); + + if (width && height) + { + return geom::Size{*width, *height}; + } + return {}; +} +} + +auto mf::Viewport::resolve_viewport(int32_t scale, geom::Size buffer_size) const + -> std::pair +{ + auto const src_bounds = + [&]() + { + auto const entire_buffer = geom::RectangleD{{0, 0}, geom::SizeD{buffer_size}}; + if (source) + { + auto raw_source = source.value(); + /* Viewport coordinates are in buffer coordinates *after* applying transformation and scale. + * That means this rectangle needs to be scaled. (And have buffer transform applied, when we support that) + */ + geom::RectangleD source_in_buffer_coords{ + {raw_source.left().as_value() * scale, raw_source.top().as_value() * scale}, + raw_source.size * scale + }; + if (!entire_buffer.contains(source_in_buffer_coords)) + { + throw wayland::ProtocolError{ + resource, + wayland::Viewport::Error::out_of_buffer, + "Source viewport (%f, %f), (%f × %f) is not entirely within buffer (%i × %i)", + source_in_buffer_coords.left().as_value(), + source_in_buffer_coords.top().as_value(), + source_in_buffer_coords.size.width.as_value(), + source_in_buffer_coords.size.height.as_value(), + buffer_size.width.as_uint32_t(), + buffer_size.height.as_uint32_t() + }; + } + return source_in_buffer_coords; + } + else + { + return entire_buffer; + } + }(); + + auto const logical_size = + [&]() + { + if (destination) + { + return destination.value(); + } + if (as_int_size_if_exact(src_bounds.size)) + { + return as_int_size_if_exact(src_bounds.size).value() / scale; + } + else + { + throw wayland::ProtocolError{ + resource, + wayland::Viewport::Error::bad_size, + "No wp_viewport destination set, and source size (%f × %f) is not integral", src_bounds.size.width.as_value(), src_bounds.size.height.as_value()}; + } + }(); + + viewport_changed = false; + return std::make_pair(src_bounds, logical_size); +} + +void mf::Viewport::set_source(double x, double y, double width, double height) +{ + if (!surface) + { + throw wayland::ProtocolError{ + resource, + Error::no_surface, + "Surface associated with viewport has been destroyed"}; + } + // We know the parameters are coming from a 16.16 wl_fixed value, so doubles give exact representation + if (x < 0 || y < 0 || width <= 0 || height <= 0) + { + // All values == -1 means “unset the source viewport” + if ((x == y) && (y == width) && (width == height) && (height == -1.0f)) + { + source = std::nullopt; + } + else + { + throw wayland::ProtocolError{ + resource, + Error::bad_value, + "Source viewport (%f, %f), (%f × %f) invalid: (x,y) must be non-negative, (width, height) must be positive", + x, y, + width, height + }; + } + } + else + { + source = geometry::RectangleD{{x, y}, {width, height}}; + } + viewport_changed = true; +} + +void mf::Viewport::set_destination(int32_t width, int32_t height) +{ + if (!surface) + { + throw wayland::ProtocolError{ + resource, + Error::no_surface, + "Surface associated with viewport has been destroyed"}; + } + if (width <= 0 || height <= 0) + { + if (width == -1 && height == -1) + { + destination = std::nullopt; + } + else + { + throw wayland::ProtocolError{ + resource, + Error::bad_value, + "Destination viewport (%i × %i) invalid: (width, height) must both be positive", + width, height + }; + } + } + else + { + destination = geometry::Size{width, height}; + } + + viewport_changed = true; +} diff --git a/src/server/frontend_wayland/wp_viewporter.h b/src/server/frontend_wayland/wp_viewporter.h new file mode 100644 index 00000000000..139bd80995b --- /dev/null +++ b/src/server/frontend_wayland/wp_viewporter.h @@ -0,0 +1,78 @@ + +/* + * Copyright © Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef MIR_FRONTEND_WP_VIEWPORTER_H +#define MIR_FRONTEND_WP_VIEWPORTER_H + +#include "mir/wayland/weak.h" +#include "viewporter_wrapper.h" +#include "wayland_wrapper.h" + +#include "mir/geometry/rectangle.h" + +#include + +namespace mir::frontend +{ +class WlSurface; + +class WpViewporter : public wayland::Viewporter::Global +{ +public: + explicit WpViewporter(wl_display* display); + +private: + void bind(wl_resource* new_wp_viewporter) override; +}; + +/** + * Manages the wp_viewport state + * + * Threadsafety: This is a Wayland object, and should only be accessed from the Wayland thread + */ +class Viewport : public wayland::Viewport +{ +public: + Viewport(wl_resource* new_viewport, WlSurface* surface); + ~Viewport() override; + + /** + * Have the source or destination values changed since the last `resolve_viewport`? + */ + auto changed_since_last_resolve() -> bool; + + /** + * Combine the viewport parameters with submitted buffer and scale to produce final parameters + * + * \returns The source viewport into the buffer, in buffer coordinates, and + * The logical (post-scaling) size of this wl_surface + * \throws A wayland::ProtocolError if any of the wp_viewporter preconditions are not met. + */ + auto resolve_viewport(int32_t scale, geometry::Size buffer_size) const + -> std::pair; +private: + void set_source(double x, double y, double width, double height) override; + void set_destination(int32_t width, int32_t height) override; + + bool mutable viewport_changed; + wayland::Weak surface; + std::optional source; + std::optional destination; +}; +} + +#endif // MIR_FRONTEND_WP_VIEWPORTER_H diff --git a/src/server/graphics/software_cursor.cpp b/src/server/graphics/software_cursor.cpp index a901d5d1e42..2ab0bd4df10 100644 --- a/src/server/graphics/software_cursor.cpp +++ b/src/server/graphics/software_cursor.cpp @@ -80,6 +80,11 @@ class mg::detail::CursorRenderable : public mg::Renderable return {position, buffer_->size()}; } + geom::RectangleD src_bounds() const override + { + return {{0, 0}, buffer_->size()}; + } + std::optional clip_area() const override { return std::optional(); diff --git a/src/server/input/touchspot_controller.cpp b/src/server/input/touchspot_controller.cpp index 68eade869a9..a4ac78ada4f 100644 --- a/src/server/input/touchspot_controller.cpp +++ b/src/server/input/touchspot_controller.cpp @@ -63,6 +63,11 @@ class mi::TouchspotRenderable : public mg::Renderable return {position, buffer_->size()}; } + geom::RectangleD src_bounds() const override + { + return {{0, 0}, buffer_->size()}; + } + std::optional clip_area() const override { return std::optional(); diff --git a/src/server/scene/basic_surface.cpp b/src/server/scene/basic_surface.cpp index a58e54e40aa..bc8987bf604 100644 --- a/src/server/scene/basic_surface.cpp +++ b/src/server/scene/basic_surface.cpp @@ -719,6 +719,9 @@ class SurfaceSnapshot : public mg::Renderable geom::Rectangle screen_position() const override { return screen_position_; } + geom::RectangleD src_bounds() const override + { return entry->source_rect(); } + std::optional clip_area() const override { return clip_area_; } diff --git a/src/server/shell/basic_idle_handler.cpp b/src/server/shell/basic_idle_handler.cpp index e5f901d95fd..51057549a79 100644 --- a/src/server/shell/basic_idle_handler.cpp +++ b/src/server/shell/basic_idle_handler.cpp @@ -65,6 +65,11 @@ struct DimmingRenderable : public mg::Renderable return {{-coverage_size / 2, -coverage_size / 2}, {coverage_size, coverage_size}}; } + auto src_bounds() const -> geom::RectangleD override + { + return {{0, 0}, buffer_->size()}; + } + auto clip_area() const -> std::optional override { return {}; diff --git a/src/wayland/CMakeLists.txt b/src/wayland/CMakeLists.txt index 99d4f9a2dc1..baa50acdcab 100644 --- a/src/wayland/CMakeLists.txt +++ b/src/wayland/CMakeLists.txt @@ -37,6 +37,7 @@ mir_generate_protocol_wrapper(mirwayland "zwlr_" wlr-virtual-pointer-unstable-v1 mir_generate_protocol_wrapper(mirwayland "ext_" ext-session-lock-v1.xml) mir_generate_protocol_wrapper(mirwayland "zmir_" mir-shell-unstable-v1.xml) mir_generate_protocol_wrapper(mirwayland "z" xdg-decoration-unstable-v1.xml) +mir_generate_protocol_wrapper(mirwayland "wp_" viewporter.xml) target_link_libraries(mirwayland PUBLIC diff --git a/tests/acceptance-tests/wayland/miral_integration.cpp b/tests/acceptance-tests/wayland/miral_integration.cpp index 1cc042790bc..bcfa7d26394 100644 --- a/tests/acceptance-tests/wayland/miral_integration.cpp +++ b/tests/acceptance-tests/wayland/miral_integration.cpp @@ -31,7 +31,8 @@ WlcsExtensionDescriptor const extensions[] = { {"zxdg_shell_unstable_v6", 1}, {"wlr_layer_shell_unstable_v1", 1}, {"zwp_pointer_constraints_v1", 1}, - {"zwp_relative_pointer_manager_v1", 1} + {"zwp_relative_pointer_manager_v1", 1}, + {"wp_viewporter", 1}, }; WlcsIntegrationDescriptor const descriptor{ diff --git a/tests/include/mir/test/doubles/fake_renderable.h b/tests/include/mir/test/doubles/fake_renderable.h index 232e40d884e..5b10f861a64 100644 --- a/tests/include/mir/test/doubles/fake_renderable.h +++ b/tests/include/mir/test/doubles/fake_renderable.h @@ -92,6 +92,11 @@ class FakeRenderable : public graphics::Renderable { return rect; } + + geometry::RectangleD src_bounds() const override + { + return {{0, 0}, buf->size()}; + } std::optional clip_area() const override { diff --git a/tests/include/mir/test/doubles/mock_renderable.h b/tests/include/mir/test/doubles/mock_renderable.h index 0e3b667c29e..7e0160cbed3 100644 --- a/tests/include/mir/test/doubles/mock_renderable.h +++ b/tests/include/mir/test/doubles/mock_renderable.h @@ -48,6 +48,7 @@ struct MockRenderable : public graphics::Renderable MOCK_CONST_METHOD0(id, ID()); MOCK_CONST_METHOD0(buffer, std::shared_ptr()); MOCK_CONST_METHOD0(screen_position, geometry::Rectangle()); + MOCK_CONST_METHOD0(src_bounds, geometry::RectangleD()); MOCK_CONST_METHOD0(clip_area, std::optional()); MOCK_CONST_METHOD0(alpha, float()); MOCK_CONST_METHOD0(transformation, glm::mat4()); diff --git a/tests/include/mir/test/doubles/stub_renderable.h b/tests/include/mir/test/doubles/stub_renderable.h index f8ead8e46aa..d7554b53b91 100644 --- a/tests/include/mir/test/doubles/stub_renderable.h +++ b/tests/include/mir/test/doubles/stub_renderable.h @@ -72,6 +72,10 @@ class StubRenderable : public graphics::Renderable { return rect; } + geometry::RectangleD src_bounds() const override + { + return {{0, 0}, stub_buffer->size()}; + } std::optional clip_area() const override { return std::optional(); diff --git a/tests/platform_test_harness/graphics_platform_test_harness.cpp b/tests/platform_test_harness/graphics_platform_test_harness.cpp index ffb23f9d17a..58321760d28 100644 --- a/tests/platform_test_harness/graphics_platform_test_harness.cpp +++ b/tests/platform_test_harness/graphics_platform_test_harness.cpp @@ -426,6 +426,11 @@ void basic_software_buffer_drawing( return mir::geometry::Rectangle{top_left, buffer()->size()}; } + auto src_bounds() const -> mir::geometry::RectangleD override + { + return {{0, 0}, buffer()->size()}; + } + auto alpha() const -> float override { return 1.0f; diff --git a/tests/unit-tests/geometry/test-size.cpp b/tests/unit-tests/geometry/test-size.cpp index cb0fc684730..8c0fba7b7a8 100644 --- a/tests/unit-tests/geometry/test-size.cpp +++ b/tests/unit-tests/geometry/test-size.cpp @@ -61,3 +61,10 @@ TEST(geometry, size_can_be_converted_from_float_to_int) EXPECT_EQ(Width(1), i.width); EXPECT_EQ(Height(3), i.height); } + +// Test that non-lossy implicit conversions exist +static_assert(std::convertible_to, "Implicit conversion int->double preserves precision"); +static_assert(std::convertible_to, "Implicit conversion float->double preserves precision"); +// Test that lossy implicit conversions fail +static_assert(!std::convertible_to, "Conversion float->int is lossy"); +static_assert(!std::convertible_to, "Conversion double->float is lossy"); diff --git a/tests/unit-tests/gl/test_tessellation_helpers.cpp b/tests/unit-tests/gl/test_tessellation_helpers.cpp index fe99461903e..1e1067c9d98 100644 --- a/tests/unit-tests/gl/test_tessellation_helpers.cpp +++ b/tests/unit-tests/gl/test_tessellation_helpers.cpp @@ -14,6 +14,7 @@ * along with this program. If not, see . */ +#include "mir/test/doubles/mock_buffer.h" #include #include #include @@ -24,6 +25,7 @@ namespace geom = mir::geometry; namespace mt = mir::test; namespace mtd = mir::test::doubles; namespace mgl = mir::gl; +namespace mg = mir::graphics; struct BoundingBox { @@ -60,6 +62,10 @@ class Tessellation : public testing::Test { ON_CALL(renderable, screen_position()) .WillByDefault(Return(rect)); + ON_CALL(renderable, src_bounds()) + .WillByDefault(Return(geom::RectangleD{{0, 0}, {geom::SizeD{rect.size}}})); + ON_CALL(renderable, buffer()) + .WillByDefault(Return(buffer_)); } static auto bounding_box(mgl::Primitive const& primitive) -> BoundingBox @@ -95,6 +101,7 @@ class Tessellation : public testing::Test geom::Rectangle const rect{{4, 6}, {10, 20}}; NiceMock const renderable; + std::shared_ptr const buffer_ = std::make_shared>(rect.size, geom::Stride{}, mir_pixel_format_argb_8888); }; TEST_F(Tessellation, has_4_vertices) diff --git a/wayland-protocols/viewporter.xml b/wayland-protocols/viewporter.xml new file mode 100644 index 00000000000..1374aeca065 --- /dev/null +++ b/wayland-protocols/viewporter.xml @@ -0,0 +1,177 @@ + + + + + Copyright © 2013-2016 Collabora, Ltd. + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice (including the next + paragraph) shall be included in all copies or substantial portions of the + Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. + + + + + The global interface exposing surface cropping and scaling + capabilities is used to instantiate an interface extension for a + wl_surface object. This extended interface will then allow + cropping and scaling the surface contents, effectively + disconnecting the direct relationship between the buffer and the + surface size. + + + + + Informs the server that the client will not be using this + protocol object anymore. This does not affect any other objects, + wp_viewport objects included. + + + + + + + + + + Instantiate an interface extension for the given wl_surface to + crop and scale its content. If the given wl_surface already has + a wp_viewport object associated, the viewport_exists + protocol error is raised. + + + + + + + + + An additional interface to a wl_surface object, which allows the + client to specify the cropping and scaling of the surface + contents. + + This interface works with two concepts: the source rectangle (src_x, + src_y, src_width, src_height), and the destination size (dst_width, + dst_height). The contents of the source rectangle are scaled to the + destination size, and content outside the source rectangle is ignored. + This state is double-buffered, see wl_surface.commit. + + The two parts of crop and scale state are independent: the source + rectangle, and the destination size. Initially both are unset, that + is, no scaling is applied. The whole of the current wl_buffer is + used as the source, and the surface size is as defined in + wl_surface.attach. + + If the destination size is set, it causes the surface size to become + dst_width, dst_height. The source (rectangle) is scaled to exactly + this size. This overrides whatever the attached wl_buffer size is, + unless the wl_buffer is NULL. If the wl_buffer is NULL, the surface + has no content and therefore no size. Otherwise, the size is always + at least 1x1 in surface local coordinates. + + If the source rectangle is set, it defines what area of the wl_buffer is + taken as the source. If the source rectangle is set and the destination + size is not set, then src_width and src_height must be integers, and the + surface size becomes the source rectangle size. This results in cropping + without scaling. If src_width or src_height are not integers and + destination size is not set, the bad_size protocol error is raised when + the surface state is applied. + + The coordinate transformations from buffer pixel coordinates up to + the surface-local coordinates happen in the following order: + 1. buffer_transform (wl_surface.set_buffer_transform) + 2. buffer_scale (wl_surface.set_buffer_scale) + 3. crop and scale (wp_viewport.set*) + This means, that the source rectangle coordinates of crop and scale + are given in the coordinates after the buffer transform and scale, + i.e. in the coordinates that would be the surface-local coordinates + if the crop and scale was not applied. + + If src_x or src_y are negative, the bad_value protocol error is raised. + Otherwise, if the source rectangle is partially or completely outside of + the non-NULL wl_buffer, then the out_of_buffer protocol error is raised + when the surface state is applied. A NULL wl_buffer does not raise the + out_of_buffer error. + + If the wl_surface associated with the wp_viewport is destroyed, + all wp_viewport requests except 'destroy' raise the protocol error + no_surface. + + If the wp_viewport object is destroyed, the crop and scale + state is removed from the wl_surface. The change will be applied + on the next wl_surface.commit. + + + + + The associated wl_surface's crop and scale state is removed. + The change is applied on the next wl_surface.commit. + + + + + + + + + + + + + Set the source rectangle of the associated wl_surface. See + wp_viewport for the description, and relation to the wl_buffer + size. + + If all of x, y, width and height are -1.0, the source rectangle is + unset instead. Any other set of values where width or height are zero + or negative, or x or y are negative, raise the bad_value protocol + error. + + The crop and scale state is double-buffered, see wl_surface.commit. + + + + + + + + + + Set the destination size of the associated wl_surface. See + wp_viewport for the description, and relation to the wl_buffer + size. + + If width is -1 and height is -1, the destination size is unset + instead. Any other pair of values for width and height that + contains zero or negative values raises the bad_value protocol + error. + + The crop and scale state is double-buffered, see wl_surface.commit. + + + + + + +