mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-09-28 03:05:33 +00:00
ifcviewer: move initWgpu + probeAndCreatePool + shutdown into ViewportCore (#84-l)
The instance/adapter/device/queue/pool/surface-format wgpu lifecycle now lives in ViewportCore — including the OOM-scoped pool size probe and the worker-thread startup. ViewportWindow::initWgpu becomes a Qt shell that handles env-var tuning + nav-button preset wiring, then delegates to core_.initWgpu(); the VW-only pipeline builders (HiZ, edge, overlays, pick) still run after. ViewportWindow::shutdown drops the VW-only resources (depth, msaa, hiz, edge, overlays, pick) and lets core_.shutdown() release the shared wgpu handles it now owns. The wgpu-native log callback (wgpuSetLogCallback / WGPULogLevel) is gated on !__EMSCRIPTEN__: it's not part of the W3C spec header, and the emdawnwebgpu port doesn't ship wgpu.h — validation errors there land in the browser console regardless. Drive-by: update test_federation to compare HomeView::target as Eigen::Vector3f (left stale by #79 when QVector3D was retired).
This commit is contained in:
@@ -26,6 +26,14 @@
|
|||||||
#include <limits>
|
#include <limits>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
|
// wgpu-native extensions (log callback, MULTI_DRAW_INDIRECT). The web
|
||||||
|
// build (emdawnwebgpu / Dawn) doesn't ship this header — validation
|
||||||
|
// errors there go to the browser console, so the log-callback path
|
||||||
|
// simply compiles out under __EMSCRIPTEN__.
|
||||||
|
#if !defined(__EMSCRIPTEN__)
|
||||||
|
# include <webgpu/wgpu.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
#include "CameraMath.h"
|
#include "CameraMath.h"
|
||||||
#include "InstanceCompose.h"
|
#include "InstanceCompose.h"
|
||||||
#include "Log.h"
|
#include "Log.h"
|
||||||
@@ -1112,3 +1120,250 @@ void ViewportCore::uploadSelectionFlagsIfDirty() {
|
|||||||
selection_flags_scratch_.size() * sizeof(uint32_t));
|
selection_flags_scratch_.size() * sizeof(uint32_t));
|
||||||
selection_.markClean();
|
selection_.markClean();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ===========================================================================
|
||||||
|
// Lifecycle (#84-l): initWgpu + probeAndCreatePool + shutdown
|
||||||
|
// ===========================================================================
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
// String-view → std::string for log output. WGPU_STRLEN is the
|
||||||
|
// sentinel meaning "nul-terminated", in which case strlen() gives the
|
||||||
|
// length.
|
||||||
|
std::string svToStr(WGPUStringView s) {
|
||||||
|
if (!s.data) return {};
|
||||||
|
const std::size_t len = (s.length == WGPU_STRLEN)
|
||||||
|
? std::strlen(s.data)
|
||||||
|
: s.length;
|
||||||
|
return std::string(s.data, len);
|
||||||
|
}
|
||||||
|
|
||||||
|
#if !defined(__EMSCRIPTEN__)
|
||||||
|
// wgpu-native callback: route every log line to Log::warn / Log::info
|
||||||
|
// so backend init problems surface in the console instead of being
|
||||||
|
// swallowed by the native runtime. Not available on emdawnwebgpu —
|
||||||
|
// see the include guard above.
|
||||||
|
void onWgpuLog(WGPULogLevel level, WGPUStringView message, void* /*userdata*/) {
|
||||||
|
const std::string m = svToStr(message);
|
||||||
|
switch (level) {
|
||||||
|
case WGPULogLevel_Error: Log::warn() << "[wgpu err] " << m; break;
|
||||||
|
case WGPULogLevel_Warn: Log::warn() << "[wgpu warn] " << m; break;
|
||||||
|
case WGPULogLevel_Info: Log::info() << "[wgpu info] " << m; break;
|
||||||
|
case WGPULogLevel_Debug: Log::info() << "[wgpu dbg] " << m; break;
|
||||||
|
case WGPULogLevel_Trace: Log::info() << "[wgpu trace] " << m; break;
|
||||||
|
default: break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Per-device uncaptured-error callback. Validation failures land here
|
||||||
|
// when no error scope is open. Surfacing them into Log::warn makes
|
||||||
|
// otherwise-silent driver complaints attributable.
|
||||||
|
void onUncapturedError(WGPUDevice const* /*device*/,
|
||||||
|
WGPUErrorType type, WGPUStringView message,
|
||||||
|
void* /*ud1*/, void* /*ud2*/) {
|
||||||
|
Log::warn() << "[wgpu device error " << int(type) << "] " << svToStr(message);
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
bool ViewportCore::probeAndCreatePool() {
|
||||||
|
// Discover the largest single buffer the runtime will grant. Each
|
||||||
|
// attempt sits inside OOM + Validation error scopes so a failed
|
||||||
|
// allocation doesn't surface as a noisy uncaptured-error warning.
|
||||||
|
WGPULimits device_limits = {};
|
||||||
|
wgpuDeviceGetLimits(device_, &device_limits);
|
||||||
|
|
||||||
|
constexpr uint64_t MIN_POOL_CAPACITY = 64ull * 1024 * 1024;
|
||||||
|
constexpr uint64_t MAX_PROBE_START = 4ull * 1024 * 1024 * 1024;
|
||||||
|
uint64_t try_size = std::min<uint64_t>(device_limits.maxBufferSize,
|
||||||
|
MAX_PROBE_START);
|
||||||
|
if (try_size < MIN_POOL_CAPACITY) try_size = MIN_POOL_CAPACITY;
|
||||||
|
|
||||||
|
const WGPUBufferUsage pool_usage = WGPUBufferUsage_Storage
|
||||||
|
| WGPUBufferUsage_CopyDst;
|
||||||
|
|
||||||
|
while (try_size >= MIN_POOL_CAPACITY) {
|
||||||
|
wgpuDevicePushErrorScope(device_, WGPUErrorFilter_Validation);
|
||||||
|
wgpuDevicePushErrorScope(device_, WGPUErrorFilter_OutOfMemory);
|
||||||
|
|
||||||
|
WGPUBufferDescriptor desc = {};
|
||||||
|
desc.usage = pool_usage;
|
||||||
|
desc.size = try_size;
|
||||||
|
desc.label.data = "ifcviewer-wgpu.pool_probe";
|
||||||
|
desc.label.length = std::strlen("ifcviewer-wgpu.pool_probe");
|
||||||
|
WGPUBuffer probe_buf = wgpuDeviceCreateBuffer(device_, &desc);
|
||||||
|
|
||||||
|
struct PopResult { bool done = false; bool error = false; };
|
||||||
|
auto pop = [&](PopResult& pr) {
|
||||||
|
WGPUPopErrorScopeCallbackInfo pcb = {};
|
||||||
|
pcb.mode = WGPUCallbackMode_AllowProcessEvents;
|
||||||
|
pcb.callback = [](WGPUPopErrorScopeStatus, WGPUErrorType type,
|
||||||
|
WGPUStringView, void* ud1, void* /*ud2*/) {
|
||||||
|
auto* p = static_cast<PopResult*>(ud1);
|
||||||
|
p->done = true;
|
||||||
|
p->error = (type != WGPUErrorType_NoError);
|
||||||
|
};
|
||||||
|
pcb.userdata1 = ≺
|
||||||
|
wgpuDevicePopErrorScope(device_, pcb);
|
||||||
|
while (!pr.done) wgpuInstanceProcessEvents(instance_);
|
||||||
|
};
|
||||||
|
PopResult oom_pop, validation_pop;
|
||||||
|
pop(oom_pop);
|
||||||
|
pop(validation_pop);
|
||||||
|
|
||||||
|
if (probe_buf) wgpuBufferRelease(probe_buf);
|
||||||
|
if (probe_buf && !oom_pop.error && !validation_pop.error) {
|
||||||
|
pool_.configure(instance_, device_, pool_usage, try_size,
|
||||||
|
"ifcviewer-wgpu.pool");
|
||||||
|
Log::info() << "wgpu: pool per-sub-buffer capacity = "
|
||||||
|
<< (try_size / (1024 * 1024)) << " MB"
|
||||||
|
<< " (device maxBufferSize = "
|
||||||
|
<< (device_limits.maxBufferSize / (1024 * 1024))
|
||||||
|
<< " MB); pool will grow on demand";
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
try_size /= 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
Log::warn() << "wgpu: pool probe found no allocatable size >= "
|
||||||
|
<< (MIN_POOL_CAPACITY / (1024 * 1024)) << " MB";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ViewportCore::initWgpu(bool web_limits) {
|
||||||
|
#if !defined(__EMSCRIPTEN__)
|
||||||
|
wgpuSetLogCallback(onWgpuLog, nullptr);
|
||||||
|
wgpuSetLogLevel(WGPULogLevel_Warn);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
instance_ = wgpuCreateInstance(nullptr);
|
||||||
|
if (!instance_) {
|
||||||
|
Log::warn() << "wgpuCreateInstance returned null";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Surface comes from the host (X11/HWND/CAMetalLayer on desktop;
|
||||||
|
// Emscripten canvas selector on web).
|
||||||
|
surface_ = host_->createSurface(instance_);
|
||||||
|
if (!surface_) {
|
||||||
|
Log::warn() << "host createSurface returned null";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Async request adapter -------------------------------------------
|
||||||
|
struct AdapterReq { WGPUAdapter adapter = nullptr; bool done = false; bool ok = false; };
|
||||||
|
AdapterReq areq;
|
||||||
|
|
||||||
|
WGPURequestAdapterOptions adapter_opts = {};
|
||||||
|
adapter_opts.compatibleSurface = surface_;
|
||||||
|
adapter_opts.powerPreference = WGPUPowerPreference_HighPerformance;
|
||||||
|
|
||||||
|
WGPURequestAdapterCallbackInfo acb = {};
|
||||||
|
acb.mode = WGPUCallbackMode_AllowProcessEvents;
|
||||||
|
acb.callback = [](WGPURequestAdapterStatus status, WGPUAdapter adapter,
|
||||||
|
WGPUStringView message, void* ud1, void* /*ud2*/) {
|
||||||
|
auto* r = static_cast<AdapterReq*>(ud1);
|
||||||
|
r->done = true;
|
||||||
|
if (status == WGPURequestAdapterStatus_Success) {
|
||||||
|
r->adapter = adapter;
|
||||||
|
r->ok = true;
|
||||||
|
} else {
|
||||||
|
Log::warn() << "RequestAdapter failed: " << svToStr(message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
acb.userdata1 = &areq;
|
||||||
|
|
||||||
|
wgpuInstanceRequestAdapter(instance_, &adapter_opts, acb);
|
||||||
|
while (!areq.done) wgpuInstanceProcessEvents(instance_);
|
||||||
|
if (!areq.ok) return false;
|
||||||
|
adapter_ = areq.adapter;
|
||||||
|
|
||||||
|
// ---- Async request device --------------------------------------------
|
||||||
|
struct DeviceReq { WGPUDevice device = nullptr; bool done = false; bool ok = false; };
|
||||||
|
DeviceReq dreq;
|
||||||
|
|
||||||
|
WGPULimits adapter_limits = {};
|
||||||
|
wgpuAdapterGetLimits(adapter_, &adapter_limits);
|
||||||
|
|
||||||
|
WGPULimits web_floor_limits = adapter_limits;
|
||||||
|
web_floor_limits.maxStorageBufferBindingSize = 128ull * 1024 * 1024;
|
||||||
|
web_floor_limits.maxBufferSize = 256ull * 1024 * 1024;
|
||||||
|
|
||||||
|
WGPUDeviceDescriptor dev_desc = {};
|
||||||
|
dev_desc.requiredLimits = web_limits ? &web_floor_limits : &adapter_limits;
|
||||||
|
if (web_limits) {
|
||||||
|
Log::info() << "wgpu --web-limits: requesting browser-floor limits "
|
||||||
|
"(maxStorageBufferBindingSize=128MB, maxBufferSize=256MB)";
|
||||||
|
}
|
||||||
|
dev_desc.uncapturedErrorCallbackInfo.callback = onUncapturedError;
|
||||||
|
|
||||||
|
WGPURequestDeviceCallbackInfo dcb = {};
|
||||||
|
dcb.mode = WGPUCallbackMode_AllowProcessEvents;
|
||||||
|
dcb.callback = [](WGPURequestDeviceStatus status, WGPUDevice device,
|
||||||
|
WGPUStringView message, void* ud1, void* /*ud2*/) {
|
||||||
|
auto* r = static_cast<DeviceReq*>(ud1);
|
||||||
|
r->done = true;
|
||||||
|
if (status == WGPURequestDeviceStatus_Success) {
|
||||||
|
r->device = device;
|
||||||
|
r->ok = true;
|
||||||
|
} else {
|
||||||
|
Log::warn() << "RequestDevice failed: " << svToStr(message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
dcb.userdata1 = &dreq;
|
||||||
|
|
||||||
|
wgpuAdapterRequestDevice(adapter_, &dev_desc, dcb);
|
||||||
|
while (!dreq.done) wgpuInstanceProcessEvents(instance_);
|
||||||
|
if (!dreq.ok) return false;
|
||||||
|
device_ = dreq.device;
|
||||||
|
queue_ = wgpuDeviceGetQueue(device_);
|
||||||
|
|
||||||
|
if (!probeAndCreatePool()) {
|
||||||
|
Log::warn() << "wgpu: streaming pool probe failed; cannot start";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
streaming_thread_.start();
|
||||||
|
|
||||||
|
WGPUSurfaceCapabilities caps = {};
|
||||||
|
if (wgpuSurfaceGetCapabilities(surface_, adapter_, &caps) != WGPUStatus_Success
|
||||||
|
|| caps.formatCount == 0) {
|
||||||
|
Log::warn() << "wgpuSurfaceGetCapabilities returned no formats";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
surface_format_ = caps.formats[0];
|
||||||
|
wgpuSurfaceCapabilitiesFreeMembers(caps);
|
||||||
|
|
||||||
|
Log::info() << "wgpu init OK; surface format = " << int(surface_format_);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ViewportCore::shutdown() {
|
||||||
|
// Stop streaming first so no late results land in the pool after
|
||||||
|
// we've torn down model state. Worker drains its queue then joins.
|
||||||
|
streaming_thread_.stop();
|
||||||
|
|
||||||
|
for (auto& [mid, m] : models_gpu_) releaseWgpuModelGpuData(m, pool_);
|
||||||
|
models_gpu_.clear();
|
||||||
|
|
||||||
|
if (frame_bind_group_) { wgpuBindGroupRelease(frame_bind_group_); frame_bind_group_ = nullptr; }
|
||||||
|
if (frame_uniform_buffer_) { wgpuBufferRelease(frame_uniform_buffer_); frame_uniform_buffer_ = nullptr; }
|
||||||
|
if (selection_flags_buffer_) { wgpuBufferRelease(selection_flags_buffer_); selection_flags_buffer_ = nullptr; }
|
||||||
|
selection_flags_capacity_ = 0;
|
||||||
|
if (main_pipeline_) { wgpuRenderPipelineRelease(main_pipeline_); main_pipeline_ = nullptr; }
|
||||||
|
if (main_pipeline_transparent_) { wgpuRenderPipelineRelease(main_pipeline_transparent_); main_pipeline_transparent_ = nullptr; }
|
||||||
|
if (main_shader_module_) { wgpuShaderModuleRelease(main_shader_module_); main_shader_module_ = nullptr; }
|
||||||
|
if (pipeline_layout_) { wgpuPipelineLayoutRelease(pipeline_layout_); pipeline_layout_ = nullptr; }
|
||||||
|
if (model_bgl_) { wgpuBindGroupLayoutRelease(model_bgl_); model_bgl_ = nullptr; }
|
||||||
|
if (frame_bgl_) { wgpuBindGroupLayoutRelease(frame_bgl_); frame_bgl_ = nullptr; }
|
||||||
|
|
||||||
|
// Destroy the streaming pool while device_ is still alive — it owns
|
||||||
|
// the underlying WGPUBuffer.
|
||||||
|
pool_.destroy();
|
||||||
|
|
||||||
|
if (queue_) { wgpuQueueRelease(queue_); queue_ = nullptr; }
|
||||||
|
if (device_) { wgpuDeviceRelease(device_); device_ = nullptr; }
|
||||||
|
if (adapter_) { wgpuAdapterRelease(adapter_); adapter_ = nullptr; }
|
||||||
|
if (surface_) { wgpuSurfaceRelease(surface_); surface_ = nullptr; }
|
||||||
|
if (instance_) { wgpuInstanceRelease(instance_); instance_ = nullptr; }
|
||||||
|
wgpu_initialized_ = false;
|
||||||
|
surface_configured_ = false;
|
||||||
|
}
|
||||||
|
|||||||
@@ -223,6 +223,33 @@ public:
|
|||||||
void ensureSelectionFlagsBuffer();
|
void ensureSelectionFlagsBuffer();
|
||||||
void uploadSelectionFlagsIfDirty();
|
void uploadSelectionFlagsIfDirty();
|
||||||
|
|
||||||
|
// ---- wgpu lifecycle ----------------------------------------------------
|
||||||
|
//
|
||||||
|
// initWgpu brings up the wgpu instance, gets the platform surface
|
||||||
|
// from host_->createSurface, requests adapter + device + queue,
|
||||||
|
// probes the streaming pool capacity, starts the background loader
|
||||||
|
// thread, and picks the swap-chain surface format. Does NOT build
|
||||||
|
// pipelines — the host runs the pipeline construction after this
|
||||||
|
// (so VW can still keep its HiZ / edge / pick / overlay builders
|
||||||
|
// co-located).
|
||||||
|
//
|
||||||
|
// `web_limits` forces the WebGPU spec mandatory floor (128 MB max
|
||||||
|
// storage binding, 256 MB max buffer) instead of the adapter's
|
||||||
|
// actual maximum — used by --web-limits to verify chunking fits
|
||||||
|
// through browser constraints.
|
||||||
|
//
|
||||||
|
// shutdown tears down everything ViewportCore owns. The host's own
|
||||||
|
// teardown (depth/MSAA/HiZ/edge/pick attachments + overlays) must
|
||||||
|
// run BEFORE this call so its device-owned resources release
|
||||||
|
// against a still-live device.
|
||||||
|
bool initWgpu(bool web_limits);
|
||||||
|
void shutdown();
|
||||||
|
|
||||||
|
private:
|
||||||
|
bool probeAndCreatePool();
|
||||||
|
|
||||||
|
public:
|
||||||
|
|
||||||
// Friend access for ViewportWindow's reference proxies. As each
|
// Friend access for ViewportWindow's reference proxies. As each
|
||||||
// render method moves into ViewportCore it stops needing these
|
// render method moves into ViewportCore it stops needing these
|
||||||
// (it touches the fields directly); once everything has migrated
|
// (it touches the fields directly); once everything has migrated
|
||||||
|
|||||||
@@ -163,24 +163,6 @@ static QString sv(WGPUStringView s) {
|
|||||||
return QString::fromUtf8(s.data, len);
|
return QString::fromUtf8(s.data, len);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void onWgpuLog(WGPULogLevel level, WGPUStringView message, void* /*userdata*/) {
|
|
||||||
const QString m = sv(message);
|
|
||||||
switch (level) {
|
|
||||||
case WGPULogLevel_Error: Log::warn().noquote() << "[wgpu err]" << m; break;
|
|
||||||
case WGPULogLevel_Warn: Log::warn().noquote() << "[wgpu warn]" << m; break;
|
|
||||||
case WGPULogLevel_Info: Log::info() << "[wgpu info] " << m; break;
|
|
||||||
case WGPULogLevel_Debug: Log::info() << "[wgpu dbg] " << m; break;
|
|
||||||
case WGPULogLevel_Trace: Log::info() << "[wgpu trace] " << m; break;
|
|
||||||
default: break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static void onUncapturedError(WGPUDevice const* /*device*/,
|
|
||||||
WGPUErrorType type, WGPUStringView message,
|
|
||||||
void* /*ud1*/, void* /*ud2*/) {
|
|
||||||
Log::warn().noquote() << "[wgpu device error" << int(type) << "]" << sv(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Allocate a wgpu buffer of `size_bytes` with the given usage, and upload
|
// Allocate a wgpu buffer of `size_bytes` with the given usage, and upload
|
||||||
// `data` into it via the queue. Returns nullptr when size_bytes == 0 (wgpu
|
// `data` into it via the queue. Returns nullptr when size_bytes == 0 (wgpu
|
||||||
// rejects zero-sized buffer creation). `label` is informational; it shows up
|
// rejects zero-sized buffer creation). `label` is informational; it shows up
|
||||||
@@ -328,13 +310,65 @@ ViewportWindow::~ViewportWindow() {
|
|||||||
// forwards to the existing Q_SIGNAL so bonsai-side consumers see no
|
// forwards to the existing Q_SIGNAL so bonsai-side consumers see no
|
||||||
// change.
|
// change.
|
||||||
|
|
||||||
WGPUSurface ViewportWindow::createSurface(WGPUInstance /*instance*/) {
|
// Platform-specific WGPUSurface creation. Called by ViewportCore::initWgpu
|
||||||
// initWgpu() still drives surface creation through the private
|
// (#84-l) once the wgpu instance is up. The platform branches reach into
|
||||||
// no-arg createSurface() helper that populates surface_. Once
|
// Qt's QNativeInterface to fish out the native window handle (X11
|
||||||
// ViewportCore takes over the wgpu init flow it'll call this
|
// Display + Window, Win32 HWND, or NSView wrapped in CAMetalLayer) and
|
||||||
// override instead and the private helper goes away; for now the
|
// wrap each in the corresponding WGPUSurfaceSource* descriptor. The
|
||||||
// override is a getter.
|
// returned WGPUSurface is owned by the caller (ViewportCore stores it
|
||||||
return surface_;
|
// on `core_.surface_`).
|
||||||
|
WGPUSurface ViewportWindow::createSurface(WGPUInstance instance) {
|
||||||
|
WGPUSurfaceDescriptor surface_desc = {};
|
||||||
|
|
||||||
|
#if defined(Q_OS_LINUX)
|
||||||
|
const QString platform = QGuiApplication::platformName();
|
||||||
|
if (platform == "xcb") {
|
||||||
|
# if __has_include(<X11/Xlib.h>)
|
||||||
|
auto* x11 = qApp->nativeInterface<QNativeInterface::QX11Application>();
|
||||||
|
if (!x11 || !x11->display()) {
|
||||||
|
Log::warn() << "Could not get X11 Display* from Qt";
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
WGPUSurfaceSourceXlibWindow xlib = {};
|
||||||
|
xlib.chain.sType = WGPUSType_SurfaceSourceXlibWindow;
|
||||||
|
xlib.display = x11->display();
|
||||||
|
xlib.window = static_cast<uint64_t>(winId());
|
||||||
|
surface_desc.nextInChain = &xlib.chain;
|
||||||
|
return wgpuInstanceCreateSurface(instance, &surface_desc);
|
||||||
|
# else
|
||||||
|
Log::warn() << "Built without Xlib headers; cannot create X11 surface";
|
||||||
|
return nullptr;
|
||||||
|
# endif
|
||||||
|
} else if (platform == "wayland") {
|
||||||
|
Log::warn() << "Wayland wgpu surface creation not yet wired (stage 1.5)";
|
||||||
|
return nullptr;
|
||||||
|
} else {
|
||||||
|
Log::warn().noquote() << "Unsupported Qt platform for wgpu surface:" << platform;
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
#elif defined(Q_OS_WIN)
|
||||||
|
WGPUSurfaceSourceWindowsHWND hwndsrc = {};
|
||||||
|
hwndsrc.chain.sType = WGPUSType_SurfaceSourceWindowsHWND;
|
||||||
|
hwndsrc.hinstance = ::GetModuleHandleW(nullptr);
|
||||||
|
hwndsrc.hwnd = reinterpret_cast<void*>(static_cast<uintptr_t>(winId()));
|
||||||
|
surface_desc.nextInChain = &hwndsrc.chain;
|
||||||
|
return wgpuInstanceCreateSurface(instance, &surface_desc);
|
||||||
|
#elif defined(Q_OS_MAC)
|
||||||
|
void* nsview = reinterpret_cast<void*>(static_cast<uintptr_t>(winId()));
|
||||||
|
void* layer = wgpu_macos_attach_metal_layer(nsview);
|
||||||
|
if (!layer) {
|
||||||
|
Log::warn() << "Could not attach CAMetalLayer to the Qt NSView";
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
WGPUSurfaceSourceMetalLayer metalsrc = {};
|
||||||
|
metalsrc.chain.sType = WGPUSType_SurfaceSourceMetalLayer;
|
||||||
|
metalsrc.layer = layer;
|
||||||
|
surface_desc.nextInChain = &metalsrc.chain;
|
||||||
|
return wgpuInstanceCreateSurface(instance, &surface_desc);
|
||||||
|
#else
|
||||||
|
Log::warn() << "wgpu surface creation not yet wired for this platform";
|
||||||
|
return nullptr;
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
void ViewportWindow::framebufferSize(int& width_px, int& height_px) const {
|
void ViewportWindow::framebufferSize(int& width_px, int& height_px) const {
|
||||||
@@ -1127,17 +1161,13 @@ bool ViewportWindow::event(QEvent* event) {
|
|||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
|
|
||||||
bool ViewportWindow::initWgpu() {
|
bool ViewportWindow::initWgpu() {
|
||||||
// Optional: log everything wgpu-native says at warn+ so backend init
|
// ---- Env-var tuning + nav binding setup -----------------------------
|
||||||
// problems surface in the console rather than being swallowed.
|
//
|
||||||
wgpuSetLogCallback(onWgpuLog, nullptr);
|
// These mutate VW-side state (cull thresholds, hiz_enabled_, nav
|
||||||
wgpuSetLogLevel(WGPULogLevel_Warn);
|
// button bindings) so they stay in the Qt-bound shell. ViewportCore
|
||||||
|
// doesn't know about Qt::MouseButton enums or the still-in-VW cull
|
||||||
// Env-var overrides for contribution-cull thresholds. wgpu uses
|
// tuning fields. Once those move (later #84 steps + #85 for input)
|
||||||
// view-Z (perspective-divide-correct) for projected_px, whereas the
|
// this whole prologue migrates with them.
|
||||||
// GL backend uses euclidean distance — so for off-axis instances
|
|
||||||
// wgpu computes a larger projected_px and is less aggressive at the
|
|
||||||
// same numeric threshold. These knobs exist to let us sweep matching
|
|
||||||
// values during the perf-parity push without rebuilding.
|
|
||||||
if (const char* s = std::getenv("WGPU_MIN_PX")) {
|
if (const char* s = std::getenv("WGPU_MIN_PX")) {
|
||||||
const float v = float(std::atof(s));
|
const float v = float(std::atof(s));
|
||||||
if (v >= 0.0f) min_pixel_radius_ = v;
|
if (v >= 0.0f) min_pixel_radius_ = v;
|
||||||
@@ -1165,9 +1195,6 @@ bool ViewportWindow::initWgpu() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (const char* s = std::getenv("WGPU_CULL_THREADS")) {
|
if (const char* s = std::getenv("WGPU_CULL_THREADS")) {
|
||||||
// "0" disables std::async dispatch — every model is culled on the
|
|
||||||
// main thread, sequentially. Used to measure speedup vs the
|
|
||||||
// parallel-per-model path. Any non-"0" value keeps parallelism on.
|
|
||||||
cull_threads_enabled_ = (s[0] != '0');
|
cull_threads_enabled_ = (s[0] != '0');
|
||||||
Log::info().noquote().nospace()
|
Log::info().noquote().nospace()
|
||||||
<< "[wgpu cull] WGPU_CULL_THREADS=" << s
|
<< "[wgpu cull] WGPU_CULL_THREADS=" << s
|
||||||
@@ -1179,8 +1206,6 @@ bool ViewportWindow::initWgpu() {
|
|||||||
Log::info() << "[wgpu fly] WGPU_FLY_DEBUG=1 — per-frame [fly] dt log enabled";
|
Log::info() << "[wgpu fly] WGPU_FLY_DEBUG=1 — per-frame [fly] dt log enabled";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Mouse-nav preset (matches GL AppSettings::NavPreset). blender default,
|
|
||||||
// rhino or revit as alternatives. Selection always stays on LMB.
|
|
||||||
const char* nav_env = std::getenv("WGPU_NAV_PRESET");
|
const char* nav_env = std::getenv("WGPU_NAV_PRESET");
|
||||||
applyNavPreset(nav_env ? nav_env : "blender");
|
applyNavPreset(nav_env ? nav_env : "blender");
|
||||||
Log::info().noquote().nospace()
|
Log::info().noquote().nospace()
|
||||||
@@ -1193,119 +1218,11 @@ bool ViewportWindow::initWgpu() {
|
|||||||
<< (pan_mods_ & Qt::ShiftModifier ? "+Shift" : "")
|
<< (pan_mods_ & Qt::ShiftModifier ? "+Shift" : "")
|
||||||
<< ")";
|
<< ")";
|
||||||
|
|
||||||
instance_ = wgpuCreateInstance(nullptr);
|
// ---- ViewportCore handles instance/adapter/device/queue/pool/format -
|
||||||
if (!instance_) {
|
if (!core_.initWgpu(web_limits_)) return false;
|
||||||
Log::warn() << "wgpuCreateInstance returned null";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!createSurface()) return false;
|
|
||||||
|
|
||||||
// ---- Async request adapter -------------------------------------------
|
|
||||||
struct AdapterReq { WGPUAdapter adapter = nullptr; bool done = false; bool ok = false; };
|
|
||||||
AdapterReq areq;
|
|
||||||
|
|
||||||
WGPURequestAdapterOptions adapter_opts = {};
|
|
||||||
adapter_opts.compatibleSurface = surface_;
|
|
||||||
adapter_opts.powerPreference = WGPUPowerPreference_HighPerformance;
|
|
||||||
|
|
||||||
WGPURequestAdapterCallbackInfo acb = {};
|
|
||||||
acb.mode = WGPUCallbackMode_AllowProcessEvents;
|
|
||||||
acb.callback = [](WGPURequestAdapterStatus status, WGPUAdapter adapter,
|
|
||||||
WGPUStringView message, void* ud1, void* /*ud2*/) {
|
|
||||||
auto* r = static_cast<AdapterReq*>(ud1);
|
|
||||||
r->done = true;
|
|
||||||
if (status == WGPURequestAdapterStatus_Success) {
|
|
||||||
r->adapter = adapter;
|
|
||||||
r->ok = true;
|
|
||||||
} else {
|
|
||||||
Log::warn().noquote() << "RequestAdapter failed:" << sv(message);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
acb.userdata1 = &areq;
|
|
||||||
|
|
||||||
wgpuInstanceRequestAdapter(instance_, &adapter_opts, acb);
|
|
||||||
while (!areq.done) wgpuInstanceProcessEvents(instance_);
|
|
||||||
if (!areq.ok) return false;
|
|
||||||
adapter_ = areq.adapter;
|
|
||||||
|
|
||||||
// ---- Async request device --------------------------------------------
|
|
||||||
struct DeviceReq { WGPUDevice device = nullptr; bool done = false; bool ok = false; };
|
|
||||||
DeviceReq dreq;
|
|
||||||
|
|
||||||
// Pick the limits to request on the device. Default = the adapter's
|
|
||||||
// actual maximum so large native scenes get all the headroom the GPU
|
|
||||||
// can give. --web-limits forces the WebGPU spec mandatory floor
|
|
||||||
// (128 MB max storage binding, 256 MB max buffer) so we can verify on
|
|
||||||
// desktop that the renderer's chunking actually fits through browser
|
|
||||||
// constraints — turns "trust me, web will work" into a hard test.
|
|
||||||
WGPULimits adapter_limits = {};
|
|
||||||
wgpuAdapterGetLimits(adapter_, &adapter_limits);
|
|
||||||
|
|
||||||
WGPULimits web_floor_limits = adapter_limits;
|
|
||||||
// Override just the two that BIM scenes typically blow past. Everything
|
|
||||||
// else stays at adapter max (no point making the device weaker than it
|
|
||||||
// could be on facets we know browsers grant generously, e.g. workgroup
|
|
||||||
// sizes — those are texture / compute limits and we don't hit them).
|
|
||||||
web_floor_limits.maxStorageBufferBindingSize = 128ull * 1024 * 1024;
|
|
||||||
web_floor_limits.maxBufferSize = 256ull * 1024 * 1024;
|
|
||||||
|
|
||||||
WGPUDeviceDescriptor dev_desc = {};
|
|
||||||
dev_desc.requiredLimits = web_limits_ ? &web_floor_limits : &adapter_limits;
|
|
||||||
if (web_limits_) {
|
|
||||||
Log::info() << "wgpu --web-limits: requesting browser-floor limits"
|
|
||||||
<< "(maxStorageBufferBindingSize=128MB, maxBufferSize=256MB)";
|
|
||||||
}
|
|
||||||
// Surface uncaptured errors (validation failures etc.) into Log::warn so
|
|
||||||
// they're attributable rather than silently swallowed.
|
|
||||||
dev_desc.uncapturedErrorCallbackInfo.callback = onUncapturedError;
|
|
||||||
|
|
||||||
WGPURequestDeviceCallbackInfo dcb = {};
|
|
||||||
dcb.mode = WGPUCallbackMode_AllowProcessEvents;
|
|
||||||
dcb.callback = [](WGPURequestDeviceStatus status, WGPUDevice device,
|
|
||||||
WGPUStringView message, void* ud1, void* /*ud2*/) {
|
|
||||||
auto* r = static_cast<DeviceReq*>(ud1);
|
|
||||||
r->done = true;
|
|
||||||
if (status == WGPURequestDeviceStatus_Success) {
|
|
||||||
r->device = device;
|
|
||||||
r->ok = true;
|
|
||||||
} else {
|
|
||||||
Log::warn().noquote() << "RequestDevice failed:" << sv(message);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
dcb.userdata1 = &dreq;
|
|
||||||
|
|
||||||
wgpuAdapterRequestDevice(adapter_, &dev_desc, dcb);
|
|
||||||
while (!dreq.done) wgpuInstanceProcessEvents(instance_);
|
|
||||||
if (!dreq.ok) return false;
|
|
||||||
device_ = dreq.device;
|
|
||||||
queue_ = wgpuDeviceGetQueue(device_);
|
|
||||||
|
|
||||||
// ---- Probe streaming pool capacity ----------------------------------
|
|
||||||
// Ask the device for the largest single buffer it'll actually give us.
|
|
||||||
// Replaces the per-machine "guess the OOM ceiling" knob: now the
|
|
||||||
// runtime answers the question. Failure here is fatal — without any
|
|
||||||
// pool we can't load chunks.
|
|
||||||
if (!probeAndCreatePool()) {
|
|
||||||
Log::warn() << "wgpu: streaming pool probe failed; cannot start";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Background loader for streaming reads — must outlive any
|
|
||||||
// applyCachedModel call so we can drain results into the
|
|
||||||
// pool. Stopped in shutdown() before pool_.destroy().
|
|
||||||
streaming_thread_.start();
|
|
||||||
|
|
||||||
// ---- Pick a surface format -------------------------------------------
|
|
||||||
WGPUSurfaceCapabilities caps = {};
|
|
||||||
if (wgpuSurfaceGetCapabilities(surface_, adapter_, &caps) != WGPUStatus_Success
|
|
||||||
|| caps.formatCount == 0) {
|
|
||||||
Log::warn() << "wgpuSurfaceGetCapabilities returned no formats";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
surface_format_ = caps.formats[0]; // preferred format per wgpu docs
|
|
||||||
wgpuSurfaceCapabilitiesFreeMembers(caps);
|
|
||||||
|
|
||||||
|
// ---- Pipelines + overlays (still VW-side; HiZ/edge/pick + overlay
|
||||||
|
// init haven't migrated yet) -------------------------------------
|
||||||
if (!buildPipelines()) return false;
|
if (!buildPipelines()) return false;
|
||||||
if (!buildHizPipeline()) return false;
|
if (!buildHizPipeline()) return false;
|
||||||
if (!buildEdgePipeline()) return false;
|
if (!buildEdgePipeline()) return false;
|
||||||
@@ -1314,101 +1231,9 @@ bool ViewportWindow::initWgpu() {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (!buildPickPipeline()) return false;
|
if (!buildPickPipeline()) return false;
|
||||||
|
|
||||||
Log::info() << "wgpu init OK; surface format =" << int(surface_format_);
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ViewportWindow::probeAndCreatePool() {
|
|
||||||
// Discover the largest single buffer the runtime will grant. We
|
|
||||||
// descend from the device's advertised maxBufferSize because the
|
|
||||||
// adapter promises that much per binding, but the underlying
|
|
||||||
// allocator (gpu-alloc-rs on Vulkan, Metal heap manager, browser
|
|
||||||
// internals) may refuse anything above an undocumented per-system
|
|
||||||
// ceiling. The probe answers the question honestly.
|
|
||||||
//
|
|
||||||
// Each attempt is wrapped in an OOM error scope so a failed
|
|
||||||
// allocation doesn't surface to onUncapturedError as a noisy
|
|
||||||
// validation warning — the scope captures the OOM cleanly and we
|
|
||||||
// simply halve and retry.
|
|
||||||
|
|
||||||
WGPULimits device_limits = {};
|
|
||||||
wgpuDeviceGetLimits(device_, &device_limits);
|
|
||||||
|
|
||||||
// 64 MB lower bound: below this the viewer is unusable for any real
|
|
||||||
// dataset, so we'd rather fail init than limp along.
|
|
||||||
constexpr uint64_t MIN_POOL_CAPACITY = 64ull * 1024 * 1024;
|
|
||||||
// 4 GB starting cap: this is the largest single buffer the WebGPU
|
|
||||||
// ecosystem realistically supports today (browsers stay well below;
|
|
||||||
// desktop drivers vary). Asking for the device's full advertised
|
|
||||||
// maxBufferSize first is wasteful — on wgpu-native it can be 1 TB
|
|
||||||
// (a sentinel meaning "no spec floor"), which always fails and
|
|
||||||
// forces ~10 halving steps before we land somewhere sensible.
|
|
||||||
constexpr uint64_t MAX_PROBE_START = 4ull * 1024 * 1024 * 1024;
|
|
||||||
uint64_t try_size = std::min<uint64_t>(device_limits.maxBufferSize,
|
|
||||||
MAX_PROBE_START);
|
|
||||||
if (try_size < MIN_POOL_CAPACITY) try_size = MIN_POOL_CAPACITY;
|
|
||||||
|
|
||||||
const WGPUBufferUsage pool_usage = WGPUBufferUsage_Storage
|
|
||||||
| WGPUBufferUsage_CopyDst;
|
|
||||||
|
|
||||||
while (try_size >= MIN_POOL_CAPACITY) {
|
|
||||||
// wgpu-native classifies "Not enough memory left" as Validation,
|
|
||||||
// not OutOfMemory — so we need both filters. Nested scopes: OOM
|
|
||||||
// inner (matches first), Validation outer (catches the rest).
|
|
||||||
wgpuDevicePushErrorScope(device_, WGPUErrorFilter_Validation);
|
|
||||||
wgpuDevicePushErrorScope(device_, WGPUErrorFilter_OutOfMemory);
|
|
||||||
|
|
||||||
// Test allocation. If it survives both scopes, this size works
|
|
||||||
// and becomes the pool's per-sub-buffer capacity.
|
|
||||||
WGPUBufferDescriptor desc = {};
|
|
||||||
desc.usage = pool_usage;
|
|
||||||
desc.size = try_size;
|
|
||||||
desc.label.data = "ifcviewer-wgpu.pool_probe";
|
|
||||||
desc.label.length = std::strlen("ifcviewer-wgpu.pool_probe");
|
|
||||||
WGPUBuffer probe_buf = wgpuDeviceCreateBuffer(device_, &desc);
|
|
||||||
|
|
||||||
struct PopResult { bool done = false; bool error = false; };
|
|
||||||
auto pop = [&](PopResult& pr) {
|
|
||||||
WGPUPopErrorScopeCallbackInfo pcb = {};
|
|
||||||
pcb.mode = WGPUCallbackMode_AllowProcessEvents;
|
|
||||||
pcb.callback = [](WGPUPopErrorScopeStatus, WGPUErrorType type,
|
|
||||||
WGPUStringView, void* ud1, void* /*ud2*/) {
|
|
||||||
auto* p = static_cast<PopResult*>(ud1);
|
|
||||||
p->done = true;
|
|
||||||
p->error = (type != WGPUErrorType_NoError);
|
|
||||||
};
|
|
||||||
pcb.userdata1 = ≺
|
|
||||||
wgpuDevicePopErrorScope(device_, pcb);
|
|
||||||
while (!pr.done) wgpuInstanceProcessEvents(instance_);
|
|
||||||
};
|
|
||||||
PopResult oom_pop, validation_pop;
|
|
||||||
pop(oom_pop);
|
|
||||||
pop(validation_pop);
|
|
||||||
|
|
||||||
if (probe_buf) wgpuBufferRelease(probe_buf);
|
|
||||||
if (probe_buf && !oom_pop.error && !validation_pop.error) {
|
|
||||||
// Per-sub-buffer capacity locked in; the pool can grow
|
|
||||||
// beyond this by allocating more sub-buffers of the same
|
|
||||||
// size on demand (up to whatever the driver lets us total).
|
|
||||||
pool_.configure(instance_, device_, pool_usage, try_size,
|
|
||||||
"ifcviewer-wgpu.pool");
|
|
||||||
Log::info().noquote()
|
|
||||||
<< "wgpu: pool per-sub-buffer capacity ="
|
|
||||||
<< (try_size / (1024 * 1024)) << "MB"
|
|
||||||
<< "(device maxBufferSize ="
|
|
||||||
<< (device_limits.maxBufferSize / (1024 * 1024))
|
|
||||||
<< "MB); pool will grow on demand";
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
try_size /= 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
Log::warn() << "wgpu: pool probe found no allocatable size >="
|
|
||||||
<< (MIN_POOL_CAPACITY / (1024 * 1024)) << "MB";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
// Surface creation — platform-specific native handle plumbing.
|
// Surface creation — platform-specific native handle plumbing.
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
@@ -1442,86 +1267,8 @@ bool ViewportWindow::probeAndCreatePool() {
|
|||||||
# include "MetalSurface_mac.h"
|
# include "MetalSurface_mac.h"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
bool ViewportWindow::createSurface() {
|
// Private bool createSurface() removed in #84-l — its body is now
|
||||||
WGPUSurfaceDescriptor surface_desc = {};
|
// inside the public ViewportHost override createSurface(WGPUInstance).
|
||||||
|
|
||||||
#if defined(Q_OS_LINUX)
|
|
||||||
const QString platform = QGuiApplication::platformName();
|
|
||||||
if (platform == "xcb") {
|
|
||||||
# if __has_include(<X11/Xlib.h>)
|
|
||||||
auto* x11 = qApp->nativeInterface<QNativeInterface::QX11Application>();
|
|
||||||
if (!x11 || !x11->display()) {
|
|
||||||
Log::warn() << "Could not get X11 Display* from Qt";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
WGPUSurfaceSourceXlibWindow xlib = {};
|
|
||||||
xlib.chain.sType = WGPUSType_SurfaceSourceXlibWindow;
|
|
||||||
xlib.display = x11->display();
|
|
||||||
xlib.window = static_cast<uint64_t>(winId());
|
|
||||||
surface_desc.nextInChain = &xlib.chain;
|
|
||||||
surface_ = wgpuInstanceCreateSurface(instance_, &surface_desc);
|
|
||||||
# else
|
|
||||||
Log::warn() << "Built without Xlib headers; cannot create X11 surface";
|
|
||||||
return false;
|
|
||||||
# endif
|
|
||||||
} else if (platform == "wayland") {
|
|
||||||
# if __has_include(<wayland-client-core.h>)
|
|
||||||
auto* wl = qApp->nativeInterface<QNativeInterface::QWaylandApplication>();
|
|
||||||
if (!wl || !wl->display()) {
|
|
||||||
Log::warn() << "Could not get Wayland wl_display* from Qt";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
// The wl_surface for a window is exposed via the QPA window-handle
|
|
||||||
// accessor on the native interface (not the application-wide one).
|
|
||||||
// For stage 1 we fail loud; stage-1.5 fills this in.
|
|
||||||
Log::warn() << "Wayland wgpu surface creation not yet wired (stage 1.5)";
|
|
||||||
return false;
|
|
||||||
# else
|
|
||||||
Log::warn() << "Built without Wayland headers; cannot create Wayland surface";
|
|
||||||
return false;
|
|
||||||
# endif
|
|
||||||
} else {
|
|
||||||
Log::warn().noquote() << "Unsupported Qt platform for wgpu surface:" << platform;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
#elif defined(Q_OS_WIN)
|
|
||||||
// wgpu-native maps WGPUSurfaceSourceWindowsHWND.hwnd to a Win32 HWND
|
|
||||||
// it never dereferences directly (it only hands the handle to D3D12 /
|
|
||||||
// Vulkan WSI). winId() is the HWND for top-level Qt windows on the
|
|
||||||
// Windows platform plugin, returned as WId (== quintptr).
|
|
||||||
WGPUSurfaceSourceWindowsHWND hwndsrc = {};
|
|
||||||
hwndsrc.chain.sType = WGPUSType_SurfaceSourceWindowsHWND;
|
|
||||||
hwndsrc.hinstance = ::GetModuleHandleW(nullptr);
|
|
||||||
hwndsrc.hwnd = reinterpret_cast<void*>(static_cast<uintptr_t>(winId()));
|
|
||||||
surface_desc.nextInChain = &hwndsrc.chain;
|
|
||||||
surface_ = wgpuInstanceCreateSurface(instance_, &surface_desc);
|
|
||||||
#elif defined(Q_OS_MAC)
|
|
||||||
// QWindow::winId() returns the backing NSView* on macOS (as WId,
|
|
||||||
// which is quintptr — same width as void* on all macOS arches we
|
|
||||||
// care about). Hand it to the Cocoa bridge to attach a
|
|
||||||
// CAMetalLayer, then wrap that layer in WGPUSurfaceSourceMetalLayer.
|
|
||||||
void* nsview = reinterpret_cast<void*>(static_cast<uintptr_t>(winId()));
|
|
||||||
void* layer = wgpu_macos_attach_metal_layer(nsview);
|
|
||||||
if (!layer) {
|
|
||||||
Log::warn() << "Could not attach CAMetalLayer to the Qt NSView";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
WGPUSurfaceSourceMetalLayer metalsrc = {};
|
|
||||||
metalsrc.chain.sType = WGPUSType_SurfaceSourceMetalLayer;
|
|
||||||
metalsrc.layer = layer;
|
|
||||||
surface_desc.nextInChain = &metalsrc.chain;
|
|
||||||
surface_ = wgpuInstanceCreateSurface(instance_, &surface_desc);
|
|
||||||
#else
|
|
||||||
Log::warn() << "wgpu surface creation not yet wired for this platform";
|
|
||||||
return false;
|
|
||||||
#endif
|
|
||||||
|
|
||||||
if (!surface_) {
|
|
||||||
Log::warn() << "wgpuInstanceCreateSurface returned null";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
// Surface (re)configure + render
|
// Surface (re)configure + render
|
||||||
@@ -6919,15 +6666,8 @@ void ViewportWindow::wheelEvent(QWheelEvent* event) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void ViewportWindow::shutdown() {
|
void ViewportWindow::shutdown() {
|
||||||
// Stop the streaming worker first so no late results land in the
|
// VW-only resources first — these depend on core_'s device_ being
|
||||||
// pool after we've torn down the model state. Pending in-flight
|
// alive, so they must be released before core_.shutdown() releases it.
|
||||||
// reads are completed (worker drains its queue) then thread joins.
|
|
||||||
streaming_thread_.stop();
|
|
||||||
|
|
||||||
// Release per-model buffers before the device they were created from.
|
|
||||||
for (auto& [mid, m] : models_gpu_) releaseWgpuModelGpuData(m, pool_);
|
|
||||||
models_gpu_.clear();
|
|
||||||
|
|
||||||
releaseDepthTexture();
|
releaseDepthTexture();
|
||||||
releaseMsaaColorTexture();
|
releaseMsaaColorTexture();
|
||||||
releaseHizResources();
|
releaseHizResources();
|
||||||
@@ -6935,28 +6675,8 @@ void ViewportWindow::shutdown() {
|
|||||||
overlays_.destroy();
|
overlays_.destroy();
|
||||||
releasePickResources();
|
releasePickResources();
|
||||||
|
|
||||||
if (frame_bind_group_) { wgpuBindGroupRelease(frame_bind_group_); frame_bind_group_ = nullptr; }
|
// Core owns the rest: streaming thread, models, pool, frame/selection
|
||||||
if (frame_uniform_buffer_) { wgpuBufferRelease(frame_uniform_buffer_); frame_uniform_buffer_ = nullptr; }
|
// buffers, pipelines/shaders/layouts, queue/device/adapter/surface/
|
||||||
if (selection_flags_buffer_) { wgpuBufferRelease(selection_flags_buffer_); selection_flags_buffer_ = nullptr; }
|
// instance.
|
||||||
selection_flags_capacity_ = 0;
|
core_.shutdown();
|
||||||
if (main_pipeline_) { wgpuRenderPipelineRelease(main_pipeline_); main_pipeline_ = nullptr; }
|
|
||||||
if (main_pipeline_transparent_) { wgpuRenderPipelineRelease(main_pipeline_transparent_); main_pipeline_transparent_ = nullptr; }
|
|
||||||
if (main_shader_module_) { wgpuShaderModuleRelease(main_shader_module_); main_shader_module_ = nullptr; }
|
|
||||||
if (pipeline_layout_) { wgpuPipelineLayoutRelease(pipeline_layout_); pipeline_layout_ = nullptr; }
|
|
||||||
if (model_bgl_) { wgpuBindGroupLayoutRelease(model_bgl_); model_bgl_ = nullptr; }
|
|
||||||
if (frame_bgl_) { wgpuBindGroupLayoutRelease(frame_bgl_); frame_bgl_ = nullptr; }
|
|
||||||
|
|
||||||
// Destroy the streaming pool while device_ is still alive (it owns
|
|
||||||
// the underlying WGPUBuffer). All chunks have already returned their
|
|
||||||
// ranges via releaseWgpuModelGpuData above; pool's free-list count
|
|
||||||
// should equal capacity at this point.
|
|
||||||
pool_.destroy();
|
|
||||||
|
|
||||||
if (queue_) { wgpuQueueRelease(queue_); queue_ = nullptr; }
|
|
||||||
if (device_) { wgpuDeviceRelease(device_); device_ = nullptr; }
|
|
||||||
if (adapter_) { wgpuAdapterRelease(adapter_); adapter_ = nullptr; }
|
|
||||||
if (surface_) { wgpuSurfaceRelease(surface_); surface_ = nullptr; }
|
|
||||||
if (instance_) { wgpuInstanceRelease(instance_); instance_ = nullptr; }
|
|
||||||
wgpu_initialized_ = false;
|
|
||||||
surface_configured_ = false;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -297,11 +297,6 @@ private:
|
|||||||
// candidate needs the space. Triggers requestUpdate() if more remain.
|
// candidate needs the space. Triggers requestUpdate() if more remain.
|
||||||
void driveStreamingLoads();
|
void driveStreamingLoads();
|
||||||
|
|
||||||
// Discover the largest single buffer wgpu will give us by descending
|
|
||||||
// from the device's limits.maxBufferSize through OOM error scopes.
|
|
||||||
// Allocates pool_ at the discovered size. Returns false only when
|
|
||||||
// even a tiny pool can't be created (i.e. the device is unusable).
|
|
||||||
bool probeAndCreatePool();
|
|
||||||
void ensureDepthTexture(int w, int h);
|
void ensureDepthTexture(int w, int h);
|
||||||
void releaseDepthTexture();
|
void releaseDepthTexture();
|
||||||
void ensureMsaaColorTexture(int w, int h);
|
void ensureMsaaColorTexture(int w, int h);
|
||||||
|
|||||||
@@ -31,7 +31,7 @@
|
|||||||
#include <QJsonObject>
|
#include <QJsonObject>
|
||||||
#include <QSignalSpy>
|
#include <QSignalSpy>
|
||||||
#include <QTemporaryDir>
|
#include <QTemporaryDir>
|
||||||
#include <QVector3D>
|
#include <Eigen/Dense>
|
||||||
|
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
|
|
||||||
@@ -121,7 +121,7 @@ TEST_CASE("setHomeView / clearHomeView toggle dirty + has_home_view", "[federati
|
|||||||
QSignalSpy spy(&fed, &Federation::dirtyChanged);
|
QSignalSpy spy(&fed, &Federation::dirtyChanged);
|
||||||
|
|
||||||
Federation::HomeView hv;
|
Federation::HomeView hv;
|
||||||
hv.target = QVector3D(1, 2, 3);
|
hv.target = Eigen::Vector3f(1, 2, 3);
|
||||||
hv.distance = 12.5f;
|
hv.distance = 12.5f;
|
||||||
hv.yaw = 33.0f;
|
hv.yaw = 33.0f;
|
||||||
hv.pitch = 22.0f;
|
hv.pitch = 22.0f;
|
||||||
@@ -207,7 +207,7 @@ TEST_CASE("save then load round-trips models, transform, visibility, home view",
|
|||||||
src.setModelVisible(id2, false);
|
src.setModelVisible(id2, false);
|
||||||
|
|
||||||
Federation::HomeView hv;
|
Federation::HomeView hv;
|
||||||
hv.target = QVector3D(10, 20, 30);
|
hv.target = Eigen::Vector3f(10, 20, 30);
|
||||||
hv.distance = 77.0f;
|
hv.distance = 77.0f;
|
||||||
hv.yaw = 11.0f;
|
hv.yaw = 11.0f;
|
||||||
hv.pitch = 7.0f;
|
hv.pitch = 7.0f;
|
||||||
@@ -236,7 +236,7 @@ TEST_CASE("save then load round-trips models, transform, visibility, home view",
|
|||||||
REQUIRE_FALSE(dst.models()[1].visible);
|
REQUIRE_FALSE(dst.models()[1].visible);
|
||||||
|
|
||||||
REQUIRE(dst.hasHomeView());
|
REQUIRE(dst.hasHomeView());
|
||||||
REQUIRE(dst.homeView().target == QVector3D(10, 20, 30));
|
REQUIRE(dst.homeView().target == Eigen::Vector3f(10, 20, 30));
|
||||||
REQUIRE(dst.homeView().distance == 77.0f);
|
REQUIRE(dst.homeView().distance == 77.0f);
|
||||||
REQUIRE(dst.homeView().yaw == 11.0f);
|
REQUIRE(dst.homeView().yaw == 11.0f);
|
||||||
REQUIRE(dst.homeView().pitch == 7.0f);
|
REQUIRE(dst.homeView().pitch == 7.0f);
|
||||||
|
|||||||
Reference in New Issue
Block a user