diff --git a/repos/libports/run/webcam.inc b/repos/libports/run/webcam.inc
new file mode 100644
index 0000000000..e0d90fc2b1
--- /dev/null
+++ b/repos/libports/run/webcam.inc
@@ -0,0 +1,185 @@
+assert_spec x86
+
+set build_components { }
+
+# fuji4
+proc libuvc_vendor_id {} { return "0x04f2" }
+proc libuvc_product_id {} { return "0xb564" }
+
+# c270
+#proc libuvc_vendor_id {} { return "0x046d" }
+#proc libuvc_product_id {} { return "0x0825" }
+
+# quickcam
+#proc libuvc_vendor_id {} { return "0x046d" }
+#proc libuvc_product_id {} { return "0x09c1" }
+
+# t470
+#proc libuvc_vendor_id {} { return "0x0bda" }
+#proc libuvc_product_id {} { return "0x58db" }
+
+
+
+create_boot_directory
+
+import_from_depot [depot_user]/src/[base_src] \
+ [depot_user]/src/init \
+ [depot_user]/src/nitpicker \
+ [depot_user]/src/dynamic_rom \
+ [depot_user]/src/rom_reporter \
+ [depot_user]/src/report_rom \
+ [depot_user]/src/pc_usb_host_drv \
+ [depot_user]/src/vesa_drv \
+ [depot_user]/pkg/usb_webcam
+
+import_from_depot $test_imports
+
+source ${genode_dir}/repos/base/run/platform_drv.inc
+append_platform_drv_build_components
+build $build_components
+
+
+append config {
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+}
+
+append_platform_drv_config
+
+append config {
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ }
+append config $test_vfs_config
+append config {
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+}
+
+install_config $config
+
+append_platform_drv_boot_modules
+
+append boot_modules { }
+
+build_boot_image $boot_modules
+
+append qemu_args { -usb -device usb-host,vendorid=[libuvc_vendor_id],productid=[libuvc_product_id] }
+
+run_genode_until forever
diff --git a/repos/libports/run/webcam.run b/repos/libports/run/webcam.run
index cc4a740ef3..0dc358af92 100644
--- a/repos/libports/run/webcam.run
+++ b/repos/libports/run/webcam.run
@@ -1,182 +1,7 @@
-assert_spec x86
+set test_imports "[depot_user]/src/test-capture"
-set build_components { }
+set test_binary "test-capture"
-# fuji4
-proc libuvc_vendor_id {} { return "0x04f2" }
-proc libuvc_product_id {} { return "0xb564" }
+set test_vfs_config { }
-# c270
-#proc libuvc_vendor_id {} { return "0x046d" }
-#proc libuvc_product_id {} { return "0x0825" }
-
-# quickcam
-#proc libuvc_vendor_id {} { return "0x046d" }
-#proc libuvc_product_id {} { return "0x09c1" }
-
-# t470
-#proc libuvc_vendor_id {} { return "0x0bda" }
-#proc libuvc_product_id {} { return "0x58db" }
-
-
-
-create_boot_directory
-
-import_from_depot [depot_user]/src/[base_src] \
- [depot_user]/src/init \
- [depot_user]/src/nitpicker \
- [depot_user]/src/dynamic_rom \
- [depot_user]/src/rom_reporter \
- [depot_user]/src/report_rom \
- [depot_user]/src/pc_usb_host_drv \
- [depot_user]/src/vesa_drv \
- [depot_user]/src/test-capture \
- [depot_user]/pkg/usb_webcam
-
-source ${genode_dir}/repos/base/run/platform_drv.inc
-append_platform_drv_build_components
-build $build_components
-
-
-append config {
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-}
-
-append_platform_drv_config
-
-append config {
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-}
-
-install_config $config
-
-append_platform_drv_boot_modules
-
-append boot_modules { }
-
-build_boot_image $boot_modules
-
-append qemu_args { -usb -device usb-host,vendorid=[libuvc_vendor_id],productid=[libuvc_product_id] }
-
-run_genode_until forever
+source ${genode_dir}/repos/libports/run/webcam.inc
diff --git a/repos/libports/run/webcam_vfs.run b/repos/libports/run/webcam_vfs.run
new file mode 100644
index 0000000000..2fc6ff0032
--- /dev/null
+++ b/repos/libports/run/webcam_vfs.run
@@ -0,0 +1,8 @@
+set test_imports "[depot_user]/src/test-vfs_capture \
+ [depot_user]/src/vfs_capture"
+
+set test_binary "test-vfs_capture"
+
+set test_vfs_config { }
+
+source ${genode_dir}/repos/libports/run/webcam.inc
diff --git a/repos/os/recipes/src/test-vfs_capture/content.mk b/repos/os/recipes/src/test-vfs_capture/content.mk
new file mode 100644
index 0000000000..3b05fbe502
--- /dev/null
+++ b/repos/os/recipes/src/test-vfs_capture/content.mk
@@ -0,0 +1,2 @@
+SRC_DIR = src/test/vfs_capture
+include $(GENODE_DIR)/repos/base/recipes/src/content.inc
diff --git a/repos/os/recipes/src/test-vfs_capture/hash b/repos/os/recipes/src/test-vfs_capture/hash
new file mode 100644
index 0000000000..48adfa5f06
--- /dev/null
+++ b/repos/os/recipes/src/test-vfs_capture/hash
@@ -0,0 +1 @@
+2022-03-26 d1f95998600be00ef2f130aa12d6c177e0c82bf2
diff --git a/repos/os/recipes/src/test-vfs_capture/used_apis b/repos/os/recipes/src/test-vfs_capture/used_apis
new file mode 100644
index 0000000000..3d59d3f181
--- /dev/null
+++ b/repos/os/recipes/src/test-vfs_capture/used_apis
@@ -0,0 +1,8 @@
+base
+os
+blit
+gui_session
+input_session
+framebuffer_session
+capture_session
+vfs
diff --git a/repos/os/src/test/vfs_capture/main.cc b/repos/os/src/test/vfs_capture/main.cc
new file mode 100644
index 0000000000..fa6452f8e9
--- /dev/null
+++ b/repos/os/src/test/vfs_capture/main.cc
@@ -0,0 +1,251 @@
+/*
+ * \brief Capture test
+ * \author Norman Feske
+ * \date 2020-06-26
+ */
+
+/*
+ * Copyright (C) 2020-2022 Genode Labs GmbH
+ *
+ * This file is part of the Genode OS framework, which is distributed
+ * under the terms of the GNU Affero General Public License version 3.
+ */
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+namespace Test {
+
+ using namespace Genode;
+
+ struct View;
+ struct Main;
+}
+
+
+class Test::View
+{
+ private:
+
+ using View_handle = Gui::Session::View_handle;
+
+ Gui::Session_client &_gui;
+ View_handle const _handle = _gui.create_view(View_handle{});
+ Gui::Rect const _rect;
+
+ public:
+
+ View(Gui::Session_client &gui, Gui::Rect rect) : _gui(gui), _rect(rect)
+ {
+ using Command = Gui::Session::Command;
+
+ _gui.enqueue(_handle, rect);
+ _gui.enqueue(_handle, View_handle());
+ _gui.execute();
+ }
+
+ virtual ~View() { }
+};
+
+
+struct Test::Main
+{
+ Env &_env;
+
+ using Pixel = Capture::Pixel;
+ using Affected_rects = Capture::Session::Affected_rects;
+
+ Attached_rom_dataspace _config { _env, "config" };
+
+ Heap _heap { _env.ram(), _env.rm() };
+
+ Root_directory _root_dir { _env, _heap, _config.xml().sub_node("vfs") };
+
+ static Gui::Point _point_from_xml(Xml_node node)
+ {
+ return Gui::Point((int)node.attribute_value("xpos", 0L),
+ (int)node.attribute_value("ypos", 0L));
+ }
+
+ static Gui::Area _area_from_xml(Xml_node node, Gui::Area default_area)
+ {
+ return Gui::Area(node.attribute_value("width", default_area.w()),
+ node.attribute_value("height", default_area.h()));
+ }
+
+ struct Output
+ {
+ struct Invalid_config : Exception { };
+
+ Env &_env;
+
+ Allocator &_alloc;
+
+ Gui::Connection _gui { _env, "" };
+
+ Framebuffer::Mode const _mode;
+
+ void _validate_mode() const
+ {
+ if (_mode.area.count() == 0) {
+ error("invalid or missing 'width' and 'height' config attributes");
+ throw Invalid_config();
+ }
+ }
+
+ bool _gui_buffer_init = ( _validate_mode(), _gui.buffer(_mode, false), true );
+
+ Attached_dataspace _fb_ds { _env.rm(), _gui.framebuffer()->dataspace() };
+
+ Registry> _views { };
+
+ Output(Env &env, Allocator &alloc, Xml_node const &config)
+ :
+ _env(env), _alloc(alloc),
+ _mode({ .area = _area_from_xml(config, Area { }) })
+ {
+ auto view_rect = [&] (Xml_node node)
+ {
+ return Gui::Rect(_point_from_xml(node),
+ _area_from_xml(node, _mode.area));
+ };
+
+ config.for_each_sub_node("view", [&] (Xml_node node) {
+ new (_alloc)
+ Registered(_views, _gui, view_rect(node)); });
+ }
+
+ ~Output()
+ {
+ _views.for_each([&] (Registered &view) {
+ destroy(_alloc, &view); });
+ }
+
+ template
+ void with_surface(FN const &fn)
+ {
+ Surface surface(_fb_ds.local_addr(), _mode.area);
+
+ fn(surface);
+ }
+ };
+
+ Constructible