Add Quake 3 menu and weapon workflow steps

This commit is contained in:
2026-05-02 21:26:20 +01:00
parent 48b6f6ce76
commit 4905d85667
22 changed files with 990 additions and 22 deletions
+4
View File
@@ -283,6 +283,9 @@ if(BUILD_SDL3_APP)
src/services/impl/workflow/geometry/workflow_geometry_create_cube_step.cpp
src/services/impl/workflow/geometry/workflow_geometry_create_plane_step.cpp
src/services/impl/workflow/geometry/workflow_geometry_cube_generate_step.cpp
src/services/impl/workflow/quake3/workflow_q3_menu_update_step.cpp
src/services/impl/workflow/quake3/workflow_q3_overlay_draw_step.cpp
src/services/impl/workflow/quake3/workflow_q3_weapon_update_step.cpp
src/services/impl/workflow/rendering/workflow_bsp_build_collision_step.cpp
src/services/impl/workflow/rendering/workflow_bsp_build_geometry_step.cpp
src/services/impl/workflow/rendering/workflow_bsp_entity_update_step.cpp
@@ -290,6 +293,7 @@ if(BUILD_SDL3_APP)
src/services/impl/workflow/rendering/workflow_bsp_lightmap_atlas_step.cpp
src/services/impl/workflow/rendering/workflow_bsp_load_step.cpp
src/services/impl/workflow/rendering/workflow_bsp_parse_spawn_step.cpp
src/services/impl/workflow/rendering/workflow_bsp_portal_view_step.cpp
src/services/impl/workflow/rendering/workflow_bsp_upload_geometry_step.cpp
src/services/impl/workflow/rendering/workflow_draw_map_step.cpp
src/services/impl/workflow/rendering/workflow_draw_textured_box_step.cpp
@@ -28,13 +28,13 @@ fragment float4 main0(
sampler shadowSampler [[sampler(1)]],
texture2d<float> lightmapTex [[texture(2)]],
sampler lightmapSampler [[sampler(2)]],
texture2d<float> portalTex [[texture(3)]],
sampler portalSampler [[sampler(3)]],
constant PBRUniforms& pbr [[buffer(0)]])
{
(void)shadowMap;
(void)shadowSampler;
(void)in.worldNormal;
(void)in.worldPos;
(void)in.cameraPos;
float3 albedo = albedoTex.sample(albedoSampler, in.uv).rgb;
float3 lightmap = lightmapTex.sample(lightmapSampler, in.lightmapUv).rgb;
@@ -42,5 +42,32 @@ fragment float4 main0(
float3 ambient = pbr.u_ambient.rgb * albedo;
float exposure = (pbr.u_lightColor.a > 0.0) ? pbr.u_lightColor.a : 1.0;
if (pbr.u_material.w > 0.5) {
float time = pbr.u_material.y;
float3 viewDir = normalize(in.cameraPos - in.worldPos);
float3 n = normalize(in.worldNormal);
float3 reflected = reflect(-viewDir, n);
float fresnel = pow(1.0 - saturate(dot(viewDir, n)), 3.0);
float2 reflectUv = reflected.xz * 0.32 + float2(0.5, 0.5);
reflectUv += float2(sin(time * 1.7 + in.worldPos.y * 0.35),
cos(time * 1.3 + in.worldPos.x * 0.28)) * 0.035;
float3 portalBase = albedoTex.sample(albedoSampler, reflectUv).rgb;
float2 portalUv = fract(in.uv);
portalUv.y = 1.0 - portalUv.y;
float2 centered = portalUv - float2(0.5, 0.5);
float radial = length(centered);
float centerMask = 1.0 - smoothstep(0.34, 0.49, radial);
float3 destination = portalTex.sample(portalSampler, portalUv).rgb;
float pulse = 0.5 + 0.5 * sin(time * 3.0 + length(in.worldPos.xz) * 0.45);
float3 glow = float3(0.18, 0.42, 0.95) * (0.35 + 0.35 * pulse);
float3 reflectedColor = mix(portalBase, glow, 0.18 + 0.32 * fresnel);
float3 color = mix(reflectedColor, destination, centerMask * 0.88);
color += glow * (1.0 - centerMask) * 0.45;
float alpha = mix(0.18 + 0.24 * fresnel, 0.92, centerMask);
return float4((color * 1.08 + ambient * 0.08) * exposure, alpha);
}
return float4((albedo * lightmap * overbright + ambient) * exposure, 1.0);
}
@@ -0,0 +1,13 @@
#include <metal_stdlib>
using namespace metal;
struct FragmentInput {
float4 position [[position]];
float2 uv;
};
fragment float4 main0(FragmentInput in [[stage_in]],
texture2d<float> overlayTex [[texture(0)]],
sampler overlaySampler [[sampler(0)]]) {
return overlayTex.sample(overlaySampler, in.uv);
}
@@ -0,0 +1,19 @@
#include <metal_stdlib>
using namespace metal;
struct VertexInput {
float3 position [[attribute(0)]];
float2 uv [[attribute(1)]];
};
struct VertexOutput {
float4 position [[position]];
float2 uv;
};
vertex VertexOutput main0(VertexInput in [[stage_in]]) {
VertexOutput out;
out.position = float4(in.position, 1.0);
out.uv = in.uv;
return out;
}
@@ -8,6 +8,12 @@
"typeVersion": 1,
"position": [0, 0]
},
{
"id": "q3_menu",
"type": "q3.menu.update",
"typeVersion": 1,
"position": [100, 0]
},
{
"id": "physics_move",
"type": "physics.fps.move",
@@ -58,12 +64,24 @@
"far": 500.0
}
},
{
"id": "q3_weapon",
"type": "q3.weapon.update",
"typeVersion": 1,
"position": [690, 0]
},
{
"id": "render_prepare",
"type": "render.prepare",
"typeVersion": 1,
"position": [750, 0]
},
{
"id": "portal_view",
"type": "bsp.portal_view",
"typeVersion": 1,
"position": [775, 0]
},
{
"id": "frame_begin",
"type": "frame.gpu.begin",
@@ -99,6 +117,12 @@
"typeVersion": 1,
"position": [1225, 0]
},
{
"id": "q3_overlay",
"type": "q3.overlay.draw",
"typeVersion": 1,
"position": [1235, 0]
},
{
"id": "postfx_taa",
"type": "postfx.taa",
@@ -132,17 +156,21 @@
}
],
"connections": {
"input_poll": { "main": { "0": [{ "node": "physics_move", "type": "main", "index": 0 }] } },
"input_poll": { "main": { "0": [{ "node": "q3_menu", "type": "main", "index": 0 }] } },
"q3_menu": { "main": { "0": [{ "node": "physics_move", "type": "main", "index": 0 }] } },
"physics_move": { "main": { "0": [{ "node": "physics_step", "type": "main", "index": 0 }] } },
"physics_step": { "main": { "0": [{ "node": "sync_transforms", "type": "main", "index": 0 }] } },
"sync_transforms": { "main": { "0": [{ "node": "bsp_entities_update", "type": "main", "index": 0 }] } },
"bsp_entities_update": { "main": { "0": [{ "node": "camera_update", "type": "main", "index": 0 }] } },
"camera_update": { "main": { "0": [{ "node": "render_prepare", "type": "main", "index": 0 }] } },
"render_prepare": { "main": { "0": [{ "node": "frame_begin", "type": "main", "index": 0 }] } },
"camera_update": { "main": { "0": [{ "node": "q3_weapon", "type": "main", "index": 0 }] } },
"q3_weapon": { "main": { "0": [{ "node": "render_prepare", "type": "main", "index": 0 }] } },
"render_prepare": { "main": { "0": [{ "node": "portal_view", "type": "main", "index": 0 }] } },
"portal_view": { "main": { "0": [{ "node": "frame_begin", "type": "main", "index": 0 }] } },
"frame_begin": { "main": { "0": [{ "node": "draw_map", "type": "main", "index": 0 }] } },
"draw_map": { "main": { "0": [{ "node": "end_scene", "type": "main", "index": 0 }] } },
"end_scene": { "main": { "0": [{ "node": "overlay_fps", "type": "main", "index": 0 }] } },
"overlay_fps": { "main": { "0": [{ "node": "postfx_taa", "type": "main", "index": 0 }] } },
"overlay_fps": { "main": { "0": [{ "node": "q3_overlay", "type": "main", "index": 0 }] } },
"q3_overlay": { "main": { "0": [{ "node": "postfx_taa", "type": "main", "index": 0 }] } },
"postfx_taa": { "main": { "0": [{ "node": "postfx_ssao", "type": "main", "index": 0 }] } },
"postfx_ssao": { "main": { "0": [{ "node": "bloom_extract", "type": "main", "index": 0 }] } },
"bloom_extract": { "main": { "0": [{ "node": "bloom_blur", "type": "main", "index": 0 }] } },
@@ -37,15 +37,15 @@
"parameters": { "stage": "vertex", "output_key": "bsp_vertex_shader", "num_uniform_buffers": 1, "num_samplers": 0 },
"inputs": { "shader_path": "shader_bsp_vert_path" } },
{ "id": "compile_bsp_frag", "type": "graphics.gpu.shader.compile", "typeVersion": 1, "position": [1400, 0],
"parameters": { "stage": "fragment", "output_key": "bsp_fragment_shader", "num_uniform_buffers": 1, "num_samplers": 3 },
"parameters": { "stage": "fragment", "output_key": "bsp_fragment_shader", "num_uniform_buffers": 1, "num_samplers": 4 },
"inputs": { "shader_path": "shader_bsp_frag_path" } },
{ "id": "create_bsp_pipeline", "type": "graphics.gpu.pipeline.create", "typeVersion": 1, "position": [1500, 0],
"parameters": { "vertex_shader_key": "bsp_vertex_shader", "fragment_shader_key": "bsp_fragment_shader", "vertex_format": "position_uv_lmuv_normal", "pipeline_key": "gpu_pipeline_bsp" } },
"parameters": { "vertex_shader_key": "bsp_vertex_shader", "fragment_shader_key": "bsp_fragment_shader", "vertex_format": "position_uv_lmuv_normal", "pipeline_key": "gpu_pipeline_bsp", "alpha_blend": 1 } },
{ "id": "tex_walls", "name": "Load Texture", "type": "texture.load", "typeVersion": 1, "position": [1600, 0],
"parameters": { "inputs": { "image_path": "tex_walls_path" }, "outputs": { "texture": "walls_texture" } } },
{ "id": "physics_world", "type": "physics.world.create", "typeVersion": 1, "position": [0, 200] },
{ "id": "load_bsp", "name": "Load Q3 BSP", "type": "bsp.load", "typeVersion": 1, "position": [200, 200],
"parameters": { "pk3_path": "${env:QUAKE3_PAK0}", "map_name": "q3dm7", "scale": 0.03125 } },
"parameters": { "pk3_path": "${env:QUAKE3_PAK0}", "map_name": "${env:QUAKE3_MAP}", "scale": 0.03125 } },
{ "id": "bsp_lightmap", "name": "BSP Lightmap Atlas", "type": "bsp.lightmap_atlas", "typeVersion": 1, "position": [400, 200] },
{ "id": "bsp_geometry", "name": "BSP Build Geometry", "type": "bsp.build_geometry", "typeVersion": 1, "position": [600, 200],
"parameters": { "patch_tess_level": 4 } },
@@ -42,6 +42,7 @@ void WorkflowGpuPipelineCreateStep::Execute(const WorkflowStepDefinition& step,
const bool release_shaders = static_cast<int>(getNum("release_shaders", 1)) != 0;
const std::string color_format_str = getStr("color_format", "swapchain");
const bool has_depth = static_cast<int>(getNum("has_depth", 1)) != 0;
const bool alpha_blend = static_cast<int>(getNum("alpha_blend", 0)) != 0;
// Get GPU device
SDL_GPUDevice* device = context.Get<SDL_GPUDevice*>("gpu_device", nullptr);
@@ -155,6 +156,16 @@ void WorkflowGpuPipelineCreateStep::Execute(const WorkflowStepDefinition& step,
color_target.format = SDL_GPU_TEXTUREFORMAT_B8G8R8A8_UNORM;
}
}
if (alpha_blend) {
color_target.blend_state.enable_blend = true;
color_target.blend_state.src_color_blendfactor = SDL_GPU_BLENDFACTOR_SRC_ALPHA;
color_target.blend_state.dst_color_blendfactor = SDL_GPU_BLENDFACTOR_ONE_MINUS_SRC_ALPHA;
color_target.blend_state.color_blend_op = SDL_GPU_BLENDOP_ADD;
color_target.blend_state.src_alpha_blendfactor = SDL_GPU_BLENDFACTOR_ONE;
color_target.blend_state.dst_alpha_blendfactor = SDL_GPU_BLENDFACTOR_ZERO;
color_target.blend_state.alpha_blend_op = SDL_GPU_BLENDOP_ADD;
}
}
// Build pipeline create info
@@ -0,0 +1,48 @@
#include "services/interfaces/workflow/quake3/workflow_q3_menu_update_step.hpp"
#include <nlohmann/json.hpp>
#include <algorithm>
#include <string>
namespace sdl3cpp::services::impl {
WorkflowQ3MenuUpdateStep::WorkflowQ3MenuUpdateStep(std::shared_ptr<ILogger> logger)
: logger_(std::move(logger)) {}
std::string WorkflowQ3MenuUpdateStep::GetPluginId() const {
return "q3.menu.update";
}
void WorkflowQ3MenuUpdateStep::Execute(const WorkflowStepDefinition& step, WorkflowContext& context) {
bool open = context.GetBool("q3.menu_open", true);
if (context.GetBool("input_key_escape_pressed", false)) {
open = !open;
}
context.Set<bool>("q3.menu_open", open);
auto maps = context.Get<nlohmann::json>("q3.maps", nlohmann::json::array());
if (!maps.is_array() || maps.empty()) {
maps = nlohmann::json::array({"q3dm7"});
}
int selected = context.Get<int>("q3.menu_selected_map", 0);
selected = std::clamp(selected, 0, static_cast<int>(maps.size()) - 1);
if (open) {
if (context.GetBool("input_key_up_pressed", false)) {
selected = (selected + static_cast<int>(maps.size()) - 1) % static_cast<int>(maps.size());
}
if (context.GetBool("input_key_down_pressed", false)) {
selected = (selected + 1) % static_cast<int>(maps.size());
}
if (context.GetBool("input_key_enter_pressed", false)) {
const std::string map = maps[selected].get<std::string>();
context.Set<std::string>("q3.pending_map", map);
if (logger_) logger_->Info("q3.menu.update: selected map " + map + " (restart with QUAKE3_MAP=" + map + ")");
}
}
context.Set<int>("q3.menu_selected_map", selected);
}
} // namespace sdl3cpp::services::impl
@@ -0,0 +1,296 @@
#include "services/interfaces/workflow/quake3/workflow_q3_overlay_draw_step.hpp"
#include <SDL3/SDL.h>
#include <nlohmann/json.hpp>
#include <cstring>
#include <fstream>
#include <string>
#include <vector>
namespace sdl3cpp::services::impl {
namespace {
std::vector<uint8_t> LoadBinary(const char* path) {
std::ifstream f(path, std::ios::binary | std::ios::ate);
if (!f.is_open()) return {};
auto size = f.tellg();
std::vector<uint8_t> data(static_cast<size_t>(size));
f.seekg(0);
f.read(reinterpret_cast<char*>(data.data()), size);
return data;
}
void Text(SDL_Renderer* r, float x, float y, const char* text, SDL_Color color) {
SDL_SetRenderDrawColor(r, color.r, color.g, color.b, color.a);
SDL_RenderDebugText(r, x, y, text);
}
} // namespace
WorkflowQ3OverlayDrawStep::WorkflowQ3OverlayDrawStep(std::shared_ptr<ILogger> logger)
: logger_(std::move(logger)) {}
WorkflowQ3OverlayDrawStep::~WorkflowQ3OverlayDrawStep() {
if (renderer_) SDL_DestroyRenderer(renderer_);
if (surface_) SDL_DestroySurface(surface_);
if (device_) {
if (sampler_) SDL_ReleaseGPUSampler(device_, sampler_);
if (vtx_buf_) SDL_ReleaseGPUBuffer(device_, vtx_buf_);
if (transfer_) SDL_ReleaseGPUTransferBuffer(device_, transfer_);
if (tex_) SDL_ReleaseGPUTexture(device_, tex_);
if (pipeline_) SDL_ReleaseGPUGraphicsPipeline(device_, pipeline_);
}
}
std::string WorkflowQ3OverlayDrawStep::GetPluginId() const {
return "q3.overlay.draw";
}
void WorkflowQ3OverlayDrawStep::TryInit(SDL_GPUDevice* device, SDL_Window* window) {
if (disabled_ || ready_) return;
device_ = device;
const char* driver = SDL_GetGPUDeviceDriver(device);
const std::string driverName = driver ? driver : "";
SDL_GPUShaderFormat shaderFormat = SDL_GPU_SHADERFORMAT_INVALID;
std::vector<uint8_t> vert;
std::vector<uint8_t> frag;
const char* entry = "main";
if (driverName == "metal") {
shaderFormat = SDL_GPU_SHADERFORMAT_MSL;
vert = LoadBinary("packages/quake3/shaders/msl/overlay.vert.metal");
frag = LoadBinary("packages/quake3/shaders/msl/overlay.frag.metal");
entry = "main0";
} else if (driverName == "vulkan") {
shaderFormat = SDL_GPU_SHADERFORMAT_SPIRV;
vert = LoadBinary("packages/quake3/shaders/spirv/overlay.vert.spv");
frag = LoadBinary("packages/quake3/shaders/spirv/overlay.frag.spv");
} else {
disabled_ = true;
return;
}
if (vert.empty() || frag.empty()) {
disabled_ = true;
return;
}
SDL_GPUShaderCreateInfo vsi = {};
vsi.code = vert.data();
vsi.code_size = vert.size();
vsi.entrypoint = entry;
vsi.format = shaderFormat;
vsi.stage = SDL_GPU_SHADERSTAGE_VERTEX;
SDL_GPUShaderCreateInfo fsi = {};
fsi.code = frag.data();
fsi.code_size = frag.size();
fsi.entrypoint = entry;
fsi.format = shaderFormat;
fsi.stage = SDL_GPU_SHADERSTAGE_FRAGMENT;
fsi.num_samplers = 1;
auto* vs = SDL_CreateGPUShader(device, &vsi);
auto* fs = SDL_CreateGPUShader(device, &fsi);
if (!vs || !fs) {
if (vs) SDL_ReleaseGPUShader(device, vs);
if (fs) SDL_ReleaseGPUShader(device, fs);
disabled_ = true;
return;
}
SDL_GPUVertexBufferDescription vbd = {};
vbd.slot = 0;
vbd.pitch = sizeof(float) * 5;
vbd.input_rate = SDL_GPU_VERTEXINPUTRATE_VERTEX;
SDL_GPUVertexAttribute attrs[2] = {};
attrs[0] = {0, 0, SDL_GPU_VERTEXELEMENTFORMAT_FLOAT3, 0};
attrs[1] = {1, 0, SDL_GPU_VERTEXELEMENTFORMAT_FLOAT2, sizeof(float) * 3};
SDL_GPUVertexInputState vis = {};
vis.vertex_buffer_descriptions = &vbd;
vis.num_vertex_buffers = 1;
vis.vertex_attributes = attrs;
vis.num_vertex_attributes = 2;
SDL_GPUColorTargetDescription ctd = {};
ctd.format = window ? SDL_GetGPUSwapchainTextureFormat(device, window) : SDL_GPU_TEXTUREFORMAT_B8G8R8A8_UNORM;
ctd.blend_state.enable_blend = true;
ctd.blend_state.src_color_blendfactor = SDL_GPU_BLENDFACTOR_SRC_ALPHA;
ctd.blend_state.dst_color_blendfactor = SDL_GPU_BLENDFACTOR_ONE_MINUS_SRC_ALPHA;
ctd.blend_state.color_blend_op = SDL_GPU_BLENDOP_ADD;
ctd.blend_state.src_alpha_blendfactor = SDL_GPU_BLENDFACTOR_ONE;
ctd.blend_state.dst_alpha_blendfactor = SDL_GPU_BLENDFACTOR_ZERO;
ctd.blend_state.alpha_blend_op = SDL_GPU_BLENDOP_ADD;
SDL_GPUGraphicsPipelineCreateInfo pci = {};
pci.vertex_shader = vs;
pci.fragment_shader = fs;
pci.vertex_input_state = vis;
pci.primitive_type = SDL_GPU_PRIMITIVETYPE_TRIANGLELIST;
pci.rasterizer_state.fill_mode = SDL_GPU_FILLMODE_FILL;
pci.rasterizer_state.cull_mode = SDL_GPU_CULLMODE_NONE;
pci.rasterizer_state.front_face = SDL_GPU_FRONTFACE_COUNTER_CLOCKWISE;
pci.depth_stencil_state.enable_depth_test = false;
pci.depth_stencil_state.enable_depth_write = false;
pci.target_info.num_color_targets = 1;
pci.target_info.color_target_descriptions = &ctd;
pipeline_ = SDL_CreateGPUGraphicsPipeline(device, &pci);
SDL_ReleaseGPUShader(device, vs);
SDL_ReleaseGPUShader(device, fs);
if (!pipeline_) {
disabled_ = true;
return;
}
SDL_GPUTextureCreateInfo tci = {};
tci.type = SDL_GPU_TEXTURETYPE_2D;
tci.format = SDL_GPU_TEXTUREFORMAT_R8G8B8A8_UNORM;
tci.width = kW;
tci.height = kH;
tci.layer_count_or_depth = 1;
tci.num_levels = 1;
tci.usage = SDL_GPU_TEXTUREUSAGE_SAMPLER;
tex_ = SDL_CreateGPUTexture(device, &tci);
SDL_GPUTransferBufferCreateInfo tbci = {};
tbci.usage = SDL_GPU_TRANSFERBUFFERUSAGE_UPLOAD;
tbci.size = kW * kH * 4;
transfer_ = SDL_CreateGPUTransferBuffer(device, &tbci);
SDL_GPUBufferCreateInfo bci = {};
bci.usage = SDL_GPU_BUFFERUSAGE_VERTEX;
bci.size = 6u * 5u * static_cast<uint32_t>(sizeof(float));
vtx_buf_ = SDL_CreateGPUBuffer(device, &bci);
SDL_GPUSamplerCreateInfo sci = {};
sci.min_filter = SDL_GPU_FILTER_NEAREST;
sci.mag_filter = SDL_GPU_FILTER_NEAREST;
sci.mipmap_mode = SDL_GPU_SAMPLERMIPMAPMODE_NEAREST;
sci.address_mode_u = SDL_GPU_SAMPLERADDRESSMODE_CLAMP_TO_EDGE;
sci.address_mode_v = SDL_GPU_SAMPLERADDRESSMODE_CLAMP_TO_EDGE;
sampler_ = SDL_CreateGPUSampler(device, &sci);
surface_ = SDL_CreateSurface(kW, kH, SDL_PIXELFORMAT_RGBA32);
renderer_ = surface_ ? SDL_CreateSoftwareRenderer(surface_) : nullptr;
ready_ = tex_ && transfer_ && vtx_buf_ && sampler_ && surface_ && renderer_;
}
void WorkflowQ3OverlayDrawStep::DrawSurface(WorkflowContext& context, uint32_t frameW, uint32_t frameH) {
SDL_SetRenderDrawColor(renderer_, 0, 0, 0, 0);
SDL_RenderClear(renderer_);
SDL_SetRenderDrawBlendMode(renderer_, SDL_BLENDMODE_BLEND);
SDL_FRect hudBg{12, static_cast<float>(kH - 44), 230, 28};
SDL_SetRenderDrawColor(renderer_, 0, 0, 0, 150);
SDL_RenderFillRect(renderer_, &hudBg);
const std::string weapon = context.Get<std::string>("q3.current_weapon", "weapon_machinegun");
const int shots = context.Get<int>("q3.shots_fired", 0);
std::string hud = "WEAPON " + weapon.substr(7) + " SHOTS " + std::to_string(shots);
Text(renderer_, 20, static_cast<float>(kH - 36), hud.c_str(), SDL_Color{255, 216, 64, 255});
Text(renderer_, static_cast<float>(kW / 2 - 4), static_cast<float>(kH / 2 - 4), "+", SDL_Color{255, 255, 255, 220});
if (context.GetBool("q3.menu_open", false)) {
SDL_FRect panel{120, 42, 400, 250};
SDL_SetRenderDrawColor(renderer_, 0, 0, 0, 210);
SDL_RenderFillRect(renderer_, &panel);
SDL_SetRenderDrawColor(renderer_, 40, 120, 220, 255);
SDL_RenderRect(renderer_, &panel);
Text(renderer_, 170, 62, "QUAKE III ARENA", SDL_Color{255, 216, 64, 255});
Text(renderer_, 160, 88, "SKIRMISH / MAP SELECTION", SDL_Color{180, 220, 255, 255});
auto maps = context.Get<nlohmann::json>("q3.maps", nlohmann::json::array());
int selected = context.Get<int>("q3.menu_selected_map", 0);
for (int i = 0; i < 8 && i < static_cast<int>(maps.size()); ++i) {
int idx = (selected / 8) * 8 + i;
if (idx >= static_cast<int>(maps.size())) break;
std::string line = (idx == selected ? "> " : " ") + maps[idx].get<std::string>();
Text(renderer_, 176, 122 + i * 16, line.c_str(),
idx == selected ? SDL_Color{255, 255, 255, 255} : SDL_Color{140, 190, 240, 255});
}
Text(renderer_, 154, 266, "UP/DOWN SELECT ENTER SET MAP ESC RESUME Q QUIT", SDL_Color{180, 180, 180, 255});
auto pending = context.Get<std::string>("q3.pending_map", "");
if (!pending.empty()) {
std::string msg = "NEXT START: QUAKE3_MAP=" + pending;
Text(renderer_, 160, 246, msg.c_str(), SDL_Color{255, 170, 80, 255});
}
}
SDL_RenderPresent(renderer_);
}
void WorkflowQ3OverlayDrawStep::Render(SDL_GPUCommandBuffer* cmd, SDL_GPUTexture* swapchainTex,
SDL_GPUDevice* device, uint32_t frameW, uint32_t frameH) {
void* mapped = SDL_MapGPUTransferBuffer(device, transfer_, false);
if (!mapped) return;
std::memcpy(mapped, surface_->pixels, kW * kH * 4);
SDL_UnmapGPUTransferBuffer(device, transfer_);
auto* copy = SDL_BeginGPUCopyPass(cmd);
if (copy) {
SDL_GPUTextureTransferInfo src = {};
src.transfer_buffer = transfer_;
src.pixels_per_row = kW;
src.rows_per_layer = kH;
SDL_GPUTextureRegion dst = {};
dst.texture = tex_;
dst.w = kW;
dst.h = kH;
dst.d = 1;
SDL_UploadToGPUTexture(copy, &src, &dst, false);
SDL_EndGPUCopyPass(copy);
}
if (!vbuf_uploaded_) {
const float verts[6][5] = {
{-1, 1, 0, 0, 0}, { 1, 1, 0, 1, 0}, { 1, -1, 0, 1, 1},
{-1, 1, 0, 0, 0}, { 1, -1, 0, 1, 1}, {-1, -1, 0, 0, 1},
};
const uint32_t size = sizeof(verts);
SDL_GPUTransferBufferCreateInfo tb = {};
tb.usage = SDL_GPU_TRANSFERBUFFERUSAGE_UPLOAD;
tb.size = size;
auto* tmp = SDL_CreateGPUTransferBuffer(device, &tb);
if (tmp) {
void* ptr = SDL_MapGPUTransferBuffer(device, tmp, false);
if (ptr) {
std::memcpy(ptr, verts, size);
SDL_UnmapGPUTransferBuffer(device, tmp);
}
auto* cp = SDL_BeginGPUCopyPass(cmd);
if (cp) {
SDL_GPUTransferBufferLocation src = {tmp, 0};
SDL_GPUBufferRegion dst = {vtx_buf_, 0, size};
SDL_UploadToGPUBuffer(cp, &src, &dst, false);
SDL_EndGPUCopyPass(cp);
}
SDL_ReleaseGPUTransferBuffer(device, tmp);
}
vbuf_uploaded_ = true;
}
SDL_GPUColorTargetInfo target = {};
target.texture = swapchainTex;
target.load_op = SDL_GPU_LOADOP_LOAD;
target.store_op = SDL_GPU_STOREOP_STORE;
auto* pass = SDL_BeginGPURenderPass(cmd, &target, 1, nullptr);
if (!pass) return;
SDL_BindGPUGraphicsPipeline(pass, pipeline_);
SDL_GPUBufferBinding vb = {vtx_buf_, 0};
SDL_BindGPUVertexBuffers(pass, 0, &vb, 1);
SDL_GPUTextureSamplerBinding ts = {tex_, sampler_};
SDL_BindGPUFragmentSamplers(pass, 0, &ts, 1);
SDL_DrawGPUPrimitives(pass, 6, 1, 0, 0);
SDL_EndGPURenderPass(pass);
}
void WorkflowQ3OverlayDrawStep::Execute(const WorkflowStepDefinition& step, WorkflowContext& context) {
if (context.GetBool("frame_skip", false)) return;
auto* cmd = context.Get<SDL_GPUCommandBuffer*>("gpu_command_buffer", nullptr);
auto* swapchain = context.Get<SDL_GPUTexture*>("gpu_swapchain_texture", nullptr);
auto* device = context.Get<SDL_GPUDevice*>("gpu_device", nullptr);
if (!cmd || !swapchain || !device) return;
if (!ready_) TryInit(device, context.Get<SDL_Window*>("sdl_window", nullptr));
if (!ready_) return;
const auto fw = context.Get<uint32_t>("frame_width", 1280u);
const auto fh = context.Get<uint32_t>("frame_height", 960u);
DrawSurface(context, fw, fh);
Render(cmd, swapchain, device, fw, fh);
}
} // namespace sdl3cpp::services::impl
@@ -0,0 +1,97 @@
#include "services/interfaces/workflow/quake3/workflow_q3_weapon_update_step.hpp"
#include <btBulletDynamicsCommon.h>
#include <glm/glm.hpp>
#include <nlohmann/json.hpp>
#include <array>
#include <string>
namespace sdl3cpp::services::impl {
namespace {
const std::array<const char*, 9> kWeapons = {
"weapon_gauntlet",
"weapon_machinegun",
"weapon_shotgun",
"weapon_grenadelauncher",
"weapon_rocketlauncher",
"weapon_lightning",
"weapon_railgun",
"weapon_plasmagun",
"weapon_bfg"
};
bool HasWeapon(const nlohmann::json& inventory, const std::string& weapon) {
return weapon == "weapon_gauntlet" ||
weapon == "weapon_machinegun" ||
inventory.value(weapon, false);
}
} // namespace
WorkflowQ3WeaponUpdateStep::WorkflowQ3WeaponUpdateStep(std::shared_ptr<ILogger> logger)
: logger_(std::move(logger)) {}
std::string WorkflowQ3WeaponUpdateStep::GetPluginId() const {
return "q3.weapon.update";
}
void WorkflowQ3WeaponUpdateStep::Execute(const WorkflowStepDefinition& step, WorkflowContext& context) {
auto inventory = context.Get<nlohmann::json>("q3.inventory", nlohmann::json::object());
inventory["weapon_gauntlet"] = true;
inventory["weapon_machinegun"] = true;
std::string current = context.Get<std::string>("q3.current_weapon", "weapon_machinegun");
for (size_t i = 0; i < kWeapons.size(); ++i) {
if (!context.GetBool("input_key_" + std::to_string(i + 1), false)) continue;
const std::string requested = kWeapons[i];
if (HasWeapon(inventory, requested)) {
current = requested;
context.Set<std::string>("q3.current_weapon", current);
}
}
const bool fireHeld = context.GetBool("input_mouse_left", false);
const bool firePressed = context.GetBool("input_mouse_left_pressed", false);
const uint32_t frame = static_cast<uint32_t>(context.GetDouble("loop.iteration", 0.0));
uint32_t lastFire = context.Get<uint32_t>("q3.weapon_last_fire_frame", 0u);
const uint32_t interval = current == "weapon_machinegun" ? 8u :
current == "weapon_lightning" ? 3u :
current == "weapon_gauntlet" ? 18u :
28u;
bool wantsFire = firePressed || (fireHeld && current == "weapon_machinegun") || (fireHeld && current == "weapon_lightning");
if (!context.GetBool("q3.menu_open", false) && wantsFire && (lastFire == 0u || frame >= lastFire + interval)) {
lastFire = frame == 0u ? 1u : frame;
context.Set<uint32_t>("q3.weapon_last_fire_frame", lastFire);
context.Set<uint32_t>("q3.weapon_flash_until_frame", lastFire + 4u);
context.Set<int>("q3.shots_fired", context.Get<int>("q3.shots_fired", 0) + 1);
auto* world = context.Get<btDiscreteDynamicsWorld*>("physics_world", nullptr);
auto cameraState = context.Get<nlohmann::json>("camera.state", nlohmann::json::object());
if (world && cameraState.contains("position") && cameraState.contains("front")) {
const auto pos = cameraState["position"];
const auto front = cameraState["front"];
btVector3 from(pos[0].get<float>(), pos[1].get<float>(), pos[2].get<float>());
btVector3 dir(front[0].get<float>(), front[1].get<float>(), front[2].get<float>());
btVector3 to = from + dir.normalized() * 120.0f;
btCollisionWorld::ClosestRayResultCallback hit(from, to);
world->rayTest(from, to, hit);
context.Set<bool>("q3.last_shot_hit", hit.hasHit());
if (hit.hasHit()) {
context.Set("q3.last_shot_position", nlohmann::json::array({
hit.m_hitPointWorld.x(), hit.m_hitPointWorld.y(), hit.m_hitPointWorld.z()
}));
}
}
if (logger_) logger_->Info("q3.weapon.update: fired " + current);
}
context.Set("q3.inventory", inventory);
context.Set<std::string>("q3.current_weapon", current);
}
} // namespace sdl3cpp::services::impl
@@ -316,6 +316,7 @@ void WorkflowBspBuildGeometryStep::Execute(const WorkflowStepDefinition& step, W
mapNodes.push_back({
{"name", "bsp_" + map_name},
{"texture_index", texIdx},
{"texture_name", (texIdx >= 0 && texIdx < numTextures) ? std::string(bspTextures[texIdx].name) : std::string{}},
{"index_offset", indexOffset},
{"index_count", group.indices.size()}
});
@@ -28,19 +28,27 @@ bool ReadVec3(const nlohmann::json& value, btVector3& out) {
return true;
}
bool InBounds(const btVector3& p, const nlohmann::json& bounds, float pad) {
bool ReadBounds(const nlohmann::json& bounds, btVector3& mn, btVector3& mx) {
if (!bounds.is_object() || !bounds.contains("min") || !bounds.contains("max")) return false;
btVector3 mn, mx;
if (!ReadVec3(bounds["min"], mn) || !ReadVec3(bounds["max"], mx)) return false;
return p.x() >= mn.x() - pad && p.x() <= mx.x() + pad &&
p.y() >= mn.y() - pad && p.y() <= mx.y() + pad &&
p.z() >= mn.z() - pad && p.z() <= mx.z() + pad;
return ReadVec3(bounds["min"], mn) && ReadVec3(bounds["max"], mx);
}
bool AabbIntersectsBounds(const btVector3& bodyMin,
const btVector3& bodyMax,
const nlohmann::json& bounds,
float pad) {
btVector3 triggerMin, triggerMax;
if (!ReadBounds(bounds, triggerMin, triggerMax)) return false;
return bodyMax.x() >= triggerMin.x() - pad && bodyMin.x() <= triggerMax.x() + pad &&
bodyMax.y() >= triggerMin.y() - pad && bodyMin.y() <= triggerMax.y() + pad &&
bodyMax.z() >= triggerMin.z() - pad && bodyMin.z() <= triggerMax.z() + pad;
}
void TeleportBody(btRigidBody* body, const btVector3& dest) {
btTransform xform;
xform.setIdentity();
xform.setOrigin(dest);
body->setCenterOfMassTransform(xform);
body->setWorldTransform(xform);
if (body->getMotionState()) body->getMotionState()->setWorldTransform(xform);
body->setLinearVelocity(btVector3(0, 0, 0));
@@ -81,6 +89,9 @@ void WorkflowBspEntityUpdateStep::Execute(const WorkflowStepDefinition& step, Wo
btTransform xform;
body->getMotionState()->getWorldTransform(xform);
const btVector3 playerPos = xform.getOrigin();
btVector3 playerAabbMin;
btVector3 playerAabbMax;
body->getCollisionShape()->getAabb(xform, playerAabbMin, playerAabbMax);
const uint32_t frame = static_cast<uint32_t>(context.GetDouble("loop.iteration", 0.0));
auto collected = context.Get<nlohmann::json>("q3.collected", nlohmann::json::object());
@@ -111,7 +122,10 @@ void WorkflowBspEntityUpdateStep::Execute(const WorkflowStepDefinition& step, Wo
}
if (classname != "trigger_push" && classname != "trigger_teleport") continue;
if (!ent.contains("bounds") || !InBounds(playerPos, ent["bounds"], 0.15f)) continue;
if (!ent.contains("bounds") ||
!AabbIntersectsBounds(playerAabbMin, playerAabbMax, ent["bounds"], 0.15f)) {
continue;
}
const uint32_t lastFrame = cooldowns.value(id, 0u);
const uint32_t cooldownFrames = classname == "trigger_teleport" ? 45u : 15u;
@@ -124,6 +138,8 @@ void WorkflowBspEntityUpdateStep::Execute(const WorkflowStepDefinition& step, Wo
if (classname == "trigger_teleport") {
target += btVector3(0, 1.0f, 0);
TeleportBody(body, target);
playerAabbMin = target - btVector3(0.3f, 0.8f, 0.3f);
playerAabbMax = target + btVector3(0.3f, 0.8f, 0.3f);
if (logger_) logger_->Info("bsp.entities.update: teleported player via " + id);
} else {
body->setLinearVelocity(JumpPadVelocity(playerPos, target));
@@ -38,7 +38,8 @@ void WorkflowBspLoadStep::Execute(const WorkflowStepDefinition& step, WorkflowCo
};
const std::string pk3_path = getStr("pk3_path", "");
const std::string map_name = getStr("map_name", "q3dm17");
std::string map_name = getStr("map_name", "q3dm17");
if (map_name.empty()) map_name = "q3dm7";
const float scale = getNum("scale", 1.0f / 32.0f);
if (pk3_path.empty()) {
@@ -52,6 +53,18 @@ void WorkflowBspLoadStep::Execute(const WorkflowStepDefinition& step, WorkflowCo
throw std::runtime_error("bsp.load: Failed to open pk3: " + pk3_path);
}
nlohmann::json maps = nlohmann::json::array();
const zip_int64_t entries = zip_get_num_entries(archive, 0);
for (zip_uint64_t i = 0; i < static_cast<zip_uint64_t>(entries); ++i) {
const char* name = zip_get_name(archive, i, 0);
if (!name) continue;
std::string entry(name);
if (entry.rfind("maps/", 0) != 0 || entry.size() <= 9) continue;
if (entry.substr(entry.size() - 4) != ".bsp") continue;
maps.push_back(entry.substr(5, entry.size() - 9));
}
context.Set("q3.maps", maps);
std::string bsp_entry = "maps/" + map_name + ".bsp";
zip_stat_t st;
if (zip_stat(archive, bsp_entry.c_str(), 0, &st) != 0) {
@@ -0,0 +1,215 @@
#include "services/interfaces/workflow/rendering/workflow_bsp_portal_view_step.hpp"
#include "services/interfaces/workflow/rendering/rendering_types.hpp"
#include <SDL3/SDL_gpu.h>
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>
#include <nlohmann/json.hpp>
#include <cmath>
#include <cstring>
#include <string>
#include <vector>
namespace sdl3cpp::services::impl {
namespace {
bool ReadVec3(const nlohmann::json& value, glm::vec3& out) {
if (!value.is_array() || value.size() != 3) return false;
out = glm::vec3(value[0].get<float>(), value[1].get<float>(), value[2].get<float>());
return true;
}
bool FindPortalDestination(const nlohmann::json& entities, glm::vec3& out) {
if (!entities.is_array()) return false;
for (const auto& ent : entities) {
if (ent.value("classname", std::string{}) != "trigger_teleport") continue;
if (ent.contains("target_position") && ReadVec3(ent["target_position"], out)) return true;
}
return false;
}
} // namespace
WorkflowBspPortalViewStep::WorkflowBspPortalViewStep(std::shared_ptr<ILogger> logger)
: logger_(std::move(logger)) {}
std::string WorkflowBspPortalViewStep::GetPluginId() const {
return "bsp.portal_view";
}
void WorkflowBspPortalViewStep::Execute(const WorkflowStepDefinition& step, WorkflowContext& context) {
if (context.GetBool("frame_skip", false)) return;
SDL_GPUDevice* device = context.Get<SDL_GPUDevice*>("gpu_device", nullptr);
SDL_Window* window = context.Get<SDL_Window*>("sdl_window", nullptr);
auto* pipeline = context.Get<SDL_GPUGraphicsPipeline*>("gpu_pipeline_bsp", nullptr);
auto* lmTex = context.Get<SDL_GPUTexture*>("bsp_lightmap_atlas_gpu", nullptr);
auto* lmSamp = context.Get<SDL_GPUSampler*>("bsp_lightmap_atlas_sampler", nullptr);
const auto* mapNodes = context.TryGet<nlohmann::json>("map.nodes");
const auto* entities = context.TryGet<nlohmann::json>("bsp.entities");
if (!device || !window || !pipeline || !lmTex || !lmSamp ||
!mapNodes || !mapNodes->is_array() || mapNodes->empty() || !entities) {
return;
}
glm::vec3 dest;
if (!FindPortalDestination(*entities, dest)) return;
dest += glm::vec3(0.0f, 1.4f, 0.0f);
constexpr uint32_t kPortalSize = 512;
auto* portalTex = context.Get<SDL_GPUTexture*>("bsp_portal_view_texture", nullptr);
auto* portalDepth = context.Get<SDL_GPUTexture*>("bsp_portal_view_depth", nullptr);
auto* portalSampler = context.Get<SDL_GPUSampler*>("bsp_portal_view_sampler", nullptr);
if (!portalTex) {
SDL_GPUTextureCreateInfo ti = {};
ti.type = SDL_GPU_TEXTURETYPE_2D;
ti.format = SDL_GetGPUSwapchainTextureFormat(device, window);
ti.width = kPortalSize;
ti.height = kPortalSize;
ti.layer_count_or_depth = 1;
ti.num_levels = 1;
ti.usage = SDL_GPU_TEXTUREUSAGE_COLOR_TARGET | SDL_GPU_TEXTUREUSAGE_SAMPLER;
portalTex = SDL_CreateGPUTexture(device, &ti);
if (!portalTex) return;
context.Set<SDL_GPUTexture*>("bsp_portal_view_texture", portalTex);
}
if (!portalDepth) {
SDL_GPUTextureCreateInfo di = {};
di.type = SDL_GPU_TEXTURETYPE_2D;
di.format = SDL_GPU_TEXTUREFORMAT_D32_FLOAT;
di.width = kPortalSize;
di.height = kPortalSize;
di.layer_count_or_depth = 1;
di.num_levels = 1;
di.usage = SDL_GPU_TEXTUREUSAGE_DEPTH_STENCIL_TARGET;
portalDepth = SDL_CreateGPUTexture(device, &di);
if (!portalDepth) return;
context.Set<SDL_GPUTexture*>("bsp_portal_view_depth", portalDepth);
}
if (!portalSampler) {
SDL_GPUSamplerCreateInfo si = {};
si.min_filter = SDL_GPU_FILTER_LINEAR;
si.mag_filter = SDL_GPU_FILTER_LINEAR;
si.mipmap_mode = SDL_GPU_SAMPLERMIPMAPMODE_LINEAR;
si.address_mode_u = SDL_GPU_SAMPLERADDRESSMODE_CLAMP_TO_EDGE;
si.address_mode_v = SDL_GPU_SAMPLERADDRESSMODE_CLAMP_TO_EDGE;
si.address_mode_w = SDL_GPU_SAMPLERADDRESSMODE_CLAMP_TO_EDGE;
portalSampler = SDL_CreateGPUSampler(device, &si);
if (!portalSampler) return;
context.Set<SDL_GPUSampler*>("bsp_portal_view_sampler", portalSampler);
}
SDL_GPUCommandBuffer* cmd = SDL_AcquireGPUCommandBuffer(device);
if (!cmd) return;
SDL_GPUColorTargetInfo colorTarget = {};
colorTarget.texture = portalTex;
colorTarget.clear_color = {0.02f, 0.03f, 0.05f, 1.0f};
colorTarget.load_op = SDL_GPU_LOADOP_CLEAR;
colorTarget.store_op = SDL_GPU_STOREOP_STORE;
SDL_GPUDepthStencilTargetInfo depthTarget = {};
depthTarget.texture = portalDepth;
depthTarget.clear_depth = 1.0f;
depthTarget.load_op = SDL_GPU_LOADOP_CLEAR;
depthTarget.store_op = SDL_GPU_STOREOP_DONT_CARE;
SDL_GPURenderPass* pass = SDL_BeginGPURenderPass(cmd, &colorTarget, 1, &depthTarget);
if (!pass) {
SDL_CancelGPUCommandBuffer(cmd);
return;
}
const float yaw = context.Get<float>("camera_yaw", 0.0f);
const float pitch = context.Get<float>("camera_pitch", 0.0f);
glm::vec3 front;
front.x = std::cos(pitch) * (-std::sin(yaw));
front.y = std::sin(pitch);
front.z = std::cos(pitch) * (-std::cos(yaw));
front = glm::normalize(front);
glm::mat4 view = glm::lookAt(dest, dest + front, glm::vec3(0.0f, 1.0f, 0.0f));
glm::mat4 proj = glm::perspective(glm::radians(90.0f), 1.0f, 0.1f, 500.0f);
glm::mat4 model = glm::mat4(1.0f);
glm::mat4 mvp = proj * view * model;
rendering::VertexUniformData vu = {};
std::memcpy(vu.mvp, glm::value_ptr(mvp), sizeof(float) * 16);
std::memcpy(vu.model_mat, glm::value_ptr(model), sizeof(float) * 16);
vu.normal[1] = 1.0f;
vu.uv_scale[0] = 1.0f;
vu.uv_scale[1] = 1.0f;
vu.camera_pos[0] = dest.x;
vu.camera_pos[1] = dest.y;
vu.camera_pos[2] = dest.z;
auto shadowVP = context.Get<glm::mat4>("render.shadow_vp", glm::mat4(1.0f));
std::memcpy(vu.shadow_vp, glm::value_ptr(shadowVP), sizeof(float) * 16);
auto fu = context.Get<rendering::FragmentUniformData>("render.frag_uniforms", rendering::FragmentUniformData{});
fu.material[0] = 0.7f;
fu.material[1] = static_cast<float>(context.GetDouble("frame.elapsed", 0.0));
fu.material[2] = 2.0f;
fu.material[3] = 0.0f;
const auto& firstNode = (*mapNodes)[0];
std::string meshName = firstNode["name"];
auto* vb = context.Get<SDL_GPUBuffer*>("plane_" + meshName + "_vb", nullptr);
auto* ib = context.Get<SDL_GPUBuffer*>("plane_" + meshName + "_ib", nullptr);
if (!vb || !ib) {
SDL_EndGPURenderPass(pass);
SDL_SubmitGPUCommandBuffer(cmd);
return;
}
SDL_BindGPUGraphicsPipeline(pass, pipeline);
SDL_GPUBufferBinding vbBind = {};
vbBind.buffer = vb;
SDL_BindGPUVertexBuffers(pass, 0, &vbBind, 1);
SDL_GPUBufferBinding ibBind = {};
ibBind.buffer = ib;
SDL_BindGPUIndexBuffer(pass, &ibBind, SDL_GPU_INDEXELEMENTSIZE_32BIT);
SDL_PushGPUVertexUniformData(cmd, 0, &vu, sizeof(vu));
SDL_PushGPUFragmentUniformData(cmd, 0, &fu, sizeof(fu));
for (const auto& node : *mapNodes) {
const int texIdx = node.value("texture_index", -1);
SDL_GPUTexture* albedoTex = nullptr;
SDL_GPUSampler* albedoSamp = nullptr;
if (texIdx >= 0) {
const std::string texKey = "bsp_tex_" + std::to_string(texIdx);
albedoTex = context.Get<SDL_GPUTexture*>(texKey + "_gpu", nullptr);
albedoSamp = context.Get<SDL_GPUSampler*>(texKey + "_sampler", nullptr);
}
if (!albedoTex || !albedoSamp) continue;
SDL_GPUTextureSamplerBinding bindings[4] = {};
bindings[0].texture = albedoTex;
bindings[0].sampler = albedoSamp;
bindings[1].texture = albedoTex;
bindings[1].sampler = albedoSamp;
bindings[2].texture = lmTex;
bindings[2].sampler = lmSamp;
bindings[3].texture = albedoTex;
bindings[3].sampler = albedoSamp;
SDL_BindGPUFragmentSamplers(pass, 0, bindings, 4);
SDL_DrawGPUIndexedPrimitives(pass,
node["index_count"].get<uint32_t>(),
1,
node.value("index_offset", 0u),
0,
0);
}
SDL_EndGPURenderPass(pass);
SDL_SubmitGPUCommandBuffer(cmd);
}
} // namespace sdl3cpp::services::impl
@@ -7,10 +7,29 @@
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>
#include <nlohmann/json.hpp>
#include <algorithm>
#include <cctype>
#include <cstring>
namespace sdl3cpp::services::impl {
namespace {
std::string ToLower(std::string value) {
std::transform(value.begin(), value.end(), value.begin(), [](unsigned char c) {
return static_cast<char>(std::tolower(c));
});
return value;
}
bool IsPortalTexture(const std::string& textureName) {
const std::string lower = ToLower(textureName);
return lower.find("portal_sfx") != std::string::npos ||
lower.find("mapobjects/portal") != std::string::npos;
}
} // namespace
WorkflowDrawMapStep::WorkflowDrawMapStep(std::shared_ptr<ILogger> logger)
: logger_(std::move(logger)) {}
@@ -89,6 +108,8 @@ void WorkflowDrawMapStep::Execute(const WorkflowStepDefinition& step, WorkflowCo
// BSP lightmap atlas (shared across all groups)
auto* lm_tex = context.Get<SDL_GPUTexture*>("bsp_lightmap_atlas_gpu", nullptr);
auto* lm_samp = context.Get<SDL_GPUSampler*>("bsp_lightmap_atlas_sampler", nullptr);
auto* portal_tex = context.Get<SDL_GPUTexture*>("bsp_portal_view_texture", nullptr);
auto* portal_samp = context.Get<SDL_GPUSampler*>("bsp_portal_view_sampler", nullptr);
if (isBsp) {
// BSP mode: single VB + single IB, per-group draw calls
@@ -104,18 +125,19 @@ void WorkflowDrawMapStep::Execute(const WorkflowStepDefinition& step, WorkflowCo
SDL_GPUBufferBinding ibBind = {}; ibBind.buffer = ib;
SDL_BindGPUIndexBuffer(pass, &ibBind, SDL_GPU_INDEXELEMENTSIZE_32BIT);
// Push uniforms once (same for all groups)
// Vertex uniforms are shared by all BSP texture groups.
SDL_PushGPUVertexUniformData(cmd, 0, &vu, sizeof(vu));
SDL_PushGPUFragmentUniformData(cmd, 0, &fu, sizeof(fu));
// Default texture fallback
auto* defaultTex = context.Get<SDL_GPUTexture*>(defaultTexture + "_gpu", nullptr);
auto* defaultSamp = context.Get<SDL_GPUSampler*>(defaultTexture + "_sampler", nullptr);
const float elapsed = static_cast<float>(context.GetDouble("frame.elapsed", 0.0));
for (const auto& node : *mapNodes) {
uint32_t indexCount = node["index_count"];
uint32_t indexOffset = node.value("index_offset", 0u);
int texIdx = node.value("texture_index", -1);
std::string textureName = node.value("texture_name", std::string{});
// Look up per-texture albedo
SDL_GPUTexture* albedoTex = nullptr;
@@ -133,16 +155,22 @@ void WorkflowDrawMapStep::Execute(const WorkflowStepDefinition& step, WorkflowCo
}
if (!albedoTex || !albedoSamp) continue;
// Bind 3 samplers: albedo, shadow, lightmap
SDL_GPUTextureSamplerBinding bindings[3] = {};
// Bind 4 samplers: albedo, shadow, lightmap, portal destination.
SDL_GPUTextureSamplerBinding bindings[4] = {};
bindings[0].texture = albedoTex;
bindings[0].sampler = albedoSamp;
bindings[1].texture = shadow_tex ? shadow_tex : albedoTex;
bindings[1].sampler = shadow_samp ? shadow_samp : albedoSamp;
bindings[2].texture = lm_tex ? lm_tex : albedoTex;
bindings[2].sampler = lm_samp ? lm_samp : albedoSamp;
SDL_BindGPUFragmentSamplers(pass, 0, bindings, 3);
bindings[3].texture = portal_tex ? portal_tex : albedoTex;
bindings[3].sampler = portal_samp ? portal_samp : albedoSamp;
SDL_BindGPUFragmentSamplers(pass, 0, bindings, 4);
auto groupFu = fu;
groupFu.material[1] = elapsed;
groupFu.material[3] = IsPortalTexture(textureName) ? 1.0f : 0.0f;
SDL_PushGPUFragmentUniformData(cmd, 0, &groupFu, sizeof(groupFu));
SDL_DrawGPUIndexedPrimitives(pass, indexCount, 1, indexOffset, 0, 0);
}
} else {
@@ -19,6 +19,11 @@ void WorkflowInputPollStep::Execute(
const WorkflowStepDefinition& step, WorkflowContext& context) {
float mouseRelX = 0.0f, mouseRelY = 0.0f;
bool keyEscapePressed = false;
bool keyEnterPressed = false;
bool keyUpPressed = false;
bool keyDownPressed = false;
bool mouseLeftPressed = false;
SDL_Event event;
while (SDL_PollEvent(&event)) {
@@ -28,9 +33,20 @@ void WorkflowInputPollStep::Execute(
break;
case SDL_EVENT_KEY_DOWN:
if (event.key.key == SDLK_ESCAPE) {
keyEscapePressed = true;
} else if (event.key.key == SDLK_RETURN) {
keyEnterPressed = true;
} else if (event.key.key == SDLK_UP) {
keyUpPressed = true;
} else if (event.key.key == SDLK_DOWN) {
keyDownPressed = true;
} else if (event.key.key == SDLK_Q) {
context.Set<bool>("game_running", false);
}
break;
case SDL_EVENT_MOUSE_BUTTON_DOWN:
if (event.button.button == SDL_BUTTON_LEFT) mouseLeftPressed = true;
break;
case SDL_EVENT_MOUSE_MOTION:
mouseRelX += event.motion.xrel;
mouseRelY += event.motion.yrel;
@@ -41,6 +57,11 @@ void WorkflowInputPollStep::Execute(
// Store accumulated mouse motion for this frame
context.Set<float>("input_mouse_rel_x", mouseRelX);
context.Set<float>("input_mouse_rel_y", mouseRelY);
context.Set<bool>("input_key_escape_pressed", keyEscapePressed);
context.Set<bool>("input_key_enter_pressed", keyEnterPressed);
context.Set<bool>("input_key_up_pressed", keyUpPressed);
context.Set<bool>("input_key_down_pressed", keyDownPressed);
context.Set<bool>("input_mouse_left_pressed", mouseLeftPressed);
// Read keyboard state (snapshot, not event-based)
const bool* keyState = SDL_GetKeyboardState(nullptr);
@@ -52,6 +73,16 @@ void WorkflowInputPollStep::Execute(
context.Set<bool>("input_key_space", keyState[SDL_SCANCODE_SPACE]);
context.Set<bool>("input_key_shift", keyState[SDL_SCANCODE_LSHIFT]);
context.Set<bool>("input_key_ctrl", keyState[SDL_SCANCODE_LCTRL]);
context.Set<bool>("input_mouse_left", (SDL_GetMouseState(nullptr, nullptr) & SDL_BUTTON_LMASK) != 0);
context.Set<bool>("input_key_1", keyState[SDL_SCANCODE_1]);
context.Set<bool>("input_key_2", keyState[SDL_SCANCODE_2]);
context.Set<bool>("input_key_3", keyState[SDL_SCANCODE_3]);
context.Set<bool>("input_key_4", keyState[SDL_SCANCODE_4]);
context.Set<bool>("input_key_5", keyState[SDL_SCANCODE_5]);
context.Set<bool>("input_key_6", keyState[SDL_SCANCODE_6]);
context.Set<bool>("input_key_7", keyState[SDL_SCANCODE_7]);
context.Set<bool>("input_key_8", keyState[SDL_SCANCODE_8]);
context.Set<bool>("input_key_9", keyState[SDL_SCANCODE_9]);
}
}
@@ -26,6 +26,11 @@ void WorkflowPhysicsFpsMoveStep::Execute(
auto* body = context.Get<btRigidBody*>("physics_body_" + playerName, nullptr);
if (!body) return;
if (context.GetBool("q3.menu_open", false)) {
btVector3 vel = body->getLinearVelocity();
body->setLinearVelocity(btVector3(0, vel.y(), 0));
return;
}
// Read parameters from workflow JSON
WorkflowStepParameterResolver paramResolver;
@@ -48,6 +48,7 @@
#include "services/interfaces/workflow/rendering/workflow_bsp_lightmap_atlas_step.hpp"
#include "services/interfaces/workflow/rendering/workflow_bsp_parse_spawn_step.hpp"
#include "services/interfaces/workflow/rendering/workflow_bsp_entity_update_step.hpp"
#include "services/interfaces/workflow/rendering/workflow_bsp_portal_view_step.hpp"
#include "services/interfaces/workflow/rendering/workflow_bsp_build_geometry_step.hpp"
#include "services/interfaces/workflow/rendering/workflow_bsp_extract_textures_step.hpp"
#include "services/interfaces/workflow/rendering/workflow_bsp_upload_geometry_step.hpp"
@@ -112,6 +113,11 @@
// Camera (service-dependent)
#include "services/interfaces/workflow/workflow_generic_steps/workflow_camera_build_view_state_step.hpp"
// Quake 3
#include "services/interfaces/workflow/quake3/workflow_q3_menu_update_step.hpp"
#include "services/interfaces/workflow/quake3/workflow_q3_weapon_update_step.hpp"
#include "services/interfaces/workflow/quake3/workflow_q3_overlay_draw_step.hpp"
// Audio (service-dependent, registered with nullptr)
#include "services/interfaces/workflow/workflow_generic_steps/workflow_audio_pause_step.hpp"
#include "services/interfaces/workflow/workflow_generic_steps/workflow_audio_play_step.hpp"
@@ -297,6 +303,7 @@ void WorkflowRegistrar::RegisterSteps(std::shared_ptr<IWorkflowStepRegistry> reg
registry->RegisterStep(std::make_shared<WorkflowBspLightmapAtlasStep>(logger_));
registry->RegisterStep(std::make_shared<WorkflowBspParseSpawnStep>(logger_));
registry->RegisterStep(std::make_shared<WorkflowBspEntityUpdateStep>(logger_));
registry->RegisterStep(std::make_shared<WorkflowBspPortalViewStep>(logger_));
registry->RegisterStep(std::make_shared<WorkflowBspBuildGeometryStep>(logger_));
registry->RegisterStep(std::make_shared<WorkflowBspExtractTexturesStep>(logger_));
registry->RegisterStep(std::make_shared<WorkflowBspUploadGeometryStep>(logger_));
@@ -314,6 +321,9 @@ void WorkflowRegistrar::RegisterSteps(std::shared_ptr<IWorkflowStepRegistry> reg
registry->RegisterStep(std::make_shared<WorkflowPostfxSsaoStep>(logger_));
registry->RegisterStep(std::make_shared<WorkflowPostfxBloomExtractStep>(logger_));
registry->RegisterStep(std::make_shared<WorkflowPostfxBloomBlurStep>(logger_));
registry->RegisterStep(std::make_shared<WorkflowQ3MenuUpdateStep>(logger_));
registry->RegisterStep(std::make_shared<WorkflowQ3WeaponUpdateStep>(logger_));
registry->RegisterStep(std::make_shared<WorkflowQ3OverlayDrawStep>(logger_));
count += 18;
// ── Texture ───────────────────────────────────────────────
@@ -0,0 +1,20 @@
#pragma once
#include "services/interfaces/i_workflow_step.hpp"
#include "services/interfaces/i_logger.hpp"
#include <memory>
namespace sdl3cpp::services::impl {
class WorkflowQ3MenuUpdateStep final : public IWorkflowStep {
public:
explicit WorkflowQ3MenuUpdateStep(std::shared_ptr<ILogger> logger);
std::string GetPluginId() const override;
void Execute(const WorkflowStepDefinition& step, WorkflowContext& context) override;
private:
std::shared_ptr<ILogger> logger_;
};
} // namespace sdl3cpp::services::impl
@@ -0,0 +1,46 @@
#pragma once
#include "services/interfaces/i_workflow_step.hpp"
#include "services/interfaces/i_logger.hpp"
#include "services/interfaces/workflow_context.hpp"
#include <SDL3/SDL_gpu.h>
#include <SDL3/SDL_render.h>
#include <SDL3/SDL_surface.h>
#include <cstdint>
#include <memory>
namespace sdl3cpp::services::impl {
class WorkflowQ3OverlayDrawStep final : public IWorkflowStep {
public:
explicit WorkflowQ3OverlayDrawStep(std::shared_ptr<ILogger> logger);
~WorkflowQ3OverlayDrawStep();
std::string GetPluginId() const override;
void Execute(const WorkflowStepDefinition& step, WorkflowContext& context) override;
private:
void TryInit(SDL_GPUDevice* device, SDL_Window* window);
void DrawSurface(WorkflowContext& context, uint32_t frameW, uint32_t frameH);
void Render(SDL_GPUCommandBuffer* cmd, SDL_GPUTexture* swapchainTex,
SDL_GPUDevice* device, uint32_t frameW, uint32_t frameH);
std::shared_ptr<ILogger> logger_;
bool ready_ = false;
bool disabled_ = false;
bool vbuf_uploaded_ = false;
SDL_GPUDevice* device_ = nullptr;
SDL_GPUGraphicsPipeline* pipeline_ = nullptr;
SDL_GPUTexture* tex_ = nullptr;
SDL_GPUTransferBuffer* transfer_ = nullptr;
SDL_GPUBuffer* vtx_buf_ = nullptr;
SDL_GPUSampler* sampler_ = nullptr;
SDL_Surface* surface_ = nullptr;
SDL_Renderer* renderer_ = nullptr;
static constexpr int kW = 640;
static constexpr int kH = 360;
};
} // namespace sdl3cpp::services::impl
@@ -0,0 +1,20 @@
#pragma once
#include "services/interfaces/i_workflow_step.hpp"
#include "services/interfaces/i_logger.hpp"
#include <memory>
namespace sdl3cpp::services::impl {
class WorkflowQ3WeaponUpdateStep final : public IWorkflowStep {
public:
explicit WorkflowQ3WeaponUpdateStep(std::shared_ptr<ILogger> logger);
std::string GetPluginId() const override;
void Execute(const WorkflowStepDefinition& step, WorkflowContext& context) override;
private:
std::shared_ptr<ILogger> logger_;
};
} // namespace sdl3cpp::services::impl
@@ -0,0 +1,20 @@
#pragma once
#include "services/interfaces/i_workflow_step.hpp"
#include "services/interfaces/i_logger.hpp"
#include <memory>
namespace sdl3cpp::services::impl {
class WorkflowBspPortalViewStep final : public IWorkflowStep {
public:
explicit WorkflowBspPortalViewStep(std::shared_ptr<ILogger> logger);
std::string GetPluginId() const override;
void Execute(const WorkflowStepDefinition& step, WorkflowContext& context) override;
private:
std::shared_ptr<ILogger> logger_;
};
} // namespace sdl3cpp::services::impl