This commit is contained in:
numzero 2026-01-24 21:07:33 +03:00
commit 3e713b13a8
19 changed files with 3237 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
/target
/build

19
CMakeLists.txt Normal file
View File

@ -0,0 +1,19 @@
cmake_minimum_required(VERSION 3.18)
project(PROJECT-NAME 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")
# KDE-specific
find_package(ECM REQUIRED NO_MODULE)
set(CMAKE_MODULE_PATH ${ECM_MODULE_PATH})
find_package(KF6 REQUIRED COMPONENTS WidgetsAddons)
qt_standard_project_setup()
add_subdirectory(ui)

2446
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

24
Cargo.toml Normal file
View File

@ -0,0 +1,24 @@
[workspace]
members = ["ui"]
[package]
name = "PROJECT-NAME"
version = "0.1.0"
edition = "2024"
[profile.dev]
panic = 'abort'
[profile.dev.package."*"]
debug = false
opt-level = 3
[profile.test.package."*"]
debug = false
opt-level = 3
[dependencies]
glam = { version = "0.30.9" }
pollster = "0.4.0"
wgpu = "27.0.1"
winit = "0.30.12"

5
README.md Normal file
View File

@ -0,0 +1,5 @@
# Qt6+WGPU skeleton program
This example combines Qt6 GUI (C++) with WGPU rendering (Rust). It doesnt do much of either but its the bridging whats relevant.
Currently only X11 (XCB) is supported but it should be easy to extend to other platforms.

169
src/lib.rs Normal file
View File

@ -0,0 +1,169 @@
use std::error::Error;
use glam::{UVec2, Vec4};
const DEPTH_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Depth16Unorm;
const OUTPUT_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Bgra8UnormSrgb;
#[derive(Debug, Clone, Copy)]
#[repr(C)]
pub struct RedrawArgs {
pub background: Vec4,
}
pub struct Gpu {
device: wgpu::Device,
queue: wgpu::Queue,
surface: wgpu::Surface<'static>,
}
pub struct Core {
device: wgpu::Device,
queue: wgpu::Queue,
surface: wgpu::Surface<'static>,
depth: wgpu::Texture,
}
impl Core {
pub fn new(gpu: Gpu, pixel_size: UVec2) -> Self {
let Gpu {
device,
queue,
surface,
} = gpu;
let depth = Self::create_depth_buffer(&device, pixel_size);
queue.submit([]); // flush buffer updates
Self::configure_surface(&surface, &device, pixel_size);
Self {
device,
queue,
surface,
depth,
}
}
fn render(&self, output: &wgpu::Texture, args: &RedrawArgs) {
let aspect = {
let size = output.size();
let w = size.width as f32;
let h = size.height as f32;
w / h
};
self.queue.submit([]); // flush buffer updates
let view = output.create_view(&wgpu::TextureViewDescriptor::default());
let depth_view = self
.depth
.create_view(&wgpu::TextureViewDescriptor::default());
let mut encoder = self
.device
.create_command_encoder(&wgpu::CommandEncoderDescriptor::default());
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: &view,
depth_slice: None,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(wgpu::Color {
r: args.background.x.into(),
g: args.background.y.into(),
b: args.background.z.into(),
a: args.background.w.into(),
}),
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
view: &depth_view,
depth_ops: Some(wgpu::Operations {
load: wgpu::LoadOp::Clear(1.),
store: wgpu::StoreOp::Discard,
}),
stencil_ops: None,
}),
..Default::default()
});
drop(pass);
self.queue.submit(std::iter::once(encoder.finish()));
}
fn create_depth_buffer(device: &wgpu::Device, pixel_size: UVec2) -> wgpu::Texture {
device.create_texture(&wgpu::TextureDescriptor {
label: Some("depth buffer"),
size: wgpu::Extent3d {
width: pixel_size.x,
height: pixel_size.y,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: DEPTH_FORMAT,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
view_formats: &[],
})
}
fn configure_surface(surface: &wgpu::Surface, device: &wgpu::Device, pixel_size: UVec2) {
surface.configure(
device,
&wgpu::SurfaceConfiguration {
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_DST,
format: OUTPUT_FORMAT,
width: pixel_size.x,
height: pixel_size.y,
present_mode: wgpu::PresentMode::Fifo,
alpha_mode: wgpu::CompositeAlphaMode::Auto,
view_formats: vec![],
desired_maximum_frame_latency: 2,
},
);
}
/// Configures the renderer for a given target size.
pub fn configure(&mut self, pixel_size: UVec2) {
Self::configure_surface(&self.surface, &self.device, pixel_size);
self.depth = Self::create_depth_buffer(&self.device, pixel_size);
}
/// Redraws the entire surface.
///
/// [`Self::configure`] must be called at least once before this.
pub fn redraw(&mut self, args: &RedrawArgs) {
let output = self.surface.get_current_texture().unwrap();
self.render(&output.texture, args);
output.present();
}
}
pub async fn init_gpu_inner<E: Error + 'static>(
make_surface: impl FnOnce(&wgpu::Instance) -> Result<wgpu::Surface<'static>, E>,
) -> Result<Gpu, Box<dyn Error>> {
let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor {
backends: wgpu::Backends::PRIMARY,
..Default::default()
});
let surface = make_surface(&instance)?;
let adapter = instance
.request_adapter(&wgpu::RequestAdapterOptions {
power_preference: wgpu::PowerPreference::default(),
compatible_surface: Some(&surface),
force_fallback_adapter: false,
})
.await
.unwrap();
let (device, queue) = adapter
.request_device(&wgpu::DeviceDescriptor::default())
.await
.unwrap();
Ok(Gpu {
device,
queue,
surface,
})
}

82
src/main.rs Normal file
View File

@ -0,0 +1,82 @@
use std::sync::Arc;
use PROJECT_NAME::{Core, RedrawArgs, init_gpu_inner};
use glam::{uvec2, vec4};
use winit::{
application::ApplicationHandler,
event::WindowEvent,
event_loop::{ActiveEventLoop, ControlFlow, EventLoop},
window::Window,
};
const TITLE: &str = "PROJECT NAME";
struct MainWindow {
window: Arc<Window>,
core: Core,
}
impl MainWindow {
fn new(event_loop: &ActiveEventLoop) -> Self {
let window = event_loop
.create_window(Window::default_attributes().with_title(TITLE))
.unwrap();
let window = Arc::new(window);
let gpu = pollster::block_on(init_gpu_inner(|instance| {
instance.create_surface(Arc::clone(&window))
}))
.unwrap();
let core = Core::new(gpu, uvec2(1, 1));
Self { window, core }
}
fn event(&mut self, event_loop: &ActiveEventLoop, event: WindowEvent) {
match event {
WindowEvent::CloseRequested => event_loop.exit(),
WindowEvent::Resized(physical_size) => self
.core
.configure(uvec2(physical_size.width, physical_size.height)),
WindowEvent::RedrawRequested => self.core.redraw(&RedrawArgs {
background: vec4(0.05, 0.20, 0.85, 1.00),
}),
_ => {}
}
}
}
struct Application {
main_window: Option<MainWindow>,
}
impl Application {
fn new() -> Self {
Self { main_window: None }
}
}
impl ApplicationHandler for Application {
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
self.main_window = Some(MainWindow::new(event_loop));
}
fn window_event(
&mut self,
event_loop: &ActiveEventLoop,
window_id: winit::window::WindowId,
event: WindowEvent,
) {
let window = self
.main_window
.as_mut()
.expect("window must exist to recieve events");
assert_eq!(window.window.id(), window_id);
window.event(event_loop, event);
}
}
fn main() {
let event_loop = EventLoop::new().unwrap();
event_loop.set_control_flow(ControlFlow::Wait);
let mut app = Application::new();
event_loop.run_app(&mut app).unwrap();
}

14
ui/CMakeLists.txt Normal file
View File

@ -0,0 +1,14 @@
include(impl.cmake)
qt_add_executable(PROJECT-NAME
src/api.cxx
src/main.cxx
src/main_window.cxx
src/main_window.ui
src/viewport.cxx
)
target_link_libraries(PROJECT-NAME PRIVATE Qt6::Gui Qt6::Widgets)
target_link_libraries(PROJECT-NAME PRIVATE KF6::WidgetsAddons)
target_link_libraries(PROJECT-NAME PRIVATE PROJECT_NAME_impl)
target_include_directories(PROJECT-NAME PRIVATE src)

15
ui/Cargo.toml Normal file
View File

@ -0,0 +1,15 @@
[package]
name = "PROJECT-NAME-impl"
version = "0.1.0"
edition = "2024"
[lib]
crate-type = ["staticlib"]
[dependencies]
PROJECT-NAME = {path = "../"}
glam = { version = "0.30" }
pollster = "0.4.0"
raw-window-handle = "0.6.2"
wgpu = "27.0.1"

22
ui/impl.cmake Normal file
View File

@ -0,0 +1,22 @@
set(impl_basename "${CARGO_TARGET_DIR}/release/libPROJECT_NAME_impl")
add_custom_command(
OUTPUT ${impl_basename}.a
COMMAND env CARGO_TARGET_DIR=${CARGO_TARGET_DIR} ${CARGO} build --release --package PROJECT-NAME-impl
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
DEPFILE ${impl_basename}.d
USES_TERMINAL
JOB_SERVER_AWARE
DEPENDS_EXPLICIT_ONLY
)
# HACK ensure CMake *actually adds* the command above
add_custom_target(build_impl
DEPENDS ${impl_basename}.a
)
add_library(PROJECT_NAME_impl STATIC IMPORTED)
set_target_properties(PROJECT_NAME_impl PROPERTIES
IMPORTED_LOCATION ${impl_basename}.a
)

57
ui/src/api.cxx Normal file
View File

@ -0,0 +1,57 @@
#include "api.hxx"
#include <stdexcept>
#include <utility>
namespace ffi {
extern "C" Core* rt4_viewport_create(xcb_connection_t* connection, std::uint32_t window, std::uint32_t width, std::uint32_t height);
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, const RedrawArgs* args);
} // namespace ffi
BoxCore::BoxCore(BoxCore&& b)
: ptr(std::exchange(b.ptr, {})) {
}
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, std::uint32_t width, std::uint32_t height) {
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.ptr = ffi::rt4_viewport_create(connection, window, width, height);
return out;
}
void BoxCore::reset() {
auto viewport = std::exchange(ptr, {});
if (viewport)
ffi::rt4_viewport_destroy(viewport.use());
}
ffi::Core* MutCore::use() const {
if (!ptr)
throw std::logic_error("attempt to use a null Core");
return ptr;
}
void MutCore::configure(std::uint32_t width, std::uint32_t height) const {
rt4_viewport_configure(use(), width, height);
}
void MutCore::redraw(const RedrawArgs& args) const {
rt4_viewport_redraw(use(), &args);
}

53
ui/src/api.hxx Normal file
View File

@ -0,0 +1,53 @@
#pragma once
#include <cstdint>
struct xcb_connection_t;
struct alignas(16) Vec4 {
float x, y, z, w;
};
namespace ffi {
struct Core;
struct RedrawArgs {
Vec4 background;
};
} // namespace ffi
using ffi::RedrawArgs;
class MutCore {
friend class BoxCore;
public:
explicit operator bool() const { return ptr; }
void configure(std::uint32_t width, std::uint32_t height) const;
void redraw(const RedrawArgs& args) const;
private:
ffi::Core* ptr = nullptr;
ffi::Core* use() const;
};
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; }
const MutCore* operator->() const { return &ptr; }
void reset();
static BoxCore from_xcb(xcb_connection_t* connection, std::uint32_t window, std::uint32_t width, std::uint32_t height);
private:
MutCore ptr;
};

48
ui/src/lib.rs Normal file
View File

@ -0,0 +1,48 @@
use std::{ffi::c_void, num::NonZero, ptr::NonNull};
use PROJECT_NAME::{Core, RedrawArgs, init_gpu_inner};
use glam::{UVec2, uvec2};
use raw_window_handle::{RawDisplayHandle, RawWindowHandle, XcbDisplayHandle, XcbWindowHandle};
unsafe fn create_viewport(
display: impl Into<RawDisplayHandle>,
window: impl Into<RawWindowHandle>,
size: UVec2,
) -> Box<Core> {
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, size))
}
#[unsafe(no_mangle)]
unsafe extern "C" fn rt4_viewport_create(
connection: NonNull<c_void>,
window: NonZero<u32>,
width: u32,
height: u32,
) -> Box<Core> {
let display = XcbDisplayHandle::new(Some(connection), 0);
let window = XcbWindowHandle::new(window);
unsafe { create_viewport(display, window, uvec2(width, height)) }
}
#[unsafe(no_mangle)]
unsafe extern "C" fn rt4_viewport_destroy(viewport: Box<Core>) {
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, args: &RedrawArgs) {
viewport.redraw(args);
}

15
ui/src/main.cxx Normal file
View File

@ -0,0 +1,15 @@
#include "main_window.hxx"
#include <QApplication>
int main(int argc, char* argv[]) {
// only X11 is supported so, use XCB unless overriden
setenv("QT_QPA_PLATFORM", "xcb", 0);
QApplication app(argc, argv);
auto w = new PROJECTNAME;
w->show();
return app.exec();
}

27
ui/src/main_window.cxx Normal file
View File

@ -0,0 +1,27 @@
#include "main_window.hxx"
#include "ui_main_window.h"
PROJECTNAME::PROJECTNAME(QWidget* parent)
: QMainWindow(parent),
m_ui(new Ui::MainWindow) {
m_ui->setupUi(this);
updateView();
}
PROJECTNAME::~PROJECTNAME() = default;
void PROJECTNAME::updateView() {
const auto color = m_ui->inBackground->color();
RedrawArgs args{
.background = { color.redF(), color.greenF(), color.blueF(), 1.00 },
};
m_ui->viewport->setView(args);
}
void PROJECTNAME::updateViewIf(bool update) {
if (update)
updateView();
}
#include "moc_main_window.cpp"

23
ui/src/main_window.hxx Normal file
View File

@ -0,0 +1,23 @@
#pragma once
#include <QMainWindow>
#include <memory>
namespace Ui {
class MainWindow;
}
class PROJECTNAME : public QMainWindow {
Q_OBJECT
public:
explicit PROJECTNAME(QWidget* parent = nullptr);
~PROJECTNAME() override;
public slots:
void updateView();
void updateViewIf(bool update); // for radio buttons
private:
const std::unique_ptr<Ui::MainWindow> m_ui;
};

114
ui/src/main_window.ui Normal file
View File

@ -0,0 +1,114 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>MainWindow</class>
<widget class="QMainWindow" name="MainWindow">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>1600</width>
<height>1200</height>
</rect>
</property>
<property name="windowTitle">
<string>PROJECT NAME</string>
</property>
<widget class="QWidget" name="centralwidget">
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="Viewport" name="viewport" native="true"/>
</item>
</layout>
</widget>
<widget class="QMenuBar" name="menubar">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>1600</width>
<height>34</height>
</rect>
</property>
</widget>
<widget class="QStatusBar" name="statusbar"/>
<widget class="QDockWidget" name="dockWidget">
<property name="features">
<set>QDockWidget::DockWidgetFloatable|QDockWidget::DockWidgetMovable</set>
</property>
<attribute name="dockWidgetArea">
<number>2</number>
</attribute>
<widget class="QWidget" name="dockWidgetContents">
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>Background color</string>
</property>
</widget>
</item>
<item>
<widget class="KColorCombo" name="inBackground">
<property name="color">
<color>
<red>25</red>
<green>220</green>
<blue>0</blue>
</color>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>385</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
</widget>
</widget>
<customwidgets>
<customwidget>
<class>KColorCombo</class>
<extends>QComboBox</extends>
<header>kcolorcombo.h</header>
</customwidget>
<customwidget>
<class>Viewport</class>
<extends>QWidget</extends>
<header>viewport.hxx</header>
<container>1</container>
</customwidget>
</customwidgets>
<resources/>
<connections>
<connection>
<sender>inBackground</sender>
<signal>highlighted(QColor)</signal>
<receiver>MainWindow</receiver>
<slot>updateView()</slot>
<hints>
<hint type="sourcelabel">
<x>1531</x>
<y>115</y>
</hint>
<hint type="destinationlabel">
<x>799</x>
<y>599</y>
</hint>
</hints>
</connection>
</connections>
<slots>
<slot>updateView()</slot>
<slot>updateViewIf(bool)</slot>
</slots>
</ui>

74
ui/src/viewport.cxx Normal file
View File

@ -0,0 +1,74 @@
#include "viewport.hxx"
#include <QApplication>
#include <QEvent>
#include <QGuiApplication>
Viewport::Viewport(QWidget* parent, Qt::WindowFlags f)
: QWidget(parent, f) {
setAttribute(Qt::WA_DontCreateNativeAncestors);
setAttribute(Qt::WA_NativeWindow);
setAttribute(Qt::WA_PaintOnScreen);
setAttribute(Qt::WA_NoSystemBackground);
}
Viewport::~Viewport() = default;
void Viewport::setView(RedrawArgs new_args) {
args = new_args;
update();
}
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(args);
}
void Viewport::resizeEvent(QResizeEvent* event) {
if (!core)
return;
updateSize();
QWidget::resizeEvent(event);
}
void Viewport::recreate() try {
auto* app = qobject_cast<QGuiApplication*>(QApplication::instance());
if (!app)
throw std::runtime_error("not a GUI application (WTF?)");
auto* native = app->nativeInterface<QNativeInterface::QX11Application>();
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);
const QSize device_size = size() * devicePixelRatio();
core.reset();
core = BoxCore::from_xcb(xcb_connection, x11_window, device_size.width(), device_size.height());
} 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());
}

28
ui/src/viewport.hxx Normal file
View File

@ -0,0 +1,28 @@
#pragma once
#include <QWidget>
#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;
void setView(RedrawArgs new_args);
protected:
bool event(QEvent* event) override;
void paintEvent(QPaintEvent* event) override;
void resizeEvent(QResizeEvent* event) override;
private:
BoxCore core;
RedrawArgs args = {};
void recreate();
void updateSize();
};