diff --git a/river/DragIcon.zig b/river/DragIcon.zig
index 7500e62..11683ea 100644
--- a/river/DragIcon.zig
+++ b/river/DragIcon.zig
@@ -24,7 +24,6 @@ const server = &@import("main.zig").server;
 const util = @import("util.zig");
 
 const Seat = @import("Seat.zig");
-const Subsurface = @import("Subsurface.zig");
 
 seat: *Seat,
 wlr_drag_icon: *wlr.Drag.Icon,
@@ -35,63 +34,29 @@ sy: i32 = 0,
 
 // Always active
 destroy: wl.Listener(*wlr.Drag.Icon) = wl.Listener(*wlr.Drag.Icon).init(handleDestroy),
-map: wl.Listener(*wlr.Drag.Icon) = wl.Listener(*wlr.Drag.Icon).init(handleMap),
-unmap: wl.Listener(*wlr.Drag.Icon) = wl.Listener(*wlr.Drag.Icon).init(handleUnmap),
 commit: wl.Listener(*wlr.Surface) = wl.Listener(*wlr.Surface).init(handleCommit),
-new_subsurface: wl.Listener(*wlr.Subsurface) = wl.Listener(*wlr.Subsurface).init(handleNewSubsurface),
 
 pub fn init(drag_icon: *DragIcon, seat: *Seat, wlr_drag_icon: *wlr.Drag.Icon) void {
     drag_icon.* = .{ .seat = seat, .wlr_drag_icon = wlr_drag_icon };
 
     wlr_drag_icon.events.destroy.add(&drag_icon.destroy);
-    wlr_drag_icon.events.map.add(&drag_icon.map);
-    wlr_drag_icon.events.unmap.add(&drag_icon.unmap);
     wlr_drag_icon.surface.events.commit.add(&drag_icon.commit);
-    wlr_drag_icon.surface.events.new_subsurface.add(&drag_icon.new_subsurface);
-
-    if (wlr_drag_icon.mapped) handleMap(&drag_icon.map, wlr_drag_icon);
-
-    Subsurface.handleExisting(wlr_drag_icon.surface, .{ .drag_icon = drag_icon });
 }
 
-fn handleDestroy(listener: *wl.Listener(*wlr.Drag.Icon), wlr_drag_icon: *wlr.Drag.Icon) void {
+fn handleDestroy(listener: *wl.Listener(*wlr.Drag.Icon), _: *wlr.Drag.Icon) void {
     const drag_icon = @fieldParentPtr(DragIcon, "destroy", listener);
 
     drag_icon.seat.drag_icon = null;
 
     drag_icon.destroy.link.remove();
-    drag_icon.map.link.remove();
-    drag_icon.unmap.link.remove();
     drag_icon.commit.link.remove();
-    drag_icon.new_subsurface.link.remove();
-
-    Subsurface.destroySubsurfaces(wlr_drag_icon.surface);
 
     util.gpa.destroy(drag_icon);
 }
 
-fn handleMap(_: *wl.Listener(*wlr.Drag.Icon), _: *wlr.Drag.Icon) void {
-    var it = server.root.outputs.first;
-    while (it) |node| : (it = node.next) node.data.damage.?.addWhole();
-}
-
-fn handleUnmap(_: *wl.Listener(*wlr.Drag.Icon), _: *wlr.Drag.Icon) void {
-    var it = server.root.outputs.first;
-    while (it) |node| : (it = node.next) node.data.damage.?.addWhole();
-}
-
 fn handleCommit(listener: *wl.Listener(*wlr.Surface), surface: *wlr.Surface) void {
     const drag_icon = @fieldParentPtr(DragIcon, "commit", listener);
 
     drag_icon.sx += surface.current.dx;
     drag_icon.sy += surface.current.dy;
-
-    var it = server.root.outputs.first;
-    while (it) |node| : (it = node.next) node.data.damage.?.addWhole();
-}
-
-fn handleNewSubsurface(listener: *wl.Listener(*wlr.Subsurface), wlr_subsurface: *wlr.Subsurface) void {
-    const drag_icon = @fieldParentPtr(DragIcon, "new_subsurface", listener);
-
-    Subsurface.create(wlr_subsurface, .{ .drag_icon = drag_icon });
 }
diff --git a/river/LayerSurface.zig b/river/LayerSurface.zig
index 9a56a73..16e50df 100644
--- a/river/LayerSurface.zig
+++ b/river/LayerSurface.zig
@@ -26,8 +26,6 @@ const server = &@import("main.zig").server;
 const util = @import("util.zig");
 
 const Output = @import("Output.zig");
-const Subsurface = @import("Subsurface.zig");
-const XdgPopup = @import("XdgPopup.zig");
 
 const log = std.log.scoped(.layer_shell);
 
@@ -40,8 +38,6 @@ layer: zwlr.LayerShellV1.Layer,
 destroy: wl.Listener(*wlr.LayerSurfaceV1) = wl.Listener(*wlr.LayerSurfaceV1).init(handleDestroy),
 map: wl.Listener(*wlr.LayerSurfaceV1) = wl.Listener(*wlr.LayerSurfaceV1).init(handleMap),
 unmap: wl.Listener(*wlr.LayerSurfaceV1) = wl.Listener(*wlr.LayerSurfaceV1).init(handleUnmap),
-new_popup: wl.Listener(*wlr.XdgPopup) = wl.Listener(*wlr.XdgPopup).init(handleNewPopup),
-new_subsurface: wl.Listener(*wlr.Subsurface) = wl.Listener(*wlr.Subsurface).init(handleNewSubsurface),
 commit: wl.Listener(*wlr.Surface) = wl.Listener(*wlr.Surface).init(handleCommit),
 
 pub fn init(self: *Self, output: *Output, wlr_layer_surface: *wlr.LayerSurfaceV1) void {
@@ -56,19 +52,15 @@ pub fn init(self: *Self, output: *Output, wlr_layer_surface: *wlr.LayerSurfaceV1
     wlr_layer_surface.events.destroy.add(&self.destroy);
     wlr_layer_surface.events.map.add(&self.map);
     wlr_layer_surface.events.unmap.add(&self.unmap);
-    wlr_layer_surface.events.new_popup.add(&self.new_popup);
     wlr_layer_surface.surface.events.commit.add(&self.commit);
-    wlr_layer_surface.surface.events.new_subsurface.add(&self.new_subsurface);
 
     // wlroots only informs us of the new surface after the first commit,
     // so our listener does not get called for this first commit. However,
     // we do want our listener called in order to send the initial configure.
     handleCommit(&self.commit, wlr_layer_surface.surface);
-
-    Subsurface.handleExisting(wlr_layer_surface.surface, .{ .layer_surface = self });
 }
 
-fn handleDestroy(listener: *wl.Listener(*wlr.LayerSurfaceV1), wlr_layer_surface: *wlr.LayerSurfaceV1) void {
+fn handleDestroy(listener: *wl.Listener(*wlr.LayerSurfaceV1), _: *wlr.LayerSurfaceV1) void {
     const self = @fieldParentPtr(Self, "destroy", listener);
 
     log.debug("layer surface '{s}' destroyed", .{self.wlr_layer_surface.namespace});
@@ -77,15 +69,7 @@ fn handleDestroy(listener: *wl.Listener(*wlr.LayerSurfaceV1), wlr_layer_surface:
     self.destroy.link.remove();
     self.map.link.remove();
     self.unmap.link.remove();
-    self.new_popup.link.remove();
     self.commit.link.remove();
-    self.new_subsurface.link.remove();
-
-    Subsurface.destroySubsurfaces(self.wlr_layer_surface.surface);
-    var it = wlr_layer_surface.popups.iterator(.forward);
-    while (it.next()) |wlr_xdg_popup| {
-        if (@intToPtr(?*XdgPopup, wlr_xdg_popup.base.data)) |xdg_popup| xdg_popup.destroy();
-    }
 
     const node = @fieldParentPtr(std.TailQueue(Self).Node, "data", self);
     util.gpa.destroy(node);
@@ -163,16 +147,4 @@ fn handleCommit(listener: *wl.Listener(*wlr.Surface), _: *wlr.Surface) void {
         self.output.arrangeLayers(.mapped);
         server.root.startTransaction();
     }
-
-    self.output.damage.?.addWhole();
-}
-
-fn handleNewPopup(listener: *wl.Listener(*wlr.XdgPopup), wlr_xdg_popup: *wlr.XdgPopup) void {
-    const self = @fieldParentPtr(Self, "new_popup", listener);
-    XdgPopup.create(wlr_xdg_popup, .{ .layer_surface = self });
-}
-
-fn handleNewSubsurface(listener: *wl.Listener(*wlr.Subsurface), new_wlr_subsurface: *wlr.Subsurface) void {
-    const self = @fieldParentPtr(Self, "new_subsurface", listener);
-    Subsurface.create(new_wlr_subsurface, .{ .layer_surface = self });
 }
diff --git a/river/LockManager.zig b/river/LockManager.zig
index 0c108fc..eab3c4a 100644
--- a/river/LockManager.zig
+++ b/river/LockManager.zig
@@ -130,16 +130,6 @@ fn handleLockSurfacesTimeout(manager: *LockManager) c_int {
     assert(manager.state == .waiting_for_lock_surfaces);
     manager.state = .waiting_for_blank;
 
-    {
-        var it = server.root.outputs.first;
-        while (it) |node| : (it = node.next) {
-            const output = &node.data;
-            if (output.lock_render_state == .unlocked) {
-                output.damage.?.addWhole();
-            }
-        }
-    }
-
     // This call is necessary in the case that all outputs in the layout are disabled.
     manager.maybeLock();
 
diff --git a/river/LockSurface.zig b/river/LockSurface.zig
index 2ce8a63..541944f 100644
--- a/river/LockSurface.zig
+++ b/river/LockSurface.zig
@@ -25,7 +25,6 @@ const util = @import("util.zig");
 
 const Output = @import("Output.zig");
 const Seat = @import("Seat.zig");
-const Subsurface = @import("Subsurface.zig");
 
 wlr_lock_surface: *wlr.SessionLockSurfaceV1,
 lock: *wlr.SessionLockV1,
@@ -33,8 +32,6 @@ lock: *wlr.SessionLockV1,
 output_mode: wl.Listener(*wlr.Output) = wl.Listener(*wlr.Output).init(handleOutputMode),
 map: wl.Listener(void) = wl.Listener(void).init(handleMap),
 surface_destroy: wl.Listener(void) = wl.Listener(void).init(handleDestroy),
-commit: wl.Listener(*wlr.Surface) = wl.Listener(*wlr.Surface).init(handleCommit),
-new_subsurface: wl.Listener(*wlr.Subsurface) = wl.Listener(*wlr.Subsurface).init(handleSubsurface),
 
 pub fn create(wlr_lock_surface: *wlr.SessionLockSurfaceV1, lock: *wlr.SessionLockV1) void {
     const lock_surface = util.gpa.create(LockSurface) catch {
@@ -51,17 +48,12 @@ pub fn create(wlr_lock_surface: *wlr.SessionLockSurfaceV1, lock: *wlr.SessionLoc
     wlr_lock_surface.output.events.mode.add(&lock_surface.output_mode);
     wlr_lock_surface.events.map.add(&lock_surface.map);
     wlr_lock_surface.events.destroy.add(&lock_surface.surface_destroy);
-    wlr_lock_surface.surface.events.commit.add(&lock_surface.commit);
-    wlr_lock_surface.surface.events.new_subsurface.add(&lock_surface.new_subsurface);
 
     handleOutputMode(&lock_surface.output_mode, wlr_lock_surface.output);
-
-    Subsurface.handleExisting(wlr_lock_surface.surface, .{ .lock_surface = lock_surface });
 }
 
 pub fn destroy(lock_surface: *LockSurface) void {
     lock_surface.output().lock_surface = null;
-    if (lock_surface.output().damage) |damage| damage.addWhole();
 
     {
         var surface_it = lock_surface.lock.surfaces.iterator(.forward);
@@ -83,10 +75,6 @@ pub fn destroy(lock_surface: *LockSurface) void {
     lock_surface.output_mode.link.remove();
     lock_surface.map.link.remove();
     lock_surface.surface_destroy.link.remove();
-    lock_surface.commit.link.remove();
-    lock_surface.new_subsurface.link.remove();
-
-    Subsurface.destroySubsurfaces(lock_surface.wlr_lock_surface.surface);
 
     util.gpa.destroy(lock_surface);
 }
@@ -126,14 +114,3 @@ fn handleDestroy(listener: *wl.Listener(void)) void {
 
     lock_surface.destroy();
 }
-
-fn handleCommit(listener: *wl.Listener(*wlr.Surface), _: *wlr.Surface) void {
-    const lock_surface = @fieldParentPtr(LockSurface, "commit", listener);
-
-    lock_surface.output().damage.?.addWhole();
-}
-
-fn handleSubsurface(listener: *wl.Listener(*wlr.Subsurface), subsurface: *wlr.Subsurface) void {
-    const lock_surface = @fieldParentPtr(LockSurface, "new_subsurface", listener);
-    Subsurface.create(subsurface, .{ .lock_surface = lock_surface });
-}
diff --git a/river/Output.zig b/river/Output.zig
index b654193..0c7b462 100644
--- a/river/Output.zig
+++ b/river/Output.zig
@@ -55,7 +55,6 @@ const State = struct {
 };
 
 wlr_output: *wlr.Output,
-damage: ?*wlr.OutputDamage,
 
 /// All layer surfaces on the output, indexed by the layer enum.
 layers: [4]std.TailQueue(LayerSurface) = [1]std.TailQueue(LayerSurface){.{}} ** 4,
@@ -115,9 +114,8 @@ status_trackers: std.SinglyLinkedList(OutputStatus) = .{},
 destroy: wl.Listener(*wlr.Output) = wl.Listener(*wlr.Output).init(handleDestroy),
 enable: wl.Listener(*wlr.Output) = wl.Listener(*wlr.Output).init(handleEnable),
 mode: wl.Listener(*wlr.Output) = wl.Listener(*wlr.Output).init(handleMode),
+frame: wl.Listener(*wlr.Output) = wl.Listener(*wlr.Output).init(handleFrame),
 present: wl.Listener(*wlr.Output.event.Present) = wl.Listener(*wlr.Output.event.Present).init(handlePresent),
-frame: wl.Listener(*wlr.OutputDamage) = wl.Listener(*wlr.OutputDamage).init(handleFrame),
-damage_destroy: wl.Listener(*wlr.OutputDamage) = wl.Listener(*wlr.OutputDamage).init(handleDamageDestroy),
 
 pub fn init(self: *Self, wlr_output: *wlr.Output) !void {
     if (!wlr_output.initRender(server.allocator, server.renderer)) return;
@@ -146,7 +144,6 @@ pub fn init(self: *Self, wlr_output: *wlr.Output) !void {
 
     self.* = .{
         .wlr_output = wlr_output,
-        .damage = try wlr.OutputDamage.create(wlr_output),
         .usable_box = undefined,
     };
     wlr_output.data = @ptrToInt(self);
@@ -154,11 +151,9 @@ pub fn init(self: *Self, wlr_output: *wlr.Output) !void {
     wlr_output.events.destroy.add(&self.destroy);
     wlr_output.events.enable.add(&self.enable);
     wlr_output.events.mode.add(&self.mode);
+    wlr_output.events.frame.add(&self.frame);
     wlr_output.events.present.add(&self.present);
 
-    self.damage.?.events.frame.add(&self.frame);
-    self.damage.?.events.destroy.add(&self.damage_destroy);
-
     // Ensure that a cursor image at the output's scale factor is loaded
     // for each seat.
     var it = server.input_manager.seats.first;
@@ -458,17 +453,6 @@ fn arrangeLayer(
     }
 }
 
-fn handleDamageDestroy(listener: *wl.Listener(*wlr.OutputDamage), _: *wlr.OutputDamage) void {
-    const self = @fieldParentPtr(Self, "damage_destroy", listener);
-    // The wlr.OutputDamage is only destroyed by wlroots when the output is
-    // destroyed and is never destroyed manually by river.
-    self.frame.link.remove();
-    // Ensure that it is safe to call remove() again in handleDestroy()
-    self.frame.link = .{ .prev = &self.frame.link, .next = &self.frame.link };
-
-    self.damage = null;
-}
-
 fn handleDestroy(listener: *wl.Listener(*wlr.Output), _: *wlr.Output) void {
     const self = @fieldParentPtr(Self, "destroy", listener);
 
@@ -527,9 +511,7 @@ fn handleEnable(listener: *wl.Listener(*wlr.Output), wlr_output: *wlr.Output) vo
     }
 }
 
-fn handleFrame(listener: *wl.Listener(*wlr.OutputDamage), _: *wlr.OutputDamage) void {
-    // This function is called every time an output is ready to display a frame,
-    // generally at the output's refresh rate (e.g. 60Hz).
+fn handleFrame(listener: *wl.Listener(*wlr.Output), _: *wlr.Output) void {
     const self = @fieldParentPtr(Self, "frame", listener);
     render.renderOutput(self);
 }
@@ -552,7 +534,6 @@ fn handlePresent(
         .pending_blank, .pending_lock_surface => {
             if (!event.presented) {
                 self.lock_render_state = .unlocked;
-                self.damage.?.addWhole();
                 return;
             }
 
diff --git a/river/Root.zig b/river/Root.zig
index 6418203..b0229ad 100644
--- a/river/Root.zig
+++ b/river/Root.zig
@@ -91,8 +91,6 @@ pub fn init(self: *Self) !void {
         .transaction_timer = transaction_timer,
         .noop_output = .{
             .wlr_output = noop_wlr_output,
-            // TODO: find a good way to not create a wlr.OutputDamage for the noop output
-            .damage = try wlr.OutputDamage.create(noop_wlr_output),
             .usable_box = .{ .x = 0, .y = 0, .width = 0, .height = 0 },
         },
     };
@@ -390,8 +388,6 @@ fn commitTransaction(self: *Self) void {
 
         if (view_tags_changed) output.sendViewTags();
         if (urgent_tags_dirty) output.sendUrgentTags();
-
-        output.damage.?.addWhole();
     }
     server.input_manager.updateCursorState();
     server.idle_inhibitor_manager.idleInhibitCheckActive();
diff --git a/river/Subsurface.zig b/river/Subsurface.zig
deleted file mode 100644
index 58b9380..0000000
--- a/river/Subsurface.zig
+++ /dev/null
@@ -1,160 +0,0 @@
-// This file is part of river, a dynamic tiling wayland compositor.
-//
-// Copyright 2021 The River Developers
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, version 3.
-//
-// 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 .
-
-const Subsurface = @This();
-
-const std = @import("std");
-const assert = std.debug.assert;
-const wlr = @import("wlroots");
-const wl = @import("wayland").server.wl;
-
-const server = &@import("main.zig").server;
-const util = @import("util.zig");
-
-const DragIcon = @import("DragIcon.zig");
-const LayerSurface = @import("LayerSurface.zig");
-const LockSurface = @import("LockSurface.zig");
-const XdgToplevel = @import("XdgToplevel.zig");
-
-pub const Parent = union(enum) {
-    xdg_toplevel: *XdgToplevel,
-    layer_surface: *LayerSurface,
-    lock_surface: *LockSurface,
-    drag_icon: *DragIcon,
-
-    pub fn damageWholeOutput(parent: Parent) void {
-        switch (parent) {
-            .xdg_toplevel => |xdg_toplevel| xdg_toplevel.view.output.damage.?.addWhole(),
-            .layer_surface => |layer_surface| layer_surface.output.damage.?.addWhole(),
-            .lock_surface => |lock_surface| lock_surface.output().damage.?.addWhole(),
-            .drag_icon => |_| {
-                var it = server.root.outputs.first;
-                while (it) |node| : (it = node.next) node.data.damage.?.addWhole();
-            },
-        }
-    }
-};
-
-/// The parent at the root of this surface tree
-parent: Parent,
-wlr_subsurface: *wlr.Subsurface,
-
-// Always active
-subsurface_destroy: wl.Listener(*wlr.Subsurface) = wl.Listener(*wlr.Subsurface).init(handleDestroy),
-map: wl.Listener(*wlr.Subsurface) = wl.Listener(*wlr.Subsurface).init(handleMap),
-unmap: wl.Listener(*wlr.Subsurface) = wl.Listener(*wlr.Subsurface).init(handleUnmap),
-new_subsurface: wl.Listener(*wlr.Subsurface) = wl.Listener(*wlr.Subsurface).init(handleNewSubsurface),
-
-// Only active while mapped
-commit: wl.Listener(*wlr.Surface) = wl.Listener(*wlr.Surface).init(handleCommit),
-
-pub fn create(wlr_subsurface: *wlr.Subsurface, parent: Parent) void {
-    const subsurface = util.gpa.create(Subsurface) catch {
-        std.log.err("out of memory", .{});
-        wlr_subsurface.resource.getClient().postNoMemory();
-        return;
-    };
-    subsurface.* = .{ .wlr_subsurface = wlr_subsurface, .parent = parent };
-    assert(wlr_subsurface.data == 0);
-    wlr_subsurface.data = @ptrToInt(subsurface);
-
-    wlr_subsurface.events.destroy.add(&subsurface.subsurface_destroy);
-    wlr_subsurface.events.map.add(&subsurface.map);
-    wlr_subsurface.events.unmap.add(&subsurface.unmap);
-    wlr_subsurface.surface.events.new_subsurface.add(&subsurface.new_subsurface);
-
-    if (wlr_subsurface.mapped) wlr_subsurface.surface.events.commit.add(&subsurface.commit);
-
-    Subsurface.handleExisting(wlr_subsurface.surface, parent);
-}
-
-/// Create Subsurface structs to track subsurfaces already present on the
-/// given surface when river becomes aware of the surface as we won't
-/// recieve a new_subsurface event for them.
-pub fn handleExisting(surface: *wlr.Surface, parent: Parent) void {
-    var below_it = surface.current.subsurfaces_below.iterator(.forward);
-    while (below_it.next()) |parent_state| {
-        const subsurface = @fieldParentPtr(wlr.Subsurface, "current", parent_state);
-        Subsurface.create(subsurface, parent);
-    }
-
-    var above_it = surface.current.subsurfaces_above.iterator(.forward);
-    while (above_it.next()) |parent_state| {
-        const subsurface = @fieldParentPtr(wlr.Subsurface, "current", parent_state);
-        Subsurface.create(subsurface, parent);
-    }
-}
-
-/// Destroy this Subsurface and all of its children
-pub fn destroy(subsurface: *Subsurface) void {
-    subsurface.subsurface_destroy.link.remove();
-    subsurface.map.link.remove();
-    subsurface.unmap.link.remove();
-    subsurface.new_subsurface.link.remove();
-
-    if (subsurface.wlr_subsurface.mapped) subsurface.commit.link.remove();
-
-    Subsurface.destroySubsurfaces(subsurface.wlr_subsurface.surface);
-
-    subsurface.wlr_subsurface.data = 0;
-    util.gpa.destroy(subsurface);
-}
-
-pub fn destroySubsurfaces(surface: *wlr.Surface) void {
-    var below_it = surface.current.subsurfaces_below.iterator(.forward);
-    while (below_it.next()) |parent_state| {
-        const wlr_subsurface = @fieldParentPtr(wlr.Subsurface, "current", parent_state);
-        if (@intToPtr(?*Subsurface, wlr_subsurface.data)) |s| s.destroy();
-    }
-
-    var above_it = surface.current.subsurfaces_above.iterator(.forward);
-    while (above_it.next()) |parent_state| {
-        const wlr_subsurface = @fieldParentPtr(wlr.Subsurface, "current", parent_state);
-        if (@intToPtr(?*Subsurface, wlr_subsurface.data)) |s| s.destroy();
-    }
-}
-
-fn handleDestroy(listener: *wl.Listener(*wlr.Subsurface), _: *wlr.Subsurface) void {
-    const subsurface = @fieldParentPtr(Subsurface, "subsurface_destroy", listener);
-
-    subsurface.destroy();
-}
-
-fn handleMap(listener: *wl.Listener(*wlr.Subsurface), wlr_subsurface: *wlr.Subsurface) void {
-    const subsurface = @fieldParentPtr(Subsurface, "map", listener);
-
-    wlr_subsurface.surface.events.commit.add(&subsurface.commit);
-    subsurface.parent.damageWholeOutput();
-}
-
-fn handleUnmap(listener: *wl.Listener(*wlr.Subsurface), _: *wlr.Subsurface) void {
-    const subsurface = @fieldParentPtr(Subsurface, "unmap", listener);
-
-    subsurface.commit.link.remove();
-    subsurface.parent.damageWholeOutput();
-}
-
-fn handleCommit(listener: *wl.Listener(*wlr.Surface), _: *wlr.Surface) void {
-    const subsurface = @fieldParentPtr(Subsurface, "commit", listener);
-
-    subsurface.parent.damageWholeOutput();
-}
-
-fn handleNewSubsurface(listener: *wl.Listener(*wlr.Subsurface), new_wlr_subsurface: *wlr.Subsurface) void {
-    const subsurface = @fieldParentPtr(Subsurface, "new_subsurface", listener);
-
-    Subsurface.create(new_wlr_subsurface, subsurface.parent);
-}
diff --git a/river/View.zig b/river/View.zig
index e666265..5993ee3 100644
--- a/river/View.zig
+++ b/river/View.zig
@@ -462,13 +462,6 @@ pub fn fromWlrSurface(surface: *wlr.Surface) ?*Self {
         const xwayland_surface = wlr.XwaylandSurface.fromWlrSurface(surface) orelse return null;
         return @intToPtr(?*Self, xwayland_surface.data);
     }
-    if (surface.isSubsurface()) {
-        if (wlr.Subsurface.fromWlrSurface(surface)) |ss| {
-            if (ss.parent) |s| {
-                return fromWlrSurface(s);
-            }
-        }
-    }
     return null;
 }
 
diff --git a/river/XdgPopup.zig b/river/XdgPopup.zig
deleted file mode 100644
index 3471181..0000000
--- a/river/XdgPopup.zig
+++ /dev/null
@@ -1,149 +0,0 @@
-// This file is part of river, a dynamic tiling wayland compositor.
-//
-// Copyright 2020 The River Developers
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, version 3.
-//
-// 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 .
-
-const XdgPopup = @This();
-
-const std = @import("std");
-const assert = std.debug.assert;
-const wlr = @import("wlroots");
-const wl = @import("wayland").server.wl;
-
-const util = @import("util.zig");
-
-const Subsurface = @import("Subsurface.zig");
-const Parent = Subsurface.Parent;
-
-/// The parent at the root of this surface tree
-parent: Parent,
-wlr_xdg_popup: *wlr.XdgPopup,
-
-// Always active
-surface_destroy: wl.Listener(void) = wl.Listener(void).init(handleDestroy),
-map: wl.Listener(void) = wl.Listener(void).init(handleMap),
-unmap: wl.Listener(void) = wl.Listener(void).init(handleUnmap),
-new_popup: wl.Listener(*wlr.XdgPopup) = wl.Listener(*wlr.XdgPopup).init(handleNewPopup),
-new_subsurface: wl.Listener(*wlr.Subsurface) = wl.Listener(*wlr.Subsurface).init(handleNewSubsurface),
-
-// Only active while mapped
-commit: wl.Listener(*wlr.Surface) = wl.Listener(*wlr.Surface).init(handleCommit),
-
-pub fn create(wlr_xdg_popup: *wlr.XdgPopup, parent: Parent) void {
-    const xdg_popup = util.gpa.create(XdgPopup) catch {
-        std.log.err("out of memory", .{});
-        wlr_xdg_popup.resource.postNoMemory();
-        return;
-    };
-    xdg_popup.* = .{
-        .parent = parent,
-        .wlr_xdg_popup = wlr_xdg_popup,
-    };
-    assert(wlr_xdg_popup.base.data == 0);
-    wlr_xdg_popup.base.data = @ptrToInt(xdg_popup);
-
-    switch (parent) {
-        .xdg_toplevel => |xdg_toplevel| {
-            // The output box relative to the parent of the xdg_popup
-            var box = wlr.Box{
-                .x = xdg_toplevel.view.surface_box.x - xdg_toplevel.view.pending.box.x,
-                .y = xdg_toplevel.view.surface_box.y - xdg_toplevel.view.pending.box.y,
-                .width = undefined,
-                .height = undefined,
-            };
-            xdg_toplevel.view.output.wlr_output.effectiveResolution(&box.width, &box.height);
-            wlr_xdg_popup.unconstrainFromBox(&box);
-        },
-        .layer_surface => |layer_surface| {
-            // The output box relative to the parent of the xdg_popup
-            var box = wlr.Box{
-                .x = -layer_surface.box.x,
-                .y = -layer_surface.box.y,
-                .width = undefined,
-                .height = undefined,
-            };
-            layer_surface.output.wlr_output.effectiveResolution(&box.width, &box.height);
-            wlr_xdg_popup.unconstrainFromBox(&box);
-        },
-        .drag_icon, .lock_surface => unreachable,
-    }
-
-    wlr_xdg_popup.base.events.destroy.add(&xdg_popup.surface_destroy);
-    wlr_xdg_popup.base.events.map.add(&xdg_popup.map);
-    wlr_xdg_popup.base.events.unmap.add(&xdg_popup.unmap);
-    wlr_xdg_popup.base.events.new_popup.add(&xdg_popup.new_popup);
-    wlr_xdg_popup.base.surface.events.new_subsurface.add(&xdg_popup.new_subsurface);
-
-    Subsurface.handleExisting(wlr_xdg_popup.base.surface, parent);
-}
-
-pub fn destroy(xdg_popup: *XdgPopup) void {
-    xdg_popup.surface_destroy.link.remove();
-    xdg_popup.map.link.remove();
-    xdg_popup.unmap.link.remove();
-    xdg_popup.new_popup.link.remove();
-    xdg_popup.new_subsurface.link.remove();
-
-    if (xdg_popup.wlr_xdg_popup.base.mapped) xdg_popup.commit.link.remove();
-
-    Subsurface.destroySubsurfaces(xdg_popup.wlr_xdg_popup.base.surface);
-    XdgPopup.destroyPopups(xdg_popup.wlr_xdg_popup.base);
-
-    xdg_popup.wlr_xdg_popup.base.data = 0;
-    util.gpa.destroy(xdg_popup);
-}
-
-pub fn destroyPopups(wlr_xdg_surface: *wlr.XdgSurface) void {
-    var it = wlr_xdg_surface.popups.iterator(.forward);
-    while (it.next()) |wlr_xdg_popup| {
-        if (@intToPtr(?*XdgPopup, wlr_xdg_popup.base.data)) |xdg_popup| xdg_popup.destroy();
-    }
-}
-
-fn handleDestroy(listener: *wl.Listener(void)) void {
-    const xdg_popup = @fieldParentPtr(XdgPopup, "surface_destroy", listener);
-    xdg_popup.destroy();
-}
-
-fn handleMap(listener: *wl.Listener(void)) void {
-    const xdg_popup = @fieldParentPtr(XdgPopup, "map", listener);
-
-    xdg_popup.wlr_xdg_popup.base.surface.events.commit.add(&xdg_popup.commit);
-    xdg_popup.parent.damageWholeOutput();
-}
-
-fn handleUnmap(listener: *wl.Listener(void)) void {
-    const xdg_popup = @fieldParentPtr(XdgPopup, "unmap", listener);
-
-    xdg_popup.commit.link.remove();
-    xdg_popup.parent.damageWholeOutput();
-}
-
-fn handleCommit(listener: *wl.Listener(*wlr.Surface), _: *wlr.Surface) void {
-    const xdg_popup = @fieldParentPtr(XdgPopup, "commit", listener);
-
-    xdg_popup.parent.damageWholeOutput();
-}
-
-fn handleNewPopup(listener: *wl.Listener(*wlr.XdgPopup), wlr_xdg_popup: *wlr.XdgPopup) void {
-    const xdg_popup = @fieldParentPtr(XdgPopup, "new_popup", listener);
-
-    XdgPopup.create(wlr_xdg_popup, xdg_popup.parent);
-}
-
-fn handleNewSubsurface(listener: *wl.Listener(*wlr.Subsurface), new_wlr_subsurface: *wlr.Subsurface) void {
-    const xdg_popup = @fieldParentPtr(XdgPopup, "new_subsurface", listener);
-
-    Subsurface.create(new_wlr_subsurface, xdg_popup.parent);
-}
diff --git a/river/XdgToplevel.zig b/river/XdgToplevel.zig
index 1475464..0e06e1f 100644
--- a/river/XdgToplevel.zig
+++ b/river/XdgToplevel.zig
@@ -26,10 +26,8 @@ const util = @import("util.zig");
 
 const Output = @import("Output.zig");
 const Seat = @import("Seat.zig");
-const Subsurface = @import("Subsurface.zig");
 const View = @import("View.zig");
 const ViewStack = @import("view_stack.zig").ViewStack;
-const XdgPopup = @import("XdgPopup.zig");
 
 const log = std.log.scoped(.xdg_shell);
 
@@ -46,8 +44,6 @@ acked_pending_serial: bool = false,
 destroy: wl.Listener(void) = wl.Listener(void).init(handleDestroy),
 map: wl.Listener(void) = wl.Listener(void).init(handleMap),
 unmap: wl.Listener(void) = wl.Listener(void).init(handleUnmap),
-new_popup: wl.Listener(*wlr.XdgPopup) = wl.Listener(*wlr.XdgPopup).init(handleNewPopup),
-new_subsurface: wl.Listener(*wlr.Subsurface) = wl.Listener(*wlr.Subsurface).init(handleNewSubsurface),
 
 // Listeners that are only active while the view is mapped
 ack_configure: wl.Listener(*wlr.XdgSurface.Configure) =
@@ -78,10 +74,6 @@ pub fn create(output: *Output, xdg_toplevel: *wlr.XdgToplevel) error{OutOfMemory
     xdg_toplevel.base.events.destroy.add(&self.destroy);
     xdg_toplevel.base.events.map.add(&self.map);
     xdg_toplevel.base.events.unmap.add(&self.unmap);
-    xdg_toplevel.base.events.new_popup.add(&self.new_popup);
-    xdg_toplevel.base.surface.events.new_subsurface.add(&self.new_subsurface);
-
-    Subsurface.handleExisting(xdg_toplevel.base.surface, .{ .xdg_toplevel = self });
 }
 
 /// Returns true if a configure must be sent to ensure that the pending
@@ -164,11 +156,6 @@ fn handleDestroy(listener: *wl.Listener(void)) void {
     self.destroy.link.remove();
     self.map.link.remove();
     self.unmap.link.remove();
-    self.new_popup.link.remove();
-    self.new_subsurface.link.remove();
-
-    Subsurface.destroySubsurfaces(self.xdg_toplevel.base.surface);
-    XdgPopup.destroyPopups(self.xdg_toplevel.base);
 
     self.view.destroy();
 }
@@ -296,7 +283,6 @@ fn handleCommit(listener: *wl.Listener(*wlr.Surface), _: *wlr.Surface) void {
                 // before some change occured that caused shouldTrackConfigure() to return false.
                 view.dropSavedBuffers();
 
-                view.output.damage.?.addWhole();
                 server.input_manager.updateCursorState();
             }
         } else {
@@ -307,7 +293,6 @@ fn handleCommit(listener: *wl.Listener(*wlr.Surface), _: *wlr.Surface) void {
             view.sendFrameDone();
         }
     } else {
-        view.output.damage.?.addWhole();
         const size_changed = !std.meta.eql(view.surface_box, new_box);
         view.surface_box = new_box;
         // If the client has decided to resize itself and the view is floating,
@@ -320,16 +305,6 @@ fn handleCommit(listener: *wl.Listener(*wlr.Surface), _: *wlr.Surface) void {
     }
 }
 
-fn handleNewPopup(listener: *wl.Listener(*wlr.XdgPopup), wlr_xdg_popup: *wlr.XdgPopup) void {
-    const self = @fieldParentPtr(Self, "new_popup", listener);
-    XdgPopup.create(wlr_xdg_popup, .{ .xdg_toplevel = self });
-}
-
-fn handleNewSubsurface(listener: *wl.Listener(*wlr.Subsurface), new_wlr_subsurface: *wlr.Subsurface) void {
-    const self = @fieldParentPtr(Self, "new_subsurface", listener);
-    Subsurface.create(new_wlr_subsurface, .{ .xdg_toplevel = self });
-}
-
 /// Called when the client asks to be fullscreened. We always honor the request
 /// for now, perhaps it should be denied in some cases in the future.
 fn handleRequestFullscreen(listener: *wl.Listener(void)) void {
diff --git a/river/XwaylandOverrideRedirect.zig b/river/XwaylandOverrideRedirect.zig
index ad604ba..1da6bf3 100644
--- a/river/XwaylandOverrideRedirect.zig
+++ b/river/XwaylandOverrideRedirect.zig
@@ -43,9 +43,6 @@ unmap: wl.Listener(*wlr.XwaylandSurface) = wl.Listener(*wlr.XwaylandSurface).ini
 set_override_redirect: wl.Listener(*wlr.XwaylandSurface) =
     wl.Listener(*wlr.XwaylandSurface).init(handleSetOverrideRedirect),
 
-// Listeners that are only active while mapped
-commit: wl.Listener(*wlr.Surface) = wl.Listener(*wlr.Surface).init(handleCommit),
-
 /// The override redirect surface will add itself to the list in Root when it is mapped.
 pub fn create(xwayland_surface: *wlr.XwaylandSurface) error{OutOfMemory}!*Self {
     const node = try util.gpa.create(std.TailQueue(Self).Node);
@@ -89,14 +86,12 @@ fn handleDestroy(listener: *wl.Listener(*wlr.XwaylandSurface), _: *wlr.XwaylandS
 }
 
 /// Called when the xwayland surface is mapped, or ready to display on-screen.
-pub fn handleMap(listener: *wl.Listener(*wlr.XwaylandSurface), xwayland_surface: *wlr.XwaylandSurface) void {
+pub fn handleMap(listener: *wl.Listener(*wlr.XwaylandSurface), _: *wlr.XwaylandSurface) void {
     const self = @fieldParentPtr(Self, "map", listener);
 
     const node = @fieldParentPtr(std.TailQueue(Self).Node, "data", self);
     server.root.xwayland_override_redirect_views.prepend(node);
 
-    xwayland_surface.surface.?.events.commit.add(&self.commit);
-
     self.focusIfDesired();
 }
 
@@ -129,8 +124,6 @@ fn handleUnmap(listener: *wl.Listener(*wlr.XwaylandSurface), _: *wlr.XwaylandSur
     const node = @fieldParentPtr(std.TailQueue(Self).Node, "data", self);
     server.root.xwayland_override_redirect_views.remove(node);
 
-    self.commit.link.remove();
-
     // If the unmapped surface is currently focused, pass keyboard focus
     // to the most appropriate surface.
     var seat_it = server.input_manager.seats.first;
@@ -151,11 +144,6 @@ fn handleUnmap(listener: *wl.Listener(*wlr.XwaylandSurface), _: *wlr.XwaylandSur
     server.root.startTransaction();
 }
 
-fn handleCommit(_: *wl.Listener(*wlr.Surface), _: *wlr.Surface) void {
-    var it = server.root.outputs.first;
-    while (it) |node| : (it = node.next) node.data.damage.?.addWhole();
-}
-
 fn handleSetOverrideRedirect(
     listener: *wl.Listener(*wlr.XwaylandSurface),
     xwayland_surface: *wlr.XwaylandSurface,
diff --git a/river/XwaylandView.zig b/river/XwaylandView.zig
index 0eef7a4..c1be61e 100644
--- a/river/XwaylandView.zig
+++ b/river/XwaylandView.zig
@@ -29,7 +29,6 @@ const util = @import("util.zig");
 const Output = @import("Output.zig");
 const View = @import("View.zig");
 const ViewStack = @import("view_stack.zig").ViewStack;
-const XdgPopup = @import("XdgPopup.zig");
 const XwaylandOverrideRedirect = @import("XwaylandOverrideRedirect.zig");
 
 const log = std.log.scoped(.xwayland);
@@ -297,8 +296,6 @@ fn handleSetOverrideRedirect(
 fn handleCommit(listener: *wl.Listener(*wlr.Surface), surface: *wlr.Surface) void {
     const self = @fieldParentPtr(Self, "commit", listener);
 
-    self.view.output.damage.?.addWhole();
-
     self.view.surface_box = .{
         .x = 0,
         .y = 0,
diff --git a/river/command/config.zig b/river/command/config.zig
index fc56daa..145743f 100644
--- a/river/command/config.zig
+++ b/river/command/config.zig
@@ -46,9 +46,6 @@ pub fn backgroundColor(
     if (args.len > 2) return Error.TooManyArguments;
 
     server.config.background_color = try parseRgba(args[1]);
-
-    var it = server.root.outputs.first;
-    while (it) |node| : (it = node.next) node.data.damage.?.addWhole();
 }
 
 pub fn borderColorFocused(
@@ -60,9 +57,6 @@ pub fn borderColorFocused(
     if (args.len > 2) return Error.TooManyArguments;
 
     server.config.border_color_focused = try parseRgba(args[1]);
-
-    var it = server.root.outputs.first;
-    while (it) |node| : (it = node.next) node.data.damage.?.addWhole();
 }
 
 pub fn borderColorUnfocused(
@@ -74,9 +68,6 @@ pub fn borderColorUnfocused(
     if (args.len > 2) return Error.TooManyArguments;
 
     server.config.border_color_unfocused = try parseRgba(args[1]);
-
-    var it = server.root.outputs.first;
-    while (it) |node| : (it = node.next) node.data.damage.?.addWhole();
 }
 
 pub fn borderColorUrgent(
@@ -88,9 +79,6 @@ pub fn borderColorUrgent(
     if (args.len > 2) return Error.TooManyArguments;
 
     server.config.border_color_urgent = try parseRgba(args[1]);
-
-    var it = server.root.outputs.first;
-    while (it) |node| : (it = node.next) node.data.damage.?.addWhole();
 }
 
 pub fn setCursorWarp(
diff --git a/river/render.zig b/river/render.zig
index f441ff3..b992e84 100644
--- a/river/render.zig
+++ b/river/render.zig
@@ -47,20 +47,11 @@ pub fn renderOutput(output: *Output) void {
     var now: os.timespec = undefined;
     os.clock_gettime(os.CLOCK.MONOTONIC, &now) catch @panic("CLOCK_MONOTONIC not supported");
 
-    var needs_frame: bool = undefined;
-    var damage_region: pixman.Region32 = undefined;
-    damage_region.init();
-    defer damage_region.deinit();
-    output.damage.?.attachRender(&needs_frame, &damage_region) catch {
+    output.wlr_output.attachRender(null) catch {
         log.err("failed to attach renderer", .{});
         return;
     };
 
-    if (!needs_frame) {
-        output.wlr_output.rollback();
-        return;
-    }
-
     server.renderer.begin(@intCast(u32, output.wlr_output.width), @intCast(u32, output.wlr_output.height));
 
     // In order to avoid flashing a blank black screen as the session is locked