diff --git a/CMakeLists.txt b/CMakeLists.txt index 9e6c240..ac986b0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -5,7 +5,10 @@ project(photon_light VERSION 1.0.0 LANGUAGES CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) +find_package(Qt6 REQUIRED COMPONENTS Gui Widgets) find_program(CARGO cargo REQUIRED) set(CARGO_TARGET_DIR "${CMAKE_BINARY_DIR}/cargo") +qt_standard_project_setup() + add_subdirectory(ui) diff --git a/Cargo.lock b/Cargo.lock index 4975196..0d4a66e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1169,7 +1169,11 @@ dependencies = [ name = "photon-light-impl" version = "0.1.0" dependencies = [ + "glam", "photon-light", + "pollster", + "raw-window-handle", + "wgpu", ] [[package]] diff --git a/src/lib.rs b/src/lib.rs index 517efe3..bbf3c90 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -207,6 +207,7 @@ impl Core { self.queue.submit(std::iter::once(encoder.finish())); } + /// Configures the renderer for a given target size. pub fn configure(&mut self, pixel_size: UVec2) { self.surface.configure( &self.device, @@ -223,6 +224,9 @@ impl Core { ); } + /// Redraws the entire surface. + /// + /// [`Self::configure`] must be called at least once before this. pub fn redraw(&mut self) { let output = self.surface.get_current_texture().unwrap(); self.render(&output.texture); diff --git a/ui/CMakeLists.txt b/ui/CMakeLists.txt index 735ecb7..0fd6cba 100644 --- a/ui/CMakeLists.txt +++ b/ui/CMakeLists.txt @@ -1,7 +1,13 @@ include(impl.cmake) -add_executable(photon_light +qt_add_executable(photon_light + src/api.cxx src/main.cxx + src/main_window.cxx + src/main_window.ui + src/viewport.cxx ) +target_link_libraries(photon_light PRIVATE Qt6::Gui Qt6::Widgets) target_link_libraries(photon_light PRIVATE photon_light_impl) +target_include_directories(photon_light PRIVATE src) diff --git a/ui/Cargo.toml b/ui/Cargo.toml index e1e2d19..e12ba36 100644 --- a/ui/Cargo.toml +++ b/ui/Cargo.toml @@ -8,3 +8,8 @@ crate-type = ["staticlib"] [dependencies] photon-light = {path = "../"} + +glam = { version = "0.30" } +pollster = "0.4.0" +raw-window-handle = "0.6.2" +wgpu = "27.0.1" diff --git a/ui/src/api.cxx b/ui/src/api.cxx new file mode 100644 index 0000000..d85f970 --- /dev/null +++ b/ui/src/api.cxx @@ -0,0 +1,56 @@ +#include "api.hxx" + +#include +#include + +extern "C" Core* rt4_viewport_create(xcb_connection_t* connection, std::uint32_t window); +extern "C" void rt4_viewport_destroy(Core* viewport); +extern "C" void rt4_viewport_configure(Core* viewport, std::uint32_t width, std::uint32_t height); +extern "C" void rt4_viewport_redraw(Core* viewport); + +BoxCore::BoxCore(BoxCore&& b) + : ptr(std::exchange(b.ptr, nullptr)) +{ +} + +BoxCore::~BoxCore() { + reset(); +} + +BoxCore& BoxCore::operator= (BoxCore&& b) { + if (&b == this) + return *this; + std::swap(ptr, b.ptr); + b.reset(); + return *this; +} + +BoxCore BoxCore::from_xcb(xcb_connection_t* connection, std::uint32_t window) { + if (!connection) + throw std::logic_error("attempt to use a null connection"); + if (!window) + throw std::logic_error("attempt to use a null window"); + BoxCore out; + out.ptr = rt4_viewport_create(connection, window); + return out; +} + +void BoxCore::reset() { + auto viewport = std::exchange(ptr, nullptr); + if (viewport) + rt4_viewport_destroy(viewport); +} + +Core* BoxCore::use() const { + if (!ptr) + throw std::logic_error("attempt to use a null Core"); + return ptr; +} + +void BoxCore::configure(std::uint32_t width, std::uint32_t height) { + rt4_viewport_configure(use(), width, height); +} + +void BoxCore::redraw() { + rt4_viewport_redraw(use()); +} diff --git a/ui/src/api.hxx b/ui/src/api.hxx new file mode 100644 index 0000000..2b59c32 --- /dev/null +++ b/ui/src/api.hxx @@ -0,0 +1,30 @@ +#pragma once + +#include + +struct xcb_connection_t; + +struct Core; + +class BoxCore { +public: + BoxCore() = default; + BoxCore(const BoxCore&) = delete; + BoxCore(BoxCore&&); + BoxCore& operator= (const BoxCore&) = delete; + BoxCore& operator= (BoxCore&&); + ~BoxCore(); + + explicit operator bool() const { return ptr; } + + void reset(); + + static BoxCore from_xcb(xcb_connection_t* connection, std::uint32_t window); + void configure(std::uint32_t width, std::uint32_t height); + void redraw(); + +private: + Core* ptr = nullptr; + + Core* use() const; +}; diff --git a/ui/src/lib.rs b/ui/src/lib.rs index e69de29..daf90a7 100644 --- a/ui/src/lib.rs +++ b/ui/src/lib.rs @@ -0,0 +1,45 @@ +use std::{ffi::c_void, num::NonZero, ptr::NonNull}; + +use glam::uvec2; +use photon_light::{Core, init_gpu_inner}; +use raw_window_handle::{RawDisplayHandle, RawWindowHandle, XcbDisplayHandle, XcbWindowHandle}; + +unsafe fn create_viewport( + display: impl Into, + window: impl Into, +) -> Box { + let target = wgpu::SurfaceTargetUnsafe::RawHandle { + raw_display_handle: display.into(), + raw_window_handle: window.into(), + }; + let gpu = pollster::block_on(init_gpu_inner(|instance| unsafe { + instance.create_surface_unsafe(target) + })) + .unwrap(); + Box::new(Core::new(gpu)) +} + +#[unsafe(no_mangle)] +unsafe extern "C" fn rt4_viewport_create( + connection: NonNull, + window: NonZero, +) -> Box { + let display = XcbDisplayHandle::new(Some(connection), 0); + let window = XcbWindowHandle::new(window); + unsafe { create_viewport(display, window) } +} + +#[unsafe(no_mangle)] +unsafe extern "C" fn rt4_viewport_destroy(viewport: Box) { + drop(viewport); +} + +#[unsafe(no_mangle)] +unsafe extern "C" fn rt4_viewport_configure(viewport: &mut Core, width: u32, height: u32) { + viewport.configure(uvec2(width, height)); +} + +#[unsafe(no_mangle)] +unsafe extern "C" fn rt4_viewport_redraw(viewport: &mut Core) { + viewport.redraw(); +} diff --git a/ui/src/main.cxx b/ui/src/main.cxx index cb3f748..ca53693 100644 --- a/ui/src/main.cxx +++ b/ui/src/main.cxx @@ -1,3 +1,13 @@ -int main() { - return 0; +#include "main_window.hxx" + +#include + +int main(int argc, char *argv[]) +{ + QApplication app(argc, argv); + + auto w = new PhotonLight; + w->show(); + + return app.exec(); } diff --git a/ui/src/main_window.cxx b/ui/src/main_window.cxx new file mode 100644 index 0000000..de1b0d6 --- /dev/null +++ b/ui/src/main_window.cxx @@ -0,0 +1,14 @@ +#include "main_window.hxx" + +#include "ui_main_window.h" + +PhotonLight::PhotonLight(QWidget *parent) + : QMainWindow(parent) + , m_ui(new Ui::MainWindow) +{ + m_ui->setupUi(this); +} + +PhotonLight::~PhotonLight() = default; + +#include "moc_main_window.cpp" diff --git a/ui/src/main_window.hxx b/ui/src/main_window.hxx new file mode 100644 index 0000000..92db5db --- /dev/null +++ b/ui/src/main_window.hxx @@ -0,0 +1,20 @@ +#pragma once +#include + +#include + +namespace Ui { +class MainWindow; +} + +class PhotonLight : public QMainWindow +{ + Q_OBJECT + +public: + explicit PhotonLight(QWidget *parent = nullptr); + ~PhotonLight() override; + +private: + const std::unique_ptr m_ui; +}; diff --git a/ui/src/main_window.ui b/ui/src/main_window.ui new file mode 100644 index 0000000..a6b18ec --- /dev/null +++ b/ui/src/main_window.ui @@ -0,0 +1,45 @@ + + + MainWindow + + + + 0 + 0 + 1220 + 823 + + + + MainWindow + + + + + + + + + + + + 0 + 0 + 1220 + 38 + + + + + + + + Viewport + QWidget +
viewport.hxx
+ 1 +
+
+ + +
diff --git a/ui/src/viewport.cxx b/ui/src/viewport.cxx new file mode 100644 index 0000000..ebdcb9d --- /dev/null +++ b/ui/src/viewport.cxx @@ -0,0 +1,67 @@ +#include "viewport.hxx" + +#include +#include +#include + +Viewport::Viewport(QWidget* parent, Qt::WindowFlags f) : QWidget(parent, f) { + setAttribute(Qt::WA_NativeWindow); + setAttribute(Qt::WA_PaintOnScreen); + setAttribute(Qt::WA_NoSystemBackground); +} + +Viewport::~Viewport() = default; + +QPaintEngine* Viewport::paintEngine() const { + return nullptr; +} + +bool Viewport::event(QEvent* event) { + switch (event->type()) { + case QEvent::Type::WinIdChange: + recreate(); + break; + default: + break; + } + return QWidget::event(event); +} + +void Viewport::paintEvent(QPaintEvent* event) { + if (!core) + recreate(); + core.redraw(); +} + +void Viewport::resizeEvent(QResizeEvent* event) { + if (!core) + return; + updateSize(); + QWidget::resizeEvent(event); +} + +void Viewport::recreate() try { + auto* app = qobject_cast(QApplication::instance()); + if (!app) + throw std::runtime_error("not a GUI application (WTF?)"); + + auto* native = app->nativeInterface(); + if (!native) + throw std::runtime_error("X11 interface is not available"); + + auto* xcb_connection = native->connection(); + std::uint32_t x11_window = winId(); + + fprintf(stderr, "connection %p, window %#08x\n", xcb_connection, x11_window); + + core.reset(); + core = BoxCore::from_xcb(xcb_connection, x11_window); + updateSize(); +} catch (const std::exception& e) { + fprintf(stderr, "failed to recreate the viewport: %s", e.what()); +} + +void Viewport::updateSize() { + const QSize device_size = size() * devicePixelRatio(); + core.configure(device_size.width(), device_size.height()); +} diff --git a/ui/src/viewport.hxx b/ui/src/viewport.hxx new file mode 100644 index 0000000..846b53c --- /dev/null +++ b/ui/src/viewport.hxx @@ -0,0 +1,25 @@ +#pragma once + +#include + +#include "api.hxx" + +class Viewport: public QWidget { + Q_OBJECT + +public: + explicit Viewport(QWidget* parent = nullptr, Qt::WindowFlags f = Qt::WindowFlags()); + ~Viewport() override; + QPaintEngine* paintEngine() const override; + +protected: + bool event(QEvent* event) override; + void paintEvent(QPaintEvent* event) override; + void resizeEvent(QResizeEvent* event) override; + +private: + BoxCore core; + + void recreate(); + void updateSize(); +};