From 3ac7a79b7a7caf44db33edffc2931850b73ac0b9 Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Sun, 31 May 2026 08:40:54 +1000 Subject: [PATCH] wgpu: overlay labels + HUD text (QPainter rasterise, content-cached) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ports GL OverlayRenderer's setOverlayLabels + setHudText to wgpu. Each unique string is rasterised via QPainter into a QImage (dark-grey rounded background + white antialiased text) and uploaded as an RGBA8 texture; the cache is keyed by content + font size so identical strings across frames are texture-free. Per-frame work is projection, vertex assembly, and one draw per visible label. Drawn last in the frame on the resolved surface so labels sit on top of every other overlay (no depth-test). HUD uses pt 11 at top-left matching GL; world-anchored labels use pt 9 centred at the projected screen position. WebGPU has no QOpenGLPaintDevice equivalent — the GL backend's two- stage GL-rect + QPainter pass becomes one textured quad per item here. Co-Authored-By: Claude Opus 4.7 --- src/ifcviewer-wgpu/WgpuOverlayRenderer.cpp | 399 +++++++++++++++++++++ src/ifcviewer-wgpu/WgpuOverlayRenderer.h | 59 +++ src/ifcviewer-wgpu/WgpuViewportWindow.cpp | 15 + src/ifcviewer-wgpu/WgpuViewportWindow.h | 2 + 4 files changed, 475 insertions(+) diff --git a/src/ifcviewer-wgpu/WgpuOverlayRenderer.cpp b/src/ifcviewer-wgpu/WgpuOverlayRenderer.cpp index c9ad7e2e00..8db8557cca 100644 --- a/src/ifcviewer-wgpu/WgpuOverlayRenderer.cpp +++ b/src/ifcviewer-wgpu/WgpuOverlayRenderer.cpp @@ -19,6 +19,11 @@ #include "WgpuOverlayRenderer.h" +#include +#include +#include +#include +#include #include #include @@ -375,6 +380,34 @@ fn fs_main(in: VsOut) -> @location(0) vec4 { } )WGSL"; +// Label shader: textured quads in screen space. Each visible label/HUD +// item contributes 6 vertices (NDC position + uv); a pre-rasterised +// QImage carrying both the dark-grey background fill and the white +// text occupies the bound texture. Standard alpha blend. +static const char* LABELS_WGSL = R"WGSL( +@group(0) @binding(0) var samp: sampler; +@group(0) @binding(1) var tex: texture_2d; + +struct VsOut { + @builtin(position) clip_pos: vec4, + @location(0) uv: vec2, +}; + +@vertex +fn vs_main(@location(0) ndc: vec2, + @location(1) uv: vec2) -> VsOut { + var out: VsOut; + out.clip_pos = vec4(ndc, 0.0, 1.0); + out.uv = uv; + return out; +} + +@fragment +fn fs_main(in: VsOut) -> @location(0) vec4 { + return textureSample(tex, samp, in.uv); +} +)WGSL"; + // ----------------------------------------------------------------------------- // Construction / destruction // ----------------------------------------------------------------------------- @@ -396,6 +429,7 @@ bool WgpuOverlayRenderer::init(WGPUInstance instance, WGPUDevice device, if (!buildMarquee()) return false; if (!buildOverlayLines()) return false; if (!buildOverlayPoints()) return false; + if (!buildLabels()) return false; return true; } @@ -453,6 +487,18 @@ void WgpuOverlayRenderer::destroy() { if (overlay_point_vertex_buffer_) { wgpuBufferRelease(overlay_point_vertex_buffer_); overlay_point_vertex_buffer_ = nullptr; } overlay_point_vertex_capacity_ = 0; overlay_point_vertex_count_ = 0; + + // Labels + HUD + releaseLabelTextures(); + if (label_sampler_) { wgpuSamplerRelease(label_sampler_); label_sampler_ = nullptr; } + if (label_pipeline_) { wgpuRenderPipelineRelease(label_pipeline_); label_pipeline_ = nullptr; } + if (label_shader_module_) { wgpuShaderModuleRelease(label_shader_module_); label_shader_module_ = nullptr; } + if (label_pipeline_layout_) { wgpuPipelineLayoutRelease(label_pipeline_layout_); label_pipeline_layout_ = nullptr; } + if (label_bgl_) { wgpuBindGroupLayoutRelease(label_bgl_); label_bgl_ = nullptr; } + if (label_vertex_buffer_) { wgpuBufferRelease(label_vertex_buffer_); label_vertex_buffer_ = nullptr; } + label_vertex_capacity_ = 0; + labels_.clear(); + hud_text_.clear(); } // ----------------------------------------------------------------------------- @@ -1647,3 +1693,356 @@ void WgpuOverlayRenderer::encodeOverlayPoints(WGPURenderPassEncoder pass, 0, WGPU_WHOLE_SIZE); wgpuRenderPassEncoderDraw(pass, overlay_point_vertex_count_, 1, 0, 0); } + +// ----------------------------------------------------------------------------- +// Labels + HUD text (textured quads, content-cached) +// ----------------------------------------------------------------------------- + +bool WgpuOverlayRenderer::buildLabels() { + { + WGPUSamplerDescriptor sd = {}; + sd.minFilter = WGPUFilterMode_Linear; + sd.magFilter = WGPUFilterMode_Linear; + sd.mipmapFilter = WGPUMipmapFilterMode_Nearest; + sd.addressModeU = WGPUAddressMode_ClampToEdge; + sd.addressModeV = WGPUAddressMode_ClampToEdge; + sd.addressModeW = WGPUAddressMode_ClampToEdge; + sd.lodMinClamp = 0.0f; + sd.lodMaxClamp = 0.0f; + sd.maxAnisotropy = 1; + sd.label = svFromCStr("ifcviewer-wgpu.label_sampler"); + label_sampler_ = wgpuDeviceCreateSampler(device_, &sd); + } + { + WGPUBindGroupLayoutEntry entries[2] = {}; + entries[0].binding = 0; + entries[0].visibility = WGPUShaderStage_Fragment; + entries[0].sampler.type = WGPUSamplerBindingType_Filtering; + entries[1].binding = 1; + entries[1].visibility = WGPUShaderStage_Fragment; + entries[1].texture.sampleType = WGPUTextureSampleType_Float; + entries[1].texture.viewDimension = WGPUTextureViewDimension_2D; + WGPUBindGroupLayoutDescriptor bgl_desc = {}; + bgl_desc.entryCount = 2; + bgl_desc.entries = entries; + bgl_desc.label = svFromCStr("ifcviewer-wgpu.label_bgl"); + label_bgl_ = wgpuDeviceCreateBindGroupLayout(device_, &bgl_desc); + } + { + WGPUPipelineLayoutDescriptor pl_desc = {}; + pl_desc.bindGroupLayoutCount = 1; + pl_desc.bindGroupLayouts = &label_bgl_; + pl_desc.label = svFromCStr("ifcviewer-wgpu.label_pipeline_layout"); + label_pipeline_layout_ = wgpuDeviceCreatePipelineLayout(device_, &pl_desc); + } + { + WGPUShaderSourceWGSL wgsl_src = {}; + wgsl_src.chain.sType = WGPUSType_ShaderSourceWGSL; + wgsl_src.code = svFromCStr(LABELS_WGSL); + WGPUShaderModuleDescriptor sm_desc = {}; + sm_desc.nextInChain = &wgsl_src.chain; + sm_desc.label = svFromCStr("ifcviewer-wgpu.label_wgsl"); + label_shader_module_ = wgpuDeviceCreateShaderModule(device_, &sm_desc); + } + { + WGPUBufferDescriptor bdesc = {}; + bdesc.usage = WGPUBufferUsage_Vertex | WGPUBufferUsage_CopyDst; + bdesc.size = 256; + bdesc.label = svFromCStr("ifcviewer-wgpu.label_vbo"); + label_vertex_buffer_ = wgpuDeviceCreateBuffer(device_, &bdesc); + label_vertex_capacity_ = 256; + } + + // Per-vertex: vec2 NDC + vec2 uv = 16 B stride. + WGPUVertexAttribute attribs[2] = {}; + attribs[0].format = WGPUVertexFormat_Float32x2; attribs[0].offset = 0; attribs[0].shaderLocation = 0; + attribs[1].format = WGPUVertexFormat_Float32x2; attribs[1].offset = 8; attribs[1].shaderLocation = 1; + WGPUVertexBufferLayout vbl = {}; + vbl.arrayStride = 16; + vbl.stepMode = WGPUVertexStepMode_Vertex; + vbl.attributeCount = 2; + vbl.attributes = attribs; + + WGPUBlendState blend = {}; + blend.color.srcFactor = WGPUBlendFactor_SrcAlpha; + blend.color.dstFactor = WGPUBlendFactor_OneMinusSrcAlpha; + blend.color.operation = WGPUBlendOperation_Add; + blend.alpha.srcFactor = WGPUBlendFactor_One; + blend.alpha.dstFactor = WGPUBlendFactor_OneMinusSrcAlpha; + blend.alpha.operation = WGPUBlendOperation_Add; + + WGPUColorTargetState ct = {}; + ct.format = surface_format_; + ct.blend = &blend; + ct.writeMask = WGPUColorWriteMask_All; + + WGPUFragmentState frag = {}; + frag.module = label_shader_module_; + frag.entryPoint = svFromCStr("fs_main"); + frag.targetCount = 1; + frag.targets = &ct; + + WGPURenderPipelineDescriptor rp_desc = {}; + rp_desc.layout = label_pipeline_layout_; + rp_desc.label = svFromCStr("ifcviewer-wgpu.label_pipeline"); + rp_desc.vertex.module = label_shader_module_; + rp_desc.vertex.entryPoint = svFromCStr("vs_main"); + rp_desc.vertex.bufferCount = 1; + rp_desc.vertex.buffers = &vbl; + rp_desc.fragment = &frag; + rp_desc.primitive.topology = WGPUPrimitiveTopology_TriangleList; + rp_desc.primitive.cullMode = WGPUCullMode_None; + rp_desc.multisample.count = 1; + rp_desc.multisample.mask = 0xFFFFFFFFu; + label_pipeline_ = wgpuDeviceCreateRenderPipeline(device_, &rp_desc); + return label_pipeline_ != nullptr; +} + +void WgpuOverlayRenderer::releaseLabelTextures() { + for (auto it = label_tex_cache_.begin(); it != label_tex_cache_.end(); ++it) { + if (it.value().bind_group) wgpuBindGroupRelease(it.value().bind_group); + if (it.value().view) wgpuTextureViewRelease(it.value().view); + if (it.value().texture) wgpuTextureRelease(it.value().texture); + } + label_tex_cache_.clear(); +} + +void WgpuOverlayRenderer::setOverlayLabels(const std::vector